Как проверить правильность моей реализации дерева AVL?

ребята. Я думаю, что создал реализацию дерева AVL, но поскольку дерево AVL-довольно сложная структура, мне нужно ее протестировать. Вопрос в том, как я могу это проверить? У тебя есть какие-нибудь идеи? До этого момента у меня есть следующие тесты:

  1. basic sanity check-проверяет, что для каждого узла высота равна max. высота дочерних узлов + 1, баланс в [-1, 1], оставил ребенка key

  2. проверьте, что обход inorder на дереве AVL (и на двоичном дереве поиска в целом) возвращает значения из базового набора в порядке;

  3. проверьте, что высота дерева AVL строго меньше, чем 1.44 * log2 (N+2) -1 (есть N элементов) - доказано создателями дерева AVL;

  4. визуальная проверка - не работает так хорошо, я пытаюсь нарисовать дерево (rootnode в первая строка, его прямые дети на следующей строке, ребенок от прямого ребенка рутнода на третьей строке и так далее), но это работает только на маленьких деревьях, для больших деревьев это становится полным беспорядком;

  5. (?????) Русская Википедия говорит, что экспериментально доказано, что для двух вставок требуется одна перебалансировка и для пяти удалений также нужна одна перебалансировка, но так ли это на самом деле? Английская Википедия ничего не говорит об этом, а для моего AVL один перебалансировка нужна для двух вставки или для четырех удалений, что не совсем одно и то же.

может быть, этих тестов достаточно, но если есть еще тесты, не трудно реализовать, почему бы не сделать это?

5 ответов


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

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

  • создание нового дерева.
  • вставка первого значения.
  • вставка больше значение.
  • вставка меньшего значения.
  • вставка значения, которое вызывает вращение LL.
  • то же самое для других вращений.
  • то же самое для удаления.
  • все варианты поиска значений.

Если ваша реализация пройдет эти тесты, она, вероятно, пройдет их на больших деревьях. Обратите внимание, что производительность и использование памяти не испытывал.


в духе всех этих ответов я подумал, что приведу пару конкретных примеров, чтобы продемонстрировать, что основного случая недостаточно.

Вставить-Левая / Правая Перебалансировка

рассмотрим следующие сбалансированные двоичные деревья AVL для вставить работы:

  20+       20+           __20+__
 /         /  \          /       \
4         4    26       4         26
         / \           / \       /  \
        3   9         3+  9    21    30
                     /   / \
                    2   7   11

вставка 8 или 15 (например) в любое из этих деревьев вызовет по существу одинаковую левую / правую балансировку, но конечные результаты значительно отличается для каждого дерева и insert value. А именно, конечное место посадки вставленного значения и конечные коэффициенты баланса узла (4) и узла(20) полностью зависят от относительного значения правого дочернего элемента под узлом(4) - если таковые имеются. Тест только от любого из этих случаев не обязательно доказывает правильность любых других. Примечание: узел (4) должен быть первоначально сбалансирован для этих случаев; начальный дисбаланс в узле (4) в конечном счете не влияет на узел(20).

корпус 1a: вставить 15

  20+      20++         20++      15
 /        /            /         /  \
4     => 4-     =>   15+     => 4    20
          \         /
           15      4

корпус 2а: вставить 15

    20+          20++           20++         9
   /  \         /  \           /  \         / \
  4    26 =>   4-   26 =>     9+   26 =>   4+  20
 / \          / \            / \          /   /  \
3   9        3   9-         4+  15       3  15    26
                   \       /
                    15    3

корпус 3А: вставить 15

      __20+__                _20++_                  __20++_                ___9___
     /       \              /      \                /       \              /       \
    4         26    =>     4-       26    =>       9+        26    =>     4+      __20__
   / \       /  \         / \      /  \           / \       /  \         / \     /      \
  3+  9    21    30      3+  9-  21    30        4+  11-  21    30      3+  7  11-       26
 /   / \                /   / \                 / \   \                /         \      /  \
2   7   11             2   7   11-             3+  7   15             2           15  21    30
                                 \            /
                                  15         2

корпус 1b: вставить 8

  20+      20++        20++      8
 /        /           /         / \
4     => 4-     =>   8+     => 4   20
          \         /
           8       4

корпус 2b: вставить 8

    20+          20++           20++         9
   /  \         /  \           /  \         / \
  4    26 =>   4-   26 =>     9++  26 =>   4   20-
 / \          / \            /            / \    \
3   9        3   9+         4            3   8    26
                /          / \
               8          3   8

корпус 3b: вставить 8

      __20+__                _20++_                  __20++_                ___9___
     /       \              /      \                /       \              /       \
    4         26           4-       26             9+        26           4        _20-
   / \       /  \         / \      /  \           / \       /  \         / \      /    \
  3+  9    21    30 =>   3+  9+  21    30 =>     4   11   21    30 =>   3+  7-  11      26
 /   / \                /   / \                 / \                    /     \         /  \
2   7   11             2   7-  11              3+  7-                 2       8      21    30
                            \                 /     \
                             8               2       8

более сложные случаи были проблемой для меня, когда я работал над оптимизацией расчета коэффициентов баланса (то есть корректировкой коэффициентов баланса только для затронутых узлов, а не пересчет всего дерева).

Delete-Двойная Перебалансировка

Теперь рассмотрим эти деревья на удалить работы:

  2            ___6___               ___5___
 / \          /       \             /       \
1   4        2         9           2         8
   / \      / \       / \         / \       / \
  3   5    1   4     8   B       1   3     7   A
              / \   /   / \           \   /   / \
             3   5 7   A   C           4 6   9   B
                            \                     \
                             D                     C

удалить узел (1) из каждого из этих деревьев. Обратите внимание, что Случай 1 эффективно доказывает случай 2, но совсем не Случай 3.

корпус 1

  2          2            4
 / \          \          / \
1   4    =>    4    =>  2   5
   / \        / \        \
  3   5      3   5        3

корпус 2

    ___6___                ___6___                 ___6___
   /       \              /       \               /       \
  2         9            2         9             4         9
 / \       / \            \       / \           / \       / \
1   4     8   B     =>     4     8   B      => 2   5     8   B
   / \   /   / \          / \   /   / \         \       /   / \
  3   5 7   A   C        3   5 7   A   C         3     7   A   C
                 \                      \                       \
                  D                      D                       D

корпус 3

    ___5___              ___5___                 ___5___                   ____8____
   /       \            /       \               /       \                 /         \
  2         8          2         8             3         8              _5_          A
 / \       / \          \       / \           / \       / \            /   \        / \
1   3     7   A     =>   3     7   A      => 2   4     7   A     =>   3     7      9   B
     \   /   / \          \   /   / \                 /   / \        / \   /            \
      4 6   9   B          4 6   9   B               6   9   B      2   4 6              C
                 \                    \                       \
                  C                    C                       C

множество примеров ротаций AVL в книгах и в интернете, но то, что я нашел, Казалось произвольным, и ни одно место, казалось, не включало простые примеры для всех 4 случаев для вставки и удаления.

это самый простой тестовый случай, который я мог бы придумать для 4 видов вращений. Чтобы упростить описание, я использовал символы ascii в качестве ключа, поэтому тестовый случай может быть выражен в виде строки. Например, строка " abc "будет вставлять "a", вставлять "b", а затем включить "с".

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

кажется, есть две разные номенклатуры для вращений - то, что я узнал как вращение 2L, некоторые книги называют вращением rl, а вращение 2R называется вращения НВ. Текст ниже использует 2R / 2L.

это простые тестовые примеры для insert

"abc", на вставке" c " потребуется 1л вращения

a                   b
 \                 / \
  b   == 1L ==>   a   c
   \
    c

"cba", на вставке "a" потребуется 1R вращение

    c               b
   /               / \
  b   == 1R ==>   a   c
 /
a 

" acb " на вставке "b" потребует вращения 2L

a                  b
 \                / \
  c   == 2L ==>  a   c
 /
b

"кабина" на вставке "b" потребует вращения 2R

  c                b
 /                / \
a     == 2R ==>  a   c
 \
  b

для удалить

"bcad", при удалении "a" потребуется 1L вращение

  b                   c
 x \                 / \
a   c   == 1L ==>   b   d
     \
      d

"cbda", на удаление "d" потребуется 1R вращение

    c                  b
   / x                / \
  b   d  == 1R ==>   a   c
 /
a 

"bdac" при удалении "a" потребует вращения 2L

  b                  c
 x \                / \
a   d   == 2L ==>  b   d
   /
  c

"cadb" на удаление "d" потребует вращения 2R

  c                  b
 / x                / \
a   d   == 2R ==>  a   c
 \
  b

более сложные тестовые случаи имеют под-деревья, большинство из которых являются одним узлом. Чтобы сделать этот пост короче, вставить и удалить тестовые случаи объединяются. Пример delete становится примером insert, пропуская вставку символа delete. Например, с помощью простого случая удаления 2R выше " cadb "становится случай вставки" cab", пропуская вставку"d". Одно из следствий этого-случаи двойного поворота ниже требуют вставки дополнительного узла, чтобы сохранить дерево сбалансированным после вставки удаляемого узла. В результате пластины не минимальный.

"cbedfag" на исключить "a "или пропустить" a", а для вставки" g " потребуется поворот на 1 л при c

      c                 e
     / \               / \
    b   e  == 1R ==>  c   f
   x   / \           / \   \
  a   d   f         b   d   g
           \
            g

"ecfbdga" при исключении "g "или пропуске" g "и вставке" a " потребуется поворот на 1R при e

      - e -                 c
     /     \               / \
    c       f  == 1R ==>  b   e
   / \     x             /   / \
  b   d   g             a   d   f
 /
a

"ecjadhkgilbf" при исключении "b "или пропуске" b "и вставке" f " потребуется вращение на 2L при j, затем e. Случай вставки может выборочно пропустить вводить "d".

    - e -                       —- h —-
   /     \                     /       \
  c       j                   - e-      j
 / \     / \   == 2L ==>     /    \    / \
a   d   h   k               c      g  i   k
 x     / \   \             / \    /        \
  b   g   i   l           a   d  f          l
     /
    f

"hckbeiladfjg" при исключении "j" или пропуске "j" и вставка "g" потребует вращения 2R при c, а затем b. Случай вставки может выборочно пропустить вводить "l"

      - h -                    - e -
     /     \                  /     \
    c       k                c       - h -
   / \     / \  == 2R ==>   / \     /     \
  b   e   i   l            b   d   f       k
 /   / \   x              /         \     / \
a   d   f   j            a           g   i   l
         \
          g

используйте методы 1 и 2 из вопроса, чтобы проверить дерево, сбалансированное по мере необходимости (т. е. проверить, что дерево все еще в порядке и сбалансировано). Если вы хотите быть действительно уверены, преобразуйте визуальные тестовые случаи, чтобы включить список значений глубины и баланса конечного дерева для проверки во время обхода inorder.


Если вы действительно хотите забить свою реализацию, Вы должны сделать несколько тестов черного ящика с большим количеством различных шаблонов вставки и удаления. Вот некоторые идеи, которые приходят на ум:

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

вы должны не только проверить правильность, но и производительность с учетом вышеуказанных шаблонов, которые могут потребовать создания больших наборов данных, чтобы вы могли осмысленно измерить производительность. Все быстро с 100 элементы, но с 105 элементы, разница между O (N2) и O (N log N) будет огромным.

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


для insert и delete существует определенное число (около пяти для каждого, я помню) операций дерева, которые могут произойти.

необходимо настроить дерево непосредственно перед одной из этих операций таким образом, что добавление другого конкретного элемента приведет к известной одной из этих операций.

затем вы проверяете дерево-выгрузите его. Это будет довольно простое дерево, не более десяти элементов.

Если каждая операция вставки/удаления работает правильно, вы подтвердите жизненное поведение вашего дерева.

(обратите внимание, что одна из (Я думаю, что это была) операция вставки не может быть проверена таким образом - это промежуточное состояние, которое существует временно).