Как работает этот алгоритм удаления узла BST?

Я пытаюсь следовать алгоритму BST в «Структуры данных и алгоритмы» Гранвилла Барнетта, но я не понимаю описанный ниже алгоритм удаления узла.

Раздел 3.3 (стр. 22)

Удалить узел из BST довольно просто, нужно рассмотреть четыре случая:

  1. значение, которое нужно удалить, - это листовой узел; или
  2. значение, которое нужно удалить, имеет правое поддерево, но не левое поддерево; или
  3. значение, которое нужно удалить, имеет левое поддерево, но не правое поддерево; или
  4. значение, которое нужно удалить, имеет как левое, так и правое поддерево, и в этом случае мы продвигаем наибольшее значение в левое поддерево.

Рисунок 3.2 (стр. 22)

    23
   /  \
  14   31
 /
7
 \
  9
  • Случай №1 указывает на узел 9.
  • Случай № 2 указывает на узел 7.
  • Случай № 3 указывает на узел 14.
  • Случай № 4 указывает на узел 23.

Я интерпретирую приведенный выше текст для # 4 как означающий, что когда мы удаляем 23, мы повышаем 14 до корневого и делаем 31 его правым дочерним элементом:

  14
 /  \
7   31
 \
  9

... но алгоритм книги (со стр. 23) для случая № 4 вводит меня в заблуждение (я переписал его здесь на Java):

1 boolean remove(T value) {
2   // ...
3
4   // case #4
5   Node largestValueNode = nodeToRemove.left;
6   while (largestValueNode.right != null) {
7       // find the largest value in the left subtree of nodeToRemove
8       largestValueNode = largestValueNode.right;
9   }
10  
11  // delete the right child of largestValueNode's parent
12  findParent(largestValueNode.value).right = null;
13  nodeToRemove.value = largestValueNode.value;
14  
15  count--;
16  return true; // successful
17}

Если я следую алгоритму, largestValueNode - это узел 14, поэтому его родительский узел - это узел 23. Почему алгоритм аннулирует правый дочерний элемент родителя?

Почему строка 13 копирует значение largestValueNode в удаляемый узел?

Я ожидал, что строки 11-13 будут такими:

11  if (largestValueNode != null)
12      largestValueNode.right = nodeToRemove.right;
13  nodeToRemove.right = null;

РЕДАКТИРОВАТЬ:

В алгоритме книги действительно есть ошибка. Исправление ниже:

1 boolean remove(T value) {
2   // ...
3
4   // case #4
5   Node largestValueNode = nodeToRemove.left;
6   while (largestValueNode.right != null) {
7       // find the largest value in the left subtree of nodeToRemove
8       largestValueNode = largestValueNode.right;
9   }
10  
11  Node p = findParent(largestValueNode.value);
12  if (p != null) {
13      if (nodeToRemove == p)
14          nodeToRemove.left = largestValueNode.left;
15      else
16          p.right = largestValueNode.left;
17  }
18  nodeToRemove.value = largestValueNode.value;
19  
20  count--;
21  return true; // successful
22}

person tony19    schedule 07.07.2012    source источник
comment
Обратите внимание: вы опускаете важную строчку из текста, на который ссылаетесь. Непосредственно перед count-- в тексте есть следующая строка: nodeToRemove.Value = largestValueNode.Value, которую вам здесь не хватает.   -  person Dan Nissenbaum    schedule 08.07.2012
comment
Это не пропало. Возможно, вы смотрите на конец вопроса, это мой собственный код, который, как я ожидал, будет от алгоритма. Настоящий код из книги стоит прямо перед моим.   -  person tony19    schedule 08.07.2012
comment
Вы правы - я смотрел в конце вопроса. Спасибо за исправление.   -  person Dan Nissenbaum    schedule 08.07.2012


Ответы (4)


если ты сделаешь это

11  if (largestValueNode != null)
12      largestValueNode.right = nodeToRemove.right;
13  nodeToRemove.right = null;

вы не рассматриваете случай, когда у 14 может быть правильный ребенок. Например:

     23
    / \
   14  31
  / \
 7   15
  \
   9

Ваше решение при удалении 23 должно быть

     15
    / \
   14  31
  / 
 7  
  \
   9

Итак, вы устанавливаете для правого дочернего элемента исходного родителя 15 14 значение null. Это то, что делает первый код.

Изменить: обращение к вашему комментарию

С вашим решением вы получите

     23
    / 
   14  
  / \
 7   15
  \   \
   9   31

Кроме того, исходный код также неверен; попробуйте что-нибудь вроде этого:

if(nodeToRemove == findParent(largestValueNode.value))
   nodeToRemove.left = largestValueNode.left
else
   findParent(largestValueNode.value).right = largestValueNode.left
nodeToRemove.value = largestValueNode.value

Также для ответа: «Почему строка 13 копирует значение самого большогоValueNode в удаляемый узел?»

Мы удаляем largestValueNode, а перед этим сохраняем его значение в nodeToRemove

person deebee    schedule 08.07.2012
comment
В строках 5–9 выполняется поиск самого большого узла в левом поддереве удаляемого узла. Самый большой узел определяется тем, что не имеет правого дочернего элемента. Итак, если бы у вас было 15 в дереве, largestValueNode было бы 15 (у которого не было бы правого дочернего элемента). - person tony19; 08.07.2012
comment
@ user46874 отредактировал ответ, чтобы прояснить мысль, которую я пытался сделать. Также было отмечено, что исходный код также неверен. Я предложил свое решение. - person deebee; 08.07.2012
comment
Спасибо, теперь это имеет смысл. Замена строк 11-13 вашим кодом решает проблему. - person tony19; 08.07.2012
comment
Приятно знать, что исходный код неверен - я только что потратил на это несколько часов и все время думал, что мне что-то не хватает. Также есть проблема с NPE, а также нет возможности удалить корневой элемент. - person Ben; 16.02.2014
comment
Приятно знать, что исходный код неверен - я только что потратил на это несколько часов и все время думал, что мне что-то не хватает. Возникает проблема NPE, если корневой элемент передан, и в любом случае удаление корневого элемента не учитывается. - person Ben; 16.02.2014

Похоже, алгоритм книги неверен для этого конкретного примера (при условии, что вы отлично перевели на Java :)). Он делает то, что вы упомянули, но это правильно для случая:

где nodeToRemove = 23, а в вашем BST 14 был правый дочерний элемент 15. Алгоритм книги заменит 23 здесь 15 и установит для правого дочернего элемента 14 значение null. В этом случае ваш алгоритм не сработает.

person user1168577    schedule 08.07.2012
comment
Обратите внимание, что в вопросе не указана строка кода из упомянутого текста, что решает эту проблему. См. Комментарий под вопросом. - person Dan Nissenbaum; 08.07.2012
comment
... Фактически, строка кода не пропала (см. Комментарии под вопросом). - person Dan Nissenbaum; 08.07.2012

Внимательно посмотрите на строку:

largestValueNode.right = nodeToRemove.right;

Обратите внимание, как эта строка заставляет 14 выглядеть следующим образом (без учета внуков):

  14
 /  \
7   31

Но это именно то, что нужно! Поскольку 14 теперь имеет 31 в качестве своего правого дочернего элемента, для 31 больше не правильно быть правым дочерним элементом 15, поэтому для очистки правый дочерний элемент 15 устанавливается в NULL.

person Dan Nissenbaum    schedule 08.07.2012
comment
Цитированная вами строка - это мой собственный код. Я ожидал, что это будет в алгоритме, предложенном в книге. И нет ребенка _1 _... - person tony19; 08.07.2012
comment
Вы правы - я был однозначно неряшливым. Я согласен с тем, что исходный код неверен. - person Dan Nissenbaum; 08.07.2012

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

Вот моя реализация Java, которая, вероятно, нуждается в некоторой оптимизации - предложения приветствуются. O (n log n) худший случай. Тесты ниже.

public boolean remove(final T value0) {
    BinarySearchTreeNode<T> target = findNode(value0);

        // Node DNE
        if (target == null) {
            return false;
        }

        // Both children populated, no need for parent
        if (target.right != null && target.left != null) {
            BinarySearchTreeNode<T> max = maxChild(target.left);
            findParent(max.value).right = null;
            target.value = max.value;
        }
        // Root element targeted, parent DNE
        else if (target == root) {
            if (target.right == null && target.left == null) {
                root = null;
            }
            else if (target.right == null) {
                root = target.left;
            }
        else {
            root = target.right;
        }
    }
    // Non-root, single-child node - find if L or R child, update parent reference.
    else {
        BinarySearchTreeNode<T> parent = findParent(value0);

        if (target.right == null && target.left != null) {
            if (target.value.compareTo(parent.value) < 0) {
                parent.left = target.left;
            }
            else {
                parent.right = target.left;
            }
        }
        else if (target.right != null && target.left == null) {
            if (target.value.compareTo(parent.value) < 0) {
                parent.left = target.right;
            }
            else {
                parent.right = target.right;
            }
        }
    }       

    return true;
}

Модульные тесты (все проходят, очевидно):

package BinarySearchTreeTests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import org.junit.Before;
import org.junit.Test;

public class Remove {
    BinarySearchTree<Integer> tree;

@Before
public void setUp() {
    tree = new BinarySearchTree<Integer>();
}

@Test
public void fromEmptyTree() {
    assertFalse(tree.remove(8));
}

@Test
public void fromTreeWithOnlyRootNode() {
    tree.add(10);
    assertTrue(tree.remove(10));
    assertNull(tree.root);
}

@Test
public void nonexistentElement() {
    tree.add(10);
    assertFalse(tree.remove(8));
}

/**
 *     N
 * 10--|
 *     |  6
 *     5--|
 *        3
 */
@Test
public void nodeWithNoRightChildren() {
    tree.add(10);
    tree.add(5);
    tree.add(6);
    tree.add(3);
    tree.remove(10);
    assertEquals(tree.root.value, Integer.valueOf(5));
    assertEquals(tree.root.left.value, Integer.valueOf(3));
    assertEquals(tree.root.right.value, Integer.valueOf(6));
}

/**
 *         17
 *     15--|
 *     |   13
 * 10--|
 *     N
 */
@Test
public void nodeWithNoLeftChildren() {
    tree.add(10);
    tree.add(15);
    tree.add(17);
    tree.add(13);
    tree.remove(10);
    assertEquals(tree.root.value, Integer.valueOf(15));
    assertEquals(tree.root.left.value, Integer.valueOf(13));
    assertEquals(tree.root.right.value, Integer.valueOf(17));
}

/**
 *           19
 *        17-|
 *        |  16
 *     15-|
 *     |  |  14
 *     |  13-|
 *     |     12
 * 10--|
 *     N
 */       
@Test
public void nodeWithLeftAndRightChildren() {
    tree.add(10);
    tree.add(15);
    tree.add(17);
    tree.add(13);
    tree.add(19);
    tree.add(16);
    tree.add(14);
    tree.add(12);

    tree.remove(15);
    assertEquals(tree.root.right.value, Integer.valueOf(14));
    assertNull(tree.root.right.left.right);
}

/**
 *           18
 *        15-|
 *        |  [ALWAYS EMPTY]
 *     15-|
 *     |  |  13
 *     |  12-|
 *     |     11
 * 10--|
 *     N
 * 
@Test
public void removeDuplicate() {
    Above diagram shows duplicate cases are already tested implicitly.
    fail();
} */
}
person Ben    schedule 15.02.2014