20172308 《程序设计与数据结构》第七周学习总结

Posted zhouyajie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了20172308 《程序设计与数据结构》第七周学习总结相关的知识,希望对你有一定的参考价值。

教材学习内容总结

第 十一 章 二叉查找树

一、概述
二叉查找树是一种含有附加属性的二叉树,即其左孩子小于父结点,父结点小于或等于右孩子
(二叉查找树的定义是二叉树定义的扩展)
二、 用链表实现二叉查找树

  • addElement操作:
    addElement方法根据给定元素的值,在树中的恰当位置添加该元素,如果这个元素不是 comparable,则addElement方法会抛出NoComparableElemementException;
    如果树为空,则这个新元素就将成为根结点;
    如果树非空,这个新元素会与树根元素进行比较:
    如果它小于根结点中存储的那个元素且根的左孩子为null,则这个新元素就将成为根的左孩子。
    如果这个新元素小于根结点中存储的那个元素且根的左孩子不是null,则会遍历根的左孩子,并再次进行比较操作;
    如果这个新元素大于或等于树根存储的那个元素且根的右孩子为null,则这个新元素会成为根的右孩子,
    如果这个新元素大于或等于树根处存储的那个元素且根的右孩子不是null,则会遍历根的右孩子,并再次进行比较操作
    如图:
    技术分享图片

  • removeElemen操作
    removeElement方法负责从二叉查找树中删除给定的 Comparable元素;或者,当在树中找不到给定目标元素时,则抛出 Element Not FoundException异常;
    与前面的线性结构研究不同,这里不能简单地通过删除指定结点的相关引用指针而删除该结点;
    相反,这里必须推选出另一个结点来代替要被删除的那个结点,受保护方法 replacement返回指向一个结点的引用,该结点将代替要删除的结点;
    选择替换结点的三种情况如下:
  1. 如果被删除结点没有孩子,则 replacement返回null
  2. 如果被删除结点只有一个孩子,则 replacement返回这个孩子
  3. 如果被删除结点有两个孩子,则 replacement会返回中序后继者(因为相等元素会放到右边)
    技术分享图片
  • replace方法
    代码如下:
private BinaryTreeNode<T> replacement(BinaryTreeNode<T> node) {
        BinaryTreeNode<T> result = null;
        if ((node.left == null) && (node.right == null)) {
            result = null;
        } else if ((node.left != null) && (node.right == null)) {
            result = node.left;

        } else if ((node.left == null) && (node.right != null)) {
            result = node.right;
        } else {
            BinaryTreeNode<T> current = node.right;// 初始化右侧第一个结点
            BinaryTreeNode<T> parent = node;

            // 获取右边子树的最左边的结点
            while (current.left != null) {
                parent = current;
                current = current.left;
            }

            current.left = node.left;

            // 如果当前待查询的结点
            if (node.right != current) {
                parent.left = current.right;// 整体的树结构移动就可以了
                current.right = node.right;
            }

            result = current;
        }
        return result;
    }
  • removeAllOccurrences操作
    removeAllOccurrences方法负责从二叉查找树中删除指定元素的所有存在;
    或者,当在树中找不到指定元素时,则抛出 ElementNotFoundException异常;
    如果指定的元素不是Comparable,则removeAllOccurrences方法也会抛出 ClassCastException异常,该方法会调用一次 removeElement方法,以此确保当树中根本不存在指定元素时会抛出异常,
    只要树中还含有目标元素,就会再次调用 removeElement方法,注意, removeAllOccurrences方法使用了 LinkedBinaryTree类的 contans方法,还要注意,在 LinkedBinaryTree类中的find方法已经被重载了,以便利用二又查找树的有序属性

  • removeMin操作
    最小元素在二又查找树中的位置有3种可能情形:
  1. 如果树根没有左孩子,则树根就是最小元素,而树根的右孩子会变成新的根结点
  2. 如果树的最左侧结点是一片叶子,则这片叶子就是最小元素,这时只需设置其父结点的左孩子引用为mul即可
  3. 如果树的最左侧结点是一个内部结点,则需要设置其父结点的左孩子引用指向这个将删除结点的右孩子

三、用有序列表实现二叉查找树

  1. 树的主要使用之一就是为其他集合提供高效的实现
  2. 我们假定 BinarySearchTreeList实现中用到的 LinkedBinarySearchTree实现是一种带有附加属性的平衡二叉查找树,这种附加属性是:任何结点的最大深度为log2 n其中n为树中存储的元素数目。
    在我们的平衡二又查找树假设之下,add操作和remove操作都要求重新平衡化树,这一点根据所使用的算法会对分析有所影响。
    另外还要注意一点:虽然树实现中的有些操作更为有效,比如 removelast,last和contains,但在利用树实现时,也有一些操作会变得低效,比如 removeFirst和first

四、平衡二叉查找树

  1. 如果二叉查找树不平衡,其效率可能比线性结构的还要低
  2. 蜕化树:看起来更像一个链表,但实际上它的效率比链表的还低,因为每个结点还附带额外的开销
    如图(b)所示:
    技术分享图片

  3. 平衡化技术
  • 右旋
    要平衡化该树,我们需要:
    使树根的左孩子元素成为新的根元素
    使原根元素成为这个新树根的右孩子元素。
    使树根的左孩子的右孩子,成为原树根的新的左孩子
    技术分享图片

  • 左旋
    要平衡化该树,我们需要:
    使树根的右孩子元素成为新的根元素
    使原根元素成为这个新树根的左孩子元素。
    使原树根右孩子结点的左孩子,成为原树根的新的右孩子。
    技术分享图片

  • 右左旋
    并非所有的不平衡问题都可以只进行某一种旋转就能解决
    对于由树根右孩子的左子树中较长路径而导致的不平衡,
    我们必须先让树根右孩子的左孩子,绕着树根的右孩子进行一次右旋,然后再让所得的树根右孩子绕着树根进行一次左旋
    技术分享图片

  • 左右旋
    对于由树根左孩子的右子树中较长路径而导致的不平衡,
    我们必须先让树根左孩子的右孩子绕着树根的左孩子进行一次左旋,然后再让所得的树根左孩子绕着树根进行一次右旋
    技术分享图片

五、实现二叉查找树:AVL树

  1. 对于树中的任何结点,如果其平衡因子(其左右子树的高度差大于1或小于-1),则以该结点为树根的子树需要重新平衡
  2. 树只有两种途径变得不平衡:插入或删除结点,因此每次进行这两种操作后都必须更新平衡因子,然后从操作的结点处检查树的平衡性,一直检查上溯至根结点
    因此:AVL树通常实现为每个结点都包含一个指向其父结点的引用

六、实现二叉查找树:红黑树
(1). 红黑树:一种平衡二叉查找树,其中的每个结点存储一种颜色(红或黑,用布尔值实现,false等价于红色)
控制结点颜色的规则:

  • 根结点为黑色
  • 红色结点的所有孩子都为黑色
  • 从树根到树叶的每条路径都包含同样数目的黑色结点
    技术分享图片

(2). 在某种程度上,红黑树的平衡限制没有AVL树那么严格,但是,他们的序仍然是logn
(3). 红黑树中的元素插入
红黑树的插入操作类似于前面的addElement方法,但是这里总是把插入的新元素颜色设置为红色,
插入新元素之后,必要时将重新平衡化该树,根据需要改变元素的颜色以便维持红黑树的属性

  • 红黑树插入元素之后的重新平衡化是一个迭代过程,该迭代过程的终止条件有两种形式:
形式1:current == root (current是当前正在处理的结点)
我们总是设置根结点颜色为黑色,而所有路径都包括树根,因此不能违背各条路径都拥有同样数目黑色元素这一规则

形式2:current.parent.color == black(即当前结点的父结点颜色为黑色)
current所指向的结点总是一个红色结点,这意味着,如果当前结点的父结点是黑色,则可满足所有规则,因为红色结点并不影响路径中的黑色结点数目;
另外由于是从插入点处上溯处理,因此早已平衡了当前结点下面的子树
  • 重新平衡化的每次迭代,我们都关注于如何着色当前结点的兄弟结点:
    当前结点的父结点只有两种可能:左孩子或右孩子

可能一:
如果是左孩子,利用 current.parent.parent.left.color 得到颜色信息(null元素的颜色为黑色),且存在两种情况:
父结点的兄弟为红色或黑色:这两种情况下,我们阐述的处理步骤都将发生在一个循环内部(该循环的终止条件如前所述)
如果父结点的兄弟为红色,这时的处理步骤如下:

设置current的父亲的颜色为 black
设置父结点的兄弟的颜色为black
设置 current的祖父的颜色为red
设置 current指向current的祖父

如果父结点的兄弟为黑色,首先要查看current是左孩子还是右孩子:
如果current是右孩子,则必须设置current等于其父亲,在继续之前还要再向左旋转current.right;
后面的步骤,与开始时current为左孩子一样

如果current是左孩子:
设置current的父亲的颜色为black
设置current的祖父的颜色为red
如果current的祖父不等于null,则让current的父亲绕着current的祖父向右旋转

可能二:
如果是右孩子,存在两种情况:
父结点的兄弟为红色或黑色:这两种情况下,我们阐述的处理步骤都将发生在一个循环内部(该循环的终止条件如前所述)

如果父结点的兄弟为红色,这时的处理步骤如下(同当前结点的父结点的兄弟颜色为红时):

设置current的父亲的颜色为 black
设置父结点的兄弟的颜色为black
设置 current的祖父的颜色为red
设置 current指向current的祖父

如果父结点的兄弟为黑色,首先要查看current是左孩子还是右孩子(与当前结点的父结点的兄弟颜色为黑时,操作对称):
如果current是左孩子,则必须设置current等于其父亲,在继续之前还要再向右旋转current.left;
后面的步骤,与开始时current为右孩子一样

如果current是右孩子:
设置current的父亲的颜色为black
设置current的祖父的颜色为red
如果current的祖父不等于null,则让current的父亲绕着current的祖父向左旋转

(4). 红黑树中的元素删除
与元素插入的那些情况一样,删除的两种情况也是对称的——取决于 current是左孩子还是右孩子。
当 current为右孩子时:
(在插入时,我们最关注的是当前结点的父亲的兄弟的颜色)
而对删除而言,焦点要放在当前结点的兄弟的颜色上(用 current.parent. left.color来指代这种颜色):
还要观察该兄弟的孩子的颜色,要注意的重要一点是:颜色的默认值是black;
这样,任何时刻如果试图取得mull对象的颜色,结果都将是black

其他的情况很容易推导出来,只要把上述情况中的“左”换成“右”、“右”换成“左”即可

如果兄弟的颜色是red,则在做其他事之前必须完成如下处理步骤:

* 设置兄弟的颜色为black
* 设置current的父亲的颜色为red
* 让兄弟绕着 current的父亲向右旋转
* 设置兄弟等于 current的父亲的左孩子

下面再继续处理过程:不管这个初始兄弟是red还是 black,这里的处理会根据兄弟的孩子的颜色分成两种情况:
如果兄弟的两个孩子都是black(或null),则需要

* 设置兄弟的颜色为red
* 设置 current等于 current的父亲

如果兄弟的两个孩子不全为black,则将查看兄弟的左孩子是否是black,如果是,则在继续之前必须完成如下步骤:

* 设置兄弟的右孩子的颜色为 black
* 设置兄弟的颜色为red
* 让兄弟的右孩子绕着兄弟本身向右旋转
* 设置兄弟等于cumt的父亲的左孩子

最后是兄弟的两个孩子都不为 black这一情况,这时必须:

* 设置兄弟的颜色为 current的父亲的颜色。
* 设置 current的父亲的颜色为black
* 设置兄弟的左孩子的颜色为black
* 让兄弟绕着 current的父亲向右旋转
* 设置 current等于树根

该循环终止之后,我们要酬除该结点,并设置其父亲的孩子引用为mull

教材学习中的问题和解决过程

问题1:红黑树这样设计的意义在哪里呢?

问题1解析:

【参考资料】

问题2:平衡化的迭代过程中,如果当前结点的父结点是左孩子,为什么不用像父结点是右孩子时那样讨论父结点是红的还是黑的?

问题2解析:

【参考资料】

代码运行中的问题及解决过程

问题1:
问题1解决过程:

本周错题

错题1:

错题1解析:

代码托管

结对及互评

  • 博客中值得学习的或问题:
    • 侯泽洋同学的博客排版工整,界面很美观,并且本周还对博客排版、字体做了调整,很用心
    • 问题总结做得很全面:对课本上不懂的代码会做透彻的分析,即便可以直接拿过来用而不用管他的含义
    • 对教材中的细小问题都能够关注,并且主动去百度学习,printTree()方法分析的很到位
    • 代码中值得学习的或问题:
    • 对于编程的编写总能找到角度去解决
  • 本周结对学习情况
    • 20172302
    • 结对学习内容
      • 第十章内容:树

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 0/0 1/1 4/4
第二周 560/560 1/2 6/10
第三周 415/975 1/3 6/16
第四周 1055/2030 1/4 14/30
第五周 1051/3083 1/5 8/38
第六周 785/3868 1/6 16/54

以上是关于20172308 《程序设计与数据结构》第七周学习总结的主要内容,如果未能解决你的问题,请参考以下文章

20172322 《程序设计与数据结构》第七周学习总结

20172301 《程序设计与数据结构》第七周学习总结

《程序设计与数据结构》第七周学习总结

20172313 2017-2018-2 《程序设计与数据结构》第七周学习总结

20172328《程序设计与数据结构》第七周学习总结

20172327 2018-2019-1 《程序设计与数据结构》第七周学习总结