参考自:
Java 面试一战到底
-周冠亚
算法 第 4 版
- Robert Sedgewick,Kevin Wayne
大话数据结构
-程杰
内容说明:主要是想了解下红黑树
以图示的方式分析了 从 AVL 过渡到 2-3树然后到 红黑树
最后介绍了 Huffman Tree
整体而言是对常见的 Tree 类型的数据结构的初步认识
流程图
提取码:6m55
保存到本地 —> draw.io 从本地打开即可,可能需要加载一会儿,强烈建议自己手绘一遍,帮助理解
问题:AVL , 2-3-树, 红黑树之间的关系?
AVL : ① 二叉排序树 ② 左右子树高度差 ≤ 1
2-3-树:AVL + 完美树【根节点到叶节点路径高度相同】
红黑树:AVL + 红黑属性
红黑树的性质:
① 每个节点子树数目 ≤ 2
② 根节点总是黑色,一个节点不能链接两条红链
问题:红黑树中为什么要区分红黑节点
初步理解,将 2-3-4 中的 3-结点和4-结点转化为二叉树的形式,但又要保持黑平衡,将多余的结点设置为红结点
问题:红黑树的使用场景
Linux内核设计与实现第三版-内核数据结构
:存储大量数据,且需快速检索
使用 AVL 树的原因:当二叉排序树退化成链表时,访问时间复杂度从O(log2n)
退化为O(n)
1. 二叉树
① 二叉树的特点
- 每个结点的度 ≤ 2
- 第 i 层最多有 2^(i - 1) 个结点
- 高度为 h 的二叉树,最多有 2^(h) - 1 个结点
2
和3
的结论会在堆排序中用到,大道至简,先记住第一条吧
② 斜树:每个结点的度 ≤ 1
斜树是退化至链表的树,由于遍历的时候不具有二叉搜索树的优势,需要 AVL 树调整斜树,降低高度
③ 满二叉树:非叶子结点的度为 2
④ 完全二叉树:在满二叉树的基础上,非叶子结点的度为 2 或者 1
若非叶子结点的度为 1,该结点只有左子树
2 AVL
2.1 AVL 左旋 & 右旋
大话数据结构
BF平衡因子
:二叉树【左子树深度 - 右子树深度】
深度:
① 左旋转
向 AVL 树插入 10
后,根节点6
的左右子树高度大于1
,发生左旋转
BF > 1 左子树不平衡,进行右旋转
BF < -1 右子树不平衡,进行左旋转
上述将 根节点进行左旋转后,【3,6,7】作为根节点8的左子树,按照二叉排序树的规则进行组合
② 右旋转
根节点4
的BF = 2 左子树不平衡,进行右旋操作
【5,6,8】作为新根3
的右子树分支,按照排序树规则连接到根节点右子树上
总结:节点左右旋转的轴点,为左右子树深度值较大的节点,旋转的目的是将引起不平衡的左子树或右子树的根节点交换为新的根节点。
2.2 LL-LR-RR-RL
插入或删除元素后,从变更节点向根节点回溯,遇到的第一个不平衡节点最低失衡根节点
① LL:导致失衡节点出现在最低失衡根节点的左子树的左子树中
旋转方式:找到最低失衡点6
,将最低失衡根节点右旋转
即 :对应基本的右旋转操作
② RR :导致失衡节点出现在最低失衡根节点的右子树的右子树中
旋转方式:找到最低失衡点6
,将最低失衡点左旋转
③ LR :导致失衡节点出现在最低失衡根节点的左子树的右子树中
旋转方式:
-
最低失衡点
5
的左孩子2
左旋转 -
最低失衡点
5
右旋转
④ RL :导致失衡节点出现在最低失衡根节点的右子树的左子树中
旋转方式:
-
最低失衡点
2
的右孩子5
右旋转 -
最低失衡点
2
左旋转
总结:LL,RR 为基本的操作,分别对应最低失衡根节点右旋和左旋操作
LL 说明左子树不平衡需要提升高度,因此根节点以左子树为轴进行右旋转降到下一层,同时左子树根节点提升到上一层
RR 同理
LR,RL 在进行操作时,先进行最低失衡点的子树的旋转操作转化为 LL,和 RR 然后再执行相应的左旋或右旋操作
2.3 调整子树
① LL 操作后,新根节点的原右子树变为旧根节点的左子树
LL 操作根节点右旋转,左子树提升到根节点,原根节点成为新根右节点,需要调整新根原本的左子树
需要调整节点【5,6,8】,根左子树元素小于根节点,因此新根原左子树节点挂到旧根左子树上
左旋转操作代码
private Node leftLeftRotate(Node root){
// LL 左子成为新根节点 leftChild
Node leftChild = root.left;
// 旧根的左子树为新根的右子树
root.left = leftChild.right;
if(root.left != null){
root.left.parent = root;
}
// 旧根为新根右子树
leftChild.right = root;
leftChild.right.parent = leftChild;
// 计算新根高度
leftChild.height = max(height(leftChild.left), height(leftChild.right)) + 1;
// 计算旧根高度
root.height = max(height(root.left), height(root.right)) + 1;
return leftChild;
}
② RR 操作后,新根节点的原左子树变为旧根节点的右子树
需要调整的节点:【3,6,7】
左旋操作,右子树旋转到根节点位置,原根节点成为左子树节点,此时新根节点的左子树需要调整
右子树所有子节点必然大于原根节点,所以新根节点的原左子树节点调整到旧根节点的右子树上
右旋转操作代码
private Node rightRightRotate(Node root){
// RR 右子成为新根节点 rightChild
Node rightChild = root.right;
// 旧根右子树为新根的左子树
root.right = rightChild.left;
if(root.right != null){
root.right.parent = root;
}
// 旧根为新根左子树
rightChild.left = root;
rightChild.left.parent = rightChild;
rightChild.height = max(height(rightChild.left), height(rightChild.right)) + 1;
root.height = max(height(root.left), height(root.right)) + 1;
// 返回新根节点
return rightChild;
}
提取码:emh6
3 2-3
树
2-3-4 在 AVL 树的基础上,限制:根结点到每个叶子结点的路径高度均相等
2-3-4 树的基本性质:
- 每个结点可存储 1,2,或3个元素值
- 每个结点的元素值递增,且每个结点元素值与其子树按照搜索树的方式组织
- 根节点到所有叶子结点的路径长度相等
2-3-4 树示意图
3.1 添加元素
① 向 2-结点添加元素
② 向 3-结点添加元素
③ 向 4-结点添加元素
4-结点已是最大结点,不能插入,需要分裂待插入的4-结点
3.2 删除元素
删除元素的规则,没看太明白,原本是在上述添加的最后一张图上画删除的图,
结果 【16,23】那个分支不知道怎么处理
2-3-4
树的执行效率
2-3-4 树结点范围:(1/2O(log2n), O(log2n)]
4 结点的父结点或子结点不能是 4 结点
原因是:在插入过程中,4-结点会向上分解。子节点由于要插入新元素,因此不可能为 4-结点算法第4版
对于 100 万个结点,2-3-4树的高度为 10-20 【1024 * 1024】
对于 10 亿个结点,2-3-4树的高度为 15-30 【1024 * 1024 * 1024】
标题以 2-3 树命名,因看了算法第4版
,4-结点仅是临时结点
4. 红黑树
红黑树与2-3 树的关系?
下图是将算法第4版 pg 276 示例中红链转化为红黑结点,转换规则根据java interview in action
周冠亚老师那本书中的转换规则
转换规则:
① 3-结点:红色结点 + 黑色结点
问题:是否允许红色右链?
算法 第 4 版
:红色右链是可用的,只允许红色左链,能减少可能出现的情况,实现代码会少很多
还是不太理解?
② 4-结点:黑色父结点 + 红色左右子结点
与2-3树对应的红黑树满足的条件:
该树是完美黑色平衡,任意空连接到根结点的路径上的黑链接数量相同
红黑树结点与2-3-4树结点的转化关系:
普通 2- 结点:黑色
3- 结点:其中一个为黑色
4- 结点:仅设置一个为黑色
也即从黑色结点属性而言,将2-3-4中的节点处理为单个黑色结点
由于 2-3-4 树是在 AVL 的基础上使得根节点到叶子结点的深度均相同,则每个结点都可以视为一个黑色结点,必然从根结点到叶子结点的深度都相同
基本的平衡操作:
① 连续红色结点:右旋转黑色结点使其将为子节点,并设置为红色;上方的红色结点设置为根结点,设置为黑色
② 连续红色结点且不再同一侧:旋转下方的红色结点到同一侧,然后再执行 ①
③ 左右子节点均为红色结点则将其转换为红色结点,子节点设置为黑色结点
4.1 添加元素
4.1.1 父节点为黑色
插入新节点默认为黑色,设置为红色
4.1.2 LL-LR-RR-RL
① 连续红结点在左子树同侧
② 连续红结点在左子树异侧
③ 连续红结点在右子树同侧
④ 连续红结点在右子树异侧
基本思路:将连续红结点旋转到同一侧,之后提升一个红色结点到上一级,将红色属性向上传递
上面两本书中介绍的有区别,算法4
中,不设置红色右链所以将红色属性向上传递
而在算法可视化网站上:可以有结点连接两个红结点,只是要求红节点不连续
上述 LL, RR, LR, RL 的情况只能作为某红黑树的中间部分存在,因为叶子结点插入前其父节点和叔叔结点应该是相同颜色的
初学,理解不深,主要是想作为阅读hashmap, treemap 源码的前菜
4.2 删除元素
4.2.1 删除红色叶子结点
删除红色叶子结点,不会对黑色平衡造成影响,也不会引起结构变化,直接删除即可
4.2.2 删除黑色叶子结点
① 删除右黑子结点:左黑高,右旋转,使得左子树根结点成为新的根结点
若此树为某红黑树的一部分,8结点可黑可红
待删除结点 10 为叶子结点的话,2结点和6结点定为红
旋转后 4 结点继承 8 结点的颜色,可黑可红
右旋转代码:
private void rightRotate(Node node){
// 1. 父节点右旋转,左孩子成为新父结点
Node leftChild = node.left;
// 2. 左孩子的右结点连接到旧父结点的左侧
node.left = leftChild.right;
if(leftChild.right != null){
leftChild.right.parent = node;
}
// 3. 左孩子成为祖父结点的子节点
leftChild.parent = node.parent;
// 4. 若旧父结点没有父结点,则左孩子成为根结点
if(node.parent == null){
root = leftChild;
}else{
// 5. 祖父结点不为空判断父节点是左孩子还是右孩子,左孩子替换父节点的位置
if(node.parent.right == node){
node.parent.right = leftChild;
}else{
node.parent.left = leftChild;
}
}
// 6. 新父结点的右侧连接旧父结点
leftChild.right = node;
node.parent = leftChild;
}
② 删除左黑叶子结点:右黑高,左旋转,使得右子树的根结点成为新的根结点
左旋转代码示例:
private void leftRotate(Node node){
// 1. 父节点左旋转,右孩子成为新父结点
Node rightChild = node.right;
// 2. 右孩子的左结点连接到旧父结点的右侧
node.right = rightChild.left;
if(rightChild.left != null){
rightChild.left.parent = node;
}
// 3. 右孩子成为祖父结点的子节点
rightChild.parent = node.parent;
// 4. 若旧父结点没有父结点,则左孩子成为根结点
if(node.parent == null){
root = rightChild;
}else{
// 5. 祖父结点不为空判断父节点是左孩子还是右孩子,右孩子替换父节点的位置
if(node.parent.right == node){
node.parent.right = rightChild;
}else{
node.parent.left = rightChild;
}
}
// 6. 新父结点的左侧连接旧父结点
rightChild.left = node;
node.parent = rightChild;
}
4.2.3 删除黑色父结点【仅一个孩子】
黑色父结点仅一个孩子,则一定为红孩子
从反面来思考,若为左黑,则父节点到右侧的路径少一个黑色结点,非黑平衡。右黑同理
问题:4的右子树若存在是什么颜色?【不作为某棵红黑树的一部分】
只能是黑色,红黑树满足黑色结点平衡【根结点到叶子结点所有路径上的黑色结点数目相同】
右分支只有两个黑色结点,左分支的子结点必须为红色
删除黑色父结点,唯一的红孩子会顶替父节点的位置,变为黑色结点
4.2.4 删除黑色父结点-左右红子树
删除图中的 13 结点
13 结点的左子树中的最大子节点12 取代 13 的位置
11 结点修改为黑色,原因是:若该结点仅有一个黑色子节点,则会打破黑平衡
如上图所示:假设到根结点到 11 结点所经过的黑色结点为 m,
若不调整 11 的黑色独子结点,左侧路径黑色结点数目为 m + 1,右侧为 m;
4.2.5 删除黑色父结点-左右黑子树
删除图中的 2 结点
2 结点在红黑树的左侧,左子树的1顶替2 的位置【需要验证下是否取左子树的最小值】
同理上述由于 1 结点仅有一个黑孩子,需要将其转化为红孩子
删除 9 结点验证:删除左侧带有左右黑子树的结点,删除后是否为最小值代替删除结点
验证结果:从左子树中寻找最大值代替待删除结点,之后再进行平衡调整操作
实际上无论该结点处于根结点的左子树还是右子树上,无论左右孩子均是红结点或均是黑结点
只要待删除结点含有左右子树,均会从左子树中寻找最大值代替该结点的位置
为什么会是左子树的最大值呢?
① 右子树所有结点均大于左子树的最大值,因此当左子树最大值为新父结点后,右子树不用调整
② 左子树最大值一定在左分支的叶子结点上,删除叶子结点所需要的调整较小
4.2.6 删除红色父结点-左右黑子树
没有第二种情况,即红色父节点的左右子树不可能为红色,否则出现红连续
① 红色父节点仅有两个黑色子结点
红色结点若为父结点,必然含有左右黑子节点,否则只能为红色叶子结点【黑平衡】
操作过程同上述 4.2.5
删除黑色父节点-左右黑子树
此处也说明:结点的红黑属性是相对于位置动态调整的,本质上结点没有差别,红黑子节点间可以根据需要转化
② 15 的示例有点多余,作用是对照
即:无论是删除黑父还是红父,都会找左子树的最大值代替该结点的位置
删除结点 15 ,从左子树中找到最大值 14 替代原有位置,之后进行平衡调整
5. 哈夫曼树-Huffman Tree
哈夫曼树:最优二叉树,带权路径长度最短树
哈夫曼树的用途:构建哈夫曼树编码,得到哈夫曼编码可用于压缩编码
5.1 哈夫曼编码
哈夫曼编码最初是用于解决原距离通信的数据传输最优化问题【此处安利下 茨威格-越过大洋的第一次通话】
huffman tree 为哈夫曼编码的中间产物
情景设定:
传输BADCADFEED
① 传统方式
基本字符串 ABCDEF,有六个数,需要用到3位二进制进行编码
则编码后的结果为:001.000.011.010.000.011.101.100.100.011
10个字符每个字符3位编码需要30位二进制编码
② 利用权重构建哈夫曼树
尝试根据上述字符出现的频率构建哈夫曼树
构建的哈夫曼树
哈夫曼编码
BADCADFEED
编码后的结果:1010,00,11,1011,00,11,100,01,01,11
共 25 个字符,此处文本数量较小,差距不是很明显
上述区别主要是:传统的编码无论字母出现频次高低都要根据总的字符种类进行编码
而哈夫曼编码根据字母出现的频次,出现频次较高的,权重大,距离根节点较近,编码长度反而较短
Huffman 编码的代码示例
public void huffmanEncode(Node parent, String code){
// 1. 若为叶子结点则输出编码
if(parent != null && parent.left == null && parent.right == null){
System.out.println(parent.weight + " -- " + code);
}
// 2. 左子树非空则递归至叶子结点
if(parent != null && parent.left != null){
code += "0";
huffmanEncode(parent.left, code);
// 回溯
code = code.substring(0, code.length() - 1);
}
// 3. 右子树非空则递归至叶子结点
if(parent != null && parent.right != null){
code += "1";
huffmanEncode(parent.right, code);
// 回溯
code = code.substring(0, code.length() - 1);
}
}
5.2 构建哈夫曼树
WPL 树的带权路径长度:所有叶子节点的带权路径长度之和
哈夫曼树的特性:
-
包含
n
个叶子结点的哈夫曼树中共有2n - 1
个结点 -
哈夫曼树的结点度数为
0
或2
,没有度为 1 的结点
从上图可以看出,哈夫曼树是从底部开始向上构建的,增加的结点Tx是由两个待编码的结点组成的
而待编码的结点全都分布在叶子结点,叶子结点两两之间构建一个新结点
若有 n
个叶子结点,则会增加 n - 1
个临时结点,构成hafuman-tree 需要 2n - 1
个结点
上述哈夫曼树的构建过程
① 根据字符出现的频率【权重】进行排序
选取权值最小的B 和 C 构建二叉树,其和为父结点,两者中较小值为左结点
② 将产生的新结点T1 放入集合中排序,重复①,利用T1 构建新的二叉树
排序后,选择 F 和 T1 构建二叉树
③ 重复①,利用 T2 构建新的二叉树
排序后,选择 A 和 E 构建二叉树
④ 更新权值表,继续重复 ①
选择 T2 和 D 构建二叉树
⑤ 组合最后两个结点
哈夫曼树满足二叉搜索树的规则,因此 T3 作为左子树,T4 作为右子树
右侧为编码树:将左侧哈夫曼树的左分支的权值标记为 0
,右分支的权值标记为 1
每个字符的编码:从根结点依次衔接路径上的权值直到叶子结点【该字符】
构建Huffman tree 编码示例
public void createHuffmanTree(List<Node> nodeList){
if(nodeList == null || nodeList.isEmpty()){
return;
}
while(nodeList.size() > 1){
// 1. Node 类重写 comopareTo(obj) 降序排列
Collections.sort(nodeList);
// 2. 选取末尾最小元素组建二叉树
Node left = nodeList.get(nodeList.size() - 1);
Node right = nodeList.get(nodeList.size() - 2);
// 3. 新增父节点
Node parent = new Node(left.weight + right.weight, left, right, null);
parent.left = left;
parent.right = right;
// 4. 从集合中移除参与构建的结点
nodeList.remove(left);
nodeList.remove(right);
// 5. 新增父节点添加到集合中
nodeList.add(parent);
}
root = nodeList.get(0);
}
总结:红黑树部分还有点问题待纠正,AVL 树的删除操作没有太理解
写这篇博客的目的是,想看懂 HashMap 的树化以及添加树结点部分的代码