Ответ 1
Как я уже упоминал в своих комментариях выше, ваш код имеет некоторые ошибки компиляции, и вы оставили его много, что затрудняет 100-процентную уверенность в том, как именно он должен работать после завершения кода, Но после завершения кода, исправляя одну четкую опечатку (вы написали idx2 = xs.lastIndexOf(o2[i])
, но я уверен, что вы имели в виду idx2 = ys.lastIndexOf(o2[i])
), и одна вещь, о которой я думаю, является опечаткой (я не думаю, что вы имели в виду if (!equal(o1[i], o2[i], xs, ys))
для вставки внутри if (idx1 == idx2)
), удаления некоторого кода no-op и реструктуризации бит (к стилю, который я нахожу более ясным, YMMV), я получаю следующее:
boolean equal(final Object[] o1, final Object[] o2)
{
return _equal(o1, o2, new ArrayList<Object>(), new ArrayList<Object>());
}
private static boolean _equal(final Object[] o1, final Object[] o2,
final List<Object> xs, final List<Object> ys)
{
if(o1.length != o2.length)
return false;
xs.add(o1);
ys.add(o2);
try
{
for(int i = 0; i < o1.length; i++)
{
if(o1[i] == null && o2[i] == null)
continue;
if(o1[i] == null || o2[i] == null)
return false;
if(o1[i].equals(o2[i]))
continue;
if(! (o1[i] instanceof Object[]) || ! (o2[i] instanceof Object[]))
return false;
final int idx1 = xs.lastIndexOf(o1[i]);
if(idx1 >= 0 && idx1 == ys.lastIndexOf(o2[i]))
continue;
if(! _equal((Object[])o1[i], (Object[])o2[i], xs, ys))
return false;
}
return true;
}
finally
{
xs.remove(xs.size() - 1);
ys.remove(ys.size() - 1);
}
}
который в основном работает. Логика заключается в том, что всякий раз, когда она получает два Object[]
s, она проверяет, будет ли она в настоящее время сравнивать каждую из них выше в стеке, и если это так, она проверяет, является ли самый верхний стек-кадр, сравнивающий один из них также является самым верхним стеком, который сравнивает другой. (Это логика, которую вы намеревались, верно?)
Единственная серьезная ошибка, которую я вижу, заключается в такой ситуации:
// a one-element array that directly contains itself:
final Object[] a = { null }; a[0] = a;
// a one-element array that contains itself via another one-element array:
final Object[][] b = { { null } }; b[0][0] = b;
// should return true (right?); instead, overflows the stack:
equal(a, b, new ArrayList<Object>(), new ArrayList<Object>());
Вы видите, что в предыдущем случае последний элемент xs
всегда будет a
, но последний элемент ys
будет чередоваться между b
и b[0]
. В каждом рекурсивном вызове xs.lastIndexOf(a)
всегда будет наибольшим индексом xs
, а ys.lastIndexOf(b)
или ys.lastIndexOf(b[0])
(в зависимости от того, что требуется) всегда будет на один меньше наибольшего индекса ys
.
Проблема в том, что логики не должно быть, "самое верхнее сравнение o1[i]
находится в том же стеке-фрейме, что и самое верхнее сравнение o2[i]
"; скорее, это должно быть: "существует некоторый стек-кадр, любой кадр стека вообще, который сравнивает o1[i]
с o2[i]
". Но для эффективности мы можем фактически использовать логику "существует или когда-либо была стек-кадр, который/сравнивал o1[i]
с o2[i]
"; и мы можем использовать Set
пар массивов вместо двух List
массивов. С этой целью я написал следующее:
private static boolean equal(final Object[] a1, final Object[] a2)
{
return _equal(a1, a2, new HashSet<ArrayPair>());
}
private static boolean _equal
(final Object[] a1, final Object[] a2, final Set<ArrayPair> pairs)
{
if(a1 == a2)
return true;
if(a1.length != a2.length)
return false;
if(! pairs.add(new ArrayPair(a1, a2)))
{
// If we're here, then pairs already contained {a1,a2}. This means
// either that we've previously compared a1 and a2 and found them to
// be equal (in which case we obviously want to return true), or
// that we're currently comparing them somewhere higher in the
// stack and haven't *yet* found them to be unequal (in which case
// we still want to return true: if it turns out that they're
// unequal because of some later difference we haven't reached yet,
// that fine, because the comparison higher in the stack will
// still find that).
return true;
}
for(int i = 0; i < a1.length; ++i)
{
if(a1[i] == a2[i])
continue;
if(a1[i] == null || a2[i] == null)
return false;
if(a1[i].equals(a2[i]))
continue;
if(! (a1[i] instanceof Object[]) || ! (a2[i] instanceof Object[]))
return false;
if(! _equal((Object[]) a1[i], (Object[]) a2[i], pairs))
return false;
}
return true;
}
private static final class ArrayPair
{
private final Object[] a1;
private final Object[] a2;
public ArrayPair(final Object[] a1, final Object[] a2)
{
if(a1 == null || a2 == null)
throw new NullPointerException();
this.a1 = a1;
this.a2 = a2;
}
@Override
public boolean equals(final Object that)
{
if(that instanceof ArrayPair)
if(a1 == ((ArrayPair)that).a1)
return a2 == ((ArrayPair)that).a2;
else
if(a1 == ((ArrayPair)that).a2)
return a2 == ((ArrayPair)that).a1;
else
return false;
else
return false;
}
@Override
public int hashCode()
{ return a1.hashCode() + a2.hashCode(); }
}
Должно быть ясно, что приведенное выше не может привести к бесконечной рекурсии, потому что если программа имеет конечное число массивов, то она имеет конечное число пар массивов, и только один стек-кадр за раз может сравниваться заданная пара массивов (поскольку, как только пара начинает сравниваться, она добавляется к pairs
, и любая будущая попытка сравнить эту пару немедленно вернет true
), что означает, что общая глубина стека равна любое время. (Конечно, если количество массивов огромно, то вышеупомянутое может все еще переполнять стек, рекурсия ограничена, но максимальный размер стека. Я бы рекомендовал, чтобы for
-loop был разделен в два for
-loops, один за другим: в первый раз пропустите все элементы, которые являются массивами, а во второй раз пропустите все элементы, которые не являются. Это во многих случаях может избежать дорогостоящих сравнений.)
Также должно быть ясно, что указанное выше никогда не вернет false
, когда оно должно вернуться true
, так как оно возвращает только false
, когда оно находит фактическую разницу.
Наконец, я думаю, должно быть ясно, что выше не вернется true
, когда он должен вернуться false
, так как для каждой пары объектов один полный цикл всегда выполняется по всем элементам. Эта часть сложнее доказать, но в сущности мы определили структурное равенство таким образом, чтобы два массива были только структурно неравными, если мы можем найти некоторую разницу между ними; и вышеприведенный код в конечном итоге исследует каждый элемент каждого массива, с которым он сталкивается, поэтому, если бы была найденная разница, он бы нашел его.
Примечания:
- Я не беспокоился о массивах примитивов,
int[]
иdouble[]
и так далее. Адам отвечает, что вы хотите, чтобы их сравнивали пополам; если это необходимо, оно легко добавляется (поскольку для него не требуется рекурсия: массивы примитивов не могут содержать массивы), но приведенный выше код просто использует для нихObject.equals(Object)
, что означает ссылочное равенство. - В приведенном выше коде предполагается, что
Object.equals(Object)
реализует симметричное отношение, как указывает его контракт. Однако на самом деле этот контракт не всегда выполняется; например,new java.util.Date(0L).equals(new java.sql.Timestamp(0L))
-true
, аnew java.sql.Timestamp(0L).equals(new java.util.Date(0L))
-false
. Если порядок имеет значение для ваших целей; если вы хотите, чтобыequal(new Object[]{java.util.Date(0L)}, new Object[]{java.sql.Timestamp(0L)})
былtrue
иequal(new Object[]{java.sql.Timestamp(0L)}, new Object[]{java.util.Date(0L)})
равнымfalse
— то вы захотите изменитьArrayPair.equals(Object)
и, возможно,ArrayPair.hashCode()
, чтобы заботиться о том, какой массив является.