在这章,我们主要学习二叉查找树这个概念。还包括set和map类的实现和使用。
4.3 查找树ADT——二叉查找树
二叉查找树的概念:使二叉树成为二叉查找树的性质,对于树中的每个节点X,它的左子树中所有项的值小于X中的项,而它的右子树中所有的项大于X中的项。
二叉树查找树类的框架如下所示:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
template<
typename Comparable>
class BinarySearchTree { public: BinarySearchTree(); //构造函数 BinarySearchTree( const BinarySearchTree &rhs); //复制构造函数 ~BinarySearchTree(); //析构函数 const Comparable &findMin() const; // 查找最小的数据 const Comparable &findMax() const; //查找最大的数据 bool contain( const Comparable &x) const; // 包含 bool isEmpty() const; // 空函数 void printTree() const; //打印树 void makeEmpty(); // 空 void insert( const Comparable &x); //插入 void remove( const Comparable &x); //删除 const BinarySearchTree & operator=( const BinarySearchTree &rhs); // operator=函数 private: //私有 struct BinaryNode //内部类 { Comparable element; BinaryNode *left; BinaryNode *right; BinaryNode( const Comparable &theElment, BinaryNode *lt, BinaryNode *rt): element(theElement), left(lt), right(rt) {} }; BinaryNode *root; void insert( const Comparable &x, BinaryNode*&t) const; void remove( const Comparable &x, BinaryNode*&t) const; BinaryNode *findMin(BinaryNode *t) const; BinaryNode *findMax(BinaryNode *t) const; bool contains( const Comparable &x, BinaryNode *t) const; void makeEmpty(BinaryNode*&t); void printTree(BinaryNode *t) const; BinaryNode *clone(BinaryNode *t) const; }; |
二叉树的contains操作,查找时间复杂度为0(logN)。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<
typename Comparable>
bool BinarySearchTree::contains( const Comparable &x, BinaryNode *t) const //使用递归实现查找包含 { if(t == NULL) return false; else if(x > t->element) //在右边查找 { return contains(x, t->right); } else if(x < t->element) { return contains(x, t->left); //在左边查找 } else { return true; } } |
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
template < typename Object, typename Comparator = less<Object> > class BinarySearchTree { public: //same method,with Object replacing Comparable private: BinaryNode *root; Comparator isLessThan; bool contains( const Comparable &x, BinaryNode *t) const //使用递归实现查找包含 { if(t == NULL) return false; else if(isLessThan(x, t->element)) //在右边查找 { return contains(x, t->left); } else if(isLessThan(t->element, x)) { return contains(x, t->right); //在左边查找 } else { return true; } } }; |
findMin和findMax分别返回指向树中包含最小元素和最大元素的节点指针。还是采用递归的方式实现
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
BinaryNode *findMin(BinaryNode *t)
const
//使用递归实现查找最小函数。 { if(t == NULL) return NULL; if(t->left = NULL) return t; return findMin(t->left); } BinaryNode *findMax(BinaryNode *t) const //使用非递归的方式查找最大函数 { if(t == NULL) return NULL; while(t->right != NULL) { t = t->right; } return t; } |
插入函数,同样采用递归来实现。
1
2 3 4 5 6 7 8 9 10 11 |
void insert(
const Comparable &x, BinaryNode *&t)
//采用递归插入新的数据 { if(t == NULL) t = new BinaryNode(x,NULL,NULL) if(x < t->element) insert(x, t->left); else if(x > t->element) insert(x, t->right) else ; //重复 } |
二叉查找树的一个难点是删除元素。一旦发现要被删除节点,就需要考虑几种可能的情况。
1、如果节点是一个叶子节点,那么它可以被立即删除。
2、如果节点有一个儿子,则该节点可以在其父节点调整它的链以绕过该节点后被删除。如下图所示:
3、复杂的情况是处理具有两个儿子的节点。一般的删除策略是用其右子树的最小数据代替该节点的数据并递归地删除那个节点。因为右子树中的最小节点不可能有左子树,所以第二次remove就很容易了。下图显示了一颗初始的数及其中一个节点被删除后的结果。要被删除的节点是根的左儿子,其键值为2.它被右子树中的最小键值3所代替,然后原节点如前例那样被删除。(惰性删除)
代码实现如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void remove(
const Comparable &x, BinaryNode *t)
{ if(t == NULL) return ; if(x < t->element) //查找 { remove(x, t->left); } else if(x > t->element) { remove(x, t->right); } else if(t->left != NULL && t->right != NULL) //双节点 { t->element = findMin(t->right)->element; remove(t->element, t->right); } else //单节点 { BinaryNode *old = t; t = (t->left != NULL) ? t->left : t->right; delete old; } } |
析构函数和复制赋值操作符
与往常一样,析构函数调用makeEmpty。公有的makeEmpty则简单地调用私有递归版本的makeEmpty函数。在递归处理t的子树之后,对t调用delete。这样一来,所有的节点就都递归地回收了。注意,最后,t就改为指向NULL,复制赋值操作符,则首先调用makeEmpty来回收内存,然后进行rhs复制,我们使用clone很老套的递归函数来处理这些啰嗦工作。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
~BinarySearch()
//析构 { makeEmpty(); } void makeEmpty(BinaryNode*&t) //递归空 { if(t != NULL) { makeEmpty(t->left); makeEmpty(t->right); delete t; } t-> NULL; } const BinarySearchTree & operator=( const BinarySearchTree &rhs) //复制赋值操作符 { if( this != &rhs) { makeEmpty(); root = clone(rhs.root); } return * this; } BinaryNode *clone(BinaryNode *t) const //递归复制 { if(t == NULL) return NULL; return new BinaryNode(t->element, clone(t->left), clone(t->right)); } |
4.4 AVL树
AVL树是带有平衡条件的二叉查找树。一颗AVL树是每个节点的左子树和右子树的高度最多差1的二叉查找树。在插入节点后,如何保证二叉树的平衡是一个本节的一个难点。
我们把必须重新平衡的节点叫做a,由于任意节点最多有两个儿子,因此高度不平衡时,a点的两棵子树的高度差2.容易看出,这种平衡可能出现在下面4个情况:
(1)对a的左儿子的左子树进行一次插入。
(2)对a的左儿子的右子树进行一次插入。
(3)对a的右儿子的左子树进行一次插入。
(4)对a的右儿子的右子树进行一次插入。
第一种情况是插入发生在“外边”的情形(即左-左,右-右的情况),该情况通过对树的一次单旋转而完成调整。第二种是插入发生在“内部”的情形(即左-右,右-左的情况),该情况采用较为复杂的双旋转来实现。
1、首先我们来讨论一下单旋转的情况。
先看图吧。下图是RR的情况。
改动其实很简单,先把Y解绑当k2的左孩子,接着k2成为k1的右孩子,代码如下:
1
2 3 4 5 6 7 8 9 10 |
/* return pointer to the new root */
static AVLNodePtr singlerotateLL(AVLNodePtr k2) { AVLNodePtr 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; } |
接着是doublerotateRL()的实现,看下图:
很明显B或者C的插入使得k3(A)不平衡了,那么首先应该是k1和k2逆时针旋转,所以调用
k3->left = singlerotateRR(k3->left);
接着是k3和new child 顺时针旋转,调用singlerotateLL(k3);
so easy, 代码如下:
1
2 3 4 5 6 |
static AVLNodePtr doublerotateRL(AVLNodePtr k3) { k3->left = singlerotateRR(k3->left); return singlerotateLL(k3); } |
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
#include <stdio.h>
#include <stdlib.h> struct AVLNode; typedef struct AVLNode *AVLNodePtr; struct AVLNode { int element; AVLNodePtr left; AVLNodePtr right; int height; }; void makeempty(AVLNodePtr T) { if (T == NULL) return; else { makeempty(T->left); makeempty(T->right); free(T); } } static int height(AVLNodePtr p) { if (p == NULL) return - 1; else return p->height; } static int Max( int ln, int rn) { return ln > rn ? ln : rn; } /* return pointer to the new root */ static AVLNodePtr singlerotateLL(AVLNodePtr k2) { AVLNodePtr 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; } static AVLNodePtr singlerotateRR(AVLNodePtr k1) { AVLNodePtr k2 = k1->right; k1->right = k2->left; k2->left = k1; k1->height = Max(height(k1->left), height(k1->right)) + 1; k2->height = Max(k1->height, height(k2->right)) + 1; return k2; } static AVLNodePtr doublerotateRL(AVLNodePtr k3) { k3->left = singlerotateRR(k3->left); return singlerotateLL(k3); } static AVLNodePtr doublerotateLR(AVLNodePtr k3) { k3->right = singlerotateLL(k3->right); return singlerotateRR(k3); } AVLNodePtr insert( int X, AVLNodePtr T) { if (T == NULL) { /* create and return a one-node tree */ T = (AVLNodePtr)malloc( sizeof( struct AVLNode)); if (T == NULL) { printf( "out of space!"); exit( 1); } 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 = singlerotateLL(T); else T = doublerotateRL(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 = singlerotateRR(T); else T = doublerotateLR(T); } } /* else X is in the tree already; we'll do nothing */ T->height = Max(height(T->left), height(T->right)) + 1; return T; } void inorder(AVLNodePtr T) { if (T == NULL) return; else { inorder(T->left); printf( "%d ", T->element); inorder(T->right); } } int main( void) { int arr[] = { 3, 2, 1, 4, 5, 6, 7, 16, 15, 14, 13, 12, 11, 10, 8, 9}; AVLNodePtr T = NULL; for ( int i = 0; i < sizeof(arr) / sizeof(arr[ 0]); i++) T = insert(arr[i], T); inorder(T); makeempty(T); return 0; } |
代码将数组元素插入后,中序遍历后输出,即1~16的顺序排列。
注意:输入数组元素就不要搞成有序的了,如果代码中没有调整的实现,整个树就是个右斜树,但即使实现了调整,也会使得每插入一次就调整一次,何必内耗啊。
很显然,平衡二叉树的优势在于不会出现普通二叉查找树的最差情况。其查找的时间复杂度为O(logN)。