为什么说堆排序没有快速排序快?

1,665 阅读6分钟
为什么说堆排序没有快速排序快?

堆的定义:
  • 堆是一个完全二叉树【完全二叉树要求,除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列】
  • 堆中的每个节点的值都必须大于等于(或小于等于)其子树中每个节点的值
  • 对每个节点的值都大于等于子树中每个节点值的堆,我们叫大顶堆。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做小顶堆

  • 完全二叉树比较适合用数组来存储。用数组存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点

解答题目:
第一点,堆排序数据访问的方式没有快速排序友好
  • 对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。比如,堆排序中,最重要的一个操作就是数据的堆化。对堆顶节点进行堆化,会一次访问数组下标1,2,4,8的元素,而不是像快排那样,局部顺序访问,所以对CPU缓存是不友好的
第二点,对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序
  • 我们在讲排序的时候,提过两个概念,有序度和逆序度。对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成的,比较和交换。快速排序数据交换的次数不会比逆序度多。
  • 但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。

堆的应用
优先级队列:数据的出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先出队
  • 如何实现一个优先级队列呢?方法有很多,但是用堆来实现是最直接、最高效的。这是因为,堆和优先级队列非常相似。一个堆就可以看作一个优先级队列。很多时候,它们只是概念上的区分而已。往优先级队列中插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素
  • 优先级队列应用
    • 合并有序小文件
      • 我们将从小文件中取出来的字符串放入到小顶堆中,那堆顶的元素,也就是优先级队列队首的元素,就是最小的字符串。我们将这个字符串放入到大文件中,并将其从堆中删除。然后再从小文件中取出下一个字符串,放入到堆中。循环这个过程,就可以将100个小文件中的数据一次放入到大文件中。
      • 我们知道删除堆顶数据和往堆中插入数据的时间复杂度都是O(logn),n表示堆中的个数。
    • 高性能定时器
      • 假设我们有一个定时器,定时器中维护了很多定时任务,每个任务都设定了一个要触发执行的时间点。定时器每过一个很小的单位时间,就扫描一遍任务,看是否有任务到达设定的执行时间。如果到达了,就拿出来执行,但是比较低效,原因有2个:
        • 任务的约定执行时间离当前时间可能还有很久,这样前面很多次扫描其实都是徒劳的;
        • 每次都要扫描整个任务列表,如果任务列表很大的话,势必会比较耗时。
      • 针对这些问题,我们可以用优先级队列来解决,我们按照任务设定的执行时间,将这些任务存储在优先级队列中,队列首部存储的是最先执行的任务
      • 这样,定时器就不需要每隔1秒就扫描一遍任务列表,它拿队首任务的执行时间点,与当前时间点相减,得到一个时间间隔T。这个时间间隔T就是从当前时间开始,需要等待多久,才会有第一个任务需要被执行。这样,定时器就可以设定在T秒之后,再来执行任务。从当前时间点到(T-1)秒这段时间里,定时器都不需要做任何事情。
      • 当T秒过后,定时器取优先级队列中队首的任务执行。然后再计算新的队首任务的执行时间点与当前时间点的差值,把这个值作为定时器执行下一个任务需要等待的时间。
      • 这样,定时器既不用间隔1秒就轮训一次,也不用遍历整个任务列表,性能也就提高了。
  • 利用堆求 Top K
    • 分为两类:1.针对静态数据集合,也就是说数据集合事先确定,不会再变
    • 2.针对动态数据集合,也就是说数据集合事先不确定,有数据动态地加入到集合中
      • 针对静态数据,如何在一个包含n个数据的数组中,查找前k大数据呢?我们可以维护一个大小为k的小顶堆,顺序遍历数组,从数组中取出数据与栈顶元素比较。如果比栈顶元素大,我们就把栈顶元素删除,并且将这个元素插入到堆中;如果比栈顶元素小,则不做处理,继续遍历数组。这样等数组中的数据都遍历完,堆中的数据就是前K大数据了。
      • 遍历数组需要O(n)的时间复杂度,一次堆化需要O(logK)的时间复杂度,所以最坏情况下,n个元素都入堆一次,时间复杂度是O(nlogK)
      • 针对动态数据如果每次询问前 K 大数据,我们都基于当前的数据重新计算的话,那时间复杂度就是 O(nlogK),n 表示当前的数据的大小。实际上,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就拿它与堆顶的元素对比。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以立刻返回给他。