Как проверить правильность реализации моего дерева AVL? - PullRequest
9 голосов
/ 18 октября 2010

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

  1. базовая проверка работоспособности - проверяет, что для каждого узла высота равна макс. высота дочерних узлов + 1, баланс в [-1, 1], левый дочерний ключ <ключ этого узла <право ключ ребенка, а нет циклические ссылки (например, левый ребенок узла - это сам узел); </p>

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

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

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

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

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

Ответы [ 5 ]

27 голосов
/ 12 декабря 2012

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

Вставка - левый / правый ребаланс

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

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

Вставка 8 или 15 (например) в любое из этих деревьев вызовет по существу одинаковую левую / правую перебалансировку, но конечные результаты значительно отличаются для каждого дерева и значения вставки. То есть, конечное место посадки вставленного значения и конечные коэффициенты баланса узла (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
5 голосов
/ 05 июня 2014

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

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

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

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

Это простые тестовые примеры для вставки

«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

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

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

Для удаления

«bcad», при удалении «а» потребуется 1 л вращения

  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

Более сложные тестовые случаи имеют поддеревья, большинство из которых представляют собой один узел. Чтобы сделать этот пост короче, вставьте и удалите контрольные примеры. Пример удаления становится примером вставки, пропуская вставку символа удаления. Например, при использовании простого случая удаления 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 из вопроса, чтобы проверить, что дерево перебалансировано по мере необходимости (т. Е. Убедиться, что дерево все еще в порядке и сбалансировано). Если вы хотите быть действительно уверенным, преобразуйте визуальные тестовые примеры, чтобы включить список значений глубины и баланса конечного дерева для проверки во время обхода по порядку.

2 голосов
/ 18 октября 2010

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

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

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

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

2 голосов
/ 18 октября 2010

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

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

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

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

1 голос
/ 18 октября 2010

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

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

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

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

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

...