1. 为什么需要树这种数据结构
  2. 树的常用术语
  3. 树结构Java代码实现
  4. 前中后序遍历
  5. 赫夫曼部分
  6. 赫夫曼树
  7. 赫夫曼编码
  8. 赫夫曼加密文件
  9. 二叉树部分
  10. 顺序存储二叉树
  11. 线索化二叉树
  12. 二叉排序树
  13. 平衡二叉树
  14. 多叉树部分
  15. B+树和B*树
  16. 红黑树

以下只给出思路与关键方法,算法的源代码放在了git中,需要的自取

https://gitee.com/leidl97/algor

为什么需要树这种数据结构

因为树结构存入读取数据相较于其他数据结构更有效率!

1、数组存储方式分析

优点:通过下标方式访问元素,速度快。对于有序数组,还可以使用二分查找来提高检索速度

缺点:如果要检索具体的某个值,或者插入会整体移动,效率降低

2、链式结构的分析

优点:插入和删除一个节点效率较快

缺点:进行检索时,效率仍然较低,需要从头节点进行遍历

3、树结构分析

提高存储,读取的效率,比如利用二叉树排序,既可以保证存储速度,也可以保证读取速度,修改、删除等同样

树的常用术语

  • 节点:一个圆圈表示一个节点
  • 根节点:最上方的节点
  • 父节点:谁指向它谁就是父节点
  • 子节点:它指向谁就表示谁是子节点
  • 叶子节点:最下面一层节点(没有子节点的节点)
  • 权:表示节点的值

二叉树,顾名思义就是两个叉,一个节点下最多有两个子节点

img

如果所有二叉树的叶子节点都在最后一层,则我们成为满二叉树

img

如果最后一层左连续,且其他层都是满的,我们称之为完全二叉树,如图

img

树结构Java代码实现

以二叉树为例

主要属性:左节点,右节点,权

属性定义

1
2
3
4
5
6
7
8
9
10
11
12
public class Node {
//左子树
public Node leftNode;
//右子树
public Node rightNode;
//节点的权
public int value;
//初始化节点
public Node(int value) {
this.value = value;
}
}

增加,修改节点都是通过不断寻找,如果节点为空进行相应的操作

但无论是哪种树,删除操作都是最复杂的。后面会详细讨论

前中后序遍历

以父为准!

前序遍历:左右

中序遍历:左

后序遍历:左右

代码实现逻辑(递归思想

前序遍历

1、先输出当前节点

2、向左节点遍历(如果节点不为空)

3、向右节点遍历(如果节点不为空)

1
2
3
4
5
6
7
8
9
10
11
12
13
//前序遍历
public void beforeTraversal(Node node) {
//首先输出当前节点
System.out.print(node.getValue() + "\t");
//向左遍历
if (node.getLeftNode()!=null) {
beforeTraversal(node.getLeftNode());
}
//向右遍历
if (node.getRightNode()!=null) {
beforeTraversal(node.getRightNode());
}
}

中序遍历

1、向左节点遍历(如果节点不为空)

2、先输出当前节点

3、向右节点遍历(如果节点不为空)

1
2
3
4
5
6
7
8
9
10
11
12
13
//中序遍历
public void centerTraversal(Node node) {
//首先找到最左节点
if (node.getLeftNode()!=null) {
centerTraversal(node.getLeftNode());
}
//其次输出当前节点
System.out.print(node.getValue() + "\t");
//最后向右进行遍历
if (node.getRightNode()!=null) {
centerTraversal(node.getRightNode());
}
}

后序遍历

1、向左节点遍历(如果节点不为空)

2、向右节点遍历(如果节点不为空)

3、先输出当前节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//后序遍历
public void afterTraversal(Node node) {
//首先找到最左节点
if (node.getLeftNode()!=null) {
centerTraversal(node.getLeftNode());
}
//其次向右遍历
if (node.getRightNode()!=null) {
centerTraversal(node.getRightNode());
}
//最后输出节点
System.out.print(node.getValue() + "\t");

}

各种遍历进行查找,道理是一样的,重新构建方法,多传入一个目标值value,以前序遍历为准,则代码实现为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//前序遍历查找
public void beforeTraversalSearch(Node node, int value) {
count++;
if (node.getValue() == value) {
System.out.println("找到该元素:"+value+",次数为:"+count);
}
//向左遍历
if (node.getLeftNode()!=null) {
beforeTraversalSearch(node.getLeftNode(),value);
}
//向右遍历
if (node.getRightNode()!=null) {
beforeTraversalSearch(node.getRightNode(),value);
}
}

赫夫曼树(wpl最小的树)

介绍:给定n个权值为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也叫赫夫曼树(HuffmanTree)

情景模拟:给你一个数组,要求创建一颗赫夫曼树。比如{13,7,8,3,29,6,1}

步骤

1、先排序(这不是重点,怎么实现快怎么来)

{1,3,6,7,8,13,29}

2、根据前两个数值相加得到一个权值,在与后面进行比较

3、直到处理完所有的数字则构建出一颗赫夫曼树

img

构建结果如图

难点

1、如何判断提供的值已经用完

用集合,每次排完之后都会删掉左右结点,然后添加父节点,当最后集合中元素数量为1时(集合大小总是先-2在+1,最后剩下的为相加的权值),即为消耗完

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
private static void huffmanBuild(int[] a) {
Arrays.sort(a);
List<Node> nodes = new ArrayList<>();
for (int value : a) {
//向集合加入node对象
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
Node leftNode = nodes.get(0);
Node rigtNode = nodes.get(1);
Node parent = new Node(leftNode.getValue() + rigtNode.getValue());
//构建一颗二叉树
parent.setLeftNode(leftNode);
parent.setRightNode(rigtNode);
nodes.remove(leftNode);
nodes.remove(rigtNode);
nodes.add(parent);
//将现有的集合进行排序
Collections.sort(nodes, new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
return o1.getValue() - o2.getValue();
}
});
}
Tree tree = new Tree(nodes.get(0));
tree.beforeTraversal(tree.getRoot());
System.out.println();
}

赫夫曼编码

1、属于一种编程算法

2、在电讯通信中的经典应用

3、可用于数据文件压缩,压缩率在20% — 90%之间

4、是可变字长编码(VLC)的一种,称之为最佳编码,是无损压缩

怎么理解

假如一串文本为:i like java,会转为ASCII码,在转为二进制,这叫做定长编码

一般不会这么做,因为太长了,所以通信领域信息的处理方式为变长编码

首先统计字符串中每个字母出现的个数,出现次数越多的,编码越小。但每个字符的编码不能是其他字符的前缀。比如a用1表示,b不能用10表示,这样机器在解码的时候扫描到1就无法进行解码,存在二义性。

第三种编码方式叫做赫夫曼编码

把各个编码出现的次数称为权值,然后构建一颗赫夫曼树,规定向左的路径为0,向右路径为1,从根节点到目标结点的路径即为编码

img

如图所示,每个字母都会有一个自己的编码,不存在二义性

意义

对文本进行压缩

用程序实现赫夫曼树编码

难点

1、如何统计字符出现的次数

设一个集合,将遍历过的放进去

2、如何为每个字符设置计数器

设置一个hashmap用来统计

步骤

1、获取需要构成赫夫曼树的叶子结点

2、利用这些结点构建赫夫曼树

3、对赫夫曼树进行编码(涉及遍历递归)

4、对生成的编码集进行压缩(将二进制编码压缩为10进制字节组)

代码实现

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
public class HuffManCode {
public static void main(String[] args) {
String s = "i like like like java do you like a java";
List<Byte> encoding = encoding(s);
System.out.println("压缩前该文本长度为:"+s.length());
System.out.println("压缩前该文本长度为:"+encoding.size()+"。压缩率为:"+ (Double.valueOf(s.length() - encoding.size()) / Double.valueOf(s.length()) * 100) + "%");
}

//将文本信息转为赫夫曼编码
private static List<Byte> encoding(String s) {
byte[] bs = s.getBytes();
//定义赫夫曼树结点
List<HuffManTree> nodes = new ArrayList<>();
//定义已经存在的字符集合
List<Byte> bytes = new ArrayList<>();
//定义已经存在的字符出现的次数集合
Map<Byte,Integer> flags = new HashMap<>();
//创建构建赫夫曼树所需的结点信息
nodeBuild(flags,bytes,nodes,bs);
//利用这些结点构建赫夫曼树
HuffManTree huffManTree = huffmanBuild(nodes);
//设置编码集
Map<Byte,String> results = new HashMap<>();
//进行编码
huffmanEncoding(huffManTree, results,"");
results.forEach((k,v) -> System.out.println("数据为:"+k+"。对应的编码值为:"+v));
//将字长进行压缩
return zip(bs,results);
}

/**
* 获取需要构成赫夫曼树的叶子结点
* @param flags 计算次数
* @param bytes 放入结果
* @param nodes 结果集中的结点
* @param bs 原始数据
*/
private static void nodeBuild(Map<Byte,Integer> flags, List<Byte> bytes,List<HuffManTree> nodes, byte[] bs) {
for (byte b : bs) {
if (bytes.contains(b)) {
//说明已经存在
flags.put(b, flags.get(b)+1);
continue;
}
//说明第一次遍历到
flags.put(b, 1);
bytes.add(b);
}

flags.forEach((k,v) -> nodes.add(new HuffManTree(k,v)));
}

private static List<Byte> zip(byte[] bs, Map<Byte,String> results) {
//替换原有编码
StringBuffer sb = new StringBuffer();
//生成新编码
List<Byte> bytes = new ArrayList<>();
System.out.print("原始数据为:");
for (byte b : bs) {
System.out.print(b + "\t");
}
System.out.println();
//将原始byte数组的值替换为对应的编码
for (Byte b : bs) {
sb.append(results.get(b));
}
String s;
for (int i = 0; i < sb.length(); i+=8) {
s = i+8 < sb.length() ? sb.substring(i,i+8) : sb.substring(i,sb.length());
byte a = (byte)Integer.parseInt(s, 2);
bytes.add(a);
}
System.out.println(bytes);
System.out.println(sb);
System.out.println("该编码的字长为:"+sb.length());
return bytes;
}

/**
* 进行编码
* @param huffManTree 赫夫曼树
* @param results 编码值哈希表
* @param s 记录编码值
*/
private static void huffmanEncoding(HuffManTree huffManTree, Map<Byte,String> results, String s) {
//进行遍历,然后处理
if (huffManTree.b != 0) {
//当前遍历到的元素不为0,说明为结点元素,需要给一个编码值
results.put(huffManTree.b, s);
}
if (huffManTree.left != null) {
//这里不能写s += "0",因为返回上一层已经在上一层先赋值在使用,所以上层会延续下层的值
huffmanEncoding(huffManTree.left, results, s + "0");
}
if (huffManTree.right != null) {
huffmanEncoding(huffManTree.right, results, s + "1");
}
}

private static HuffManTree huffmanBuild(List<HuffManTree> list) {
//需要实现Comparable接口并重写对应的方法
Collections.sort(list);
while (list.size() > 1) {
HuffManTree leftNode = list.get(0);
HuffManTree rigtNode = list.get(1);
HuffManTree parent = new HuffManTree(leftNode.weight + rigtNode.weight);
//构建一颗二叉树
parent.left = leftNode;
parent.right = rigtNode;
list.remove(leftNode);
list.remove(rigtNode);
list.add(parent);
//将现有的集合进行排序
Collections.sort(list);
}
//最后剩下的是这颗赫夫曼树的头节点
System.out.println("成功构建该树,前序遍历结果如下");
preTraversal(list.get(0));
return list.get(0);
}

private static void preTraversal(HuffManTree node) {
if (node != null) {
System.out.println(node);
}
if (node.left != null) {
preTraversal(node.left);
}
if (node.right != null) {
preTraversal(node.right);
}
}
}

赫夫曼解码

能编就会解,反着来就可以

步骤

1、根据编码集results创建解码集

2、将给定的byte编码集先转为8位2进制字符串

3、将得到的二进制串按照解码集去解码得到byte集合

4、将byte集合转为byte数组

代码实现

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
/**
* 赫夫曼解码
* @param bytes 需要解密的文本
* @param results 编码集
*/
private static String decoding(List<Byte> bytes, Map<Byte,String> results) {
//根据编码集results创建解码集
Map<String, Byte> decoding = new HashMap<>();
results.forEach((k,v) -> decoding.put(v,k));
//将给定的byte编码集先转为8位2进制字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.size(); i++) {
byte b = bytes.get(i);
//返回的为二进制的补码
String string = Integer.toBinaryString(b);
if (b < 0) {
string = string.substring(string.length() - 8);
}
//表示8位整数,不够高位补0,最后一位且为正数不做处理
if (i != bytes.size() - 1 && b > 0) {
string = String.format("%08d",Integer.parseInt(string));
}
sb.append(string);
}
String result = sb.toString();
//创建结果集
String temp = "";
List<Byte> byteResults = new ArrayList<>();
char[] array = result.toCharArray();
//将得到的二进制串按照解码集去解码得到byte集合
for (char c : array) {
temp += c;
Byte b = decoding.get(temp);
if (b != null) {
byteResults.add(b);
temp = "";
}
}
//将byte集合转为byte数组
byte[] bs = new byte[byteResults.size()];
for (int i = 0; i < bs.length; i++) {
bs[i] = byteResults.get(i);
}
return new String(bs);
}

赫夫曼加密文件

思路:用IO流的方式转为字节,传入编码方法,得到新的字节及编码集,写入对象流中。解压同理,利用对象流读取字节和编码集后,传入解码方法得到对应的字节数据

可以去git地址看源码

顺序存储二叉树

作用是堆排序的时候会用到

用一组连续的存储单元存放二叉树中的结点数组转二叉树

顺序存储二叉树的特点为

  1. 顺序二叉树通常只考虑完全二叉树
  2. 第n个元素的左子节点为2*n+1
  3. 第n个元素的右子节点为2*n+2
  4. 第n个元素的父节点为(n-1)/2
  5. 其中n表示二叉树中第几个元素,从0开始

有这几个规则后就可以很容易的写出实现代码

就是用数的遍历方式输出数组中的值,比如前序遍历代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//index为数组下标
public void preOrder(int[] a, int index) {
if (a == null || a.length == 0) {
System.out.println("数组为空,无需遍历");
return;
}
System.out.println(a[index]);
if ((2*index + 1) < a.length) {
//向左遍历
preOrder(a, 2*index + 1);
}
if ((2*index + 2) < a.length) {
//向右遍历
preOrder(a, 2*index + 2);
}
}

线索化二叉树

相关概念

空指针域:一些节点的左右子树有的并没有,没有的就叫做空指针域,一个节点可能有0-2个空指针域,空指针域= 节点 + 1也就是n+1

怎么算出来的(了解)

因为n个节点有2n个指针 又因为n个节点中有n-1条边(节点之间的线被称之为边,比如两个节点只有一条边,三个有两条边,以此类推) 剩下的空链域就是2n-(n-1)=n+1,即n+1个空指针域(有边的则表示存在指针依赖关系,一个节点最多有两条边,减去有关系的也就是n-1条边,剩下的n+1即为没有指向关系的即为空指针域)

线索二叉树它解决了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题,出现了二叉链表找左、右孩子困难的问题,线索二叉树又分为前序线索化,中序线索化和后序线索化,分别用不同的逻辑去实现。

img

二叉排序树

概念,任何一个非叶子结点,其左子结点都比其小,右子结点都比其大

现在给定一个数组,将其中的值构建为一颗二叉排序树

代码实现

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
 public static Node buildTree(int[] a) {
Node node = new Node(a[0]);
for (int i = 1; i< a.length; i++) {
int temp = a[i];
findNullPoint(temp, node);
}
return node;
}

//找到空结点
private static void findNullPoint(int a, Node node) {
if (a <= node.value) {
if (node.leftNode == null) {
node.leftNode = new Node(a);
return;
}
findNullPoint(a, node.leftNode);
}
if (a > node.value) {
if (node.rightNode == null) {
node.rightNode = new Node(a);
return;
}
findNullPoint(a, node.rightNode);
}
}

删除结点

考虑三种情况

  • 删除节点没有子节点
  • 删除节点只有一个子节点
  • 删除节点有两个子节点

处理方式

  • 直接删除(没有子节点)
  • 将删除节点的父节点指向其子节点(只有一个子节点)
  • 如果删除节点是父节点的左子节点,则替换为删除节点的最右子树(一直向右遍历)
  • 如果删除删除节点为父节点的右子节点则将删除位置替换为删除节点的最左子树

难点

1、当删除结点有两个子节点的时候应该如何处理

将该节点下的所有结点的值存入数组中,然后重新构建树节点,放入父节点下(我当时的做法,会比较消耗性能,所以稳妥的做法是上面提到的解决方法,也就是第二种方法)

第二种方法为找到待删除结点的右子树中最小的结点N,然后取出,new一个结点使指等于结点N,左子树指向待删除的左子树,右子树指向待删除的右子树,当然取出结点如果有右子树,那么将其父节点与其链接,如图所示

img

来源于《算法第四版》

代码实现

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
//删除结点,需要考虑叶子结点,有一个子节点,有两个子节点
public static void delete(Node node, Node deleteNode) {
if (deleteNode.value == node.value) {
//说明删除的为根节点,直接返回
System.out.println("不能删除根节点!");
return;
}
if (deleteNode.value < node.value) {
if (node.leftNode == null) {
System.out.println("未找到需要删除的结点");
return;
}
if (node.leftNode.value == deleteNode.value) {
//相等的就判断结点类型
if (node.leftNode.leftNode == null && node.leftNode.rightNode == null) {
//说明为叶子结点,直接将其置为空
node.leftNode = null;
} else if(node.leftNode.leftNode != null && node.leftNode.rightNode != null) {
//说明有两个子节点
buildSort(node.leftNode);
list.remove(Integer.valueOf(node.leftNode.value));
int[] temp = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
temp[i] = list.get(i);
}
Node tempNode = buildTree(temp);
node.leftNode = tempNode;
} else if (node.leftNode.leftNode != null) {
//只有一个左子节点,将其替换
node.leftNode = node.leftNode.leftNode;
} else {
//只有一个右子结点,将其替换
node.leftNode = node.leftNode.rightNode;
}
return;
}
delete(node.leftNode, deleteNode);
return;
}
if (deleteNode.value > node.value) {
if (node.rightNode == null) {
System.out.println("未找到需要删除的结点");
return;
}
if (node.rightNode.value == deleteNode.value) {
//相等的就判断结点类型
if (node.rightNode.leftNode == null && node.rightNode.rightNode == null) {
//说明为叶子结点,直接将其置为空
node.rightNode = null;
} else if(node.rightNode.leftNode != null && node.rightNode.rightNode != null) {
//说明有两个子节点
buildSort(node.rightNode);
list.remove(Integer.valueOf(node.rightNode.value));
int[] temp = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
temp[i] = list.get(i);
}
Node tempNode = buildTree(temp);
node.rightNode = tempNode;
} else if (node.rightNode.leftNode != null) {
//只有一个左子节点,将其替换
node.rightNode = node.rightNode.leftNode;
} else {
//只有一个右子结点,将其替换
node.rightNode = node.rightNode.rightNode;
}
return;
}
delete(node.rightNode, deleteNode);
return;
}
}

public static void buildSort(Node node) {
//首先找到最左节点
if (node.getLeftNode()!=null) {
buildSort(node.getLeftNode());
}
//其次输出当前节点,list为全局静态变量
list.add(node.getValue());
//最后向右进行遍历
if (node.getRightNode()!=null) {
buildSort(node.getRightNode());
}
}

平衡二叉树(AVL)

二叉树可能会存在的问题(复杂度会出现O(N)的情况)

img

这样的构造如同一个链表

更像一个单链表,查询速度明显降低

解决方案:平衡二叉树(AVL)

小科普:为什么叫AVL?(了解)

由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。我还纳闷了,怎么平衡二叉树缩写能没有B

1
2
3
4
5
6
7
8
9
10
11
12
//构建一颗平衡二叉树
public static Node buildBalanceTree(int[] a) {
Node node = new Node(a[0]);
int status;
for (int i = 1; i < a.length; i++) {
int temp = a[i];
Node.findNullPoint(temp, node);
//对不满足平衡二叉树的条件进行旋转
turn(node);
}
return node;
}

特点

左右两个子树高度差绝对值不能超过1,并且左右两颗子树都是一颗平衡二叉树

平衡二叉树实现的方式有:红黑树,AVL(算法),替罪羊树,Treap,伸展树等。

相关概念:左旋,右旋,双旋

AVL最对进行两次旋转就可以达到平衡

右子树高度高,进行左旋转

相反,左子树高度好,进行右旋转

旋转的目的:降低树的高度(不是为了好看)

img

假如此时的一颗树状况如上,右子树高,则进行左旋操作,分为6步

1、new一个结点,值为当前结点(默认为根节点)

2、将新节点的左子树指向当前结点的左子节点

3、将新结点的右子节点指向当前结点的右子节点的左子节点

4、将当前结点的值变为右子树结点的值

5、将当前结点的左子树指向为新节点

6、将当前结点的右子树指向当前结点的右子节点的右子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 进行左旋
* @param node
*/
private static void turnLeft(Node node) {
//1、创建新节点,值与当前结点相同
Node temp = new Node(node.value);
//2、将新节点左子树指向当前结点的左子树
temp.leftNode = node.leftNode;
//3、将新节点的右子树指向当前结点的右子树的左子树
temp.rightNode = node.rightNode.leftNode;
//4、将当前结点的值变为当前结点的右子树结点的值
node.value = node.rightNode.value;
//5、将当前结点的左子树指向新节点
node.leftNode = temp;
//6、将当前结点的右子树指向当前结点右子树的右子树
node.rightNode = node.rightNode.rightNode;
}

右旋反过来就行

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 进行右旋
* @param node
*/
private static void turnRight(Node node) {
//1、创建新节点,值与当前结点相同
Node temp = new Node(node.value);
//2、将新节点右子树指向当前结点的右子树
temp.rightNode = node.rightNode;
//3、将新节点的左子树指向当前结点的左子树的右子树
temp.leftNode = node.leftNode.rightNode;
//4、将当前结点的值变为当前结点的左子树结点的值
node.value = node.leftNode.value;
//5、将当前结点的右子树指向新节点
node.rightNode = temp;
//6、将当前结点的左子树指向当前结点左子树的左子树
node.leftNode = node.leftNode.leftNode;
}

旋转的条件

高度差超过1

难点

1、判断当前树的高度

1
2
3
private static int getHeight(Node node) {
return Math.max(node.leftNode == null ? 0 : getHeight(node.leftNode), node.rightNode == null ? 0 : getHeight(node.rightNode)) + 1;
}

此方法妙在+1,如果没有+1那么在返回上一层的时候将是0,最终结果始终是0,不会发生变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 检查该树的状态 0 表示平衡二叉树
* -1表示左子树高
* 1表示右子树高
* @param node
* @return
*/
private static int isCheckNode(Node node) {
int lh = getLeftHeight(node);
int rh = getRightHeight(node);
if (lh > rh && lh - rh > 1) {
return -1;
}
if(rh > lh && rh -lh > 1) {
return 1;
}
return 0;
}

img

上图为特殊情况,一次旋转并不能称为平衡二叉树,所以需要进行双旋。需要在旋转前在进行一次判断,看是否需要进行双旋(旋转一次不能满足,所以需要旋转两次)

难点

1、左旋前先判断当前结点的右节点的左子树高度是否大于当前结点的左子树高度,如果大于则对当前结点的右子树进行右旋

2、同样的,右旋前先判断当前结点左子节点的右子树高度是否大于当前结点的右子树高度,如果大于则对当前结点的左子树进行左旋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//进行旋转
private static void turn(Node node) {
int status = isCheckNode(node);
if (status == -1) {
//进行右旋
if (getHeight(node.leftNode.rightNode) > getHeight(node.rightNode)) {
//进行局部旋转
turnLeft(node.leftNode);
}
turnRight(node);
}
if (status == 1) {
//进行左旋
if (isCheckNode(node.rightNode.leftNode) > isCheckNode(node.leftNode)) {
//进行局部旋转
turnRight(node.rightNode);
}
turnLeft(node);
}
}

多叉树

为什么需要多叉树

二叉树虽然说效率比较高,但是也会存在问题,结点数 = 2的高度次方 -1 。假如节点数过多的话,在构建二叉树的时候,需要进行多次IO操作,速度会有影响,高度同样也会很高,降低操作速度

多叉树的优点

通过重新组织结点,降低树的高度

几个分叉就表示几结点

文件系统几数据库系统的设计者利用磁盘预读的原理,将一个结点大小设为等于一个页(页的大小通常为4k),这样每个结点只需要一次IO就可以完全载入。所以B树应用广泛(B+也是)

2-3树是最简单的B树结构,特点如下

那什么是B树?

B表示balance,也就是平衡树

1、所有叶子结点都在同一层(也是B树的特点)

2、有两个子节点的节点叫二节点,二节点要么没有节点,要么有两个子节点

3、有三个子节点的节点叫二节点,三节点要么没有节点,要么有三个子节点

4、2-3树是由二节点和三节点构成的树

img

如图所示

除了2-3树外,还有2-3-4树,道理是一样的

img

B+树和B*树(不详细,后续补充)

B即balanced,没有B-树 ,其实是B树

B树的阶:指最多节点的个数,比如2-3树的阶为3

数据分布在整个树中,即叶子节点和非叶子节点都存放数据

搜索性能相当于在关键字全集内做一次二分查找

B+树是B树的变体,所有的数据存于叶子节点中,也叫做稠密索引,且列表中的关键字恰好是有序的,不可能在非叶子节点命中,非叶子节点中的叫索引不是数据,非叶子节点的数据也叫做稀疏索引,所以说更加适合文件系统,所以B树与B+树各自都有用途,取决于应用场景

这个思想是怎么来的,假设给定一个单链表有27条数据,则可以分为3 * 9 层

img

B*树是B+树的变体,在B+树的非根和非叶子节点在增加指向兄弟的指针

img

红黑树(过于复杂,后续补充代码)

前置条件:二叉排序树(BST),平衡二叉树(AVL)

跟AVL类似,都是平衡二叉树,不同的是AVL是以左右子树的高度差不超过1做限制,所以是全局变动,数据量变大之后会出现性能问题。而红黑树的出现解决了这个问题,它采用局部变动的方式达到一种自平衡,靠的就是旋转变色,旋转操作前面也提到了,变色就是红变黑,黑变红。难的是什么时候旋转?什么时候变色?由于它是一种局部自平衡,所以并不是一颗完美的平衡二叉查找树,但左右子树的黑节点层数是相等的,所以我们又称这种平衡叫做黑色完美平衡

跟AVL树一样都需要一个规则来进行判断,不满足的话则进行平衡操作

红黑树有5点规则需要遵守,也可以说红黑树的性质

1、结点不是红的就是黑的。

2、根节点是黑色

3、每个叶子结点都是黑的(指null结点)

4、每个红节点的两个子节点都是黑的,不能出现连续的两个红色结点

5、从任意结点到叶子结点都包含相同数目的黑色结点

插入

插入的时候还是按照二叉排序树的规则去插,逐一比较插入,插入后不满足条件在进行平衡。插入结点为红节点

分场景讨论

场景1:插入时该树没有结点

将红节点变色

场景2:插入该值已存在

直接找到该值然后重新赋值

场景3:插入的时候父节点为黑色

满足红黑树的性质直接插入即可

img

场景4:插入的时候父节点为红色

不满足性质4,也可以得出插入结点一定有祖父结点(父节点的父节点)(根据性质2)

在分情况讨论

4.1:叔叔结点为红色

当前结点情况为黑红红,最简单的处理方式为红黑红,然后在左后续处理(需要注意的是叔叔也需要变为黑色)

img

如果此时pp结点的父节点为黑色,则无需做任何处理

如果pp结点父节点为红色,则违反性质4,需要设置pp结点为当前结点,继续做平衡处理

4.2:叔叔结点不存在或者为黑色,并且父节点为左子节点

首先来说,叔叔不可能为黑节点,否则违反性质5

4.2.1:插入结点为P的左子节点

怎么处理:首先变色:父节点变为黑色,祖父为红,然后进行右旋

img

4.2.2:插入结点为P的右子节点

将p左旋(变为上面的情况),然后变色,最后将PP右旋

img

4.3:叔叔结点不存在或者为黑色,并且父节点为右子节点

4.3.1:插入为左子节点

4.3.2:插入为右子节点

情况同上,反过来就行

删除过于复杂,捋顺了再来补充

以上图片大多来源于网络,侵删