Проблемы с Promote() с использованием реализации красно-черного дерева из The Tomes of Delphi - программирование

Проблемы с Promote() с использованием реализации красно-черного дерева из The Tomes of Delphi

Я использую реализацию Red-Black tree, написанную Джулианом Бакналом в его знаменитой книге The Tomes Of Delphi. Исходный код может быть загружен здесь, и я использую код как есть в Delphi 2010 с изменениями в TdBasics.pas, чтобы он компилировался в современной версии Delphi (в основном, комментируя большую часть этого текста - только двоеточие требует только нескольких определений).

Это известная реализация известного автора в часто рекомендуемой книге. Я чувствую, что я должен быть на твердой земле, используя его. Но я сталкиваюсь с авариями с использованием Delete() и Promote(). Возвращаясь к написанию модульных тестов с DUnit, эти проблемы легко воспроизводятся. Некоторый пример кода (фрагменты из моих тестов DUnit):

// Tests that require an initialised tree start with one with seven items
const
  NumInitialItems : Integer = 7;

...

// Data is an int, not a pointer
function Compare(aData1, aData2: Pointer): Integer;
begin
  if NativeInt(aData1) < NativeInt(aData2) then Exit(-1);
  if NativeInt(aData1) > NativeInt(aData2) then Exit(1);
  Exit(0);
end;

// Add seven items (0..6) to the tree.  Node.Data is a pointer field, just cast.
procedure TestTRedBlackTree.SetUp;
var
  Loop : Integer;
begin
  FRedBlackTree := TtdRedBlackTree.Create(Compare, nil);
  for Loop := 0 to NumInitialItems - 1 do begin
    FRedBlackTree.Insert(Pointer(Loop));
  end;
end;

...

// Delete() crashes for the first item, no matter if it is 0 or 1 or... 
procedure TestTRedBlackTree.TestDelete;
var
  aItem: Pointer;
  Loop : Integer;
begin
  for Loop := 1 to NumInitialItems - 1 do begin // In case 0 (nil) causes problems, but 1 fails too
    aItem := Pointer(Loop);
    Check(FRedBlackTree.Find(aItem) = aItem, 'Item not found before deleting');
    FRedBlackTree.Delete(aItem);
    Check(FRedBlackTree.Find(aItem) = nil, 'Item found after deleting');
    Check(FRedBlackTree.Count = NumInitialItems - Loop, 'Item still in the tree');
  end;
end;

Я не достаточно прочен в алгоритмах, чтобы знать, как исправить это, не внося дополнительных проблем (неуравновешенное или неправильное дерево.) Я знаю, потому что я пробовал:)

Сбой кода

Вышеуказанный тест не удается выполнить в Promote() при удалении элемента в строке, помеченной !!!:

function TtdRedBlackTree.rbtPromote(aNode : PtdBinTreeNode)
                                          : PtdBinTreeNode;
var
  Parent : PtdBinTreeNode;
begin
  {make a note of the parent of the node we're promoting}
  Parent := aNode^.btParent;

  {in both cases there are 6 links to be broken and remade: the node's
   link to its child and vice versa, the node link with its parent
   and vice versa and the parent link with its parent and vice
   versa; note that the node child could be nil}

  {promote a left child = right rotation of parent}
  if (Parent^.btChild[ctLeft] = aNode) then begin
    Parent^.btChild[ctLeft] := aNode^.btChild[ctRight];
    if (Parent^.btChild[ctLeft] <> nil) then
      Parent^.btChild[ctLeft]^.btParent := Parent;
    aNode^.btParent := Parent^.btParent;
    if (aNode^.btParent^.btChild[ctLeft] = Parent) then //!!!
      aNode^.btParent^.btChild[ctLeft] := aNode
    else
      aNode^.btParent^.btChild[ctRight] := aNode;
    aNode^.btChild[ctRight] := Parent;
    Parent^.btParent := aNode;
  end
  ...

Parent.btParent (становление aNode.btParent) составляет nil, таким образом, сбой. Изучая древовидную структуру, родитель node является корнем node, который, очевидно, имеет родительский nil.

Некоторые нерабочие попытки его фиксации

Я попробовал просто протестировать это и только запустить этот оператор if/then/else, когда существовал бабушка и дедушка. Хотя это кажется логичным, это своего рода наивное исправление; Я не понимаю вращения достаточно хорошо, чтобы узнать, действительно ли это, или что-то еще должно произойти вместо этого - и это вызывает другую проблему, упомянутую после фрагмента. (Обратите внимание, что здесь есть дубликат этого кода ниже фрагмента, скопированного выше для левого вращения, и там тоже происходит ошибка.)

if aNode.btParent <> nil then begin //!!! Grandparent doesn't exist, because parent is root node
  if (aNode^.btParent^.btChild[ctLeft] = Parent) then
    aNode^.btParent^.btChild[ctLeft] := aNode
  else
    aNode^.btParent^.btChild[ctRight] := aNode;
  aNode^.btChild[ctRight] := Parent;
end;
Parent^.btParent := aNode;
...

С помощью этого кода тест для удаления все еще не выполняется, но с чем-то более странным: после вызова метода Delete() вызов метода Find() корректно возвращает nil, указывая, что элемент удален. Однако последняя итерация цикла, удаляющая элемент 6, вызывает сбой в TtdBinarySearchTree.bstFindItem:

Walker := FBinTree.Root;
CmpResult := FCompare(aItem, Walker^.btData);

FBinTree.Root nil, сбой при вызове FCompare.

Итак - в этот момент я могу сказать, что мои модификации явно просто вызывают больше проблем, а что-то еще более фундаментальное не так с кодом, реализующим алгоритм. К сожалению, даже с книгой в качестве справочной информации я не могу понять, что не так, точнее, какая правильная реализация будет выглядеть и что здесь происходит.

Я изначально думал, что это был мой код неправильно, используя дерево, вызывая проблемы. Это все еще очень возможно! Автор, книга и, следовательно, неявный код хорошо известны в мире Delphi. Но аварии легко воспроизводятся, написав некоторые очень простые модульные тесты для класса, используя исходный код книги, загруженный с сайта автора. Кто-то еще должен был использовать этот код за последнее десятилетие, и столкнулся с той же проблемой (если только ошибка не является моей, и оба моих кода и модульные тесты неправильно используют дерево). Я ищу ответы, помогая с помощью:

  • Идентификация и исправление ошибок в Promote и в других местах класса. Обратите внимание, что я также написал модульные тесты для базового класса TtdBinarySearchTree, и все они проходят. (Это не значит, что это идеально - я, возможно, не обнаружил неудачные случаи, но это поможет.)
  • Поиск обновленной версии кода. Джулиан не опубликовал никаких ошибок для реализации красно-черного дерева.
  • Если все остальное не удается найти другую известную хорошую реализацию красно-черного дерева для Delphi. Я использую дерево для решения проблемы, а не для написания дерева. Если мне нужно, я с радостью заменит базовую реализацию другим (учитывая правильные условия лицензирования и т.д.). Тем не менее, учитывая родословную книги и кода, проблемы удивляют, и их решение поможет большему количеству людей, чем мне. широко рекомендованная книга в сообществе Delphi.

Изменить: дополнительные примечания

Комментарий MBo указывает Джулианскую библиотеку EZDSL, которая содержит еще одну реализацию красно-черного дерева. Модульные тесты на этой версии проходят. В настоящее время я сравниваю два источника, чтобы попытаться увидеть, где алгоритмы отклоняются, чтобы найти ошибку.

Одна из возможностей - просто использовать красно-черное дерево EZDSL, а не красно-черное дерево Tomes of Delphi, но есть несколько проблем с библиотекой, которые заставляют меня не использовать ее: она написана для 32-битного x86 только; некоторые методы предоставляются только в сборке, а не в Pascal (хотя большинство из них имеют две версии); деревья структурированы совершенно по-другому, например, используя курсоры для узлов вместо указателей - совершенно правильный подход, но пример того, как отличается код "пример" в книге ToD, где навигация семантически отличается; код, на мой взгляд, гораздо сложнее понять и использовать: он довольно сильно оптимизирован, переменные и методы не так четко обозначены, есть множество магических функций, структура node на самом деле представляет собой запись объединения/случая, скрежеща в деталях для стеков, очередей, делений и списков, двойных списков, списков пропусков, деревьев, двоичных деревьев и кучи в одной структуре, которая почти непонятна в отладчике и т.д. Это не код, который я очень хочу использовать в производстве, где мне нужно будет его поддерживать, и нелегко учиться. Исходный код Tomes of Delphi гораздо читабельнее и гораздо удобнее обслуживать... но также и неверно. Вы видите дилемму:)

Я пытаюсь сравнить код, чтобы попытаться найти различия между юлианским кодексом практики (EZDSL) и его учебным кодом (Tomes of Delphi.) Но этот вопрос все еще открыт, и я все равно буду благодарен за ответы. Я не могу быть единственным человеком, использующим красно-черные деревья из Томеса Дельфи за двенадцать лет с момента его публикации:)

Изменить: дальнейшие примечания

Я сам это ответил (несмотря на то, что предлагал щедрость. Ой.) У меня возникли проблемы с поиском ошибок, просто просмотрев код и сравнив с описанием алгоритма ToD, поэтому вместо этого я переопределял ошибочные методы, основанные на хорошая страница, описывающая структуру, которая поставляется с лицензией C, лицензированной MIT; подробности ниже. Один из бонусов заключается в том, что я думаю, что новая реализация на самом деле намного понятнее.

4b9b3361

Ответ 1

Мне не удалось выяснить, что неправильно, изучив исходный код Tomes of Delphi и сравнив либо с алгоритмом, либо с другой реализацией Джулиана, сильно оптимизированной реализацией библиотеки EZDSL (таким образом, этот вопрос!), но Вместо этого я повторно выполнил Delete, и для хорошей меры также Insert, на основе примера C-код для красно-черного дерева на сайте Грамотного программирования, один из самых ярких примеров красно-черного дерева, который я нашел. (На самом деле довольно сложная задача найти ошибку, просто измельчив код и проверив, что она реализует что-то правильно, особенно если вы не полностью понимаете алгоритм. Могу сказать, у меня теперь гораздо лучшее понимание!) дерево хорошо документировано - я думаю, что Tomes of Delphi дает лучший обзор причин того, почему дерево работает так, как оно есть, но этот код является лучшим примером читаемой реализации.

Заметки об этом:

  • Комментарии часто являются прямыми кавычками на странице объяснения конкретных методов.
  • Достаточно легко было переносить, хотя я переместил процедурный код C в объектно-ориентированную структуру. Есть некоторые незначительные причуды, такие как дерево Bucknall, имеющее FHead node, дочерний элемент которого является корнем дерева, о котором вы должны знать при преобразовании. (Тесты часто проверялись, если родитель node был NULL в качестве способа тестирования, если node был корнем node. Я извлек эту и другую аналогичную логику для вспомогательных методов или node или древовидных методов.)
  • Читатели также могут найти Eternally Confuzzled page на красно-черных деревьях. Хотя я не использовал его при написании этой реализации, я, вероятно, должен был иметь, и если в этой реализации есть ошибки, я перейду туда для понимания. Это была первая страница, которую я нашел при исследовании деревьев RB при отладке ToD, чтобы упомянуть о связи между красно-черными деревьями и 2-3-4 дерева по имени.
  • Если это не ясно, этот код изменяет пример Tomes Delphi TtdBinaryTree, TtdBinarySearchTree и TtdRedBlackTree, найденный в TDBinTre.pas (source кода на странице ToD.) Чтобы использовать его, отредактируйте этот файл. Это не новая реализация, и она не завершена сама по себе. В частности, он сохраняет структуру кода ToD, такую ​​как TtdBinarySearchTree не являющийся потомком TtdBinaryTree, но владеющий им как член (т.е. Его перенос), используя FHead node вместо родителя nil для Root и т.д.
  • Исходный код имеет лицензию MIT. (Сайт переходит на другую лицензию, он может быть изменен к тому моменту, когда вы его проверяете. Для будущих читателей на момент написания кода код определенно соответствовал лицензии MIT.) Я не уверен в лицензии на Tomes кода Delphi; так как он в книге алгоритмов, вероятно, разумно предположить, что вы можете его использовать - это подразумевается в справочнике, я думаю. Насколько мне известно, если вы согласны с оригинальными лицензиями, вы можете использовать его:) Пожалуйста, оставьте комментарий, если это полезно, я хотел бы знать.
  • Реализация Tomes of Delphi работает путем вставки с использованием метода вставки двоичного дерева по умолчанию, а затем "продвижения" node. Логика находится в любом из этих двух мест. Эта реализация также реализует вставку, а затем переходит в ряд случаев, чтобы проверить положение и изменить его с помощью явных поворотов. Эти повороты находятся в отдельных методах (RotateLeft и RotateRight), которые я нахожу полезными - код ToD говорит о поворотах, но явно не вставляет их в отдельные именованные методы. Delete похож: он идет в несколько случаев. Каждый случай объясняется на странице и как комментарии в моем коде. Некоторые из них я назвал, но некоторые из них слишком сложны для ввода имени метода, так что это просто "случай 4", "случай 5" и т.д., С пояснениями комментариев.
  • На странице также был код, чтобы проверить структуру дерева и свойства red-black. Я начал делать это как часть написания модульных тестов, но еще не полностью добавил все красно-черные ограничения дерева, и поэтому добавил этот код к дереву. Он присутствует только в сборке отладки и утверждает, что что-то не так, поэтому в модульных тестах, выполненных при отладке, будут возникать проблемы.
  • Теперь дерево проходит мои модульные тесты, хотя они могут быть гораздо более обширными - я написал их, чтобы упростить отладку дерева Tomes of Delphi. Этот код не имеет никаких гарантий или гарантий. Считайте его непроверенным. Напишите тесты перед тем, как использовать их. Прокомментируйте, если вы нашли ошибку:)

На код!

Node модификации

Я добавил следующие вспомогательные методы в node, чтобы сделать код более грамотным при чтении. Например, исходный код часто тестировался, если node был левым дочерним элементом его родителя путем тестирования (слепое преобразование в Delphi и немодифицированные структуры ToD) if Node = Node.Parent.btChild[ctLeft] then..., тогда как теперь вы можете протестировать if Node.IsLeft then... и т.д. Прототипы метода в определение записи не включается для экономии места, но должно быть очевидно:)

function TtdBinTreeNode.Parent: PtdBinTreeNode;
begin
  assert(btParent <> nil, 'Parent is nil');
  Result := btParent;
end;

function TtdBinTreeNode.Grandparent: PtdBinTreeNode;
begin
  assert(btParent <> nil, 'Parent is nil');
  Result := btParent.btParent;
  assert(Result <> nil, 'Grandparent is nil - child of root node?');
end;

function TtdBinTreeNode.Sibling: PtdBinTreeNode;
begin
  assert(btParent <> nil, 'Parent is nil');
  if @Self = btParent.btChild[ctLeft] then
    Exit(btParent.btChild[ctRight])
  else
    Exit(btParent.btChild[ctLeft]);
end;

function TtdBinTreeNode.Uncle: PtdBinTreeNode;
begin
  assert(btParent <> nil, 'Parent is nil');
  // Can be nil if grandparent has only one child (children of root have no uncle)
  Result := btParent.Sibling;
end;

function TtdBinTreeNode.LeftChild: PtdBinTreeNode;
begin
  Result := btChild[ctLeft];
end;

function TtdBinTreeNode.RightChild: PtdBinTreeNode;
begin
  Result := btChild[ctRight];
end;

function TtdBinTreeNode.IsLeft: Boolean;
begin
  Result := @Self = Parent.LeftChild;
end;

function TtdBinTreeNode.IsRight: Boolean;
begin
  Result := @Self = Parent.RightChild;
end;

Я также добавил дополнительные методы, такие как существующий IsRed(), чтобы проверить, является ли он черным (IMO-код сканирует лучше, если он говорит if IsBlack(Node) не if not IsRed(Node)), и чтобы получить цвет, включая обработку nil node Обратите внимание, что они должны быть согласованными - например, IsRed возвращает false для nil node, поэтому nil node является черным (это также относится к свойствам красно-черного дерева и согласованное количество черных узлов на пути к листу.)

function IsBlack(aNode : PtdBinTreeNode) : boolean;
begin
  Result := not IsRed(aNode);
end;

function NodeColor(aNode :PtdBinTreeNode) : TtdRBColor;
begin
  if aNode = nil then Exit(rbBlack);
  Result := aNode.btColor;
end;

Проверка ограничения черного-черного цвета

Как упоминалось выше, эти методы проверяют структуру дерева и красно-черные ограничения и являются прямым переводом тех же методов в исходном коде С. Verify объявляется как встроенный, если не отлаживается в определении класса. Если не отладка, метод должен быть пустым и, мы надеемся, полностью удалим компилятор. Verify вызывается в начале и конце методов Insert и Delete, чтобы гарантировать правильность дерева до и после модификации.

procedure TtdRedBlackTree.Verify;
begin
{$ifdef DEBUG}
  VerifyNodesRedOrBlack(FBinTree.Root);
  VerifyRootIsBlack;
  // 3 is implicit
  VerifyRedBlackRelationship(FBinTree.Root);
  VerifyBlackNodeCount(FBinTree.Root);
{$endif}
end;

procedure TtdRedBlackTree.VerifyNodesRedOrBlack(const Node : PtdBinTreeNode);
begin
  // Normally implicitly ok in Delphi, due to type system - can't assign something else
  // However, node uses a union / case to write to the same value, theoretically
  // only for other tree types, so worth checking
  assert((Node.btColor = rbRed) or (Node.btColor = rbBlack));
  if Node = nil then Exit;
  VerifyNodesRedOrBlack(Node.LeftChild);
  VerifyNodesRedOrBlack(Node.RightChild);
end;

procedure TtdRedBlackTree.VerifyRootIsBlack;
begin
  assert(IsBlack(FBinTree.Root));
end;

procedure TtdRedBlackTree.VerifyRedBlackRelationship(const Node : PtdBinTreeNode);
begin
  // Every red node has two black children; or, the parent of every red node is black.
  if IsRed(Node) then begin
    assert(IsBlack(Node.LeftChild));
    assert(IsBlack(Node.RightChild));
    assert(IsBlack(Node.Parent));
  end;
  if Node = nil then Exit;
  VerifyRedBlackRelationship(Node.LeftChild);
  VerifyRedBlackRelationship(Node.RightChild);
end;

procedure VerifyBlackNodeCountHelper(const Node : PtdBinTreeNode; BlackCount : NativeInt; var PathBlackCount : NativeInt);
begin
  if IsBlack(Node) then begin
    Inc(BlackCount);
  end;

  if Node = nil then begin
    if PathBlackCount = -1 then begin
      PathBlackCount := BlackCount;
    end else begin
      assert(BlackCount = PathBlackCount);
    end;
    Exit;
  end;
  VerifyBlackNodeCountHelper(Node.LeftChild, BlackCount, PathBlackCount);
  VerifyBlackNodeCountHelper(Node.RightChild, BlackCount, PathBlackCount);
end;

procedure TtdRedBlackTree.VerifyBlackNodeCount(const Node : PtdBinTreeNode);
var
  PathBlackCount : NativeInt;
begin
  // All paths from a node to its leaves contain the same number of black nodes.
  PathBlackCount := -1;
  VerifyBlackNodeCountHelper(Node, 0, PathBlackCount);
end;

Вращения и другие полезные методы дерева

Вспомогательные методы для проверки, является ли node корнем node, чтобы установить node в качестве корня, заменить один node на другой, выполнить левое и правое вращение и следовать за деревом вниз по правым узлам к листу. Сделайте эти защищенные члены красно-черного дерева.

procedure TtdRedBlackTree.RotateLeft(Node: PtdBinTreeNode);
var
  R : PtdBinTreeNode;
begin
  R := Node.RightChild;
  ReplaceNode(Node, R);
  Node.btChild[ctRight] := R.LeftChild;
  if R.LeftChild <> nil then begin
    R.LeftChild.btParent := Node;
  end;
  R.btChild[ctLeft] := Node;
  Node.btParent := R;
end;

procedure TtdRedBlackTree.RotateRight(Node: PtdBinTreeNode);
var
  L : PtdBinTreeNode;
begin
  L := Node.LeftChild;
  ReplaceNode(Node, L);
  Node.btChild[ctLeft] := L.RightChild;
  if L.RightChild <> nil then begin
    L.RightChild.btParent := Node;
  end;
  L.btChild[ctRight] := Node;
  Node.btParent := L;
end;

procedure TtdRedBlackTree.ReplaceNode(OldNode, NewNode: PtdBinTreeNode);
begin
  if IsRoot(OldNode) then begin
    SetRoot(NewNode);
  end else begin
    if OldNode.IsLeft then begin // // Is the left child of its parent
      OldNode.Parent.btChild[ctLeft] := NewNode;
    end else begin
      OldNode.Parent.btChild[ctRight] := NewNode;
    end;
  end;
  if NewNode <> nil then begin
    newNode.btParent := OldNode.Parent;
  end;
end;

function TtdRedBlackTree.IsRoot(const Node: PtdBinTreeNode): Boolean;
begin
  Result := Node = FBinTree.Root;
end;

procedure TtdRedBlackTree.SetRoot(Node: PtdBinTreeNode);
begin
  Node.btColor := rbBlack; // Root is always black
  FBinTree.SetRoot(Node);
  Node.btParent.btColor := rbBlack; // FHead is black
end;

function TtdRedBlackTree.MaximumNode(Node: PtdBinTreeNode): PtdBinTreeNode;
begin
  assert(Node <> nil);
  while Node.RightChild <> nil do begin
    Node := Node.RightChild;
  end;
  Result := Node;
end;

Вставка и удаление

Красно-черное дерево является оберткой вокруг внутреннего дерева, FBinTree. Слишком сложным образом этот код изменяет дерево напрямую. И FBinTree, и красно-черное дерево обертки содержат счетчик FCount количества узлов, и для удаления этого очистителя я удалил TtdBinarySearchTree (предок красно-черного дерева) FCount и перенаправил Count, чтобы вернуть FBinTree.Count, т.е. спросить фактическое внутреннее дерево, используемое двоичным деревом поиска и красно-черными древовидными классами - это все, что принадлежит узлам. Я также добавил методы уведомления NodeInserted и NodeRemoved для увеличения и уменьшения количества отсчетов. Код не включен (тривиальный).

Я также извлек некоторые способы выделения node и удаления node - не вставлять и не удалять из дерева или ничего не делать с соединениями или присутствием node; они должны заботиться о создании и уничтожении самого node. Обратите внимание, что для создания node необходимо установить цвет node в красный цвет, после чего будут исправлены следующие изменения цвета. Это также гарантирует, что при освобождении node есть возможность освободить связанные с ним данные.

function TtdBinaryTree.NewNode(const Item : Pointer): PtdBinTreeNode;
begin
  {allocate a new node }
  Result := BTNodeManager.AllocNode;
  Result^.btParent := nil;
  Result^.btChild[ctLeft] := nil;
  Result^.btChild[ctRight] := nil;
  Result^.btData := Item;
  Result.btColor := rbRed; // Red initially
end;

procedure TtdBinaryTree.DisposeNode(Node: PtdBinTreeNode);
begin
  // Free whatever Data was pointing to, if necessary
  if Assigned(FDispose) then FDispose(Node.btData);
  // Free the node
  BTNodeManager.FreeNode(Node);
  // Decrement the node count
  NodeRemoved;
end;

С помощью этих дополнительных методов используйте следующий код для вставки и удаления. Код прокомментирован, но я рекомендую вам прочитать исходную страницу, а также книгу Tomes of Delphi для объяснения поворотов и различные случаи, когда тесты кода для.

Вставка

procedure TtdRedBlackTree.Insert(aItem : pointer);
var
  NewNode, Node : PtdBinTreeNode;
  Comparison : NativeInt;
begin
  Verify;
  newNode := FBinTree.NewNode(aItem);
  assert(IsRed(NewNode)); // new node is red
  if IsRoot(nil) then begin
    SetRoot(NewNode);
    NodeInserted;
  end else begin
    Node := FBinTree.Root;
    while True do begin
      Comparison := FCompare(aItem, Node.btData);
      case Comparison of
        0: begin
          // Equal: tree doesn't support duplicate values
          assert(false, 'Should not insert a duplicate item');
          FBinTree.DisposeNode(NewNode);
          Exit;
        end;
        -1: begin
          if Node.LeftChild = nil then begin
            Node.btChild[ctLeft] := NewNode;
            Break;
          end else begin
            Node := Node.LeftChild;
          end;
        end;
        else begin
          assert(Comparison = 1, 'Only -1, 0 and 1 are valid comparison values');
          if Node.RightChild = nil then begin
            Node.btChild[ctRight] := NewNode;
            Break;
          end else begin
            Node := Node.RightChild;
          end;
        end;
      end;
    end;
    NewNode.btParent := Node; // Because assigned to left or right child above
    NodeInserted; // Increment count
  end;
  InsertCase1(NewNode);
  Verify;
end;

// Node is now the root of the tree.  Node must be black; because it the only
// node, there is only one path, so the number of black nodes is ok
procedure TtdRedBlackTree.InsertCase1(Node: PtdBinTreeNode);
begin
  if not IsRoot(Node) then begin
    InsertCase2(Node);
  end else begin
    // Node is root (the less likely case)
    Node.btColor := rbBlack;
  end;
end;

// New node has a black parent: all properties ok
procedure TtdRedBlackTree.InsertCase2(Node: PtdBinTreeNode);
begin
  // If it is black, then everything ok, do nothing
  if not IsBlack(Node.Parent) then InsertCase3(Node);
end;

// More complex: uncle is red. Recolor parent and uncle black and grandparent red
// The grandparent change may break the red-black properties, so start again
// from case 1.
procedure TtdRedBlackTree.InsertCase3(Node: PtdBinTreeNode);
begin
  if IsRed(Node.Uncle) then begin
    Node.Parent.btColor := rbBlack;
    Node.Uncle.btColor := rbBlack;
    Node.Grandparent.btColor := rbRed;
    InsertCase1(Node.Grandparent);
  end else begin
    InsertCase4(Node);
  end;
end;

// "In this case, we deal with two cases that are mirror images of one another:
// - The new node is the right child of its parent and the parent is the left child
// of the grandparent. In this case we rotate left about the parent.
// - The new node is the left child of its parent and the parent is the right child
// of the grandparent. In this case we rotate right about the parent.
// Neither of these fixes the properties, but they put the tree in the correct form
// to apply case 5."
procedure TtdRedBlackTree.InsertCase4(Node: PtdBinTreeNode);
begin
  if (Node.IsRight) and (Node.Parent = Node.Grandparent.LeftChild) then begin
    RotateLeft(Node.Parent);
    Node := Node.LeftChild;
  end else if (Node.IsLeft) and (Node.Parent = Node.Grandparent.RightChild) then begin
    RotateRight(Node.Parent);
    Node := Node.RightChild;
  end;
  InsertCase5(Node);
end;

// " In this final case, we deal with two cases that are mirror images of one another:
// - The new node is the left child of its parent and the parent is the left child
// of the grandparent. In this case we rotate right about the grandparent.
// - The new node is the right child of its parent and the parent is the right child
// of the grandparent. In this case we rotate left about the grandparent.
// Now the properties are satisfied and all cases have been covered."
procedure TtdRedBlackTree.InsertCase5(Node: PtdBinTreeNode);
begin
  Node.Parent.btColor := rbBlack;
  Node.Grandparent.btColor := rbRed;
  if (Node.IsLeft) and (Node.Parent = Node.Grandparent.LeftChild) then begin
    RotateRight(Node.Grandparent);
  end else begin
    assert((Node.IsRight) and (Node.Parent = Node.Grandparent.RightChild));
    RotateLeft(Node.Grandparent);
  end;
end;

Удаление

procedure TtdRedBlackTree.Delete(aItem : pointer);
var
  Node,
  Predecessor,
  Child : PtdBinTreeNode;
begin
  Node := bstFindNodeToDelete(aItem);
  if Node = nil then begin
    assert(false, 'Node not found');
    Exit;
  end;
  if (Node.LeftChild <> nil) and (Node.RightChild <> nil) then begin
    Predecessor := MaximumNode(Node.LeftChild);
    Node.btData := aItem;
    Node := Predecessor;
  end;

  assert((Node.LeftChild = nil) or (Node.RightChild = nil));
  if Node.LeftChild = nil then
    Child := Node.RightChild
  else
    Child := Node.LeftChild;

  if IsBlack(Node) then begin
    Node.btColor := NodeColor(Child);
    DeleteCase1(Node);
  end;
  ReplaceNode(Node, Child);
  if IsRoot(Node) and (Child <> nil) then begin
    Child.btColor := rbBlack;
  end;

  FBinTree.DisposeNode(Node);

  Verify;
end;

// If Node is the root node, the deletion removes one black node from every path
// No properties violated, return
procedure TtdRedBlackTree.DeleteCase1(Node: PtdBinTreeNode);
begin
  if IsRoot(Node) then Exit;
  DeleteCase2(Node);
end;

// Node has a red sibling; swap colors, and rotate so the sibling is the parent
// of its former parent.  Continue to one of the next cases
procedure TtdRedBlackTree.DeleteCase2(Node: PtdBinTreeNode);
begin
  if IsRed(Node.Sibling) then begin
    Node.Parent.btColor := rbRed;
    Node.Sibling.btColor := rbBlack;
    if Node.IsLeft then begin
      RotateLeft(Node.Parent);
    end else begin
      RotateRight(Node.Parent);
    end;
  end;
  DeleteCase3(Node);
end;

// Node parent, sibling and sibling children are black; paint the sibling red.
// All paths through Node now have one less black node, so recursively run case 1
procedure TtdRedBlackTree.DeleteCase3(Node: PtdBinTreeNode);
begin
  if IsBlack(Node.Parent) and
     IsBlack(Node.Sibling) and
     IsBlack(Node.Sibling.LeftChild) and
     IsBlack(Node.Sibling.RightChild) then
  begin
    Node.Sibling.btColor := rbRed;
    DeleteCase1(Node.Parent);
  end else begin
    DeleteCase4(Node);
  end;
end;

// Node sibling and sibling children are black, but node parent is red.
// Swap colors of sibling and parent Node; restores the tree properties
procedure TtdRedBlackTree.DeleteCase4(Node: PtdBinTreeNode);
begin
  if IsRed(Node.Parent) and
     IsBlack(Node.Sibling) and
     IsBlack(Node.Sibling.LeftChild) and
     IsBlack(Node.Sibling.RightChild) then
  begin
    Node.Sibling.btColor := rbRed;
    Node.Parent.btColor := rbBlack;
  end else begin
    DeleteCase5(Node);
  end;
end;

// Mirror image cases: Node sibling is black, sibling left child is red,
// sibling right child is black, and Node is the left child.  Swap the colors
// of sibling and its left sibling and rotate right at S
// And vice versa: Node sibling is black, sibling right child is red, sibling's
// left child is black, and Node is the right child of its parent.  Swap the colors
// of sibling and its right sibling and rotate left at the sibling.
procedure TtdRedBlackTree.DeleteCase5(Node: PtdBinTreeNode);
begin
  if Node.IsLeft and
     IsBlack(Node.Sibling) and
     IsRed(Node.Sibling.LeftChild) and
     IsBlack(Node.Sibling.RightChild) then
  begin
    Node.Sibling.btColor := rbRed;
    Node.Sibling.LeftChild.btColor := rbBlack;
    RotateRight(Node.Sibling);
  end else if Node.IsRight and
    IsBlack(Node.Sibling) and
    IsRed(Node.Sibling.RightChild) and
    IsBlack(Node.Sibling.LeftChild) then
  begin
    Node.Sibling.btColor := rbRed;
    Node.Sibling.RightChild.btColor := rbBlack;
    RotateLeft(Node.Sibling);
  end;
  DeleteCase6(Node);
end;

// Mirror image cases:
// - "N sibling S is black, S right child is red, and N is the left child of its
// parent. We exchange the colors of N parent and sibling, make S right child
// black, then rotate left at N parent.
// - N sibling S is black, S left child is red, and N is the right child of its
// parent. We exchange the colors of N parent and sibling, make S left child
// black, then rotate right at N parent.
// This accomplishes three things at once:
// - We add a black node to all paths through N, either by adding a black S to those
// paths or by recoloring N parent black.
// - We remove a black node from all paths through S red child, either by removing
// P from those paths or by recoloring S.
// - We recolor S red child black, adding a black node back to all paths through
// S red child.
// S left child has become a child of N parent during the rotation and so is
// unaffected."
procedure TtdRedBlackTree.DeleteCase6(Node: PtdBinTreeNode);
begin
  Node.Sibling.btColor := NodeColor(Node.Parent);
  Node.Parent.btColor := rbBlack;
  if Node.IsLeft then begin
    assert(IsRed(Node.Sibling.RightChild));
    Node.Sibling.RightChild.btColor := rbBlack;
    RotateLeft(Node.Parent);
  end else begin
    assert(IsRed(Node.Sibling.LeftChild));
    Node.Sibling.LeftChild.btColor := rbBlack;
    RotateRight(Node.Parent);
  end;
end;

Заключительные заметки

  • Надеюсь, это полезно! Если вы сочтете это полезным, пожалуйста, оставьте комментарий о том, как вы его использовали. Я бы очень хотел знать.
  • Он поставляется без каких-либо гарантий или гарантии. Он проходит мои модульные тесты, но они могут быть более полными - все, что я могу сказать, это то, что этот код преуспевает там, где код Tomes of Delphi терпит неудачу. Кто знает, если он потерпит неудачу другими способами. Используйте на свой риск. Я рекомендую вам написать тесты для этого. Если вы нашли ошибку, прокомментируйте здесь!
  • Удачи:)

Ответ 2

Bucknall пишет, что его реализация двоичного дерева использует фиктивную головку node в качестве родителя root node (чтобы избежать особых случаев). Эта головка создается в конструкторе:

  constructor TtdBinaryTree.Create
   ...
 {allocate a head node, eventually the root node of the tree will be
   its left child}
  FHead := BTNodeManager.AllocNodeClear;

и используется во время первой вставки node:

function TtdBinaryTree.InsertAt
  ...
  {if the parent node is nil, assume this is inserting the root}
  if (aParentNode = nil) then begin
    aParentNode := FHead;
    aChildType := ctLeft;
  end;

Итак, ваша ситуация "the node parent is the root node, which obviously has a nil parent itself" выглядит очень странно, если вы не переписали ключевые методы