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

Posted yu-kunpeng

tags:

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

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

教材学习内容总结

  • 概述
    • 二叉查找树:二叉查找树是一种含有附加属性的二叉树,即其左孩子小于父结点,而父结点又小于或等于右孩子。二叉查找树的定义是二叉树定义的扩展
    • 二叉查找树的各种操作
操作 说明
addElement 往树中添加一个元素 
removeElement 从树中删除一个元素 
removeAllOccurrences 从树中删除所指定元素的任何存在 
removeMin 删除树中的最小元素 
removeMax 删除树中的最大元素 
findMin 返回一个指向树中最小元素的引用 
findMax 返回一个指向树中最大元素的引用 
  • 用链表实现二叉查找树:每个BinaryTreeNode对象要维护一个指向结点所存储元素的引用,另外还要维护指向结点的每个孩子的引用。
  • addElement操作:我们只要利用二叉查找树的特性(即对每个父结点,它的左子树中所有项的值小于父结点中的值,而它的右子树中所有项的值都大于T中的值),找到只对应的插入位置即可,如果树为空,则这个新元素就将成为新结点,如果树非空,沿着树查找(根据element的大小来判断向左还是向右)。假如我们现在要插入element为4的结点,如果找到element(4),则什么也不做,否则将element插入到遍历的路径上的最后一个点,如下图所示:
    技术分享图片
  • removeElement操作:对于二叉查找树来说,删除元素的时候要考虑三种情况:
    • ①如果要删除的结点q恰好是叶子结点,那么它可以立即被删除
    • ② 如果要删除的结点q拥有一个孩子结点,则应该调整要被删除的父结点(p.left 或 p.right)指向被删除结点的孩子结点(q.left 或 q.right)
    • ③如果要删除的结点q拥有两个孩子结点,则删除策略是用q的右子树的最小的数据替代要被删除结点的数据,并递归删除用于替换的结点(此时该结点已为空),此时二叉查找树的结构并不会被打乱,其特性仍旧生效。采用这样策略的主要原因是右子树的最小结点的数据替换要被删除的结点后可以满足维持二叉查找树的结构和特性,又因为右子树最小结点不可能有左孩子,删除起来也相对简单些。
      技术分享图片
  • removeAllOccurrences操作:可以看做调用了removeElement,当在树中找不到指定元素是,则抛出ElementNotFoundException异常,如果指定的元素不是Comparable,则removeAllOccurrences方法也会抛出ClassCaseException异常。只要树中还含有目标元素,就会再次调用removeElement方法。
    public void removeAllOccurrences(T targetElement)
            throws ElementNotFoundException
    {
        removeElement(targetElement);

        try
        {
            while (contains((T)targetElement))
                removeElement(targetElement);
        }

        catch (Exception ElementNotFoundException)
        {
        }
    }
  • removeMin和removeMax操作:对于findMin(),则需要从根结点开始并且只要有左孩子就向左进行即可,其终止点即为最小值的元素;而对于findMax(),也需要从根结点开始并且只要有右孩子就向右进行即可,终止点即为值最大的元素。
    技术分享图片
  • 用有序列表实现二叉查找树:add操作和remove操作都可能导致树变得不平衡。
操作 说明 LinkedList BinarySearchTreeList
removeFirst 删除列表的首元素  O(1) O(logn)
removeLast 删除列表的末元素  O(n) (logn)
remove 删除列表中的一个特定元素 O(n) O(logn)*
first 考察列表前端的那个元素  O(1) O(logn)
last 考察列表末端的那个元素  O(n) O(logn)
contains 判断列表是否含有一个特定元素  O(n) O(logn)
is Empty 判定列表是否为空  O(1) O(1)
size 判定列表中的元素数目  O(1) O(1)
add(有序列表特有) 向列表添加一个元素  O(n) O(logn)*
  • 平衡二叉查找树:如果没有平衡假设,最坏情况下addElement操作的实践复杂性是O(n)而不是O(logn),因为可能存在树根是树中的最小元素,而将被插入的元素可能是树中的最大元素,这种情况下,它的效率比链表的还低,因为每个结点还附带额外的开销。
    技术分享图片
  • AVL树的旋转:当一棵树的最大路径长度大于log2^n,或最小路径长度小于log2^n-1时,就要平衡化该树。对于AVL树,树中的每一个结点,我们都会跟踪其左右子树的高度。对于树中的任何结点,如果其平衡因子(左右子树的高度差)大于1或小于-1,则以该节点为树根的子树需要重新平衡。
    技术分享图片
  • 上左图是一棵平衡二叉树,它每个结点的左子树和右子树的高度最多相差1,它同时也是一棵二叉查找树,而上右图虽然也是一棵二叉查找树,但是它每个结点的左子树和右子树的高度相差为2,所以它不是平衡二叉树。当引起结点数量变化时,即进行删除和插入操作,如下图,插入一个新的结点,原本的平衡二叉树就失去了平衡。
    技术分享图片
  • 既然二叉树失去了平衡,我们就要使用适当的操作来使它恢复平衡。如果某结点的平衡因子为-2,左孩子的平衡因子是-1,这就意味着该结点的左子树中有一条过长的路径,所以应该采用右旋。在原始AVL树插入7结点后,结点9变为失衡点,树再满足AVL性质,因此需要对9结点进行左左单旋转(即向右旋转)后,得到下右图,我们发现此时并没有操作树的根结点(6),实际上这是因为正常情况下,不必从树的根结点进行旋转,而是从插入结点处开始,向上遍历树,并更新和修复在这个路径上的每个结点的平衡及其平衡信息(高度)即可。(左旋类似,当某结点的平衡因子是+2,右孩子的平衡因子是+1的时候使用左旋,左旋中的“左”,意味着“被旋转的结点将变成一个左结点”。右旋同理)
    技术分享图片
  • 当某结点的平衡因子是+2,如果其右孩子的平衡因子是-1,这时子树太“深”了,无论是左旋还是右旋,都无法使操作后的数成为AVL树,这个时候就需要使用双旋,首先让出初始结点右孩子的左孩子,绕着初始结点的右孩子进行一次右旋,然后再让初始结点的右孩子,绕着初始结点进行一次左旋。(左右旋类似,当某结点的平衡因子是-2,左孩子的平衡因子是+1的时候使用左右旋)
    技术分享图片
  • 红黑树
    • 树中的每一个结点都储存着一种颜色(红色或黑色,通常使用一个布尔值来实现,值false等价于红色)。
    • 根结点为黑色。
    • 每个叶子结点(null)是黑色。(**注意:这里的叶子结点,是指为空(null)的叶子结点!)
    • 从树根到树叶的每条路径都包含有同样数目的黑色结点。
    • 如果一个结点的颜色为红色,那么它的子结点必定是黑色。
    • 在红黑树中,元素的查找仍然是一种O(n)操作,由于红色结点不能有红色孩子,于是路径中至多有一半结点时红色结点、至少有一半结点是黑色结点,据此我们可以论证红黑树的最大高度约为2*logn,于是遍历最长路径的序仍然是logn。
      技术分享图片
  • 红黑树的添加操作:红黑树本身就是一棵二叉查找树,所以当添加元素或删除元素后,我们仍然需要使所得到的是一棵二叉查找树,这就使得我们要对红黑树进行重新着色。我们先来回头看上面所说的红黑树的性质,如果要保证从树根到树叶的每条路径都包含有同样数目的黑色结点,那么我们把插入的元素设置为红色,就可以保持,接下来我们只需从该结点向上遍历,保证红色结点的子结点必定为红色即可满足红黑树的所有要求。我们把要插入的结点设置为红色,那么根据父结点的颜色又可以分为三种情况。
    • ①被插入的结点是根结点。(我们可以直接将该结点涂成黑色)
    • ②被插入的结点的父结点是黑色。 (我们无需进行操作,插入之后仍为红黑树)
    • ③被插入的结点的父结点是红色。(我们对该种情况要进行着重讨论,因为被插入的结点的父结点是红色,所以该结点的祖父结点必定存在(即父结点的父结点),父结点的兄弟结点也必定存在。(即“叔叔”结点,即使叔叔结点为空,我们也视之为存在,空结点本身就是黑色结点)我们根据叔叔结点的颜色又可以分成三种情况)
情况 处理方式
当前结点的父结点是红色,且当前结点的祖父结点的另一个子结点(叔叔结点)也是红色。 ①将“父结点”设为黑色。
② 将“叔叔结点”设为黑色。
③ 将“祖父结点”设为“红色”。
④ 将“祖父结点”设为“当前结点”(红色结点);指针current由指向插入的结点变为“当前结点“”,之后继续对“当前结点”向上进行操作。
当前结点的父结点是红色,叔叔结点是黑色,且当前结点是其父结点的右孩子 ①将“父结点”作为“新的当前结点”。
②以“新的当前结点”为支点进行左旋。
当前结点的父结点是红色,叔叔结点是黑色,且当前结点是其父结点的左孩子 ① 将“父节点”设为“黑色”。
②将“祖父节点”设为“红色”。
③以“祖父结点”为支点进行右旋。
  • 如上图介绍了如何进行操作,下面我们再来谈谈为什么要这么操作。
    • 第一种情况,由于红色结点的子结点不能是红色,所以我们把父结点要变为黑色,但当我们把父结点变为黑色以后,从树根到树叶之间的黑色结点的个数就不相等了,所以把祖父结点变为红色,同样的,因为红色结点的子孩子不能是红色,所以要把叔叔结点变为黑色。祖父结点一定是黑色吗?答案是肯定的,因为在元素添加之前,该二叉树就是红黑树,父结点是红色的,那么祖父结点一定是黑色的。按照上述步骤处理之后,当前结点,父结点,叔叔结点都满足了红黑树的性质。若此时,祖父结点是根结点,直接将祖父结点设为“黑色”,那就完全解决这个问题了;若祖父结点不是根结点,那我们需要将“祖父结点”设为“新的当前结点”,接着对“新的当前结点”进行分析。
    • 第二种情况,我们在上面表中说到,要以父结点为支点进行左旋,那么为什么要进行左旋呢?处理红黑树的核心思想:将红色的节点移到根节点;然后,将根节点设为黑色。新插入的结点为红色,破坏了整棵红黑树,所以我们要通过左旋来将它上移。上移之后,如果该结点变成了根结点,那么直接把它涂成黑色,若该结点不是 根结点,那么我们需要对父结点进行操作(在下图中也就是40) ,为什么不直接对60的当前结点进行操作,而是转而处理原来的父结点40呢?因为通过左旋之后,原来的父结点(40)变成了子结点(60)的子结点,而处理红黑树的时候需要从下往上处理,所以要先对40的结点操作。
      技术分享图片
    • 第三种情况,当按照上图第二种情况左旋后,就变成了下面这种情况。由于(40)和(60)两个结点都是红色,所以我们可以先把(60)结点变为黑色,但变为黑色的话,由根结点经过(60)结点的路径黑色结点数就会增加,所以我们可以让(60)的父结点(即(80))变为红色,并以该父结点作为支点进行右旋。
      技术分享图片
  • 红黑树的删除操作:红黑树的删除和常规二叉树删除元素的操作一样,也分为三种情况。
    • ①被删除的结点没有子结点(即叶子节点),直接将该结点删除。
    • ②被删除的结点有一个子结点,将该结点删除,并让父结点指向该结点的子结点。(可以看前文的示意图)
    • ③被删除的结点有两个子结点,用该结点的右子树的最小的数据替代要被删除结点的数据,并递归删除用于替换的结点。(前文有,在此不再过多赘述)
  • 在删除结点的时候,我们先来想一下所删结点的位置,如果删除的是根结点,那么根结点就可能不为黑色,如果它有子结点,删除后可能会导致红色结点的子结点有红色结点还有可能会导致从根到各个路径的黑色结点的数量不同。我们就来解决上面上说的这些问题。

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

  • 问题一:在学习教材的时候,p225的代码中有这样一行判断条件“(!(element instanceof Comparable))”,不是很理解这行代码的意思。
  • 问题一解决方法:原来是学习过的,时间久了有些忘记。java 中的instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例。instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例。用法:result = object instanceof class参数:Result:布尔类型。Object:必选项。任意对象表达式。Class:必选项。任意已定义的对象类。说明:如果 object 是 class 的一个实例,则 instanceof 运算符返回 true。如果 object 不是指定类的一个实例,或者 object 是 null,则返回 false。 举例:
interface A{}
    class B implements A{

    }
    class C extends B {

    }

    class instanceoftest {
        public static void main(String[] args){
            A a=null;
            B b=null;
            boolean res;

            System.out.println("instanceoftest test case 1: ------------------");
            res = a instanceof A;
            System.out.println("a instanceof A: " + res);

            res = b instanceof B;
            System.out.println("b instanceof B: " + res);

            System.out.println("/ninstanceoftest test case 2: ------------------");
            a=new B();
            b=new B();

            res = a instanceof A;
            System.out.println("a instanceof A: " + res);

            res = a instanceof B;
            System.out.println("a instanceof B: " + res);

            res = b instanceof A;
            System.out.println("b instanceof A: " + res);

            res = b instanceof B;
            System.out.println("b instanceof B: " + res);

            System.out.println("/ninstanceoftest test case 3: ------------------");
            B b2=(C)new C();

            res = b2 instanceof A;
            System.out.println("b2 instanceof A: " + res);

            res = b2 instanceof B;
            System.out.println("b2 instanceof B: " + res);

            res = b2 instanceof C;
            System.out.println("b2 instanceof C: " + res);
        }
    }

技术分享图片

  • 问题二:在学习红黑树的时候,书上提到终止迭代的条件也可以是“current.parent.color == black”,子树的结点不也可以是黑色的吗?为什么判断当前结点的父结点的颜色就可以终止迭代了呢?

代码调试中的问题和解决过程

  • 问题1:在做编程项目pp9_3的时候,不知道怎么计算程序的执行时间。
  • 问题一解决方案:
一般输出日期时间经常会用到Date这个类:

 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
 System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
(1)以毫秒为单位计算
  static long currentTimeMillis() , 该方法返回值是从1970年1月1日凌晨到此时刻的毫秒数
  
 long startTime=System.currentTimeMillis();   //获取开始时间
 doSomeThing();  //测试的代码段
 long endTime=System.currentTimeMillis(); //获取结束时间
 System.out.println("程序运行时间: "+(end-start)+"ms");
(2)以纳秒为单位计算
 long startTime=System.nanoTime();   //获取开始时间
 doSomeThing();  //测试的代码段
 long endTime=System.nanoTime(); //获取结束时间
 System.out.println("程序运行时间: "+(end-start)+"ns");

代码托管

(statistics.sh脚本的运行结果截图)

上周考试错题总结

??这周没有错题哦~

结对及互评

点评过的同学博客和代码

其他(感悟、思考等,可选)

xxx
xxx

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 200/200 2/2 20/20
第二周 300/500 2/4 18/38
第三周 500/1000 3/7 22/60
第四周 300/1300 2/9 30/90
  • 计划学习时间:XX小时

  • 实际学习时间:XX小时

  • 改进情况:

(有空多看看现代软件工程 课件
软件工程师能力自我评价表
)

参考资料







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

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

20172313 2017-2018-2 《程序设计与数据结构》实验三报告

20172313 2017-2018-2 《程序设计与数据结构》实验三报告

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

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

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