二叉树,二叉搜索树,AVL树,红黑树。学习笔记

树的基本概念

维基百科对树的分类

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

树的基本概念与术语

在计算器科学中,(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 每个节点有零个或多个子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;

下图引用博主javazejian的图

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

树的基本术语

若一个结点有子树,那么该结点称为子树根的”双亲”,子树的根是该结点的”孩子”。有相同双亲的结点互为”兄弟”。一个结点的所有子树上的任何结点都是该结点的后裔。从根结点到某个结点的路径上的所有结点都是该结点的祖先。

  • 结点的度:结点拥有的子树的数目。
  • 叶子:度为零的结点。
  • 分支结点:度不为零的结点。
  • 树的度:树中结点的最大的度。
  • 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1。
  • 树的高度:树中结点的最大层次。
  • 无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。
  • 有序树:如果树中结点的各子树之间的次序是重要的, 不可以交换位置。
  • 森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。

二叉树的定义

在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

二叉树是每个节点最多有两个子树的树结构。

它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

二叉树的性质

二叉树有以下几个性质

  • 性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)。
  • 性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)。
  • 性质3:包含n个结点的二叉树的高度至少为log2 (n+1)
  • 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1

二叉树的遍历

前序遍历(Pre-Order Traversal)

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

递归方式实现前序遍历

具体过程:

  1. 先访问根节点
  2. 再序遍历左子树
  3. 最后序遍历右子树

代码实现

public void preOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    System.out.print(root.getValue());
    preOrder(root.getLeft());
    preOrder(root.getRight());
}

中序遍历(In-Order Traversal)

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

递归方式实现中序遍历

具体过程:

  1. 先中序遍历左子树
  2. 再访问根节点
  3. 最后中序遍历右子树
public void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    inOrder(root.getLeft());
    System.out.print(root.getValue());
    inOrder(root.getRight());
}

后序遍历(Post-Order Traversal)

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

递归方式实现后序遍历

  1. 先后序遍历左子树
  2. 再后序遍历右子树
  3. 最后访问根节点
public void postOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    postOrder(root.getLeft());
    postOrder(root.getRight());
    System.out.print(root.getValue());
}

Java代码实现二叉树:

TreeNode.java

public class TreeNode {
    private final char value;
    private TreeNode left;
    private TreeNode right;

    public TreeNode(char value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }

    public char getValue() {
        return value;
    }

    public TreeNode getLeft() {
        return left;
    }

    public void setLeft(TreeNode left) {
        this.left = left;
    }

    public TreeNode getRight() {
        return right;
    }

    public void setRight(TreeNode right) {
        this.right = right;
    }
}

TreeCreator.java

public class TreeCreator {
    public TreeNode createSampleTree() {
        TreeNode root = new TreeNode('A');
        root.setLeft(new TreeNode('B'));
        root.getLeft().setLeft(new TreeNode('D'));
        root.getLeft().setRight(new TreeNode('E'));
        root.getLeft().getRight().setLeft(new TreeNode('G'));
        root.setRight(new TreeNode('C'));
        root.getRight().setRight(new TreeNode('F'));
        return root;
    }
}

TreeTraversal.java

public class TreeTraversal {
    public void preOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        System.out.print(root.getValue());
        preOrder(root.getLeft());
        preOrder(root.getRight());
    }

    public void inOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        inOrder(root.getLeft());
        System.out.print(root.getValue());
        inOrder(root.getRight());
    }

    public void postOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        postOrder(root.getLeft());
        postOrder(root.getRight());
        System.out.print(root.getValue());
    }

    public static void main(String[] args) {
        TreeCreator creator = new TreeCreator();
        TreeTraversal traversal = new TreeTraversal();

        TreeNode sampleTree = creator.createSampleTree();
        System.out.print("前序遍历:");
        traversal.preOrder(sampleTree);
        System.out.print("\n中序遍历:");
        traversal.inOrder(sampleTree);
        System.out.print("\n后续遍历:");
        traversal.postOrder(sampleTree);
    }
}

特殊的二叉树

  1. 满二叉树
  2. 完全二叉树和
  3. 二叉查找树

满二叉树

定义:高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

完全二叉树

定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。
特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

二叉查找树(Binary Search Tree)

算法

平均

最差

空间

O(n)

O(n)

搜索

O(log n)

O(n)

插入

O(log n)

O(n)

删除

O(log n)

O(n)

二叉查找树定义

二叉查找树(英语:Binary Search Tree),也称为二叉搜索树有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;
  4. 没有键值相等的节点。

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

提示:二分搜索树,英文原名为(Binary Search Tree),中文翻译过来有多个版本,二分搜索树、二分查询树、二查搜索树等等。下面统一用英文缩写BST表示

BST 的操作代价分析:

(1) 查找代价: 任何一个数据的查找过程都需要从根结点出发,沿某一个路径朝叶子结点前进。因此查找中数据比较次数与树的形态密切相关。

当树中每个结点左右子树高度大致相同时,树高为logN。则平均查找长度与logN成正比,查找的平均时间复杂度在O(logN)数量级上。

当先后插入的关键字有序时,BST退化成单支树结构。此时树高n。平均查找长度为(n+1)\/2,查找的平均时间复杂度在O(N)数量级上。

(2) 插入代价: 新结点插入到树的叶子上,完全不需要改变树中原有结点的组织结构。插入一个结点的代价与查找一个不存在的数据的代价完全相同。

(3) 删除代价: 当删除一个结点P,首先需要定位到这个结点P,这个过程需要一个查找的代价。然后稍微改变一下树的形态。如果被删除结点的左、右子树只有一个存在,则改变形态的代价仅为O(1)。如果被删除结点的左、右子树均存在,只需要将当P的左孩子的右孩子的右孩子的…的右叶子结点与P互换,在改变一些左右子树即可。因此删除操作的时间复杂度最大不会超过O(logN)。

BST效率总结 :

查找最好时间复杂度O(logN),最坏时间复杂度O(N)。

插入删除操作算法简单,时间复杂度与查找差不多

二分搜索树存在的问题

回顾下定义

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;
  4. 没有键值相等的节点。

按照这个定义,缺点就是可能导致不平衡:某一边的子树高度远远大于另外一边的子树。这样在插入,删除,查找时最坏情况的复杂度达到了O(n)。

比如下图,左边是理想情况,但是可有可能会出现右边最差的情况

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

所以为了避免这种最坏的情况,我们引出了AVL树和红黑树

AVL树

算法

平均

最差

空间

O(n)

O(n)

搜索

O(log n)

O(log n)

插入

O(log n)

O(log n)

删除

O(log n)

O(log n)

在计算机科学中,AVL是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log n)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL树得名于它的发明者G. M. Adelson-Velsky和Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。

节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。

简单总结:

  • AVL树也是二叉查找树(BST)
  • 为了维持平衡,引入了高度和平衡因子的概念。
  • 在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树
  • 查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(log n)。相比于二叉查找树最坏情况为O(n)有提高

AVL如何保持平衡?

答案就是:“旋转”

以下图表以四列表示四种情况,每行表示在该种情况下要进行的操作。在左左和右右的情况下,只需要进行一次旋转操作;在左右和右左的情况下,需要进行两次旋转操作。

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

AVL 的操作代价分析:

(1) 查找代价: AVL是严格平衡的BST(平衡因子不超过1)。那么查找过程与BST一样,只是AVL不会出现最差情况的BST(单支树)。因此查找效率最好,最坏情况都是O(logN)数量级的。

(2) 插入代价: AVL必须要保证严格平衡(|bf|<=1),那么每一次插入数据使得AVL中某些结点的平衡因子超过1就必须进行旋转操作。事实上,AVL的每一次插入结点操作最多只需要旋转1次(单旋转或双旋转)。因此,总体上插入操作的代价仍然在O(logN)级别上(插入结点需要首先查找插入的位置)。

(3) 删除代价:AVL删除结点的算法可以参见BST的删除结点,但是删除之后必须检查从删除结点开始到根结点路径上的所有结点的平衡因子。因此删除的代价稍微要大一些。每一次删除操作最多需要O(logN)次旋转。因此,删除操作的时间复杂度为O(logN)+O(logN)=O(2logN)

AVL 效率总结 :

查找的时间复杂度维持在O(logN),不会出现最差情况

AVL树在执行每个插入操作时最多需要1次旋转,其时间复杂度在O(logN)左右。

AVL树在执行删除时代价稍大,执行每个删除操作的时间复杂度需要O(2logN)。

AVL的问题

二叉查找树(BST)的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢?

能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。

红黑树(Red–black tree)

算法

平均

最差

空间

O(n)

O(n)

搜索

O(log n)

O(log n)

插入

O(log n)

O(log n)

删除

O(log n)

O(log n)

提示:红黑树可以与2-3树互相转换,有兴趣的可以研究下

红黑树性质

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

下面是一个具体的红黑树的图例:

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些性质并使算法复杂。为此,本文中我们使用”nil叶子”或”空(null)叶子”,如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。

RBT 的操作代价分析

(1) 查找代价:由于红黑树的性质(最长路径长度不超过最短路径长度的2倍),可以说明红黑树虽然不像AVL一样是严格平衡的,但平衡性能还是要比BST要好。其查找代价基本维持在O(logN)左右,但在最差情况下(最长路径是最短路径的2倍少1),比AVL要略逊色一点。

(2) 插入代价:RBT插入结点时,需要旋转操作和变色操作。但由于只需要保证RBT基本平衡就可以了。因此插入结点最多只需要2次旋转,这一点和AVL的插入操作一样。虽然变色操作需要O(logN),但是变色操作十分简单,代价很小。

(3) 删除代价:RBT的删除操作代价要比AVL要好的多,删除一个结点最多只需要3次旋转操作。

RBT 效率总结 : 

查找 效率最好情况下时间复杂度为O(logN),但在最坏情况下比AVL要差一些,但也远远好于BST。

插入和删除操作改变树的平衡性的概率要远远小于AVL(RBT不是高度平衡的)。因此需要的旋转操作的可能性要小,而且一旦需要旋转,插入一个结点最多只需要旋转2次,删除最多只需要旋转3次(小于AVL的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在O(logN),但是实际上,这种操作由于简单所需要的代价很小。

AVL与红黑树比较

图来自知乎https://www.zhihu.com/question/19856999

《二叉树,二叉搜索树,AVL树,红黑树。学习笔记》

 

红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。

参考:

https://blog.csdn.net/javazejian/article/details/53727333

https://www.cnblogs.com/skywang12345/p/3576328.html

二叉树各种遍历的动画演示:https://jsrun.net/FcpKp

二叉树:https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8F%89%E6%A0%91

二叉树插入、删除动画:https://visualgo.net/zh/bst

http://btv.melezinek.cz/binary-search-tree.html

有非递归遍历的动画演示https://cloud.tencent.com/developer/article/1150590

https://study.163.com/course/courseLearn.htm?courseId=468002#/learn/video?lessonId=1060074&courseId=468002

红黑树比 AVL 树具体更高效在哪里?https://www.zhihu.com/question/19856999

这个对AVL和红黑树对比讲的比较好:

https://troywu0.gitbooks.io/spark/content/b-%E6%A0%91-%E7%BA%A2%E9%BB%91%E6%A0%91-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91-avl%E6%A0%91-%E6%AF%94%E8%BE%83.html

 

 

    原文作者:AVL树
    原文地址: https://blog.csdn.net/CrankZ/article/details/83301702
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞