- 二叉树、二叉查找树、红黑树

二叉树

树的基本概念

  • 结点的度:结点拥有的子树的数目;
  • 结点层次:跟为第一层,跟的孩子为第二层,…
  • 树的深度:树的结点的最大层次数;也称为树的高度;

二叉树

二叉树:是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成;

特点

  • 每个结点最多有两棵子树,二叉树中结点的度不能大于2;
  • 左子树和右子树是有顺序的,次序不能颠倒;
  • 树的结点只有一棵子树时,也要区分它是左子树还是右子树;

满二叉树

满二叉树:在二叉树中,所有结点都有左子树和右子树,并且所有叶子结点都在同一层上,这样的二叉树称为满二叉树;

特点

  • 所有叶子结点都出现在最下一层;
  • 所有非叶子结点的度都是2,所有叶子结点的度都是0;
  • 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多;

在这里插入图片描述


完全二叉树

完全二叉树:是满二叉树的一种,对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树;

特点

  • 叶子结点只能出现在最下一层和次下层;
  • 最下层叶子结点集中在树的左侧;
  • 倒数第二层若存在叶子结点,一定是在右侧连续位置;
  • 如果结点度是1,那么该结点只有左孩子,没有右孩子;
  • 同样结点数目的二叉树,完全二叉树的深度最小;
  • 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树;

在这里插入图片描述


二叉树遍历

https://www.jianshu.com/p/bf73c8d50dc2


二叉查找树 ( 二叉搜索树)

转自此文

对于任意一个结点 n,

  • 其左子树下的每个后代结点的值都小于结点 n 的值;
  • 其右子树下的每个后代结点的值都大于结点 n 的值;

在这里插入图片描述

基于二叉查找树的这种特点,我们在查找某个结点的时候,可以采取类似于二分查找的思想,快速找到某个结点;查找所需最大次数等同于二叉查找树的高度;

n 个结点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logn)

首先将想要查找找的值x与树的根结点比较,若x小于根结点的值,则在根结点的左子树中找,若x大于根结点的值,则在根结点的右子树中找,若相等,则找到,结束查找,直接返回结点的值;
插入的时候也是一样,通过一层一层比较,最后找到插入的位置;

之所以说是正常情况下,是因为二叉查找树有可能出现一种极端的情况,例如:

在这里插入图片描述

这种情况也是满足二叉查找树的条件,然而,此时的二叉查找树已经近似退化为一条链表,这样的二叉查找树的查找时间复杂度顿时变成了 O(n),可想而知,我们必须不能让这种情况发生,为了解决这个问题,于是我们引申出了平衡二叉树;


平衡二叉树 AVL

平衡二叉树就是为了解决二叉查找树退化成一颗链表而诞生了,平衡树具有如下特点:

  • 具有二叉查找树的全部特性;
  • 每个结点的左子树和右子树的高度差至多等于1;

例如:图一就是一颗平衡树了,而图二则不是(结点右边标的是这个结点的高度);
因为图2 结点9的左孩子高度为2,而右孩子高度为0,他们之间的差值超过1了;
在这里插入图片描述

平衡树基于这种特点就可以保证不会出现大量结点偏向于一边的情况了;

于是,通过平衡树,我们解决了二叉查找树的缺点;对于有 n 个结点的平衡树,最坏的查找时间复杂度也为 O(logn)


但是插入新结点时,可能会破坏AVL树的特性:例如图1中插入结点3,按照查找二叉树的特性,我们只能把3作为结点4的左子树插进去,可是插进去之后,又会破坏了AVL树的特性;

解决这个问题就要使用左旋、右旋

1、右旋: 左-左型
即:顺时针旋转两个结点,使得父结点被自己的左孩子取代,而自己成为自己的右孩子;
在这里插入图片描述
在这里插入图片描述
这里要注意,结点4的右孩子成为了结点6的左孩子了;
在这里插入图片描述

2、左旋: 右-右型
在这里插入图片描述

在这里插入图片描述

3、左-右型:先做左旋,后做右旋;

4、右-左型:先做右旋,再做左旋;


例子:一次插入3,2,1,4,5,6,7,10,9,8

  • 插入3、2、1,左-左型,需要右旋调整;
    在这里插入图片描述

  • 插入4、5,右-右型,需要左旋转调整;
    在这里插入图片描述

注意:这里的结点3,
- 若是只有一个右孩子,则将它的右孩子作为旋转结点,它变成自己右孩子的左子结点,它的右孩子变为它的父结点的右孩子;
- 若结点3有两个孩子,则将它作为旋转结点,将它的左孩子变为父结点的右孩子,父结点作为它的左孩子;

  • 插入6,右-右型,需要进行左旋;
    在这里插入图片描述

  • 插入7,右-右型,需要进行左旋;
    在这里插入图片描述

  • 插入10、9,右-左型,先对结点10进行右旋把它变成右-右型,然后在进行左旋;
    在这里插入图片描述

  • 若是左-右型,则先左旋,再右旋;
    在这里插入图片描述

//定义结点
class AvlNode {
   int data;
   AvlNode lchild;//左孩子
   AvlNode rchild;//右孩子
   int height;//记录结点的高度
}

//在这里定义各种操作
public class AVLTree{
   //计算结点的高度
   static int height(AvlNode T) {
       if (T == null) {
           return -1;
       }else{
           return T.height;
       }
   }

   //左左型,右旋操作
   static AvlNode R_Rotate(AvlNode K2) {
       AvlNode K1;

       //进行旋转
       K1 = K2.lchild;
       K2.lchild = K1.rchild;
       K1.rchild = K2;

       //重新计算结点的高度
       K2.height = Math.max(height(K2.lchild), height(K2.rchild)) + 1;
       K1.height = Math.max(height(K1.lchild), height(K1.rchild)) + 1;

       return K1;
   }

   //进行左旋
   static AvlNode L_Rotate(AvlNode K2) {
       AvlNode K1;

       K1 = K2.rchild;
       K2.rchild = K1.lchild;
       K1.lchild = K2;

       //重新计算高度
       K2.height = Math.max(height(K2.lchild), height(K2.rchild)) + 1;
       K1.height = Math.max(height(K1.lchild), height(K1.rchild)) + 1;

       return K1;
   }

   //左-右型,进行右旋,再左旋
   static AvlNode R_L_Rotate(AvlNode K3) {
       //先对其孩子进行左旋
       K3.lchild = R_Rotate(K3.lchild);
       //再进行右旋
       return L_Rotate(K3);
   }

   //右-左型,先进行左旋,再右旋
   static AvlNode L_R_Rotate(AvlNode K3) {
       //先对孩子进行左旋
       K3.rchild = L_Rotate(K3.rchild);
       //在右旋
       return R_Rotate(K3);
   }

   //插入数值操作
   static AvlNode insert(int data, AvlNode T) {
       if (T == null) {
           T = new AvlNode();
           T.data = data;
           T.lchild = T.rchild = null;
       } else if(data < T.data) {
           //向左孩子递归插入
           T.lchild = insert(data, T.lchild);
           //进行调整操作
           //如果左孩子的高度比右孩子大2
           if (height(T.lchild) - height(T.rchild) == 2) {
               //左-左型
               if (data < T.lchild.data) {
                   T = R_Rotate(T);
               } else {
                   //左-右型
                   T = R_L_Rotate(T);
               }
           }
       } else if (data > T.data) {
           T.rchild = insert(data, T.rchild);
           //进行调整
           //右孩子比左孩子高度大2
           if(height(T.rchild) - height(T.lchild) == 2)
               //右-右型
               if (data > T.rchild.data) {
                   T = L_Rotate(T);
               } else {
                   T = L_R_Rotate(T);
               }
       }
       //否则,这个结点已经在书上存在了,我们什么也不做
       
       //重新计算T的高度
       T.height = Math.max(height(T.lchild), height(T.rchild)) + 1;
       return T;
   }
}

红黑树

参考文章:http://www.360doc.com/content/18/0904/19/25944647_783893127.shtml
参考文章:https://www.jianshu.com/p/e136ec79235c

虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个结点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除结点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树;

显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树;红黑树能够在最坏情况下,也能在 O(logn) 的时间复杂度查找到某个结点;

不过,与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因;但是单单在查找方面的效率的话,平衡树比红黑树快;所以,我们也可以说,红黑树是一种不大严格的平衡树;也可以说是一个折中发方案;

平衡树是为了解决二叉查找树退化为链表的情况,而红黑树是为了解决平衡树在插入、删除等操作需要频繁调整的情况;


红黑树性质

红黑树:是一种含有红黑结点并且能 自平衡的二叉查找树,在进行插入和删除等可能会破坏树的平衡的操作时,需要重新自处理达到平衡状态;

红黑树特点
  • 1、具有二叉查找树的特点;
  • 2、根结点是黑色的,其余结点是红色或黑色;
  • 3、每个叶子结点都是黑色的空结点(NIL),也就是说,叶子结点不存数据;
  • 4、每个红色结点的两个子结点都是黑色,(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  • 5、从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点;
    • 5-1、黑色结点可以有黑色子结点,如果一个结点存在黑子结点,那么该结点肯定有两个子结点;

在这里插入图片描述
在这里插入图片描述

红黑树并不是一个完美平衡二叉查找树,从图2可以看出,根结点的左子树比右子树高2层;但是左子树和右子树的黑色节点的层数是相等的,也就是说,任意一个节点到每个叶子结点的路径所包含的黑节点的数量相同,所以红黑树也叫黑色完美平衡


红黑树能自平衡,靠的是三种操作:左旋、右旋和变色:

  • 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
  • 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
  • 变色:结点的颜色由红变黑或由黑变红。

旋转操作是局部的,不会影响旋转节点的父节点,父节点以上操作还是保持不变的;
左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了;
右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了;

要保持红黑树的性质,结点不能乱挪,还得靠变色;


红黑树的插入(8种场景)

插入操作包括两部分工作:一是查找插入的位置,二是插入后自平衡;

查找插入的父节点很简单,跟查找操作区别不大:

  1. 从根结点开始查找;
  2. 若根结点为空,那么把插入结点作为根结点,颜色为黑色,结束;
  3. 若根节点不为空,则把根节点做为当前结点;
  4. 若当前结点为null,返回当前结点的父节点,结束;
  5. 若当前结点的key等于查找的key,那么该key所在结点就是插入结点,更新节点的值,结束;
  6. 若当前结点的key大于查找的key,把当前结点的左子节点设置为当前结点,重复步骤4;
  7. 若当前结点的key小于查找的key,把当前结点的右子节点设置为当前结点,重复步骤4;

思考:

1、黑结点可以同时包含一个红子结点和一个黑子结点吗?

2、插入结点是应该是什么颜色呢?
红色
理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作;但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做自平衡;

3、除了场景2,所有插入操作都是在叶子结点进行的:
因为查找插入位置时,我们就是在找子结点为空的父结点的;

在这里插入图片描述

场景1:红黑树为空树

直接把插入结点作为根节点,然后设置为黑色即可;(性质2:根节点是黑色的)

场景2:插入结点的Key已存在

插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入

场景3:插入结点的父结点为黑色

直接插入;
由于插入的结点是红色的,当插入结点的父节点是黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡;

场景4:插入结点的父结点为红色

如果插入的父节点是红色的,那么该父节点不可能是根节点,因为根据性质2:根节点是黑色的;所以插入的节点总是存在祖父节点,并且祖父节点肯定是黑色的,因为根据性质4:不可以同时存在两个相连的红色节点;

整理不下去了…还是直接看原文吧…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值