逻辑结构
# 2. 逻辑结构
# 树
树是一种数据结构,它是n(n>=0)个节点的有限集。n=0时称为空树。n>0时,有限集的元素构成一个具有层次感的数据结构。
区别于线性表一对一的元素关系,树中的节点是一对多的关系。树具有以下特点:
- n>0时,根节点是唯一的,不可能存在多个根节点。
- 每个节点有零个至多个子节点;除了根节点外,每个节点有且仅有一个父节点。根节点没有父节点。
# 树的相关概念
树有许多相关的术语与概念,在学习树的结构之前,我们要熟悉这些概念。
子树
: 除了根节点外,每个子节点都可以分为多个不相交的子树。孩子与双亲
: 若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。在图一中,B、H是A的孩子,A是B、H的双亲。兄弟
: 具有相同双亲的节点互为兄弟,例如B与H互为兄弟。节点的度
: 一个节点拥有子树的数目。例如A的度为2,B的度为1,C的度为3.叶子
: 没有子树,也即是度为0的节点。分支节点
: 除了叶子节点之外的节点,也即是度不为0的节点。内部节点
: 除了根节点之外的分支节点。层次
: 根节点为第一层,其余节点的层次等于其双亲节点的层次加1.树的高度
: 也称为树的深度,树中节点的最大层次。有序树
: 树中节点各子树之间的次序是重要的,不可以随意交换位置。无序树
: 树种节点各子树之间的次序是不重要的。可以随意交换位置。森林
: 0或多棵互不相交的树的集合。
树的遍历
- 先(根)序遍历(根左右)
- 中(根)序遍历(左根右)
- 后(根)序遍历(左右根)
- 层次遍历:逐层按一定顺序访问节点
# 二叉树、完全二叉树、满二叉树
二叉树: 最多有两棵子树的树被称为二叉树
斜树: 所有节点都只有左子树的二叉树叫做左斜树,所有节点都只有右子树的二叉树叫做右斜树。(本质就是链表)
满二叉树: 二叉树中所有非叶子结点的度都是2,且叶子结点都在同一层次上 可以看出满二叉树的各个层的结点数形成一个首项为1,公比为2的等比数列,所以由等比公式 $$ 等比数列通项公式、求和公式 \\\begin{array}{l}\a_{n}=a_{1} \cdot q^{(n-1)} \\S_{n}=\frac{a_{1}\left(1-q^{n}\right)}{1-q}(q \neq 1)\\end{array} $$
得出一个二叉树的层数为K,则节点总数是
,第k层的节点数为 ,非叶子节点数为完全二叉树: 如果一个二叉树与满二叉树前m个节点的结构相同,这样的二叉树被称为完全二叉树
# 二叉查找树(BST)
# BST的定义
- 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树。
- 没有键值相等的节点。
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低为 O ( log n ) 。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
# BST的实现
- 节点
BSTree是二叉树,它保护了二叉树的根节点mRoot;mRoot是BSTNode类型,而BSTNode是二叉查找树的节点,它是BSTree的内部类。BSTNode包含二叉查找树的几个基本信息:
key -- 它是关键字,是用来对二叉查找树的节点进行排序的。
left -- 它指向当前节点的左孩子。
right -- 它指向当前节点的右孩子。
parent -- 它指向当前节点的父结点。
public class BSTree<T extends Comparable<T>> {
private BSTNode<T> mRoot; // 根结点
public class BSTNode<T extends Comparable<T>> {
T key; // 关键字(键值)
BSTNode<T> left; // 左孩子
BSTNode<T> right; // 右孩子
BSTNode<T> parent; // 父结点
public BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right) {
this.key = key;
this.parent = parent;
this.left = left;
this.right = right;
}
}
......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
遍历
前序遍历
private void preOrder(BSTNode<T> tree) {
if(tree != null) {
System.out.print(tree.key+" ");
preOrder(tree.left);
preOrder(tree.right);
}
}
//根节点开始
public void preOrder() {
preOrder(mRoot);
}
2
3
4
5
6
7
8
9
10
11
12
- 中序遍历
private void inOrder(BSTNode<T> tree) {
if(tree != null) {
inOrder(tree.left);
System.out.print(tree.key+" ");
inOrder(tree.right);
}
}
public void inOrder() {
inOrder(mRoot);
}
2
3
4
5
6
7
8
9
10
11
12
- 后序遍历
private void postOrder(BSTNode<T> tree) {
if(tree != null)
{
postOrder(tree.left);
postOrder(tree.right);
System.out.print(tree.key+" ");
}
}
public void postOrder() {
postOrder(mRoot);
}
2
3
4
5
6
7
8
9
10
11
12
13
- 例如:
对于上面的二叉树而言,
前序遍历结果: 8 3 1 6 4 7 10 14 13
中序遍历结果: 1 3 4 6 7 8 10 13 14
后序遍历结果: 1 4 7 6 3 13 14 10 8
查找
递归版本的代码
/*
* (递归实现)查找"二叉树x"中键值为key的节点
*/
private BSTNode<T> search(BSTNode<T> x, T key) {
if (x==null)
return x;
int cmp = key.compareTo(x.key);
if (cmp < 0)
return search(x.left, key);
else if (cmp > 0)
return search(x.right, key);
else
return x;
}
public BSTNode<T> search(T key) {
return search(mRoot, key);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 迭代版本的代码
/*
* (迭代)查找"二叉树x"中键值为key的节点
*/
private BSTNode<T> iterativeSearch(BSTNode<T> x, T key) {
while (x!=null) {
int cmp = key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else
return x;
}
return x;
}
public BSTNode<T> iterativeSearch(T key) {
return iterativeSearch(mRoot, key);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 最大值和最小值
- 查找最大结点
/*
* 查找最大结点: 返回tree为根结点的二叉树的最大结点。
*/
private BSTNode<T> maximum(BSTNode<T> tree) {
if (tree == null)
return null;
while(tree.right != null)
tree = tree.right;
return tree;
}
public T maximum() {
BSTNode<T> p = maximum(mRoot);
if (p != null)
return p.key;
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 查找最小结点
/*
* 查找最小结点: 返回tree为根结点的二叉树的最小结点。
*/
private BSTNode<T> minimum(BSTNode<T> tree) {
if (tree == null)
return null;
while(tree.left != null)
tree = tree.left;
return tree;
}
public T minimum() {
BSTNode<T> p = minimum(mRoot);
if (p != null)
return p.key;
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 前驱和后继
节点的前驱: 是该节点的左子树中的最大节点。 节点的后继: 是该节点的右子树中的最小节点。
- 查找前驱节点
public BSTNode<T> predecessor(BSTNode<T> x) {
// 如果x存在左孩子,则"x的前驱结点"为 "以其左孩子为根的子树的最大结点"。
if (x.left != null)
return maximum(x.left);
// 如果x没有左孩子。则x有以下两种可能:
// (01) x是"一个右孩子",则"x的前驱结点"为 "它的父结点"。
// (02) x是"一个左孩子",则查找"x的最低的父结点,并且该父结点要具有右孩子",找到的这个"最低的父结点"就是"x的前驱结点"。
BSTNode<T> y = x.parent;
while ((y!=null) && (x==y.left)) {
x = y;
y = y.parent;
}
return y;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 查找后继节点
public BSTNode<T> successor(BSTNode<T> x) {
// 如果x存在右孩子,则"x的后继结点"为 "以其右孩子为根的子树的最小结点"。
if (x.right != null)
return minimum(x.right);
// 如果x没有右孩子。则x有以下两种可能:
// (01) x是"一个左孩子",则"x的后继结点"为 "它的父结点"。
// (02) x是"一个右孩子",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。
BSTNode<T> y = x.parent;
while ((y!=null) && (x==y.right)) {
x = y;
y = y.parent;
}
return y;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 插入
/*
* 将结点插入到二叉树中
*
* 参数说明:
* tree 二叉树的
* z 插入的结点
*/
private void insert(BSTree<T> bst, BSTNode<T> z) {
int cmp;
BSTNode<T> y = null;
BSTNode<T> x = bst.mRoot;
// 查找z的插入位置
while (x != null) {
y = x;
cmp = z.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else
x = x.right;
}
z.parent = y;
if (y==null)
bst.mRoot = z;
else {
cmp = z.key.compareTo(y.key);
if (cmp < 0)
y.left = z;
else
y.right = z;
}
}
/*
* 新建结点(key),并将其插入到二叉树中
*
* 参数说明:
* tree 二叉树的根结点
* key 插入结点的键值
*/
public void insert(T key) {
BSTNode<T> z=new BSTNode<T>(key,null,null,null);
// 如果新建结点失败,则返回。
if (z != null)
insert(this, z);
}
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
- 删除
/*
* 删除结点(z),并返回被删除的结点
*
* 参数说明:
* bst 二叉树
* z 删除的结点
*/
private BSTNode<T> remove(BSTree<T> bst, BSTNode<T> z) {
BSTNode<T> x=null;
BSTNode<T> y=null;
if ((z.left == null) || (z.right == null) )
y = z;
else
y = successor(z);
if (y.left != null)
x = y.left;
else
x = y.right;
if (x != null)
x.parent = y.parent;
if (y.parent == null)
bst.mRoot = x;
else if (y == y.parent.left)
y.parent.left = x;
else
y.parent.right = x;
if (y != z)
z.key = y.key;
return y;
}
public void remove(T key) {
BSTNode<T> z, node;
if ((z = search(mRoot, key)) != null)
if ( (node = remove(this, z)) != null)
node = null;
}
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
- 打印
/*
* 打印"二叉查找树"
*
* key -- 节点的键值
* direction -- 0,表示该节点是根节点;
* -1,表示该节点是它的父结点的左孩子;
* 1,表示该节点是它的父结点的右孩子。
*/
private void print(BSTNode<T> tree, T key, int direction) {
if(tree != null) {
if(direction==0) // tree是根节点
System.out.printf("%2d is root\n", tree.key);
else // tree是分支节点
System.out.printf("%2d is %2d's %6s child\n", tree.key, key, direction==1?"right" : "left");
print(tree.left, tree.key, -1);
print(tree.right,tree.key, 1);
}
}
public void print() {
if (mRoot != null)
print(mRoot, mRoot.key, 0);
}
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
- 销毁
/*
* 销毁二叉树
*/
private void destroy(BSTNode<T> tree) {
if (tree==null)
return ;
if (tree.left != null)
destroy(tree.left);
if (tree.right != null)
destroy(tree.right);
tree=null;
}
public void clear() {
destroy(mRoot);
mRoot = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 平衡二叉树(AVL)
# 什么是AVL树
AVL树是高度平衡的二叉树。它的特点是: AVL树中任何节点的两个子树的高度最大差别为1。
上面的两张图片,左边的是AVL树,它的任何节点的两个子树的高度差别都<=1;而右边的不是AVL树,因为7的两颗子树的高度相差为2(以2为根节点的树的高度是3,而以8为根节点的树的高度是1)。
# AVL树的实现
节点
- 节点定义 AVLTree是AVL树对应的类,而AVLTreeNode是AVL树节点,它是AVLTree的内部类。AVLTree包含了AVL树的根节点,AVL树的基本操作也定义在AVL树中。AVLTreeNode包括的几个组成对象:
- key -- 是关键字,是用来对AVL树的节点进行排序的。
- left -- 是左孩子。
- right -- 是右孩子。
- height -- 是高度。
public class AVLTree<T extends Comparable<T>> { private AVLTreeNode<T> mRoot; // 根结点 // AVL树的节点(内部类) class AVLTreeNode<T extends Comparable<T>> { T key; // 关键字(键值) int height; // 高度 AVLTreeNode<T> left; // 左孩子 AVLTreeNode<T> right; // 右孩子 public AVLTreeNode(T key, AVLTreeNode<T> left, AVLTreeNode<T> right) { this.key = key; this.left = left; this.right = right; this.height = 0; } } ...... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20树的高度 关于高度,有的地方将"空二叉树的高度是-1",而本文采用维基百科上的定义: 树的高度为最大层次。即空的二叉树的高度是0,非空树的高度等于它的最大层次(根的层次为1,根的子节点为第2层,依次类推)。
/* * 获取树的高度 */ private int height(AVLTreeNode<T> tree) { if (tree != null) return tree.height; return 0; } public int height() { return height(mRoot); }
1
2
3
4
5
6
7
8
9
10
11
12
13比较大小
/* * 比较两个值的大小 */ private int max(int a, int b) { return a>b ? a : b; }
1
2
3
4
5
6旋转 如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。这种失去平衡的可以概括为4种姿态: LL(左左),LR(左右),RR(右右)和RL(右左)。下面给出它们的示意图:
上图中的4棵树都是"失去平衡的AVL树",从左往右的情况依次是: LL、LR、RL、RR。除了上面的情况之外,还有其它的失去平衡的AVL树,如下图:
上面的两张图都是为了便于理解,而列举的关于"失去平衡的AVL树"的例子。总的来说,AVL树失去平衡时的情况一定是LL、LR、RL、RR这4种之一,它们都有各自的定义: (1) LL: LeftLeft,也称为"左左"。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。 例如,在上面LL情况中,由于"根节点(8)的左子树(4)的左子树(2)还有非空子节点",而"根节点(8)的右子树(12)没有子节点";导致"根节点(8)的左子树(4)高度"比"根节点(8)的右子树(12)"高2。
- LL的旋转
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡。如下图:
图中左边是旋转之前的树,右边是旋转之后的树。从中可以发现,旋转之后的树又变成了AVL树,而且该旋转只需要一次即可完成。 对于LL旋转,你可以这样理解为: LL旋转是围绕"失去平衡的AVL根节点"进行的,也就是节点k2;而且由于是LL情况,即左左情况,就用手抓着"左孩子,即k1"使劲摇。将k1变成根节点,k2变成k1的右子树,"k1的右子树"变成"k2的左子树"。
/* * LL: 左左对应的情况(左单旋转)。 * * 返回值: 旋转后的根节点 */ private AVLTreeNode<T> leftLeftRotation(AVLTreeNode<T> k2) { AVLTreeNode<T> 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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(2) LR: LeftRight,也称为"左右"。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。 例如,在上面LR情况中,由于"根节点(8)的左子树(4)的左子树(6)还有非空子节点",而"根节点(8)的右子树(12)没有子节点";导致"根节点(8)的左子树(4)高度"比"根节点(8)的右子树(12)"高2。
- LR的旋转
LR失去平衡的情况,需要经过两次旋转才能让AVL树恢复平衡。如下图:
第一次旋转是围绕"k1"进行的"RR旋转",第二次是围绕"k3"进行的"LL旋转"。
/* * LR: 左右对应的情况(左双旋转)。 * * 返回值: 旋转后的根节点 */ private AVLTreeNode<T> leftRightRotation(AVLTreeNode<T> k3) { k3.left = rightRightRotation(k3.left); return leftLeftRotation(k3); }
1
2
3
4
5
6
7
8
9
10
(3) RL: RightLeft,称为"右左"。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。 例如,在上面RL情况中,由于"根节点(8)的右子树(12)的左子树(10)还有非空子节点",而"根节点(8)的左子树(4)没有子节点";导致"根节点(8)的右子树(12)高度"比"根节点(8)的左子树(4)"高2。
RL的旋转 RL是与LR的对称情况!RL恢复平衡的旋转方法如下:
第一次旋转是围绕"k3"进行的"LL旋转",第二次是围绕"k1"进行的"RR旋转"。
/* * RL: 右左对应的情况(右双旋转)。 * * 返回值: 旋转后的根节点 */ private AVLTreeNode<T> rightLeftRotation(AVLTreeNode<T> k1) { k1.right = leftLeftRotation(k1.right); return rightRightRotation(k1); }
1
2
3
4
5
6
7
8
9
10
(4) RR: RightRight,称为"右右"。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。 例如,在上面RR情况中,由于"根节点(8)的右子树(12)的右子树(14)还有非空子节点",而"根节点(8)的左子树(4)没有子节点";导致"根节点(8)的右子树(12)高度"比"根节点(8)的左子树(4)"高2。 如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。AVL失去平衡之后,可以通过旋转使其恢复平衡,下面分别介绍"LL(左左),LR(左右),RR(右右)和RL(右左)"这4种情况对应的旋转方法。
- 2. RR的旋转
理解了LL之后,RR就相当容易理解了。RR是与LL对称的情况!RR恢复平衡的旋转方法如下:
图中左边是旋转之前的树,右边是旋转之后的树。RR旋转也只需要一次即可完成。
/* * RR: 右右对应的情况(右单旋转)。 * * 返回值: 旋转后的根节点 */ private AVLTreeNode<T> rightRightRotation(AVLTreeNode<T> k1) { AVLTreeNode<T> 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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- LL的旋转
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡。如下图:
插入 插入节点的代码
/* * 将结点插入到AVL树中,并返回根节点 * * 参数说明: * tree AVL树的根结点 * key 插入的结点的键值 * 返回值: * 根节点 */ private AVLTreeNode<T> insert(AVLTreeNode<T> tree, T key) { if (tree == null) { // 新建节点 tree = new AVLTreeNode<T>(key, null, null); if (tree==null) { System.out.println("ERROR: create avltree node failed!"); return null; } } else { int cmp = key.compareTo(tree.key); if (cmp < 0) { // 应该将key插入到"tree的左子树"的情况 tree.left = insert(tree.left, key); // 插入节点后,若AVL树失去平衡,则进行相应的调节。 if (height(tree.left) - height(tree.right) == 2) { if (key.compareTo(tree.left.key) < 0) tree = leftLeftRotation(tree); else tree = leftRightRotation(tree); } } else if (cmp > 0) { // 应该将key插入到"tree的右子树"的情况 tree.right = insert(tree.right, key); // 插入节点后,若AVL树失去平衡,则进行相应的调节。 if (height(tree.right) - height(tree.left) == 2) { if (key.compareTo(tree.right.key) > 0) tree = rightRightRotation(tree); else tree = rightLeftRotation(tree); } } else { // cmp==0 System.out.println("添加失败: 不允许添加相同的节点!"); } } tree.height = max( height(tree.left), height(tree.right)) + 1; return tree; } public void insert(T key) { mRoot = insert(mRoot, key); }
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删除 删除节点的代码
/* * 删除结点(z),返回根节点 * * 参数说明: * tree AVL树的根结点 * z 待删除的结点 * 返回值: * 根节点 */ private AVLTreeNode<T> remove(AVLTreeNode<T> tree, AVLTreeNode<T> z) { // 根为空 或者 没有要删除的节点,直接返回null。 if (tree==null || z==null) return null; int cmp = z.key.compareTo(tree.key); if (cmp < 0) { // 待删除的节点在"tree的左子树"中 tree.left = remove(tree.left, z); // 删除节点后,若AVL树失去平衡,则进行相应的调节。 if (height(tree.right) - height(tree.left) == 2) { AVLTreeNode<T> r = tree.right; if (height(r.left) > height(r.right)) tree = rightLeftRotation(tree); else tree = rightRightRotation(tree); } } else if (cmp > 0) { // 待删除的节点在"tree的右子树"中 tree.right = remove(tree.right, z); // 删除节点后,若AVL树失去平衡,则进行相应的调节。 if (height(tree.left) - height(tree.right) == 2) { AVLTreeNode<T> l = tree.left; if (height(l.right) > height(l.left)) tree = leftRightRotation(tree); else tree = leftLeftRotation(tree); } } else { // tree是对应要删除的节点。 // tree的左右孩子都非空 if ((tree.left!=null) && (tree.right!=null)) { if (height(tree.left) > height(tree.right)) { // 如果tree的左子树比右子树高; // 则(01)找出tree的左子树中的最大节点 // (02)将该最大节点的值赋值给tree。 // (03)删除该最大节点。 // 这类似于用"tree的左子树中最大节点"做"tree"的替身; // 采用这种方式的好处是: 删除"tree的左子树中最大节点"之后,AVL树仍然是平衡的。 AVLTreeNode<T> max = maximum(tree.left); tree.key = max.key; tree.left = remove(tree.left, max); } else { // 如果tree的左子树不比右子树高(即它们相等,或右子树比左子树高1) // 则(01)找出tree的右子树中的最小节点 // (02)将该最小节点的值赋值给tree。 // (03)删除该最小节点。 // 这类似于用"tree的右子树中最小节点"做"tree"的替身; // 采用这种方式的好处是: 删除"tree的右子树中最小节点"之后,AVL树仍然是平衡的。 AVLTreeNode<T> min = maximum(tree.right); tree.key = min.key; tree.right = remove(tree.right, min); } } else { AVLTreeNode<T> tmp = tree; tree = (tree.left!=null) ? tree.left : tree.right; tmp = null; } } return tree; } public void remove(T key) { AVLTreeNode<T> z; if ((z = search(mRoot, key)) != null) mRoot = remove(mRoot, z); }
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
# 哈夫曼树
哈夫曼又称最优二叉树, 是一种带权路径长度最短的二叉树。(注意带权路径WPL是指叶子节点)
路径与路径长度
: 从树中一个节点到另一个节点之间的分支构成了两个节点之间的路径,路径上的分支数目称作路径长度。若规定根节点位于第一层,则根节点到第H层的节点的路径长度为H-1。如到40 的路径长度为1;30的路径长度为2;20的路径长度为3。节点的权
: 将树中的节点赋予一个某种含义的数值作为该节点的权值,该值称为节点的权;带权路径长度
: 从根节点到某个节点之间的路径长度与该节点的权的乘积。例如上图节点10的路径长度为3,它的带权路径长度为10 * 3 = 30;树的带权路径长度
: 树的带权路径长度为所有叶子节点的带权路径长度之和,称为WPL。上图的WPL = 1x40+2x30+3x10+3x20 = 190,而哈夫曼树就是树的带权路径最小的二叉树。
# 哈夫曼树的构建
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,哈夫曼树的构造规则为:
- 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
- 在森林中选出根结点的权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
- 从森林中删除选取的两棵树,并将新树加入森林;
- 重复上面两步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
上图中,它的叶子节点为{10,20,30,40},以这4个权值构建哈夫曼树的过程为:
# 哈夫曼编码
为{10,20,30,40}这四个权值构建了哈夫曼编码后,我们可以由如下规则获得它们的哈夫曼编码:
从根节点到每一个叶子节点的路径上,左分支记为0,右分支记为1,将这些0与1连起来即为叶子节点的哈夫曼编码。如下图:
(字母)权值 | 编码 |
---|---|
10 | 100 |
20 | 101 |
30 | 11 |
40 | 0 |
由此可见,出现频率越高的字母(也即权值越大),其编码越短。这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
具体流程如下:
# 多叉树
减少树的高度,能对二叉树进行优化
# B树、B+树、B*树
2-3树(最简单的B树结构)
- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个字节点,以此类推,234树等也是一种B树
B树的介绍
B树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率。
- 如图B树通过重新组织节点,降低了树的高度
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页的大小通常为4k),这样每个节点只需要一次I/O 就可以完全载入
- 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素,B树(B+)广泛应用于文件存储系统以及数据库系统中
对上图的说明:
1)B树的阶:节点的最多字节点个数。比如2-3树的阶是3,2-3-4树的阶是4
2)B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结
3)关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据
4)搜索有可能在非叶子结点结束
5)其搜索性能等价于在关键字全集内做一次二分查找
B+树的介绍
- B+树是B树的变体,也是一种多路搜索树。
- B+树的说明:
- B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
- 不可能在非叶子结点命中
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- B+树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录
- B+树更适合文件索引系统,B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然
B 树的介绍*
- B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
- B* 树的说明:
- B* 树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2。
- 从第1个特点我们可以看出,B* 树分配新结点的概率比B+树要低,空间使用率更高
# 红黑树(R-B Tree)
红黑树与AVL树的比较:
- AVL树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异
- 红黑树的插入删除比AVL树更便于控制操作
- 红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树)
红黑树的性质
: 红黑树是一棵二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。
具体性质如下:
- 每个节点颜色不是黑色,就是红色
- 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
- 根节点是黑色的
- 如果一个节点是红色,那么它的两个子节点就是黑色的(没有连续的红节点)
- 对于每个节点,从该节点到其后代叶节点的简单路径上,均包含相同数目的黑色节点
# 图
# 图的常用概念
- 顶点(vertex)
- 边(edge)
- 路径
- 无向图
- 有向图
- 带权图
# 图的表示方式
- 邻接矩阵 邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是row和col表示的1....n个点。
- 邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失。
- 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
参考 https://www.pdai.tech/md/algorithm/alg-basic-overview.html