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

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

教材学习内容总结

本周的内容又是一次延续上一周学习内容掌握新知识的过程,本周学习了一种特殊形式的树——堆,学习了两种实现堆的方法:用链表实现和用数组实现,同时还学习了使用堆来实现一种特殊队列——优先队列以及基于堆实现的另外一种排序方法:堆排序。html

1、堆的概述

  • 概念:堆是一种具备两种附加属性的特殊二叉树。
    • 附加属性一:堆是一颗彻底树(复习:彻底树:底层叶子都位于树的左边的平衡树称为彻底树)
    • 附加属性二:对于堆中的每个结点,该结点都小于或等于(大于或等于)它的左右孩子。
  • 类型:
    • 最小堆(小顶堆)——堆中每个结点都小于等于其左右孩子的堆
    • 最大堆(大顶堆)——堆中每个结点都大于等于其左右孩子的堆
  • 特色:
    1. 堆的最小值/最大值存储在根处
    2. 堆的每一颗子树也是一个堆

2、堆的操做

(一)堆的构造(以构造大顶堆为例)

  • 第一步:将元素按照层序遍历的顺序构造一颗二叉树
  • 第二步:从树中的第一个非叶子结点/非终端结点(寻找方法:树中的第[树中元素个数/2]个元素)开始调整,判断该结点与其孩子的大小,若是不知足堆的附加属性二则进行交换。如在下图中,第一个非叶子结点为6,它和它的右孩子9与堆的附加属性二冲突,因此将二者进行交换。
  • 第三步:向上继续寻找下一个非叶子结点直至根结点,重复上一步操做,若是在交换以后存在新的不平衡,那么针对与孩子交换以后的非叶子结点,与其新的左右孩子再次进行对比和交换。如在下图中,第二个非叶子结点为4,它比它的两个孩子都小,因此与其中较大的9进行交换,而在交换以后,4与它新的左孩子5和右孩子6进行对比,仍然与堆的附加属性二冲突,因此再次对46进行交换操做。

(二)添加操做(以小顶堆中的插入为例)

  • 第一步——插入
    • 对于插入堆中的元素来讲,插入时只会有一个正确的插入位置,而这个插入位置有两种可能,假设堆的层数为h,插入元素的位置要么在第h层的下一个靠左的空位置,要么在第h+1层的第一个位置。
  • 第二步——调整
    • 在插入以后,将插入的元素与其父结点进行对比,若是不知足堆的附加属性二,则将其元素与其父结点进行对比,在必要时进行交换,直至该元素与其父结点知足附加属性二或位于该堆的根处。

(三)删除操做(以小顶堆中的删除为例)

  • 堆中的删除操做是针对堆的根结点来进行的。
  • 第一步——删除
    • 首先将堆的根结点删除,而后将堆中原来的最后一个元素替换到根结点处。
  • 第二步——调整
    • 从根结点处向下,将根结点与其左右孩子进行对比,若是不符合堆的附加属性二,则继续交换,交换以后继续与新的左右孩子进行比较交换,直至该元素与其左右孩子知足附加属性二或位于该堆的叶子处。

3、堆的实现

(一)用链表实现堆

  • 由于咱们要求在插入元素后可以向上遍历该树,因此堆中结点必须存在指向其双亲的指针。除此以外,为了可以追踪堆中的最末一片叶子,还要设置一个lastNode来存储最末结点。
  • addElement
    • 该操做有三个步骤:
      • 添加新的元素到末尾,在该步骤中要肯定插入结点的双亲,在最坏的状况下,它可能将整个堆遍历,这个过程的时间复杂度为O(logn)。
      • 对堆进行从新调整,在调整过程当中最多要进行logn次比较,及从下至上遍历每一层(若是堆中有n个元素,则其高度为logn),因此其时间复杂度为O(logn)。
      • lastNode指针再次指向最末的结点,该步的时间复杂度为O(1)。
    • 因此添加操做的时间复杂度为O(logn)。
    • 在这个方法中应用了两个私有方法getNextParentAdd(用于返回插入结点如今的父结点)和heapifyAdd(从该结点开始,对剩余堆进行从新调整直至根处)。
  • removeMin
    • 该操做有三个步骤:
      • 用堆中的最末结点替换根结点,该步的时间复杂度为O(1)。
      • 在必要的状况下,对堆进行从新排序。该步操做与添加中的第二步相似,所以其时间复杂度为O(logn)。
      • 肯定新的最末结点,在最坏的状况下,咱们须要遍历整个堆才能找到最末结点,此时它的时间复杂度为O(logn)。
    • 因此删除操做的时间复杂度为O(logn)。
    • 在这个方法中一样应用了两个私有方法getNewLastNode(用于返回最末结点的引用)和heapifyRemove(从根结点开始,对下面的堆进行从新调整直至叶子结点)。

(二)用数组实现堆

  • 在用数组创建的堆中,根位于数组的0处,对于每个结点n,n的左孩子位于数组的2n+1处,右孩子位于2(n+1)处。
  • addElement
    • 该操做有三个步骤:
      • 在适当位置处添加新结点,在这一步中,与链表实现不一样,它不须要肯定双亲的位置(由于对于结点n,其双亲的位置是固定的,位于数组的(n-1)/2处),因此时间复杂度为O(1)。
      • 对于堆进行从新调整,该步的时间复杂度为O(logn)。
      • 将count增长1,该步的时间复杂度为O(1)。
    • 因此删除操做的时间复杂度为O(logn)。
    • 在这个方法中应用了一个私有方法heapifyAdd(在必要时,从该结点开始,对剩余堆进行从新调整直至根处)。
  • removeMin
    • 该操做有三个步骤:
      • 用堆中的最末结点替换根结点,该步的时间复杂度为O(1)。
      • 在必要的状况下,对堆进行从新排序。这一步与其余方法中的调整排序相似,所以其时间复杂度为O(logn)。
      • 返回初始的根元素,在数组中,根元素存储在数组的0处,因此这一步的时间复杂度为O(1)。
    • 因此删除操做的时间复杂度为O(logn)。
    • 在这个方法中应用了一个私有方法heapifyRemove(在必要时,对下面的堆进行从新调整直至叶子结点)。

4、堆的应用

(一)使用堆:优先级队列

  • 虽然最小堆不是一个队列,可是它却提供了一个高效的优先级队列实现
  • 优先级队列:遵循两个排序规则的集合。
    • 具备更高优先级的元素在先。
    • 具备相同优先级的项目使用先进先出方法来肯定其排序。
  • 实现方法:实现方法:定义结点类保存队列中的元素、优先级和排列次序。而后,经过实现Comparable接口定义compareTo方法,先比较优先级,再比较排列次序。

(二)堆排序

  • 堆排序由两部分构成:添加列表中的每一个元素,而后一次删除一个元素。
  • 时间复杂度分析:在插入操做中,每个元素的插入操做(即便用addElement方法)的时间复杂度为O(logn),所以n个结点的时间复杂度为O(nlogn)。在删除操做中,每个元素的删除操做(即便用removeMin方法)的时间复杂度也为O(logn),所以删除n个结点的时间复杂度为O(nlogn)。因此堆排序的时间复杂度为O(nlogn)。
  • 堆排序的过程
    • 第一步:将根结点与最末结点交换,将新的最末结点输出,对剩余的树进行调整使之从新成为堆。如在下图中,将根结点9与最末结点4进行交换,而后将9输出;对于剩下的部分,重新的根结点开始,4比它的左右孩子都小,所以与两者之间较大的8进行交换,交换后4变为叶子结点,开始进行下一步操做。

    • 第二步:重复第一步的操做直至输出全部元素。

  • 规律:使用大顶堆排序后生成的是升序排列,使用小顶堆排序后生成的是降序排列。

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

  • 问题1:在查资料的过程当中发现不少博客都喜欢把堆和栈放到一块来说,感受这二者并无什么关联,可是为何会把它们两个放在一块儿比较呢?
  • 问题1解决方案:由于栈与堆都是Java用来在Ram中存放数据的地方。Java把内存划分红两种:一种是栈内存,一种是堆内存。因此栈和堆常常被放在一块儿比较。
    • 栈内存:在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的做用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间能够当即被另做他用。
    • 堆内存:堆内存用来存放由new建立的对象和数组。
  • 栈内存与堆内存的区别
    • 堆内存用来存放由new建立的对象和数组,栈内存用来存放方法或者局部变量等。
    • 堆是先进先出,后进后出;栈是后进先出,先进后出。

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

  • 问题1:在实现PP12.1的时候,输出的结果并非按照进入队列的顺序输出的。
  • 问题1解决方法:当初张昊然同窗和我讨论PP12.1时,我说应该不能直接用书上的优先级队列,由于它和队列仍是有区别的,可是本身实现的时候虽然从新写了一个,可是感受实际上仍是实现了一个优先级队列。在查了相关资料以后,有一篇博客中写到说将生成的堆使用层序遍历就能够输出队列的结果了,本来我是使用ArrayHeap类,为了查看堆的构造而改为了LinkedHeap类,可是从树的结构来看就算使用层序遍历它也依旧不是按照元素进入队列的顺序排列的。
  • 后来我又尝试了前序、中序和后序遍历,发现都不能够。
  • 最后上网查了不少资料发现彷佛用堆只能实现特殊的优先级队列,由于堆自己的性质就限制了它。

代码托管

  • 上周代码量:15094

上周考试错题总结(正确为绿色,错误为红色)

  • 错题1:Since a heap is a binary search tree, there is only one correct location for the insertion of a new node, and that is either the next open position from the left at level h or the first position on the left at level h+1 if level h is full.
    • A .True
    • B .False
  • 错题1解决方法:只关注了后面说的插入结点的位置的表述是否正确,忽略了前面的“二叉查找树”,原话说的是彻底树,而二叉查找树并不都是彻底树,因此这道题题干的表述是错误的。
  • 错题2:The addElement operation for both the linked and array implementations is O(n log n).
    • A .True
    • B .False
  • 错题2解决方法:添加方法的时间复杂度应该为O(logn),在上面的教材内容总结里已经详细分析过了。

结对及互评

点评模板:

  • 博客中值得学习的或问题:
    • 相比较以前而言博客的内容增长了对代码的理解,值得夸奖。可是对于在学习过程当中遇到的问题记录仍是有点草率,不过感受本周的内容比较简单,确实不像以前会有那么多问题。

点评过的同窗博客和代码

  • 本周结对学习状况
    • 20172322
    • 结对学习内容
      • 讨论了PP12.1的实现

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

  • 感受本周的内容比较简单,代码方面课本上也给的很全很详细,因此这周的问题不是不少,写博客的时候找了好久才找到一个教材中遇到的问题。
  • 本学期的课本内容也基本快学完了,很大的一个感触是本学期的博客质量总体而言要比上学期好,并且对课本内容的理解,对相关知识的应用也比上学期要进步了,但愿能继续保持吧。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 10/10 1/1 10/10
第二周 246/366 2/3 20/30
第三周 567/903 1/4 10/40
第四周 2346/3294 2/6 20/60
第五周 2346/3294 2/8 30/90
第六周 1343/4637 2/8 20/110
第七周 654/5291 1/9 25/135
第八周 2967/8258 1/10 15/150
  • 计划学习时间:20小时
  • 实际学习时间:15小时
  • 改进状况:原本觉得堆会很难,但发现比本身想象的要简单

参考资料

相关文章
相关标签/搜索