Какое дополнительное вращение требуется для удаления из дерева сверху вниз 2-3-4 левого дерева красного дерева?
Я реализую пакет LLRB, который должен работать в любом из двух режимов: Bottom-Up 2-3 или Top-Down 2-3-4 описанный Sedgewick (код - улучшенный код, хотя он имеет дело только с 2-3 деревья здесь, благодаря RS для указателя).
Sedgewick дает очень четкое описание операций дерева для режима 2-3, хотя он много времени проводит о режиме 2-3-4. Он также показывает, как простое изменение порядка переворачивания цвета во время вставки может изменить поведение дерева (либо разбить по пути вниз на 2-3-4, либо разбить по пути вверх на 2-3):
private Node insert(Node h, Key key, Value value)
{
if (h == null)
return new Node(key, value);
// Include this for 2-3-4 trees
if (isRed(h.left) && isRed(h.right)) colorFlip(h);
int cmp = key.compareTo(h.key);
if (cmp == 0) h.val = value;
else if (cmp < 0) h.left = insert(h.left, key, value);
else h.right = insert(h.right, key, value);
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
// Include this for 2-3 trees
if (isRed(h.left) && isRed(h.right)) colorFlip(h);
return h;
}
Однако он замалчивает удаление в 2-3-4 LLRB со следующим:
Код на следующей странице - это полная реализация delete() для LLRB 2-3 деревьев. Он основан на обратном подходе, используемом для вставки сверху вниз 2-3-4 деревьев: мы выполняем повороты и цветные переходы по пути вниз по пути поиска, чтобы гарантировать, что поиск не заканчивается на 2- node, так что мы можем просто удалить node внизу. Мы используем метод fixUp() для совместного использования кода для переворота и поворота цвета после рекурсивных вызовов в коде insert(). С помощью fixUp() мы можем оставить правые красные ссылки и несбалансированные 4-узлы вдоль пути поиска, чтобы эти условия были исправлены по пути вверх по дереву. (Подход также эффективен с 2-3-4 деревьями, но требует дополнительного вращения, когда правый node от пути поиска - это 4- node.)
Его функция delete():
private Node delete(Node h, Key key)
{
if (key.compareTo(h.key) < 0)
{
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = delete(h.left, key);
}
else
{
if (isRed(h.left))
h = rotateRight(h);
if (key.compareTo(h.key) == 0 && (h.right == null))
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
if (key.compareTo(h.key) == 0)
{
h.val = get(h.right, min(h.right).key);
h.key = min(h.right).key;
h.right = deleteMin(h.right);
}
else h.right = delete(h.right, key);
}
return fixUp(h);
}
Моя реализация корректно поддерживает инварианты LLRB 2-3 для всех операций дерева на 2-3 деревьях, но не подходит для подкласса правосторонних делеций на 2-3-4 деревьях (эти неудачные удаления приводят к правым красным вершинам, но снежный ком для дисбаланса деревьев и, наконец, разыменования нулевых указателей). Из обзора примера кода, который обсуждает деревья LLRB и включает опции для построения деревьев в любом из режимов, кажется, что ни один из них не правильно реализует удаление из 2-3-4 LLRB (т.е. Ни один из них не имеет дополнительного вращения, например, Sedgewick java выше и здесь).
У меня возникли проблемы с выяснением того, что он подразумевает под "дополнительным вращением, когда правый node с пути поиска является 4- node"; по-видимому, это вращение влево, но где и когда?
Если я поворачиваю левую часть, проходящую мимо эквивалента 4- node (т.е. RR node) или правого наклоняющегося 3- node эквивалента (BR node) либо до вызова fixUp(), либо в конце функции fixUp, я все равно получаю такое же инвариантное противоречие.
Вот состояния дерева наименьших неудачных примеров, которые я нашел (сгенерированные последовательной вставкой элементов от 0 до соответствующего максимального значения).
Первая пара деревьев показывает переход от состояния, совместимого с инвариантом, до удаления элемента 15 до явно разбитого состояния после.
![A 2-3-4 LLRB containing 0..15]()
![State after deleting item 15]()
Вторая по существу такая же, как и выше, но с удалением 16 из 0..16 (удаление 15 результатов в той же топологии). Заметим, что инвариантное противоречие позволяет пересечь корень node.
![A 2-3-4 LLRB containing 0..16]()
![State after deleting item 16]()
Ключом будет понимание того, как отменить нарушения, сгенерированные во время ходьбы по дереву, до целевого node. Следующие два дерева показывают, как первое дерево выглядит сверху после ходьбы вниз и слева и справа (без удаления и перед тем, как идти назад с помощью fixUp()).
После попытки удалить '-1' без fixUp:
![After attempt to delete '-1' without fixUp]()
После попытки удалить '16' без fixUp:
![After attempt to delete '16' without fixUp]()
Попытка повернуть влево на прогулку назад, когда node имеет только красный правый дочерний элемент, кажется, является частью решения, но он не работает правильно с двумя красными правыми детьми подряд, перед этим с помощью flipColor когда оба ребенка красны, похоже, улучшают ситуацию дальше, но все же оставляют некоторые инварианты нарушенными.
Если я дополнительно проверю, является ли правильный дочерний элемент правильного ребенка красным, когда его брат является черным и вращается влево, если это правда, я только терпит неудачу один раз, но в этот момент я чувствую, что мне нужна новая теория, а не новый эпицикл.
Любые идеи?
Для справки моя реализация доступна здесь (Нет, это не Java).
Последующий:
Часть причин, по которым меня это интересовало, заключалась в том, чтобы подтвердить многие утверждения, что 2-3 дерева LLRB более эффективны, чем 2-3-4 дерева LLRB. Мой бенчмаркинг подтвердил это для вставки и удаления (2-3 примерно на 9% быстрее), но я считаю, что поиск очень немного быстрее для 2-3-4 деревьев.
Следующие моменты являются репрезентативными и согласованными во всех прогонах:
BU23:
BenchmarkInsert 1000000 1546 ns/op
BenchmarkDelete 1000000 1974 ns/op
BenchmarkGet 5000000 770 ns/op
TD234:
BenchmarkInsert 1000000 1689 ns/op
BenchmarkDelete 1000000 2133 ns/op
BenchmarkGet 5000000 753 ns/op
Первый столбец - имя скамьи, второе - количество операций, третье - результат. Тест на i5M 2.27.
Я просмотрел длины ветвей для 2-3 деревьев и 2-3-4 деревьев, и мало что можно объяснить разницей поиска (среднее расстояние от корня до node и SD 1000 деревьев каждый с 10000 случайных вставок):
Means:
TD234 leafs BU23 leafs
12.88940 12.84681
TD234 all BU23 all
11.79274 11.79163
StdDev:
TD234 leafs BU23 leafs
1.222458 1.257344
TD234 all BU23 all
1.874335 1.885204
Ответы
Ответ 1
Обновлено и проверено
Ключевое значение для тестирования заключается в том, что реализация не поддерживает удаление несуществующего или ранее удаленного node! Я слишком долго пытался выяснить, почему мое рабочее решение "сломано". Это можно исправить, выполнив предварительный поиск ключа и вернув false, если он не находится в дереве вообще, и это решение было использовано в связанном коде внизу.
Не отображается. Sedgewick написал удаление для удаления 2-3-4, которое доступно для публики. Его результаты касаются, в частности, 2-3 деревьев (он делает лишь поверхностное упоминание о 2-3-4 деревьях в том, что их средняя длина пути (и, следовательно, стоимость поиска), как и у других красно-черных деревьев, неотличима от 2-3 случая). Никто другой, похоже, тоже не нашел, так что вот что я нашел после отладки проблемы:
Для начала возьмите код Sedgewick и исправьте устаревшие биты. В слайдах здесь (стр. 31) вы можете видеть, что его код по-прежнему использует старое представление из 4 узлов, где это было сделано, имея два левых красных в ряд, а не баланс. Первый бит, чтобы написать процедуру удаления 2-3-4, заключается в том, чтобы исправить это, чтобы мы могли выполнить проверку работоспособности, которая поможет нам проверить наши исправления позже:
private boolean is234(Node x)
{
if (x == null)
return true;
// Note the TD234 check is here because we also want this method to verify 2-3 trees
if (isRed(x.right))
return species == TD234 && isRed(x.left);
if (!isRed(x.right))
return true;
return is234(x.left) && is234(x.right);
}
Как только у нас это получится, мы узнаем пару вещей. Во-первых, из статьи видно, что 4 узла не должны быть разбиты при использовании дерева 2-3-4. Второй - специальный случай для правого 4- node на пути поиска. Там есть третий специальный случай, который не упоминается, и тогда вы возвращаетесь к дереву, вы можете оказаться там, где у вас h.right.left
будет красный, что оставит вас недействительными, просто повернув влево. Это зеркало для случая, описанного для вставки на стр. 4 документа.
Требуется исправление вращения для 4- node:
private Node moveRedLeft(Node h)
{ // Assuming that h is red and both h.left and h.left.left
// are black, make h.left or one of its children red.
colorFlip(h);
if (isRed(h.right.left))
{
h.right = rotateRight(h.right);
h = rotateLeft(h);
colorFlip(h);
if (isRed(h.right.right) )
h.right = rotateLeft(h.right);
}
return h;
}
И это удаляет расщепление на 2-3-4, а также добавляет исправление для третьего особого случая
private Node fixUp(Node h)
{
if (isRed(h.right))
{
if (species == TD234 && isRed(h.right.left))
h.right = rotateRight(h.right);
h = rotateLeft(h);
}
if (isRed(h.left) && isRed(h.left.left))
h = rotateRight(h);
if (species == BU23 && isRed(h.left) && isRed(h.right))
colorFlip(h);
return setN(h);
}
Наконец, нам нужно проверить это и убедиться, что он работает. Они не должны быть наиболее эффективными, но, как я обнаружил во время отладки, они должны фактически работать с ожидаемым поведением дерева (т.е. Не вставлять/удалять повторяющиеся данные)! Я сделал это с помощью тестовых вспомогательных методов. Прокомментированные строки были там, когда я отлаживал, я сломал и проверил дерево на очевидный дисбаланс. Я пробовал этот метод с 100000 узлов, и он выполнялся безупречно:
public static boolean Test()
{
return Test(System.nanoTime());
}
public static boolean Test(long seed)
{
StdOut.println("Seeding test with: " + seed);
Random r = new Random(seed);
RedBlackBST<Integer, Integer> llrb = new RedBlackBST<Integer,Integer>(TD234);
ArrayList<Integer> treeValues = new ArrayList<Integer>();
for (int i = 0; i < 1000; i++)
{
int val = r.nextInt();
if (!treeValues.contains(val))
{
treeValues.add(val);
llrb.put(val, val);
}
else
i--;
}
for (int i = 0; i < treeValues.size(); i++)
{
llrb.delete(treeValues.get(i));
if (!llrb.check())
{
return false;
}
// StdDraw.clear(Color.GRAY);
// llrb.draw(.95, .0025, .008);
}
return true;
}
Полный источник можно найти здесь.