Алгоритмы поиска на основе деревьев
Оптимальные деревья
В двоичном дереве поиск одних элементов может происходить чаще, чем других, то есть существуют вероятности pk поиска k -го элемента и для различных элементов эти вероятности неодинаковы. Можно предположить, что поиск в дереве в среднем будет более быстрым, если те элементы, которые ищут чаще, будут находиться ближе к корню дерева.
Пусть даны 2n+1 вероятностей p1,p2,...,pn, q0,q1,...,qn, где pi – вероятность того, что аргументом поиска является Ki элемент; qi – вероятность того, что аргумент поиска лежит между вершинами Ki и Ki+1 ; q0 – вероятность того, что аргумент поиска меньше, чем значение элемента K1 ; qn – вероятность того, что аргумент поиска больше, чем Kn. Тогда цена дерева поиска C будет определяться следующим образом:
где – уровень узла j, а – уровень листа K.
Дерево поиска называется оптимальным, если его цена минимальна. То есть оптимальное бинарное дерево поиска – это бинарное дерево поиска, построенное в расчете на обеспечение максимальной производительности при заданном распределении вероятностей поиска требуемых данных.
Существует подход построения оптимальных деревьев поиска, при котором элементы вставляются в порядке уменьшения частот, что дает в среднем неплохие деревья поиска. Однако этот подход может дать вырожденное дерево поиска, которое будет далеко от оптимального. Еще один подход состоит в выборе корня k таким образом, чтобы максимальная сумма вероятностей для вершин левого поддерева или правого поддерева была настолько мала, насколько это возможно. Такой подход также может оказаться плохим в случае выбора в качестве корня элемента с малым значением pk.
Существуют алгоритмы, которые позволяют построить оптимальное дерево поиска. К ним относится, например, алгоритм Гарсия-Воча. Однако такие алгоритмы имеют временную сложность порядка O(n2). Таким образом, создание оптимальных деревьев поиска требует больших накладных затрат, что не всегда оправдывает выигрыш при быстром поиске.
Сбалансированные по высоте деревья
В худшем случае, когда дерево вырождено в линейный список, хранение данных в упорядоченном бинарном дереве никакого выигрыша в сложности операций по сравнению с массивом или линейным списком не дает. В лучшем случае, когда дерево сбалансировано, для всех операций получается логарифмическая сложность, что гораздо лучше. Идеально сбалансированным называется дерево, у которого для каждой вершины выполняется требование: число вершин в левом и правом поддеревьях различается не более чем на 1.
Однако идеальную сбалансированность довольно трудно поддерживать. В некоторых случаях при добавлении или удалении элементов может потребоваться значительная перестройка дерева, не гарантирующая логарифмической сложности. В 1962 году два советских математика: Г.М. Адельсон-Вельский и Е.М. Ландис – ввели менее строгое определение сбалансированности и доказали, что при таком определении можно написать программы добавления и/или удаления, имеющие логарифмическую сложность и сохраняющие дерево сбалансированным. Дерево считается сбалансированным по АВЛ (сокращения от фамилий Г.М. Адельсон-Вельский и Е.М. Ландис), если для каждой вершины выполняется требование: высота левого и правого поддеревьев различаются не более, чем на 1. Не всякое сбалансированное по АВЛ дерево идеально сбалансировано, но всякое идеально сбалансированное дерево сбалансировано по АВЛ.
При операциях добавления и удаления может произойти нарушение сбалансированности дерева. В этом случае потребуются некоторые преобразования, не нарушающие упорядоченности дерева и способствующие лучшей сбалансированности.
Рассмотрим такие преобразования. Пусть вершина a имеет правый потомок b. Обозначим через P левое поддерево вершины a, через Q и R – левое и правое поддеревья вершины b соответственно. Упорядоченность дерева требует, чтобы P<a<Q<b<R. Точно того же требует упорядоченность дерева с корнем b, его левым потомком a, в котором P и Q – левое и правое поддеревья вершины a, R – правое поддерево вершины b. Поэтому первое дерево можно преобразовать во второе, не нарушая упорядоченности. Такое преобразование называется малым правым вращением ( рис. 40.3). Аналогично определяется симметричное ему малое левое вращение.
Пусть b – правый потомок вершины a, c – левый потомок вершины b, P – левое поддерево вершины a, Q и R – соответственно левое и правое поддеревья вершины c, S – правое поддерево b. Тогда P<a<Q<c<R<b<S. Такой же порядок соответствует дереву с корнем c, имеющим левый потомок a и правый потомок b, для которых P и Q – поддеревья вершины a, а R и S – поддеревья вершины b. Соответствующее преобразование будем называть большим правым вращением ( рис. 40.4). Аналогично определяется симметричное ему большое левое вращение.
Схематично алгоритм добавления нового элемента в сбалансированное по АВЛ дерево будет состоять из следующих трех основных шагов.
Шаг 2. Вставка элемента в место, где закончился поиск, если элемент отсутствует.
Шаг 3. Восстановление сбалансированности.
Первый шаг необходим для того, чтобы убедиться в отсутствии элемента в дереве, а также найти такое место вставки, чтобы после вставки дерево осталось упорядоченным. Третий шаг представляет собой обратный проход по пути поиска: от места добавления к корню дерева. По мере продвижения по этому пути корректируются показатели сбалансированности проходимых вершин, и производится балансировка там, где это необходимо. Добавление элемента в дерево никогда не требует более одного поворота.
#include "stdafx.h" #include <iostream> #include <time.h> using namespace std; typedef int ElementType; typedef struct AvlNode *Position; typedef struct AvlNode *AvlTree; struct AvlNode { ElementType Element; AvlTree Left; AvlTree Right; int Height; }; AvlTree MakeEmpty( AvlTree T ); Position Find( ElementType X, AvlTree T ); Position FindMin( AvlTree T ); Position FindMax( AvlTree T ); AvlTree Insert( ElementType X, AvlTree T ); ElementType Retrieve( Position P ); void printTree(AvlTree T, int l = 0); int _tmain(int argc, _TCHAR* argv[]){ int i, *a, maxnum; AvlTree T; Position P; int j = 0; cout << "Введите количество элементов maxnum : "; cin >> maxnum; cout << endl; a = new int[maxnum]; srand(time(NULL)*1000); // генерация массива for (i = 0; i < maxnum; i++) a[i] = rand()%100; cout << "Вывод сгенерированной последовательности" << endl; for (i = 0; i < maxnum; i++) cout << a[i] << " "; cout << endl; cout << endl; // добавление элементов в АВЛ-дерево T = MakeEmpty( NULL ); for( i = 0; i < maxnum; i++ ) T = Insert( a[i], T ); cout << "Вывод АВЛ-дерева" << endl; printTree(T); cout << endl; cout << "Min = " << Retrieve( FindMin( T ) ) << ", Max = " << Retrieve( FindMax( T ) ) << endl; // удаление АВЛ-дерева T = MakeEmpty(T); delete [] a; system("pause"); return 0; } //функция удаления вершины и его поддеревьев AvlTree MakeEmpty( AvlTree T ) { if( T != NULL ){ MakeEmpty( T->Left ); MakeEmpty( T->Right ); free( T ); } return NULL; } // поиск вершины со значением X Position Find( ElementType X, AvlTree T ) { if( T == NULL ) return NULL; if( X < T->Element ) return Find( X, T->Left ); else if( X > T->Element ) return Find( X, T->Right ); else return T; } //функция поиска вершины с минимальным значением Position FindMin( AvlTree T ) { if( T == NULL ) return NULL; else if( T->Left == NULL ) return T; else return FindMin( T->Left ); } //функция поиска вершины с максимальным значением Position FindMax( AvlTree T ) { if( T != NULL ) while( T->Right != NULL ) T = T->Right; return T; } //функция возвращает вес вершины static int Height( Position P ) { if( P == NULL ) return -1; else return P->Height; } //функция возвращает максимальное из двух чисел static int Max( int Lhs, int Rhs ) { return Lhs > Rhs ? Lhs : Rhs; } /*функция выполняет поворот между вершинами K2 и его левым потомком*/ static Position SingleRotateWithLeft( Position K2 ) { Position K1; K1 = K2->Left; K2->Left = K1->Right; K1->Right = K2; K2->Height = Max(Height(K2->Left), Height(K2->Right)) + 1; K1->Height = Max( Height( K1->Left ), K2->Height ) + 1; return K1; //Новый корень } //функция выполняет поворот между вершинами K1 и его правым потомком static Position SingleRotateWithRight( Position K1 ) { Position K2; K2 = K1->Right; K1->Right = K2->Left; K2->Left = K1; K1->Height = Max(Height(K1->Left), Height(K1->Right)) + 1; K2->Height = Max( Height( K2->Right ), K1->Height ) + 1; return K2; //новый корень } //функция выполняет двойной левый-правый поворот static Position DoubleRotateWithLeft( Position K3 ) { // поворот между K1 и K2/ K3->Left = SingleRotateWithRight( K3->Left ); // поворот между K3 и K2 return SingleRotateWithLeft( K3 ); } //функция выполняет двойной правый-левый поворот static Position DoubleRotateWithRight( Position K1 ) { // поворот между K3 и K2 K1->Right = SingleRotateWithLeft( K1->Right ); // поворот между K1 и K2 return SingleRotateWithRight( K1 ); } //функция вставки вершины в АВЛ-дерево AvlTree Insert( ElementType X, AvlTree T ){ if( T == NULL ){ T = new AvlNode(); if( T == NULL ) fprintf( stderr, "Недостаточно памяти!!!\n" ); else { T->Element = X; T->Height = 0; T->Left = T->Right = NULL; } } else if( X < T->Element ) { T->Left = Insert( X, T->Left ); if( Height( T->Left ) - Height( T->Right ) == 2 ) if( X < T->Left->Element ) T = SingleRotateWithLeft( T ); else T = DoubleRotateWithLeft( T ); } else if( X > T->Element ) { T->Right = Insert( X, T->Right ); if( Height( T->Right ) - Height( T->Left ) == 2 ) if( X > T->Right->Element ) T = SingleRotateWithRight( T ); else T = DoubleRotateWithRight( T ); } T->Height = Max(Height(T->Left), Height(T->Right)) + 1; return T; } //функция возвращает значение, хранящееся в вершине ElementType Retrieve( Position P ) { return P->Element; } //функция вывода АВЛ-дерева на печать void printTree(AvlTree T, int l){ int i; if ( T != NULL ) { printTree(T->Right, l+1); for (i=0; i < l; i++) cout << " "; printf ("%4ld", Retrieve ( T )); printTree(T->Left, l+1); } else cout << endl; }Листинг .
Алгоритм удаления элемента из сбалансированного дерева будет выглядеть так:
Шаг 2. Удаление элемента из дерева.
Шаг 3. Восстановление сбалансированности дерева (обратный проход).
Первый шаг необходим, чтобы найти в дереве вершину, которая должна быть удалена. Третий шаг представляет собой обратный проход от места, из которого взят элемент для замены удаляемого, или от места, из которого удален элемент, если в замене не было необходимости. Операция удаления может потребовать перебалансировки всех вершин вдоль обратного пути к корню дерева, т.е. порядка log n вершин. Таким образом, алгоритмы поиска, добавления и удаления элементов в сбалансированном по АВЛ дереве имеют сложность, пропорциональную O(log n).
Деревья цифрового (поразрядного) поиска
Методы цифрового поиска достаточно громоздки и плохо иллюстрируются. Рассмотрим бинарное дерево цифрового поиска. Как и в деревьях, рассмотренных выше, в каждой вершине такого дерева хранится полный ключ, но переход по левой или правой ветви происходит не путем сравнения ключа-эталона со значением ключа, хранящегося в вершине, а на основе значения очередного бита аргумента. Реализация цифрового поиска происходит поразрядно (побитово).
Поиск начинается от корня дерева. Если содержащийся в корневой вершине ключ не совпадает с аргументом поиска, то анализируется самый левый бит аргумента. Если он равен 0, происходит переход по левой ветви, если 1 – по правой. Если не обнаруживается совпадение ключа с аргументом поиска, то анализируется следующий бит аргумента и т.д. Поиск завершается, когда будут проверены все биты аргумента либо встретится вершина с отсутствующей левой или правой ссылкой.
Ключевые термины
Бинарное дерево цифрового поиска – это дерево, в каждой вершине которого хранится полный ключ, а переход по ветвям происходит на основе значения очередного бита аргумента.
Двоичное (бинарное) дерево – это иерархическая структура, в которой каждый узел имеет не более двух потомков.
Идеально сбалансированное дерево – это дерево, у которого для каждой вершины выполняется требование: число вершин в левом и правом поддеревьях различается не более чем на 1.
Ключ поиска – это поле, по значению которого происходит поиск.
Оптимальное бинарное дерево поиска – это бинарное дерево поиска, построенное в расчете на обеспечение максимальной производительности при заданном распределении вероятностей поиска требуемых данных.
Поиск – это процесс нахождения конкретной информации в ранее созданном множестве данных.
Сбалансированное по АВЛ дерево – это дерево, для каждой вершины которого выполняется требование: высота левого и правого поддеревьев различаются не более, чем на 1.
Случайные деревья поиска – это упорядоченные бинарные деревья поиска, при создании которых элементы вставляются в случайном порядке.
Упорядоченное двоичное дерево – это двоичное дерево, в котором для любой его вершины x справедливы свойства: все элементы в левом поддереве меньше элемента, хранимого в x ; все элементы в правом поддереве больше элемента, хранимого в x ; все элементы дерева различны.
Частично упорядоченное бинарное дерево – это упорядоченное бинарное дерево, в котором встречаются одинаковые элементы.
- Поиск данных предполагает использование соответствующих алгоритмов в зависимости от ряда факторов: способ представления данных, упорядоченность множества поиска, объем данных, расположение их во внешней или во внутренней памяти.
- Двоичные деревья представляют собой иерархическую структуру, в которой каждый узел имеет не более двух потомков. Поиск на двоичных деревьях не дает выигрыша по времени по сравнению с линейными структурами.
- Упорядоченное двоичное дерево – это двоичное дерево, в котором для любой его вершины x справедливы свойства: все элементы в левом поддереве меньше элемента, хранимого в x ; все элементы в правом поддереве больше элемента, хранимого в x ; все элементы дерева различны. Поиск в худшем случае на таких деревьях имеет сложность O(n).
- Случайные деревья поиска представляют собой упорядоченные бинарные деревья поиска, при создании которых элементы (их ключи) вставляются в случайном порядке. Высота дерева зависит от случайного поступления элементов, поэтому трудоемкость определяется построением дерева.
- Оптимальное бинарное дерево поиска – это бинарное дерево поиска, построенное в расчете на обеспечение максимальной производительности при заданном распределении вероятностей поиска требуемых данных. Поиск на таких деревьях имеет сложность порядка O(n2).
- Дерево считается сбалансированным по АВЛ, если для каждой вершины выполняется требование: высота левого и правого поддеревьев различаются не более, чем на 1. Алгоритмы поиска, добавления и удаления элементов в таком дереве имеют сложность, пропорциональную O(log n).
- В деревьях цифрового поиска осуществляется поразрядное сравнение ключей.