@TOC
二叉树——应用
二叉排序树(BST)
二叉排序树的定义
或者是一棵空树,或者有如下性质的树:
(1)若左子树非空,则左子树上所有节点关键字值均小于根节点的关键字值;
(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
26BSTNode* BST_Search(BiTree T, ElemType key, BSTNode *p) {
// 查找函数返回指向关键字值为key的节点指针,如果不存在,
// 返回NULL
// p指向被查找节点的双亲节点
p = NULL;
while(T && key != T->data) {
p = T;
if (key < T -> data)
T = T -> lchild;
else
T = T -> rchild;
}
return T;
}
BSTNode* BST_RecurSearch(BiTree T, ElemType key, BSTNode *p) {
// 算法的递归实现形式
if (T) {
if (key == T -> data)
return T;
else if (key < T -> data)
BST_RecurSearch(T -> lchild, key, T)
else
BST_RecurSearch(T -> rchild, key, T)
}
}二叉排序树的插入
在查找的过程中,如果没有查找到关键字值的二叉树时,将节点插入到二叉树中,如果二叉树原本为空则要插入的节点为根节点,如果要插入的值比根节点的值大则插入到右子树中,如果比根节点的值小,则插入到左子树中。算法如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int BST_Insert(BiTree &T, ElemType key) {
// 如果二叉树中存在值为key的节点,那么返回0;
// 如果二叉树中不存在值为key的节点,那么插入节点并返回1;
if (T == NULL) {
T = (BSTNode*)malloc(sizeof(BSTNode));
T -> data = key;
T -> lchild = T -> rchild = NULL;
return 1;
} else if (T -> data == key) {
return 0;
} else if (key < T -> data) {
BST_Insert(T -> lchild, key);
} else {
BST_Insert(T -> rchild, key);
}
}二叉排序树的构造
依次输入数据元素,并且将它们插入到排序树中适当的位置。具体过程是每读入一个元素就建立一个新节点,若二叉树为空则这个节点为根节点,如果二叉树不为空的话,则将这个节点插入二叉树中。
算法如下所示:
1
2
3
4
5
6
7
8void BST_Creat(BiTree &T, ElemType str[], int n) {
T == NULL;
int i = 0;
while(i < n) {
BST_Insert(T, str[i]);
i++;
}
}二叉排序树的删除
删除二叉树节点的过程按照三种情况来处理:
(1)如果被删除节点是叶子结点,则直接删除,不会破坏二叉排序树的性质;
(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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45void BST_Delete(BiTree T, ElemType key) {
BiTree pre = NULL;
BiTree p = BST_Search(T, key, pre); // 查找节点
if (p == NULL)
return;
if (p -> lchild == NULL && p -> rchild == NULL) {
// 如果是叶子节点则直接删除;
if (pre -> lchild == p)
pre -> lchild = NULL;
else if (pre -> rchild == p)
pre -> rchild = NULL;
free(p)
} else if (p -> lchild == NULL && p -> rchild != NULL) {
// 左子树为空
if (pre -> lchild == p)
pre -> lchild = p -> rchild;
else if (pre -> rchild == p)
pre -> lchild == p -> rchild;
free(p)
} else if (p -> lchild != NULL && p -> rchild == NULL) {
// 右子树为空
if (pre -> lchild == p)
pre -> lchild == p -> lchild;
else if (pre -> rchild == p)
pre -> rchild = p -> lchild;
free(p);
} else {
// 左右子树都不为空
// 寻找右子树在中序遍历的情况下第一个输出的节点;
BiTree temp = p -> rchild;
Bitree pre2 = p;
while (temp -> lchild != NULL) {
temp = temp -> lchild;
pre2 = p;
}
p -> data = temp -> data;
// 删除temp节点;
if (pre2 -> lchild == temp)
pre2 -> lchild == temp -> rchild;
if (pre2 -> rchild == temp)
pre2 -> rchild == temp -> rchild;
free(temp);
}
}
二叉平衡树(AVL)
平衡二叉树的定义
在插入和删除节点的时候,保证任意节点的左右子树的高度差不超过1,这样的二叉树称为平衡二叉树,简称平衡树。定义左子树和右子树的高度差为平衡因子,则平衡因子的值只可能为-1, 0, 1三种。
所以平衡二叉树可定义为它或者是一颗空树,或者它的左右子树都是平衡二叉树,并且左右子树的高度差不超过1.
平衡二叉树的插入
按照二叉排序树插入节点的规则向二叉树中插入一个节点,插入之后,如果二叉树不再是平衡二叉树那么调整二叉树,使得这个二叉树成为新的平衡二叉树。
失去平衡之后调整可归纳为一下四种情况:
(1)LL平衡旋转:在A节点的左孩子(L)的左子树(L)上插入了新节点,导致A的平衡因子从1变成了2,导致以A为根的子树失去平衡,需要一次向右旋转的操作。使得A的左孩子接替A,而A的左孩子的右子节点称为旋转后的A的左孩子节点。
(2)RR平衡旋转:由于在A节点的右孩子(R)的右子树(R)上插入了新的节点,导致A的平衡因子从-1变成了-2,失去平衡。需要一次向左旋转的操作。
(3)LR平衡旋转:在A节点的左孩子(L)的右子树(R)上插入了新节点,导致A的平衡因子从1变成了2,导致以A为根的子树失去平衡,需要两次旋转,先向左旋转,再向右旋转。
(4)RL平衡旋转:在A节点的右孩子(R)的左子树(L)上插入了新节点,导致A的平衡因子从-1变成了-2,导致以A为根的子树失去平衡,需要两次旋转,先右旋转,再左旋转。
平衡二叉树的查找
查找过程与二叉排序树相同。
关于平衡二叉树的具体实现C++程序,可以参考平衡二叉树详解(本地测试过了,程序可以运行)
哈夫曼树与哈弗曼编码
哈夫曼树的定义
在含有N个带权叶子结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也成为最优二叉树。
哈夫曼树的构造
给定N个权值分别为w1, w2, …, Wn的节点。构造哈夫曼树的算法描述如下:
(1)将这N个结点分别作为N棵树仅含一个结点的二叉树,构成森林F.
(2)构造一个新节点,并从F中选取两棵根结点权值最小的树作为新节点的左、右子树,并且将新节点的权值置为左、右子树上根结点的权
值之和。
(3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
(4)重复步骤2和3,直至F中只剩下一棵树为止。哈弗曼编码
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码,它是可变长度编码。可变长编码即可以对待处理字符串中不同字符使用不等长的二进制位表示,可变长编码比固定长度编码好很多,可以对频率高的字符赋予短编码,而对频率较低的字符则赋予较长一些的编码,从而可以使平均编码长度减短,起到压缩数据的效果。
哈夫曼编码是前缀编码。如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。对前缀编码的解码也是相对简单的,因为没有一个码是另一个编码的前缀,所以可以识别出第一个编码,将它翻译为原码,再对余下的编码文件重复同样操作。哈夫曼编码首先要构造一棵哈夫曼树,首先,将每个出现的字符当做一个独立的结点,其权值作为它出现的频度(或次数),然后构造哈夫曼树。显然所有字符节点均出现在叶子结点中。我们可以将字符的编码解释为从根至该字符路径上标记的序列,其中标记为0表示”转向左孩子”,标记为1表示为”转向右孩子”。
部分题目算法编写
判断一棵二叉树是否是二叉排序树。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 方案1:直接在二叉树中比较
bool isBST_Tree(BiTree T) {
if (T) {
// 左子值大于根节点值
if (T -> lchild && T -> lchild -> data > T -> data)
return false;
// 右子值小于根节点值
if (T -> rchild && T -> rchild -> data < T -> data)
return false;
// 不存在左右节点
if (T -> lchild == NULL && T -> rchild == NULL)
return true;
else
return isBST_Tree(T -> lchild) && isBST_Tree(T -> rchild);
}
}
// 方案2:中序遍历二叉树,得到的结果始终满足前一个元素的数据小于后一个元素的数据。利用二叉树的遍历思想,判断一棵二叉树是否为平衡二叉树。
1 | void Judge_AVL(BiTree T, int &balance, int &h) { |