一、树的产生
1.数组存储与链式存储
我们在有了链表链表存储与数组存储,为什么还需要树结构尼?
对数组存储与链式存储进行分析:
1.数组存储方式:
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找、插值查找等提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低。若数组满时要插入数据就需要进行扩容。(ArrayList底层也是数组扩容实现的。)
- 数组扩容:每次底层都需要创建新的数组要将原来的数据拷贝到数组并插入数据。
- ArrayList底层扩容:
/**
* Constructs an empty list with an initial capacity of ten.
* 构造初始容量为10的空列表。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
* 共享的空数组实例,用于默认大小的空实例。 我们将此与EMPTY_ELEMENTDATA区别开来,
* 以了解添加第一个元素时需要充气多少。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Default initial capacity. 默认初始空间
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量以确保它至少可以容纳最小容量参数指定的元素数量。
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
2.链表方式存储:
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如检索某个值,需要从头节点开始遍历。
鉴于链式存储与数组存储的优缺点就有了树存储:
树存储能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
2.树认知
树的常用术语(结合示意图理解):
- 节点
- 根节点
- 父节点
- 子节点
- 叶子节点 (没有子节点的节点)
- 节点的权(节点值)
- 路径(从 root 节点找到该节点的路线)
- 层
- 子树
- 树的高度(最大层数)
- 森林 :多颗子树构成森林
二、二叉树
1.满二叉树与完全二叉树
二叉树:每个节点最多只能有两个子节点(左子节点、右子节点)的一种形式称为二叉树。
满二叉树:如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树
完全二叉树:如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
2.二叉树遍历方式
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
PS:看输出父节点的顺序,就确定是前序,中序还是后序
图解:
代码实现:
public void preOrder(){
System.out.println("=> "+this);// 打印节点
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
代码实现:
public void infixOrder(){
if (this.left != null){
this.left.infixOrder();
}
System.out.println("=> "+this);// 打印节点
if (this.right != null){
this.right.infixOrder();
}
}
代码实现:
public void postOrder(){
if (this.left != null){
this.left.postOrder();
}
if (this.right != null){
this.right.postOrder();
}
System.out.println("=> "+this);// 打印节点
}
3.二叉树查找指定节点
前序查找:
- 先判断当前结点的no是否等于要查找的节点的no
- 如果是相等,则返回当前结点
- 如果不等,则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
- 如果左递归前序查找,找到结点,则返回,否继卖判断,当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
public HeroNode preOrderSearch(int no){
System.out.println("进入前序查找~");
if (this.getNo() == no){
return this;
}
HeroNode heroNode = null;
if (this.left != null){
heroNode = this.left.preOrderSearch(no);//左子树查找
}
if (heroNode != null){
return heroNode;
}
if (this.right != null ){
heroNode = this.right.preOrderSearch(no);//左子树查找
}
return heroNode;
}
中序查找:
- 判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
- 如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点,否则继续进行右递归的中序查找
- 如果右递归中序查找,找到就返回,否则返回null
public HeroNode infixOrderSearch(int no){
System.out.println("进入中序查找~");
HeroNode heroNode = null;
if (this.left != null){
heroNode = this.left.infixOrderSearch(no);//左子树查找
}
if (heroNode != null){
return heroNode;
}
System.out.println("进入后序查找~");
if (this.getNo() == no){
return this;
}
if (this.right != null ){
heroNode = this.right.infixOrderSearch(no);//左子树查找
}
return heroNode;
}
后序查找:
- 判断当削结点的左子节点是否为空,如果不为空,则递归后序查找
- 如果找到,就返回,如果没有找到,就判断当前结点的右子节点是否为空,如果不为空,则右递归进行后序查找,如果找到,就返回
- 就和当前结点进行,比如,如果是则返回,否则返回nul
public HeroNode postOrderSearch(int no){
HeroNode heroNode = null;
if (this.left != null){
heroNode = this.left.postOrderSearch(no);//左子树查找
}
if (heroNode != null){
return heroNode;
}
if (this.right != null ){
heroNode = this.right.postOrderSearch(no);//左子树查找
}
if (heroNode != null){
return heroNode;
}
System.out.println("进入后序查找~");
if (this.getNo() == no){
return this;
}
return heroNode;
}
4.二叉树删除节点
注:如果要删除的节点是叶子节点,则删除该节点,如果删除的节点是非叶子节点,则删除该子树。
- 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点
- 如果当前结点的左子结点不为空,并且左子结点就是要删除结点,就将this.left=null并且就返回结束递归删除
- 如果当前结点的右子结点不为空,并且右子结点就是要册除结点,就将this. right=nul;并且就返回结束递归删除)
- 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除
- 如果第4步也没有删除结点,则应当向右子树进行递归删除
public void deleteHeroNode(int no){
if (this.left != null && this.getLeft().getNo() == no){ // 判断左子节点no是否相等
this.setLeft(null);
return;
}
if (this.right != null && this.getRight().getNo() == no){ // 判断右子节点是否相等
this.setRight(null);
return;
}
if (this.left != null){//向左子树递归删除
this.left.deleteHeroNode(no);
}
if (this.right != null){//向右子树递归删除
this.right.deleteHeroNode(no);
}
}
上述完整测试代码太长就不在这里板书,可以从这个链接下载从这里下载
三、顺序存储二叉树
顺序存储二叉树:从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组。
上图将二叉树转换为数组存储。
顺序存储二叉树特点:
- 顺序二叉树通常只考虑完全二叉树
- 第 n 个元素的左子节点为 2 * n + 1
- 第 n 个元素的右子节点为 2 * n + 2
- 第 n 个元素的父节点为 (n-1) / 2
- n : 表示二叉树中的第几个元素(按 0 开始编号如图所示)
代码实现使用二叉树遍历方式遍历数组 {1,2,3,4,5,6,7}(完全二叉树)
public class ArrayBinaryTree {
private int[] arr;
public ArrayBinaryTree(int[] arr) {
this.arr = arr;
}
/**
* @Description: preOrder 前序遍历
* @param: [index]
* @return: void
* @auther: zqq
* @date: 20/6/20 10:15
*/
public void preOrder(int index){
if (this.arr == null && this.arr.length == 0){
System.out.println("数组为空不能遍历");
return;
}
System.out.println("arr["+index+"] = " + arr[index]);
if (2 * index + 1 <arr.length){
preOrder(2 * index + 1);
}
if (2 * index + 2 < arr.length){
preOrder(2 * index + 2);
}
}
/**
* @Description: infixOrder 中序遍历
* @param: [index]
* @return: void
* @auther: zqq
* @date: 20/6/20 10:17
*/
public void infixOrder(int index){
if (this.arr == null && this.arr.length == 0){
System.out.println("数组为空不能遍历");
return;
}
if (2 * index + 1 <arr.length){
infixOrder(2 * index + 1);
}
System.out.println("arr["+index+"] = " + arr[index]);
if (2 * index + 2 < arr.length){
infixOrder(2 * index + 2);
}
}
/**
* @Description: postOrder 后序遍历
* @param: [index]
* @return: void
* @auther: zqq
* @date: 20/6/20 10:17
*/
public void postOrder(int index){
if (this.arr == null && this.arr.length == 0){
System.out.println("数组为空不能遍历");
return;
}
if (2 * index + 1 <arr.length){
postOrder(2 * index + 1);
}
if (2 * index + 2 < arr.length){
postOrder(2 * index + 2);
}
System.out.println("arr["+index+"] = " + arr[index]);
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
// 前序遍历
arrayBinaryTree.preOrder(0);
//中序遍历
arrayBinaryTree.infixOrder(0);
// 后序遍历
arrayBinaryTree.postOrder(0);
}
}
堆排序就需要用到二叉树
四、比较难的线索二叉树
1.引出线索二叉树
问题产生:
- 当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
- 但是 6, 8, 10, 14 这几个节点的左右指针,并没有完全的利用上.
- 如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
由此就产生了线索二叉树:
2.线索二叉树
- n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质 的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
- 线索二叉树中,一个结点的前一个结点,称为前驱结点
- 线索二叉树中,一个结点的后一个结点,称为后继结点
当一个二叉树被线索化后,以后的遍历将不再需要递归每一个节点,而是向链表一样的方式进行遍历。
中序线索化二叉树
上图中的二叉树被线性化之后:
线索化之后的二叉树拥有直接指向下一个需要访问的节点。如上图:当我们找到8时,就可以根据红线找到3,再根据蓝线找到10,接着根据红线找到1,之后根据蓝线找到6,最后找到14.这样就和中序遍历的结果一样:{8,3,10,1,6,14}
线索化后的二叉树Node节点的left和right属性就有了两种情况:
- left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点.
- right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩ 节点的 right 指向 的是后继节点.
因此在线索化二叉树时要使用一个标号来标记left和right属性是标识前驱节点还是后继节点。除此之外还要要一个节点辅助指针来连接前后的节点的关系
中序线索化二叉树代码实现:
public void infixThreadedNode(HeroNode heroNode){
if (heroNode == null){ //不能线索划
return;
}
// 1.先线索划左子树
infixThreadedNode(heroNode.getLeft());
// 2.线索划当前节点
// 2.1 线索化left指针
if (heroNode.getLeft() == null){
heroNode.setLeft(this.pre); // 让左指针指向前驱节点
heroNode.setLeftType(1); // 修改左指针类型为前驱类型
}
// 2.2 线索化right指针
if (this.pre != null && this.pre.getRight() == null){
this.pre.setRight(heroNode); // 让前驱节点的右指针指向当前节点
this.pre.setRightType(1);// 修改右指针类型为后期类型
}
//!!! 2.3 将当前节点是下一个节点的前驱节点
this.pre = heroNode;
// 3.线索划右子树
infixThreadedNode(heroNode.getRight());
}
中序线索化后的二叉树遍历方式:
public void infixThreadedList() {
HeroNode heroNode = this.root;
while (heroNode != null){ // 找到遍历开始的节点
// 循环找到leftType == 1的节点,此时该节点该节点就是按照线索化处理后的有效节点
while (heroNode.getLeftType() != 1){
heroNode = heroNode.getLeft();
}
//如果当前节点的右指针指向的是后继节点
while (heroNode.getRightType() == 1){
System.out.println("heroNode => " + heroNode); // 打印
heroNode = heroNode.getRight();
}
System.out.println("heroNode => " + heroNode);
heroNode = heroNode.getRight();
}
}
先序线索二叉树与后序线索二叉树并不完善,需要知道双亲信息。不完善原因请参考这里
但针对上图的先序线索二叉树和后续线索二叉树还是可以写出来。
前、中、后序线索化二叉树完整代码点击这里下载
五、树结构实际应用
1.堆排序
2.赫夫曼树压缩与解压
篇幅太大,想要了解可以去看看
六、二叉排序树
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
1.二叉排序树的添加
这个比较简单,就直接上代码了。
public void add(Node node){
if (node.getValue() < this.getValue()){// 如果比当前节点的值小
if (this.getLeft() == null){// 如果当前节点的左子节点为空
this.setLeft(node);
return;
}
this.getLeft().add(node); // 递归添加
}else if (node.getValue() > this.getValue()){ // 如果添加的节点大于当前节点
if (this.getRight() == null){// 如果当前节点的右子节点为空
this.setRight(node);
return;
}
this.getRight().add(node);
}
}
2.二叉排序树的删除
二叉排序树的删除有三种情况:
第一种情况: 删除叶子节点
思路 :
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 是 parent 的左子结点 还是右子结点
(4) 根据前面的情况来对应删除 左子结点 parent.left = null 右子结点 parent.right = null;
第二种情况: 删除只有一颗子树的节点
思路:
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 的子结点是左子结点还是右子结点
(4) targetNode 是 parent 的左子结点还是右子结点
(5) 如果 targetNode 有左子结点
5.1 如果 targetNode 是 parent 的左子结点parent.left = targetNode.left;
5.2 如果 targetNode 是 parent 的右子结点 parent.right = targetNode.left;
(6) 如果 targetNode 有右子结点
6.1 如果 targetNode 是 parent 的左子结点 parent.left = targetNode.right;
6.2 如果 targetNode 是 parent 的右子结点 parent.right = targetNode.right
情况三 :
删除有两颗子树的节点.
思路 :
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 从 targetNode 的右子树找到最小的结点
(4) 用一个临时变量,将 最小结点的值保存 temp = 11
(5) 删除该最小结点 (6) targetNode.value = temp
代码实现:
public void del(int value){
if (root == null){
System.out.println("此树空");
return;
}else {
Node target = searchTarget(value);
if (target == null){ // 要删除的节点为空
System.out.println("删除的节点未找到");
return;
}
Node parent = searchParent(value);
if (parent == null && target.getLeft() == null && target.getRight() == null){
// 要删除的节点没有父节点,没有左右子节点的根节点
root = null;
return;
}
if (target.getLeft() == null && target.getRight() == null){ // 如果目标节点是叶子节点
if (parent.getLeft() != null && parent.getLeft().getValue() == value){ // 如果目标节点是父节点的左子节点
parent.setLeft(null);
}else if (parent.getRight() != null && parent.getRight().getValue() == value){ // 如果目标节点是父节点的右子节点
parent.setRight(null);
}
}else if (target.getLeft() != null && target.getRight() != null){ // 目标节点是一个包含两个子节点的树
int temp = delRightTreeNode(target.getRight());
target.setValue(temp);
}else { // 目标节点是只有左子节点或者右节点的树
if (parent == null){ // 目标节点的父节点为空,且目标节点只有一个子节点,那么目标节点为根节点
if (target.getLeft() != null){
root = target.getLeft();
}else {
root = target.getRight();
}
// root = target.getLeft() == null ? target.getRight() : target.getLeft();
return;
}
if (parent.getLeft().getValue() == value){ // 目标节点是父节点的左子节点
if (target.getLeft() != null){ // 目标节点只包含一个左子节点
parent.setLeft(target.getLeft());
}else { // 目标节点只包含一个右子节点
parent.setLeft(target.getRight());
}
}else {// 目标节点是父节点的右子节点
if (target.getLeft() != null){ // 目标节点只包含一个左子节点
parent.setRight(target.getLeft());
}else { // 目标节点只包含一个右子节点
parent.setRight(target.getRight());
}
}
}
}
}
/**
* @Description: delRightTreeNode 删除并返回以当前节点为根节点的树的最小值
* @param: [node]
* @return: int
* @auther: zqq
* @date: 20/6/30 10:29
*/
public int delRightTreeNode(Node node){
Node target = node;
while (target.getLeft() != null){
target = target.getLeft();
}
del(target.getValue()); // 删除最小值节点
return target.getValue();
}
3. 完整代码
七、平衡二叉树(AVL树)
1.二叉排序树存在的问题
平衡二叉树的产生:当我们有如下的的数列{1,2,3,4,5,6},当我们需要用它来创建一个二叉排序树时,就会有如下的一颗二叉树
这颗二叉树存在有以下问题:
- 左子树全部为空,从形式来看,这棵二叉树更加像一颗单链表
- 这样就会对查询产生影响,不能发挥BST的优势,因为每一个都还需要进行比较,导致查询速度比单链表还要慢
针对以上的问题就有了平衡二叉树来解决这个问题。
2.平衡二叉树(AVL树)
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL 树,可以保证查询效率较高。
- 平衡二叉树的特点:
- 它的左右两个子树的高度差绝对值不超过1
- 并且左右两个子树都是一颗平衡二叉树
- 常见是实现方法:红黑树、AVL、替罪羊树、Treap、伸展树
上图中只有两个是平衡二叉树、而第三个不是
1)、左旋转
假如有这个数列{4,3,6,5,7,8},将他创建成为一颗二叉树,他就是不是一颗平衡二叉树,需要经过将他变成一颗平衡二叉树,图解如下图:
核心代码:
public void leftRotate(){
// 1.创建一个新的节点,值等于当前节点的值
Node node = new Node(this.getValue());
// 2.把新节点的左子树设置成当前节点的左子树
node.setLeft(this.getLeft());
// 3.把新节点的右子树设置成当前节点的右子树的左子树
node.setRight(this.getRight().getLeft());
// 4.把当前节点的值替换成右子节点的值
this.setValue(this.getRight().getValue());
// 5.把当前节点的右子树设置成右子树的右子树
this.setRight(this.getRight().getRight());
// 6.把当前节点的左子树设置为新节点
this.setLeft(node);
}
2)、右旋转
根据左旋转的原理一样,右旋转的核心代码:
public void rightRotate(){
// 1.创建一个新的节点,值等于当前节点的值
Node node = new Node(this.getValue());
// 2.把新节点的右子树设置成当前节点的右子树
node.setRight(this.getRight());
// 3.把新节点的左子树设置成当前节点的左子树的右子树
node.setLeft(this.getLeft().getRight());
// 4.把当前节点的值替换成左子节点的值
this.setValue(this.getLeft().getValue());
// 5.把当前节点的左子树设置成左子树的左子树
this.setLeft(this.getLeft().getLeft());
// 6.把当前节点的右子树设置为新节点
this.setRight(node);
}
左旋转与右旋转的使用时机,当我们添加完最后一个节点时,就可以进行旋转了
/**
* @Description: leftHeight 计算当前节点的左子树的高度
* @param: []
* @return: int
* @auther: zqq
* @date: 20/7/3 17:22
*/
public int leftHeight(){
if (this.getLeft() == null){
return 0;
}else {
return this.getLeft().height();
}
}
/**
* @Description: rightHeight 计算当前节点的右子树的高度
* @param: []
* @return: int
* @auther: zqq
* @date: 20/7/3 17:23
*/
public int rightHeight(){
if (this.getRight() == null){
return 0;
}else {
return this.getRight().height();
}
}
/**
* @Description: height 计算当前节点的高度
* @param: []
* @return: int
* @auther: zqq
* @date: 20/7/3 17:21
*/
public int height(){
return Math.max(this.getLeft() == null ? 0 : this.getLeft().height(),
this.getRight() == null ? 0 : this.getRight().height()) + 1;
}
public void add(Node node){
if (node.getValue() < this.getValue()){// 如果比当前节点的值小
if (this.getLeft() == null){// 如果当前节点的左子节点为空
this.setLeft(node);
}else {
this.getLeft().add(node); // 递归添加
}
}else if (node.getValue() > this.getValue()){ // 如果添加的节点大于当前节点
if (this.getRight() == null){// 如果当前节点的右子节点为空
this.setRight(node);
}else {
this.getRight().add(node);
}
}
// 当添加完最后一个节点后,如果:(右子树的高度-左子树的高度) > 1,左旋转
if (rightHeight() - leftHeight() > 1){
this.leftRotate();
}
// 当添加完最后一个节点后,如果:(左子树的高度-右子树的高度) > 1,右旋转
if ((leftHeight() - rightHeight() > 1)) {
this.rightRotate();
}
}
3)、双旋转
前面两个数列经过单旋转就可以将非平衡二叉树转换为平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换工作。比如数列:{10, 11, 7, 6, 8, 9}
图解:
这时:
1. 如果符合右旋转的条件,如果它的左子树的右子树高度大于它的左子树的左子树高度,先对当前节点的左子节点进行左旋转,接着在对当前节点右旋转。
2. 如果符合左旋转的条件,如果它的右子树的左子树高度大于它的右子树的右子树高度,先对当前节点的右子节点进行右旋转,接着对当前节点进行左旋转
代码改动:
public void add(Node node){
if (node.getValue() < this.getValue()){// 如果比当前节点的值小
if (this.getLeft() == null){// 如果当前节点的左子节点为空
this.setLeft(node);
}else {
this.getLeft().add(node); // 递归添加
}
}else if (node.getValue() > this.getValue()){ // 如果添加的节点大于当前节点
if (this.getRight() == null){// 如果当前节点的右子节点为空
this.setRight(node);
}else {
this.getRight().add(node);
}
}
// 当添加完最后一个节点后,如果:(右子树的高度-左子树的高度) > 1,左旋转
if (rightHeight() - leftHeight() > 1){
// 如果它的右子树的左子树高度大于它的右子树的右子树的高度
if (this.getRight() != null && this.getRight().rightHeight() < this.getRight().leftHeight()){
// 对当前节点的右子树进行右旋转
this.getRight().rightRotate();
}
this.leftRotate();
return;//!!! 保证左旋和右旋只执行一次
}
// 当添加完最后一个节点后,如果:(左子树的高度-右子树的高度) > 1,右旋转
if ((leftHeight() - rightHeight() > 1)) {
// 如果它的左子树的右子树高度大于它的左子树的左子树的高度
if (this.getLeft() != null && this.getLeft().rightHeight() > this.getLeft().leftHeight()){
// 对当前节点的左子树进行左旋转
this.getLeft().leftRotate();
}
// 之后再对当前节点进行右旋转
this.rightRotate();
}
}
八、多路查找树
1.二叉树存在的问题
二叉树是需要加载到内存中的,当我们的二叉树节点很多时就会产生以下问题:
- 在构建耳茶黄素时,需要多次进行I/O操作(海量数据存储在数据库或文件中),节点太多在构建二叉树时会造成影响
- 节点越多时,也会造成二叉树的高度很高,会降低操作速度
为了解决这一问题就产生了多叉树,每一个节点拥有更多的数据线和更多的子节点
2.2-3树
2-3树是最简单的B树,有以下特点:
- 2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
- 2-3 树是由二节点和三节点构成的树。
如:将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成 2-3 树,并保证数据插入的大小顺序。
2-3树插入规则:
- 2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
- 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层, 拆后仍然需要满足上面 3 个条件。
- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
依次这样插入就会产生最终的2-3树
3.B树
B-tree 树即 B 树,B 即 Balanced,平衡的意思。有人把 B-tree 翻译成 B-树,容易让人产生误解。会以为 B-树是一种树,而 B树又是另一种树。实际上,B-tree 就是指的 B 树。数据库索引就是基于B树或者B+树的。
说明:
- B树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4
- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询 关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
- 搜索有可能在非叶子结点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
- B树通过重新组织节点, 降低了树的高度.
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k), 这样每个节点只需要一次 I/O就可以完全载入
- 将树的度M设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O操作就可以读取到想要的元素, B 树(B+)广泛 应用于文件存储系统以及数据库系统中
4.B+树
说明:
- B+树的搜索与 B树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据) 恰好是有序的
- 不可能在非叶子结点命中
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
5.B*树
说明:
- B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针。
- B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的 1/2。
- B*树分配新结点的概率比 B+树要低,空间使用率更高