前言
【从蛋壳到满天飞】JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)
源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)
全部源代码已上传 github,点击我吧,光看文章能够掌握两成,动手敲代码、动脑思考、画图才可以掌握八成。
本文章适合 对数据结构想了解并且感兴趣的人群,文章风格一如既往如此,就觉得手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,希望对想学习数据结构的人或者正在学习数据结构的人群有帮助。
红黑树
- 红黑树是历史上最有名的一种平衡的二叉树
- 红黑树就是与红色和黑色有关,
- 事实上在红黑树中对于每一个节点都附着了一个颜色,
- 这个颜色或者是红色或者是黑色,
- 对于不同颜色的节点的意思
- 算法导论中的红黑树定义
- 红黑树一定是一棵二分搜索树,
- 红黑树在二分搜索树的基础上和 AVL 树一样添加了一些其它的性质
- 来保证它不会退化成为链表,
- 也就是来保证自己在某种程度上是一颗平衡的二叉树。
- 这些性质分别是
- 每个节点或者是红色的或者是黑色的;
- 根节点一定是黑色的;
- 每一个叶子节点(最后的空节点)是黑色的,这里并不是左右子树都为空的那个节点,
- 而是再向下递归一层的那个最后的空节点才管它叫做叶子节点,
- 也就是说每一个空节点如果也要给它上一种颜色的话,它是黑色的;
- 如果一个节点是红色的,那么它的孩子节点都是黑色的;
- 从任意一个节点到叶子节点,经过的黑色节点是一样的。
- 以上五点性质就是算法导论中对红黑树的一个定义,
- 通常一上来就告诉你这五个定义,然后说满足这五个定义就叫做红黑树,
- 然后根据这些性质开始推倒出红黑树的更多的性质或者结论,
- 以至于最终的实现其实是很难理解到底什么是红黑树,
- 这样的一个介绍方法最大的问题是,
- 没有介绍清楚对于红黑树这种数据结构来说,它到底是从哪儿来的,
- 到底为什么要把哪个节点定义成有的是红色有的是黑色,
- 而是直接给出了一个特别生硬的定义,告诉别人红黑树就是这个样子。
- 大名鼎鼎的算法 4 是红黑树的发明人写的
- 这个教材中对红黑树的定义是最好的红黑树的介绍,
- 算法 4 这本教材它的作者是一位老爷爷,叫做 Robert Sedgewick,
- 它正是红黑树的发明人,事实上红黑树的发明人不能完全归功于这位老爷爷,
- 还有一位共同作家和他一起在当年发表了非常轰动的论文提出了红黑树这样的数据结构,
- 这位老爷爷其实是大有来头的,
- 在计算机领域有一位可以称之为现代计算机科学之父的牛人叫做 Donald Knuth,
- 这位红黑树的发明人老爷爷正是这位牛人的弟子,也就是它的学生,
- 这位牛人高纳德先生是现代计算机科学的前驱,
- 近乎没有这位高纳德先生就没有现在学习的和算法分析相关的各种复杂性理论相关的内容,
- 如果没有这些内容,很有可能现在还不能非常客观的去评价算法的好坏,
- 如果不能客观的评价算法的好坏,其实也很难去进一步的优化自己的算法,
- 而这位高纳德先生他的一生做过了很多了不起的壮举,
- 离现在时间点最近的他做过的一件非常重要的事情,就是在编纂一套书,
- 中文叫做计算机编程的艺术,这套书其实还没有出版完,但是已经举世瞩目,
- 在微软最辉煌的时候,比尔盖茨就曾经说过,对于这套书,当时这套书只是出版了两本,
- 比尔盖茨就声称如果对于这两本书你曾经读过并且读懂了的话,
- 那么就可以直接把简历投给比尔盖茨,可见这套书的价值以及它的分量,
- 这套书现在也已经有了中文的译本,如果有兴趣也可以找来挑战一下,
- 是计算机科学领域尤其是算法这个领域的一套非常重要的著作。
- 大名鼎鼎的算法 4 这本教材中的介绍
- 这个教材中对红黑树的定义是最好的红黑树的介绍,
- 在算法 4 这本书中对红黑树的介绍
- 直接绕开这些算法导论中所说的一上来就摆出红黑树五个基本性质,
- 而是首先探索了另外一种平衡的树,这种平衡的树叫做 2-3 树,
- 事实上红黑树与 2-3 树本身是等价的,
- 如果理解了 2-3 树与红黑树之间的这种等价关系以后就会发现,
- 其实红黑树并不难,不仅如此,之前在算法导论中对于红黑树的五个基本性质,
- 你再去看时就会发现其实它们是非常自然的,所以要首先介绍 2-3 树,
- 如果真正的能够掌握 2-3 树这种数据结构,不仅对理解红黑树有巨大的帮助,
- 同时对于理解在数据结构中另外一类非常重要的数据结构,
- 也就是通常用于磁盘存储或者文件系统数据库相应的这种数据存储的数据结构 B 类树,
- 也是有巨大的帮助的。
2-3 树
- 2-3 树这种数据结构与之前大多数数据结构有一些不同的地方
- 之前介绍的大多数数据结构每一个节点相应的它的结构是一致的,
- 而 2-3 树有所不同,首先它依然是满足二分搜索树的基本性质,
- 但是在满足这个基本性质的基础上它并不是一种二叉树,
- 事实上 2-3 树有两种节点,有一种节点可以存放一个元素,
- 还有一种节点可以存放两个元素,如下图,
- 一种节点和二分搜索树的节点一样,另外一种节点是一个节点里存放了两个元素,
- 相应的它有三个孩子,这三个孩子分别在第一个元素的左侧,两个元素的中间,
- 第二个元素的右侧,相应的就可以想到和二分搜索树一样,对于二分搜索树来说,
- 这个节点左孩子值小于这个节点的值,右孩子的值大于这个节点的值,
- 那么在二三树中,对于这个可以存放两个元素的节点它也满足二分搜索树的性质,
- 左孩子的是小于这个节点中第一个元素的值,
- 中间的这个孩子的值是在第一个元素和第二个元素之间的,
- 相应的右孩子的值是比第二个元素还要大的,
- 这就是所谓的满足二分搜索树的基本性质意义。
// (a) (b c) // / \ / | \ 复制代码
- 二三树的特征
- 二三树的这两种节点来说,每一个节点或者有两个孩子或者有三个孩子,
- 这也就是 2-3 树这个名称的由来,
- 通常管这种存放一个元素又有两个孩子的节点,在二三树中叫做二节点,
- 相应的这种存放两个元素又有三个孩子的节点,在二三树中叫做三节点,
- 对于一棵二三树来说,相应的有可能由这两种节点组成,如下图,
- 都满足二分搜索树的性质,根据二三树相应的性质可以推导出,
- 如何在一棵二三树中如何进行搜索查询,其实是非常简单的,
- 和二分搜索树的基本思路也是一样的,只不过搜索的过程来到了一个三节点,
- 就要比较一下,如果小于三节点的左值就到三节点的左子树中进行寻找,
- 大于三节点的右值那么就到三节点的右子树中进行寻找,
- 但是如果如果要寻找的值在三节点中间的话,
- 就到这个三节点的中间这棵子树中继续去寻找,
- 这是非常简单的,对于二三树来说它有一个非常重要的性质,
- 实际上这个性质是和二三树本身插入元素的时候构建的方法是相关的,
- 这个性质就是二三树是一棵绝对平衡的树,
- 实际上二三树是这个课程中到现在为止所学习过的唯一一棵绝对平衡的树,
- 绝对平衡就是从根节点到任意一个叶子节点所经过的节点数量一定是相同的,
- 不平衡的二分搜索树,二分搜索树它可能会退化成链表、对于堆来说它虽然是完全二叉树,
- 但是由于最后一层叶子节点有可能没有填满,所以不能叫做绝对平衡,线段树是同理的,
- 它们的叶子节点分布在最后一层的倒数第二层,所以也不能叫做绝对平衡,
- 关于 Trie 或者是并查集更不用说了,他们肯定不是平衡的树结构,
- AVL 树虽然叫做平衡二叉树,
- 但是这个平衡二叉树的定义是对于任意一个节点左右子树的高度差是不超过一的,
- 所以是比这种绝对平衡的条件要宽松的,而对于二三树来说,
- 它满足对于任意一个节点来说,左右子树的高度一定是相等的,
- 二三树维持这种绝对的平衡是在添加节点的时候使用了一种机制来维护绝对平衡的,
- 理解添加节点的时候二三树这种维护绝对平衡的机制
- 对理解红黑树它的运作机制是非常重要的。
// ( 42 ) // / \ // ( 17 33 ) (50) // / | \ / \ // (6 2) (18) (37)(48)(66 88) 复制代码
树的绝对平衡性
- 二三树是一种既有二节点又有三节点的满足二分搜索树基本性质的这样的一种数据结构
- 二三树是一种可以保持绝对平衡的这样的一种树结构,
- 可以通过添加节点来查看二三树究竟是如何维持这种绝对的平衡,
- 理解二三树维持这种绝对平衡之后就可以看到红黑树其实和二三树是等价的。
- 二三树的添加操作
- 首先你在一棵空树添加一个新节点 42,那么新节点将作为这棵树的根节点 42,
- 这个根节点就是平衡的,然后你再添加一个新节点 37,
- 虽然根节点 42 左子树都为空,但是这个新节点不会添加到这个空的位置,
- 而是会融合到这个根节点中,根节点本来是一个二节点,
- 但是通过融合操作,根节点变成了一个三节点,
- 此时这棵二三树依然是平衡的,它依然只有一个节点,
- 只不过这个节点从二节点变成了三节点,同时里面有两个元素,
- 如果向这棵二三树再添加一个节点 12,
- 按道理讲因该添加进根节点(37, 42)的左子树中去,
- 但是根节点(37, 42)的左子树为空,而在二三树中添加节点,
- 新的节点永远不会去那个空的位置,只会和最后找到的叶子节点做融合,
- 那么在这里最后找到的叶子节点是根节点(37, 42)这样的一个三节点,
- 在此时依然先进行一下融合,暂时形成一个四节点,
- 也就是容纳了三个元素的节点,相应的它可以有四个孩子,
- 但是对于二三树来说它不可以有四节点,它最多只能有三节点,
- 也就是一个节点中容纳两个元素,有三个孩子,
- 对于这种四节点可以非常容易的直接将它分裂成一棵子树,
- 也就是将一个四节点转而变成了一个由三个二节点组成的一棵平衡的树,
- 这样一来就很像是一棵正常的二分搜索树,也可以把它理解成是一棵二三树,
- 只不过每一个节点都是二节点,同时这棵树依然保持着绝对的平衡,
- 从一个空树开始添加节点,添加一个节点添加两个节点添加三个节点
- 都能保持一个绝对的平衡,如果在这棵树的基础上再来添加一个节点 18,
- 对于 18 这个元素来说根节点是 37,所以需要将 18 添加到根节点 37 的左子树中,
- 这是和二分搜索树是一致的,那么对于它的左子树 12 来说,
- 节点 18 是比节点 12 要大的,那么就应该把节点 18 添加到节点 12 的右子树中去,
- 此时节点 12 的右子树已经是空了,在这种情况下,对于二三树的添加来说,
- 它并不会像二分搜索树那样添加到一个空位置上去,
- 而是和它最后找到的那个位置的叶子节点做一个融合,
- 这个叶子节点是节点 12,它是一个二节点,所以它还有空间融合成一个三节点,
- 这样就不会破坏这个二三树的性质,此时这棵二三树依然保持着绝对的平衡,
- 如果再来添加一个新的节点 6,节点 6 比根节点 37 要小,
- 所以它需要添加到根节点 37 的左子树中去,
- 对于这个左子树是一个三节点(12, 18),
- 节点 6 比节点 12 还要小,所以它要添加到这个三节点的左子树中去,
- 不过对于三节点的左子树为空,由于对于二三树添加节点来说,
- 不会把一个新的节点添加到一个空节点上去,
- 而是找到最后它添加的那个位置的叶子节点,和这个叶子节点做融合,
- 如果现在这个叶子节点是三节点的话,那么就会暂时形成一个四节点,
- 之后对这个四节点再进行一个拆解,之前是对根节点是一个四节点进行拆解,
- 是拆解成一棵包含有三个二节点的子树,但是现在对于这个叶子节点进行拆解,
- 拆解成一棵包含有三个二节点的子树,那么这棵二三树就不是一棵绝对平衡的树,
- 对于二三树来说如果一个叶子节点它本身已经是一个三节点了,
- 添加了一个新的节点变成四节点的话,
- 那么对于这个新的四节点拆解成三个二节点的形式之后,
- 这棵子树它有一个新的根节点 12,这个节点 12 要向上去和上面的父亲节点融合去,
- 对于节点 12 的父亲节点是节点 37,是一个二节点,那么就非常容易了,
- 节点 12 和节点 37 可以直接的融合成一个三节点,近而原来节点 12 的这个左右节点 6 和 18,
- 就可以变成这个新的三节点对应的左孩子和中间的这个孩子,
- 那么这个二三树经过刚才的操作,依然保持了绝对的平衡,
- 如果再添加一个新的元素 11,对于根节点(12, 37)来说比 12 要小,
- 所以要插入根节点的左子树中去,根节点的左子树是节点 6,节点 11 比节点 6 要大,
- 所以要插入到节点 12 的右子树中去,不过节点 6 的右子树已经为空了,
- 所以节点 11 和节点 6 直接做一个融合,变成了一个包含了 6 和 11 的三节点,
- 如果再添加一个新节点 5,对于节点 5 这个元素,从根节点(12, 37)开始,
- 节点 5 比根节点要小,所以还是要插入到根节点的左子树中来,
- 对于左子树的这个根节点(6, 11)它也是一个三节点,那么节点 5 比节点 6 还要小,
- 所以节点 5 要插入到节点(6, 11)这个节点的左子树中去,
- 不过节点(6, 11)这个三节点的左子树已经为空了,所以对于二三树的添加来说,
- 节点 5 和最后找到的这个叶子节点(6, 11)做融合,
- 不过这个叶子节点本身是一个三节点,所以首先暂时形成一个四节点,
- 对于这样的一个四节点把他变成三个二节点的子树,对于这样的一棵子树,
- 它的新的根节点,也就是节点 6,相应的融合到父亲节点中去,
- 不过它的父亲节点又是一个三节点,不过没有关系,但是照样做融合,
- 形成一个暂时的四节点,原来节点 6 的两个子树就可以挂接到融合后的四节点上,
- 成为这个四节点相应的两棵子树,依然没有打破二分搜索树的性质,
- 不过对于这个四节点,由于在二三树中最多只能是三节点,
- 所以这个四节点还要继续进行分裂,它的分裂方式和之前依然是一样的,
- 把这一个四节点的三个元素化成是三个二节点,
- 由于之前的那个四节点本身也是根节点,
- 化成七个二节点的树形状之后,也就不需要继续向上去融合新的父亲节点了,
- 因为根节点已经到头了,至此这次添加操作也完成了,
- 现在二三树变成了所有的节点都是二节点的样子,它依然满足二三树的性质,
- 于此同时它依然保持着绝对的平衡,这整个过程是一个很极端的情况,
- 添加的第一个节点其实是 42,之后添加了节点 37,之后添加了节点 18,
- 依此类推,第一次添加的节点是整棵树中存储元素中最大的那个元素,
- 然后后续添加的元素都比这个最大的元素要小,
- 换句话说其实一直向着最大的那个元素的左侧去添加元素,
- 这样的一种添加方式,如果你使用的是一种二分搜索树的话,
- 那么早就已经非常偏斜了,不过在这个过程中,
- 二三树整体的非常神奇的维护了整棵树的绝对平衡的性质,
- 不管怎么添加元素,二三树整体都保持着平衡,
- 这就是二三树可以维持一种绝对平衡的。
总结二三树的添加操作
-
二三树添加元素的过程整体上在二三树中添加一个新元素的过程,
- 不会像二分搜索树那样添加到一个空的节点的位置,
- 它一定是添加到最后搜索到的那个叶子节点的位置,然后和它进行融合操作,
- 如果融合的叶子节点本身是一个二节点,融合之后就形成了一个三节点,
- 非常的容易,但是如果待融合的叶子节点它本身就是一个三节点,
- 其实也并不难,本来是节点(6, 12)这样一个三节点,插入新的元素 2,
- 那么就暂时临时的形成这样的一个四节点(2, 6, 12),它有三个元素四个孩子,
- 那么对于这个四节点可以进行一下变形,变形之后形成了一个三个二节点的子树,
- 对于这个子树来说它有三个二节点,如果融合的这个三节点它本身就是一个根节点,
- 这样做那就直接结束了,非常的容易,可是关键是通常融合的这个三节点,
- 它可能不是一个根节点而是一个叶子节点,在这种情况下,还需要进行处理,
- 其实这个处理过程也非常的简单,整体来讲分成两种情况,
- 如果插入的这个三节点它是一个叶子节点,
- 同时这个样子节点它的父亲节点还是二节点的话,
- 首先暂时将这个元素插入到叶子节点中形成一个临时的四节点,
- 那么对这个临时的四节点,依然是把它拆分成由三个二节点组成的这样的一个子树,
- 只不过在这种时候,
- 把它拆分三个二节点的子树的时候会打破现在的这个二三树的绝对的平衡,
- 那么此时这个四节点变成的三个二节点组成的这个子树的根节点
- 就需要向上进行一个融合,和它的父亲节点进行一个融合,
- 如果它的父亲节点是一个二节点,那么这个融合就非常的简单,
- 相当于就是让它的父亲节点变成一个新的三节点就好了,
- 融合后依然保持的绝对的平衡,
- 同时原来这个节点左右两个孩子也可以正确的放到根节点的子树中,
- 因为根节点是一个三节点了,三节点可以放三个孩子;
- 但是向上融合的父亲节点是一个三节点的话,情况就会稍微复杂一些,
- 但是也非常的简单,那么在这种情况下,对于这个二三树也是一样,
- 先将一个四节点拆分成三个二节点的子树,
- 对应这个子树它的根节点依然进行向上的融合,
- 它向上融合以后,由于它的父亲节点是一个三节点,
- 所以向上融合后它的父亲节点变成了一个临时的四节点,
- 原来这个节点的两个子树也成为了这个临时四节点的孩子节点,
- 由于它的父亲节点变成了一个临时的四节点就需要进行拆分,
- 所以处理方式和之前一样,
- 依然是把这个四节点拆分成由三个二节点组成的这样的一个子树,
- 拆成这样的一个子树之后,这棵子树又有一个新的根节点,
- 这个节点继续向上融合,
- 如果这个节点继续向上融合它的父亲节点是一个二节点,那么非常容易,
- 融合成一个三节点就可以结束了,如果它的父亲节点还是一个三节点,
- 那么又形成了一个新的临时的四节点,对这个新的临时的四节点做同样的操作,
- 一直向上推,直到最终到达了根节点的时候,就不需要向上进行融合了,
- 因为已经到顶了,那么这一轮添加操作就此结束,
- 使用这样的规则就可以保证二三树这样的一种树结构可以维持绝对的平衡。
// 添加元素4 // ( 6, 8 ) ( 6, 8 ) ( 4, 6, 8 ) // / | \ ---> / | \ ---> / | | \ // (2,5) (7) (12) (2,4,5)(7) (12) (2) (5)(7) (12) // (6) // / \ // ---> (4) (8) // / \ / \ // (2) (5) (7) (12) // 复制代码
-
学习二三树的这种数据结构的理解,
- 不仅可以帮助理解红黑树这种数据结构,
- 也可以对学习 B 类树这种数据结构有巨大的帮助。
学习算法的方式
- 学习抽象的算法和数据结构的时候
- 有一个非常重要的学习方法,
- 其实就是用比较小的数据集对自己所设想的算法
- 或者数据结构或者已经有的算法或数据结构的代码进行模拟,
- 在这个模拟的过程中可以更深刻的理解这个逻辑整体的运转过程,
- 很多时候做这样的一个事情是比只是生对着代码去看去想要有效的多。
红黑树和 2-3 树的等价性
-
红黑树这种数据结构本质上是和二三树等价的
- 对于二三树来说就是包含两种节点的树结构,
- 分别管他们叫做二节点和三节点,二节点中存储这样的一个元素,
- 三节点中存储两个元素,相应的二节点就有两个孩子,
- 三节点就有三个孩子。
- 在之前所学习的所有的树结构每一个节点中只能存储一个元素,
- 那么对于红黑树来讲依然是这个样子,
- 这是因为每一个节点中如果只保持含有一个元素的话,
- 那么对这个节点的操作,包括对整个树的操作在具体的代码编写上会简单很多,
- 基于这样的一种方式也可以实现出和二三树一样的逻辑,
- 实际上这样的一种数据结构就是红黑树。
- 对于二三树中的二节点非常简单,因为二节点本身这个节点中就存有一个元素,
- 这和之前所实现的二分搜索树中的节点是一致的,
- 在红黑树中相应的也是相应的这样的一个节点,这个节点只存一个元素,
- 它有左右两个孩子,这就表示一个二节点,非常的简单,
- 但是复杂的是三节点,三节点是二三树中特有的一种节点,
- 那么对于三节点来说相应的它包含有两个元素,可是现在想实现的这种树结构中,
- 每一个节点只能存一个元素,那么非常的简单,由于这个三节点中有两个元素,
- 只好使用两个节点来表示这样的一种三节点,
- 相应的表示的方法就是也和三节点差不多,也是将两个节点平行的连接,
- 它本质上和二三树中的三节点是一致的,相应的两个元素分别存在一个节点中,
- 只是这两个节点并行的连接在一起了,于此同时,
- 由于在二三树中这个三节点是有大小关系的,节点中左边的元素小于右边的元素,
- 相应的在红黑树中并行连接的两个节点,
- 那么左边的节点就应该是右边节点的左孩子,
- 如下图中的对比图,在二分搜索树中就是这个样子,
- 在二三树中的一个三节点就等价成在这个二分搜索树中的样子,
- 其中节点 b 是节点 c 的左孩子,因为 b 比 c 小,
- 为了表示 b 和 c 在原来的二三树中是一个并列的关系,
- 是在一起存放在一个三节点中,那么就在下图的红黑树中以这样的虚线边来连接,
- 之前所实现的二分搜索树其实对边这样的一个对象是并没有相应的类来表示的,
- 同样在红黑树中也没有必要对于每两个节点它们之间所连接的这个边
- 实现一个特殊的类来表示,可是这个虚线的边应该是红色的,
- 怎么来表示这个特殊颜色的边,由于每一个节点它只有一个父亲,
- 换句话说每一个节点和他父亲节点所相连接的那个边的只有一根边,
- 可以把这个边的信息存放在节点上,换句话说把节点 b 做一个特殊的标识,
- 比如让它变成是红颜色,
- 在这种情况下其实就表示节点 b 和父亲节点相连接的那个边是红色的,
- 它是一个特殊的边,实际上它的意思就是节点 b 和他的父亲节点 c
- 在原来的二三树中是一个并列的关系,是一起存放在一个三节点中的,
- 这样一来就巧妙的把特殊的边的信息存放在了节点上,
- 也可以表示同样的属性或者说是同样的逻辑,
- 而不需要添加特殊的代码来维护节点之间的这个边相应的信息,
- 到这里就可以理解了,这个红黑树和二三树是怎样等价的,
- 实际上是进行了一个特殊的定义,
- 在二分搜索树上用这样的两种方式来表示出了对于二三树来说
- 二节点和三节点这两种节点,在这里特殊的地方引入了一种叫做红色的节点,
- 对于红色的节点它的意思就是和他的父亲节点一起表示
- 原来在二三树中的三节点,现在这个二分搜索树相当于就有两种节点了,
- 一种节点是黑节点,其实就是普通的节点,另外一种是红色的节点,
- 也就是定义好的一种特殊节点,所以这种树就叫做红黑树,
- 与此同时,通过这个定义就可以看到在红黑树中,
- 所有的红色节点一定都是向左倾斜的,这个结论其实是定义出来的,
- 并不是推导出来的,这是因为对于二三树中的三节点来说,
- 在红黑树中选择这样的一种方式来进行表征,
- 在其中会将三节点它左边的那个元素当作右边那个元素的左孩子来看待,
- 与此同时左边的这个元素所在的节点是一个红色的节点,
- 所以红色的节点一定是向左倾斜的。
// // 二三树 // (a) (b, c) // / \ / \ // // 红黑树 // [a] [b]---[c] // / \ / \ \ // 二分搜索树 // {a} {c} // / \ / \ // {b} // / \ 复制代码
-
如果有兴趣的话可以自己编写一个二三树
- 对于二三树来说每一个节点中既可以存一个元素也可以存两个元素,
- 如果真正深入的理解了二三树所对应的逻辑,
- 是可以编写出这样的数据结构的,
- 虽然可能代码会复杂一些,但是应该是一个很好的锻炼的过程。
-
看图理解红黑树与二三树是等价的原因
- 在图中,二三树中有三个三节点,红黑树中有三个红节点,
- 因为对于这三个三节点每一个三节点相应的在红黑树中一定会产生一个红色节点,
- 如二三树中节点(17,33)是一个三节点,在二三树中就有一个红节点{17},
- 其中这个红节点{17}是黑节点
[33]
的左孩子, - 这个红节点代表的是与它父亲相连接的边是一条红色的边是一个特殊的边,
- 这是因为红节点{17}与黑节点
[33]
本身在二三树中是合在一起的一个三节点, - 由于对这些节点进行了一个红色的标记,所以把它等价的看成是一棵二三树,
- 如下图中将红黑树绘制成类似二三树这样,
- 就可以很明显的看出来红色节点和它的父亲节点对应了二三树中的三节点,
- 通过这样的例子就更深刻的理解了红黑树和二三树是这样一个等价的关系,
- 这是因为对于任意的一棵二三树都可以使用这样的规则把它转化成一棵红黑树,
- 而且这个转化的过程其实是非常简单的。
// // 二三树中的定义 小括号中一个元素为二节点、 // 小括号中两个元素为三节点。 // ( 42 ) // / \ // ( 17, 33 ) ( 50 ) // / | \ / \ // (6, 12) (18) (37) (48) (66, 88) // // // 红黑树中的定义 中括号中为黑节点、 // 大括号中卫红节点。 // [ 42 ] // / \ // [ 33 ] [ 50 ] // / \ / \ // {17} [37] [48] [88] // / \ / // [12] [18] {66} // / // {6} // 将红黑树 绘制 成类似二三树的样子 // [ 42 ] // / \ // {17} —— [ 33 ] [ 50 ] // / \ \ / \ // {6} —— [12] [18] [37] [48] {66} —— [88] // 复制代码
-
实现红黑树只需要基于二分搜索树映射来进行修改即可
- 这和实现 AVL 树也是基于二分搜索树来进行修改是一样的。
-
红黑树中的颜色
- 红黑树的节点需要增加一个 bool 型的变量来确定这个节点是红色还是黑色,
- 可以直接给这两个颜色设置为两个常量的变量,初始化的时候直接使用这两个变量即可,
- 要么是 RED 要么是 BLACK,这样就很方便的了,每一个节点默认就是红色的,
- 之所以是默认的,是因为你添加的这个节点永远是和一个叶子节点进行一个融合,
- 在红黑树中红色的节点就是代表着它和它的父亲节点本身在二三树中是在一起的
- 是融合在一块儿的,所以在新创建一个节点的时候,也就是新添加了一个节点的时候,
- 由于添加的这个节点总是要和某一个节点进行融合,只不过融合之后还会做别的事情,
- 但不管怎样,它都是先进行一个融合,
- 融合以后或者形成一个三节点或者形成一个临时的四节点,
- 所以对应的在红黑树中新创建一个节点,这个节点的颜色总先将它设置成红颜色,
- 代表它要在这棵红黑树中和所对应的那个等价的二三树中对应的某一个节点进行融合,
- 这也是红黑树与二三树之间的那种等价关系的体现。
代码示例
-
MyRedBlackTree
// 自定义红黑树节点 RedBalckTreeNode class MyRedBalckTreeNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK; } // @Override toString 2018-11-25-jwl toString() { return ( this.key.toString() + '--->' + this.value.toString() + '--->' + (this.color ? '红色节点' : '绿色节点') ); } } // 自定义红黑树 RedBlackTree class MyRedBlackTree { constructor() { MyRedBlackTree.RED = true; MyRedBlackTree.BLACK = false; this.root = null; this.size = 0; } // 比较的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 根据key获取节点 - getNode(node, key) { // 先解决最基本的问题 if (node === null) return null; // 开始将复杂的问题 逐渐缩小规模 // 从而求出小问题的解,最后构建出原问题的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 添加操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); } // 添加操作 递归算法 - recursiveAdd(node, key, value) { // 解决最简单的问题 if (node === null) { this.size++; return new MyRedBalckTreeNode(key, value); } // 将复杂的问题规模逐渐变小, // 从而求出小问题的解,从而构建出原问题的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; return node; } // 删除操作 返回被删除的元素 + remove(key) { let node = this.getNode(this.root, key); if (node === null) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 删除操作 递归算法 + recursiveRemove(node, key) { // 解决最基本的问题 if (node === null) return null; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); return node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); return node; } else { // 当前节点的key 与 待删除的key的那个节点相同 // 有三种情况 // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了 // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了 // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法 // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点 // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } else if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); this.size++; // 开始嫁接 当前节点的左右子树 predecessor.left = node.left; predecessor.right = node.right; // 将当前节点从根节点剔除 node = node.left = node.right = null; this.size--; // 返回嫁接后的新节点 return predecessor; } } } // 删除操作的两个辅助函数 // 获取最大值、删除最大值 // 以前驱的方式 来辅助删除操作的函数 // 获取最大值 maximum(node) { // 再也不能往右了,说明当前节点已经是最大的了 if (node.right === null) return node; // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案 return this.maximum(node.right); } // 删除最大值 removeMax(node) { // 解决最基本的问题 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 开始化归 node.right = this.removeMax(node.right); return node; } // 查询操作 返回查询到的元素 + get(key) { let node = this.getNode(this.root, key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含该key的元素的判断值 + contains(key) { return this.getNode(this.root, key) !== null; } // 返回映射中实际的元素个数 + getSize() { return this.size; } // 返回映射中是否为空的判断值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [
`; // 以非递归的前序遍历 输出字符串 let stack = new MyLinkedListStack(); stack.push(this.root); if (this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()}
`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()},
`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ]
`; return mapInfo; } } 复制代码
红黑树的基本性质和复杂度分析
-
红黑树与二三树是等价的
- 二三树中的二节点和三节点这样的两种节点类型,
- 二节点在红黑树中都可以使用一个节点中存储一个元素
- 它有两个孩子的这样的一个节点来表示,
- 只不过对于三节点来说只需要有两个这样的节点来表示,
- 其中还将一个节点标注成了红色来表示它和它的父亲节点作为两个元素合在一起,
- 从而用来表示二三树中的一个三节点,这样一来就解释了红黑树这个名字的由来,
- 包含了红色的节点和黑色的节点这样的两种节点,理解了红黑树与二三树本身是等价的时候,
- 就可以回过头来,再来看一下算法导论中的红黑树。
-
算法导论中对红黑树的定义
- 每个节点或者是红色的或者是黑色的;
- 根节点一定是黑色的;
- 每一个叶子节点(最后的空节点)是黑色的,这里并不是左右子树都为空的那个节点,
- 而是再向下递归一层的那个最后的空节点才管它叫做叶子节点,
- 也就是说每一个空节点如果也要给它上一种颜色的话,它是黑色的;
- 如果一个节点是红色的,那么它的孩子节点都是黑色的;
- 从任意一个节点到叶子节点,经过的黑色节点是一样的。
-
看图理解算法导论中对红黑树的定义
- 第一条,红黑树中的确每个节点不是红就是黑;
- 第二条,在二三树中根节点要么是二节点要么是三节点,
- 但是在红黑树中,三节点是通过一个红色节点和一个黑色节点以父子连接的方式来表示的,
- 如果根节点是二节点,对应的在红黑树中相应的这个根节点就是黑色节点,
- 如果在二三树中根节点是三节点,包含有两个元素,那么相应的对应于红黑树来说,
- 和它等价的这个根节点就变成了红色节点是黑色节点的左子树这样的情况,
- 也就是红色节点变成了黑色节点的左孩子,在这种情况下红黑树中根节点还是黑色节点,
- 所以不管是在二三树中,对应的根节点是二节点还是三节点,
- 对应到红黑树中一定是一个黑色的节点,
- 如果理解了在二三树中这两种节点所对应的红黑树中的表现形式,
- 这一条性质也是非常好理解的;
- 第三条,每一个叶子节点(最后的空节点)是黑色的,
- 这里并不是左右子树都为空的那个节点,与其说这是一条性质不如说它是一条定义,
- 它相当于是在说在红黑树中定义 空这样的一个节点本身它是黑色的,
- 对于这样的一个定义,可以写一个函数,传入一个节点,判断这个节点是否为空,
- 如果为空的话就返回它是黑色的,本身也是这一条性质相应的一个逻辑体现,
- 与此同时这一条性质是与上一条性质相吻合的,
- 对于上一条的性质在红黑树中根节点一定是黑色的,
- 之前举的例子都是红黑树它的根节点是存在的,
- 这个根节点是二三树中的二节点形态或者是三节点形态,
- 对应到红黑树中都是一个黑色节点,不过还存在一种情况,
- 就是一棵空树本身它本身也是一棵红黑树,对于一棵空树来说,它本身是空,
- 相应的它的根节点也是空,第二条性质上说跟节点一定是黑色的,
- 在这里 空 这个根节点也是黑色的,这就和第三条性质其实连在了一起,
- 在极端的情况下,整棵树都是空的时候,这个空既是叶子节点又是根节点,
- 在这种情况下就定义它是一个黑色的节点;
- 第四条,如果如果在红黑树中一个节点本身是红色的,
- 那么这个红色节点对应的它的孩子节点一定是一个黑色的节点,
- 在红黑树中只在它表示的是原来二三树中的三节点时,
- 对应的三节点左侧的这个元素所在的节点在红黑树中就是一个红色的节点,
- 这个红色的节点它的孩子节点对应的就是原先在二三树中对应的左孩子或者是中间孩子,
- 不管它对应的是左孩子还是中间的孩子,
- 在原来的二三树中相应的所连接的这个节点要么是一个二节点要么是一个三节点,
- 如果它连接的孩子节点是一个二节点那么很显然对应的就是红黑树中的黑色节点,
- 此时这个红色的节点它的孩子节点一定是黑色的节点,
- 如果它连接的孩子节点是一个三节点的话,
- 那么其实和之前看根节点是黑色的节点是一样的,
- 它连接的虽然是一个三节点,但是所连接的这个三节点对应的红黑树的表现形式是
- 黑色节点为父红色节点为左孩子,所以它就需要先连接上这个黑色节点,
- 再让这个黑色节点连接上左侧的红色孩子节点,所以对于红色节点的两个孩子,
- 不管谁是三节点,首先接的一定是一个黑色的节点,
- 只不过这个黑色的节点它的左孩子又是一个红色的节点,所以在这种情况下,
- 这个红节点的孩子依然是一个黑色的节点,那么整体上就有了第四条的性质,
- 如果一个节点是红色的,那么他的孩子节点都是黑色的,
- 这个结论对黑色的节点不成立,黑色的节点的右孩子一定是黑色的,
- 它的左孩子有可能是红色的,它的原因和红色的节点它的孩子节点是黑色的原因一致;
- 第五条,也就是最后一条性质,这条性质近乎是红黑树的核心,
- 也就是在一棵红黑树中从任意一个节点出发到叶子节点,经过的黑色节点一定是一样多的,
- 这里强调的是黑色节点是一样多的,但是经过的红色节点不一定是一样多的,
- 核心还是因为红黑树和二三树之间是一个等价的关系,二三树是一颗绝对平衡的树,
- 一棵绝对平衡的树意味着从二三树中的任意一个节点出发到叶子节点
- 所经过的节点数是一样多的,这是因为二三树是绝对平衡的,
- 所以所有的叶子节点都在同一层上,他们的深度是一致的,那么从任意一个节点出发,
- 那么任意一个节点它是有一个固定的深度的,
- 从这个节点向下到达它任意一个可以达到的叶子节点,相应的向下走的深度就是一样的,
- 也就意味着它经过的节点数量是一样的,在二三树中有这样的一个性质,
- 对应到红黑树中,其实就对应着它走过的黑色的节点是一样多的,
- 这是因为在二三树中无论是二节点还是三节点,
- 相应的转换成红黑树中的节点表示的时候都会有一个黑色的节点,
- 所以从红黑树中任意一个节点出发,每经过一个黑色的节点,
- 其实就等于是一定经过了原来的二三树中的某一个节点,
- 区别只是在于经过的这个黑色的节点如果它的左孩子是红色的节点的话,
- 那么相应的其实就是经过原来二三树中的一个三节点,
- 那么此时走到的这个黑节点就是走到了这个二三树中的三节点的一半儿,
- 虽然说是走到了一半儿,但是也是经过了这个三节点,
- 所以由于二三树中不管是二节点还是三节点,在红黑树中都一定有一个黑色的节点,
- 而在二三树中,从任何一个节点到叶子节点经过的节点个数是一样的,
- 相应的在红黑树中就变成了从任意一个节点到叶子节点,经过的黑色节点数量是一样的,
- 可以在下图中进行一下实验,从任意一个节点出发,一直到一个叶子节点,
- 看看经过的叶子节点它的数目是一样的,
- 可以结合在红黑树中从任意一个节点到叶子节点这个路径是什么样子的,
- 然后对应到二三树中相应的是什么样子的,
- 更进一步的深刻理解红黑树和二三树之间的这个等价关系,
- 与此同时理解这条性质在红黑树中,
- 从任意一个节点到叶子节点经过的黑色节点是一样的,
- 根节点肯定是任意节点中的一个节点,也就是说在红黑树中从根节点出发,
- 到任意一个叶子节点经过的黑色节点是一样的,这本身就是红黑树的一个重要的性质。
// // 红黑树中的定义 中括号中为黑节点、 // 大括号中卫红节点。 // 将红黑树 绘制 成类似二三树的样子 // [ 42 ] // / \ // {17} —— [ 33 ] [ 50 ] // / \ \ / \ // {6} —— [12] [18] [37] [48] {66} —— [88] // // 二三树中的定义 小括号中一个元素为二节点、 // 小括号中两个元素为三节点。 // ( 42 ) // / \ // ( 17, 33 ) ( 50 ) // / | \ / \ // (6, 12) (18) (37) (48) (66, 88) 复制代码
-
红黑树是一个保持“黑平衡”的二叉树
- 这个黑平衡是指对于从根节点开始搜索,
- 一直搜索到叶子节点所经历的黑色节点的个数是一样多的,
- 是黑色的一种绝对平衡的这样的一种二叉树,
- 这种黑平衡的二叉树,严格意义上来讲,不是平衡的二叉树,
- 在 AVL 树中对平衡二叉树进行了严格的定义,
- 是指左右子树的高度差不能够超过一,
- 而对于红黑树来说是有可能打破平衡二叉树的定义的,
- 换句话说,在红黑树中一个节点的左右子树的高度差是有可能大于一的,
- 但是红黑树保持了一个看起来非常奇怪的性质,
- 就是它的左右子树的黑色节点的高度差保持的绝对的平衡
- 它的本质其实是在于二三树本身是一棵保持着绝对平衡的树结构。
-
红黑树的复杂度分析
- 对于红黑树来说如果它的节点个数为 n 的话相应的它的最大的高度并不是 logn,
- 而是 2logn,这是因为在最次的情况下从根节点出发,一直到最深的那个叶子节点,
- 可能经过了 logn 这个级别的黑色节点,
- 同时每一个黑色节点它的左子树又都是一个红色的节点,
- 换句话说这条路径上所对应的二三树都是三节点,那么这样一来就有 logn 个红色的节点,
- 所以它的最大高度是 2 倍的 logn,但是 2 这个数是一个常数,
- 所以放在复杂度分析的领域来讲对于红黑树来说它的高度依然是 logn,
- 所以对应的时间复杂度就是
O(logn)
这个级别, - 换句话说在一个红黑树中查找一个元素从根节点出发,
- 依然是使用二分搜索树的方式去查找这个元素,
- 相应的时间复杂度是
O(logn)
这个级别的, - 虽然遍历经历的节点个数最多可能是 2 倍的 logn,
- 修改一个元素首先要查找到这个元素再修改它,它的时间复杂度是
O(logn)
这个级别的, - 添加一个元素和删除一个元素也是在这棵红黑树上
- 从根节点出发向下在一条路径上进行遍历,它们的时间复杂度都是
O(logn)
级别的, - 所以这就是红黑树不会像二分搜索树那样退化成一个链表的具体原因,
- 对于红黑树增删改查的操作相应的时间复杂度都是
O(logn)
这个级别的。
-
红黑树对比 AVL 树的优缺点
- 由于红黑树的最大高度是 2 倍的 logn,这个高度其实会比 AVL 树的最大高度要高,
- 所以其实在红黑树上进行元素的查找相比 AVL 树来说会慢一点,
- 虽然这二者都是
O(logn)
这个级别的, - 但是这不影响红黑树成为一个非常重要的数据结构,
- 甚至比 AVL 树还要重要还要常用,这背后的原因其实是在于对于红黑树来说,
- 添加元素和删除元素这两个操作相比于 AVL 树来说要快速一些,
- 对于数据结构来说如果存储的数据经常要发生这种添加或者删除的变动,
- 相应的使用红黑树就是一个更好的选择,但是如果在数据结构中,
- 存储的这个数据近乎是不会动的话,只是创建好这个数据结构以后,
- 之后的主要操作只在于查询的话其实 AVL 树性能会高一点,
- 虽然这二者查询的时间复杂度都是 O(logn)级别的,
- 对于红黑树和 AVL 树这二者之间相应的这些性能比较还会具体的做实验,
- 只有这样才能够直观的看到这二者的差别。
红黑树添加新元素
-
在一般的面试中了解以上基本概念之后就可以应付大多数的面试问题
- 很少有真正的面试让你从底层去实现一个红黑树,
- 大多数情况只需要了解什么是红黑树,以及他的优缺点到底在哪里,
- 它内部工作的原理到底是怎样的,主要是这些概念性的问题,
- 如果在面试中面试官真的让你白板编程一个红黑树,
- 可能面试官是稍微有一点在刁难你了,
- 像红黑树中添加一个元素这整个的过程其实相对是比较复杂的,
- 代码量也是比较大的,在面试这样的一个环节中,短时间完成这样一个复杂的逻辑,
- 更关键的是这个复杂的逻辑背后其实并不能特别的考察
- 你的算法设计能力或者对数据结构的深入程度,
- 很多时候可能只是有没有准备这部分的内容而已,
- 所以通常情况下认为要面试者去白板编程红黑树中某一个具体的操作
- 并不是一个明显的面试问题,为了能够更深入的理解红黑树,
- 所以还是要从底层对红黑树的一些基本操作进行一下编程。
-
红黑树与二三树是等价的
- 在二三树中添加新的元素,先查找新添加的这个元素的位置,
- 在二三树添加新的元素永远不会在一个空的位置,
- 而会是找到的最后一个叶子节点进行融合,
- 如果你找到的最后一个叶子节点是是一个二节点的话,
- 那么这个新的元素就会直接添加进这个新的节点,从而形成一个三节点,
- 这种情况非常的容易,如果找到的最后一个叶子节点是一个三节点,
- 那么新添加的这个元素也是先融合进这个三节点,暂时形成一个四节点,
- 然后再对这个四节点进行分裂处理,就是分裂成三个二节点,也就是变成一颗子树,
- 作为根节点的那个二节点会再向上与父节点进行融合,
- 如果父节点是一个二节点,那么就会融合成一个三节点,
- 这样一来整棵树的高度还是没有变,还是一棵绝对平衡的树,
- 如果父节点是一个三节点,那么就会融合成一个暂时的四节点,
- 那么会对这个四节点再进行分裂处理,这时会再分裂成一棵子树,
- 然后作为根节点的那个二节点会继续向上进行融合,循环往复,
- 直到到达了这棵二三树最顶层的根节点为止,因为无法再进行融合操作了,
- 整棵树一定会是一棵绝对平衡的树。
-
在红黑树中添加新节点
- 在二三树中添加一个新的元素,首先都是把这个新的元素融合进二三树已有的节点中,
- 之前有讲过在红黑树中红色的节点其实就是表示的是在二三树中的
- 三节点里两个元素中最左侧的那个元素,所以把它设计成红色,
- 它代表的是这个节点和他的父亲节点这两个节点本身应该合在一起,
- 等价于二三树中的一个三节点,正是因为这个原因,
- 在红黑树中添加新的元素的时候,这个新的元素所在的节点永远让它是一个红色的节点,
- 这代表的是 等价于在二三树中添加一个新的元素的时候,
- 这个新的元素永远是首先要融合进一个已有的节点中,
- 在红黑树中添加一个红色的节点之后有可能会破坏红黑树的基本性质,
- 之后再做相应的一些调整工作,让它继续维持红黑树的基本性质就好了,
- 所以对于红黑树中的节点添加了一个 color 属性值,
- 那么相应的红黑树中的节点的构造函数中默认让这个 color 是等于 RED 的等于红色的,
- 就是这个原因,在红黑树中,每当 new 一个新的节点的时候这个节点都是一个红色的节点。
-
在红黑树中添加一个元素最初始的情况
- 最初始的情况就是整棵红黑树为空,添加一个节点 42,添加的这个节点默认是红色,
- 这个节点会作为根节点,但是在红黑树中有一个非常重要的性质,
- 根节点必须是黑色的,那么就需要做一件事情,就是让根节点变成黑色的。
-
保持根节点为黑色的节点
- 添加重新给根节点赋值之后,就可以给根节点进行染色操作,直接将 color 设置为黑色。
-
添加操作的情况
- 已经有一个根节点 42,插入一个新节点 37,那么这个节点是红色的,
- 按照二分搜索树的添加原则,直接添加为根节点的左孩子,
- 此时依然满足红黑树的定义,所以还是一棵红黑树。
- 假设根节点是 37,但是如果插入一个新节点 42,根据二分搜树的添加原则,
- 节点 42 比节点 37 大,就会被添加为根节点的右孩子,
- 此时不满足红黑树的定义了,因为在红黑树中定义了红色节点只能放在左子树的位置,
- 所以破坏了红黑树的性质,需要进行左旋转。
-
左旋转
- 此时的做法和在 AVL 树中的操作是一样的,需要做一次左旋转,
- 通过左旋转将新节点 42 变成根节点,节点 37 变成红色节点,
- 然后添加为根节点的左孩子,操作过程如下图,
- 让节点 x 与其左子树 T2 断开连接,再让节点 node 与右子树 X 断开连接,
- 让节点 X 的左子树与节点 node 进行连接,让节点 node 的右子树与 T2 连接,
- 有一个逆时针的旋转过程,还有染色过程,如果原来 node 是黑色,那么 x 也就是黑色,
- 如果原来 node 是红色,那么 x 也就是红色,但是 node 成为了 x 的左孩子,
- 并且和 x 形成了一个三节点,所以 node 需要变成红颜色的节点,
- 也许问题来了,如果原来 node 是红色,x 后来也变成了红色,
- 然后 node 还是红色,node 与其父节点 x 一起组成了一个三节点,
- 在红黑树中三节点两个元素都是红色,那么就违背了红黑树的定义,
- 可是左旋转只是一个子过程,虽然在左旋转之后有可能产生连续的两个红色节点,
- 但是左旋转之后会将新的根节点 x 传回去之后,在添加逻辑里,会进行更多的后续处理,
- 这些后续处理会让最终的二叉树不会破坏红黑树的性质,
- 所以在左旋转的过程中并不会去维护红黑树的性质,
- 左旋转的作用只是让这两个节点对应成二三树中的三节点。
// 原来是这样的 , // 中括号为黑色节点,大括号为红色节点, // 小括号只是参与演示,并不真实存在 // [37] node // / \ // (T1) {42} X // / \ // (T2) (T3) // // 进行左旋转后 // [42] x // / \ // node {37} (T3) // / \ // (T1) (T2) // // 代码如此。 // node.right = x.left; // x.left = node; // // x.color = BLACK; // x.color = node.color; // node.color = RED; 复制代码
代码示例
-
MyRedBlackTree
// 自定义红黑树节点 RedBalckTreeNode class MyRedBalckTreeNode { constructor(key = null, value = null, left = null, right = null) { this.key = key; this.value = value; this.left = left; this.right = right; this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK; } // @Override toString 2018-11-25-jwl toString() { return ( this.key.toString() + '--->' + this.value.toString() + '--->' + (this.color ? '红色节点' : '绿色节点') ); } } // 自定义红黑树 RedBlackTree class MyRedBlackTree { constructor() { MyRedBlackTree.RED = true; MyRedBlackTree.BLACK = false; this.root = null; this.size = 0; } // 判断节点node的颜色 isRed(node) { // 定义:空节点颜色为黑色 if (!node) return MyRedBlackTree.BLACK; return node.color; } // node x // / \ 左旋转 / \ // T1 x ---------> node T3 // / \ / \ // T2 T3 T1 T2 leftRotate(node) { const x = node.right; // 左旋转过程 node.right = x.left; x.left = node; // 染色过程 x.color = node.color; node.color = MyRedBlackTree.RED; // 返回这个 x return x; } // 比较的功能 compare(keyA, keyB) { if (keyA === null || keyB === null) throw new Error("key is error. key can't compare."); if (keyA > keyB) return 1; else if (keyA < keyB) return -1; else return 0; } // 根据key获取节点 - getNode(node, key) { // 先解决最基本的问题 if (node === null) return null; // 开始将复杂的问题 逐渐缩小规模 // 从而求出小问题的解,最后构建出原问题的解 switch (this.compare(node.key, key)) { case 1: // 向左找 return this.getNode(node.left, key); break; case -1: // 向右找 return this.getNode(node.right, key); break; case 0: // 找到了 return node; break; default: throw new Error( 'compare result is error. compare result : 0、 1、 -1 .' ); break; } } // 添加操作 + add(key, value) { this.root = this.recursiveAdd(this.root, key, value); this.root.color = MyRedBlackTree.BLACK; } // 添加操作 递归算法 - recursiveAdd(node, key, value) { // 解决最简单的问题 if (node === null) { this.size++; return new MyRedBalckTreeNode(key, value); } // 将复杂的问题规模逐渐变小, // 从而求出小问题的解,从而构建出原问题的答案 if (this.compare(node.key, key) > 0) node.left = this.recursiveAdd(node.left, key, value); else if (this.compare(node.key, key) < 0) node.right = this.recursiveAdd(node.right, key, value); else node.value = value; return node; } // 删除操作 返回被删除的元素 + remove(key) { let node = this.getNode(this.root, key); if (node === null) return null; this.root = this.recursiveRemove(this.root, key); return node.value; } // 删除操作 递归算法 + recursiveRemove(node, key) { // 解决最基本的问题 if (node === null) return null; if (this.compare(node.key, key) > 0) { node.left = this.recursiveRemove(node.left, key); return node; } else if (this.compare(node.key, key) < 0) { node.right = this.recursiveRemove(node.right, key); return node; } else { // 当前节点的key 与 待删除的key的那个节点相同 // 有三种情况 // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了 // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了 // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法 // 1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点 // 2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点 if (node.left === null) { let rightNode = node.right; node.right = null; this.size--; return rightNode; } else if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } else { let predecessor = this.maximum(node.left); node.left = this.removeMax(node.left); this.size++; // 开始嫁接 当前节点的左右子树 predecessor.left = node.left; predecessor.right = node.right; // 将当前节点从根节点剔除 node = node.left = node.right = null; this.size--; // 返回嫁接后的新节点 return predecessor; } } } // 删除操作的两个辅助函数 // 获取最大值、删除最大值 // 以前驱的方式 来辅助删除操作的函数 // 获取最大值 maximum(node) { // 再也不能往右了,说明当前节点已经是最大的了 if (node.right === null) return node; // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案 return this.maximum(node.right); } // 删除最大值 removeMax(node) { // 解决最基本的问题 if (node.right === null) { let leftNode = node.left; node.left = null; this.size--; return leftNode; } // 开始化归 node.right = this.removeMax(node.right); return node; } // 查询操作 返回查询到的元素 + get(key) { let node = this.getNode(this.root, key); if (node === null) return null; return node.value; } // 修改操作 + set(key, value) { let node = this.getNode(this.root, key); if (node === null) throw new Error(key + " doesn't exist."); node.value = value; } // 返回是否包含该key的元素的判断值 + contains(key) { return this.getNode(this.root, key) !== null; } // 返回映射中实际的元素个数 + getSize() { return this.size; } // 返回映射中是否为空的判断值 + isEmpty() { return this.size === 0; } // @Override toString() 2018-11-05-jwl toString() { let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `; document.body.innerHTML += `MyBinarySearchTreeMap: size = ${ this.size }, data = [
`; // 以非递归的前序遍历 输出字符串 let stack = new MyLinkedListStack(); stack.push(this.root); if (this.root === null) stack.pop(); while (!stack.isEmpty()) { let node = stack.pop(); if (node.left !== null) stack.push(node.left); if (node.right !== null) stack.push(node.right); if (node.left === null && node.right === null) { mapInfo += ` ${node.toString()} \r\n`; document.body.innerHTML += ` ${node.toString()}
`; } else { mapInfo += ` ${node.toString()}, \r\n`; document.body.innerHTML += ` ${node.toString()},
`; } } mapInfo += ` ] \r\n`; document.body.innerHTML += ` ]
`; return mapInfo; } } 复制代码