【重学算法】堆排序

130 阅读4分钟
  1. 堆的定义

堆是一个近似完全二叉树的结构,堆中子节点的值总是小于(或者大于)它的父节点。

通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:

父节点i的左子节点在位置(2i+1);

父节点i的右子节点在位置(2i+2);

子节点i的父节点在位置((i-1)/2);

以升序排序为例,建最大堆,重复从最大堆取出数值最大的结点(把根结点和最后一个结点交换,把交换后的最后一个结点移出堆(最大值在最后)),并让残余的堆维持最大堆性质。

  1. 堆排序的Java代码实现

    public void sort() { int len = arr.length - 1; int beginNoLeaf = (arr.length >> 1)- 1; for (int i = beginNoLeaf; i >= 0; i--) maxHeapify(i, len);

     for (int i = len; i > 0; i--) {
         swap(0, i);
         maxHeapify(0, i - 1);
     }
    

    }

    private void maxHeapify(int index, int len) { int left = (index << 1) + 1; int right = left + 1;
    int max = left;
    if (left > len) return;
    if (right <= len && arr[right] > arr[left]) max = right; if (arr[max] > arr[index]) { swap(max, index);
    maxHeapify(max, len); } }

    private void swap(int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } (1) 往堆中插入元素

让新插入的节点与父节点对比大小,自下往上推,如果不满足子节点小于等于父节点的大小关系,我们就互换两个节点,一直重复这个过程。

(2) 删除堆顶元素

把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是从上往下的堆化方法。

  1. 堆的应用:优先级队列、求 Top K 和求中位数

(1) 优先级队列

Q:如何实现一个优先级队列呢?

A:一个堆就可以看作一个优先级队列。很多时候,它们只是概念上的区分而已。往优先级队列中插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素。很多语言中,都提供了优先级队列的实现,比如,Java 的 PriorityQueue。

例子:合并有序小文件

Q:假设我们有 100 个小文件,每个文件的大小是 100MB,每个文件中存储的都是有序的字符串。我们希望将这些 100 个小文件合并成一个有序的大文件。这里就会用到优先级队列。

A:这里就可以用到优先级队列,也可以说是堆。我们将从小文件中取出来的字符串放入到小顶堆中,那堆顶的元素,也就是优先级队列队首的元素,就是最小的字符串。我们将这个字符串放入到大文件中,并将其从堆中删除。然后再从小文件中取出下一个字符串,放入到堆中。循环这个过程,就可以将 100 个小文件中的数据依次放入到大文件中。

(2) 利用堆求 Top K

求 Top K 的问题可以抽象成两类。一类是针对静态数据集合,也就是说数据集合事先确定,不会再变。另一类是针对动态数据集合,也就是说数据集合事先并不确定,有数据动态地加入到集合中。

维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理,继续遍历数组。这样等数组中的数据都遍历完之后,堆中的数据就是前 K 大数据了。

遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK) 的时间复杂度,所以最坏情况下,n 个元素都入堆一次,时间复杂度就是 O(nlogK)。

(3) 求中位数

维护两个堆,一个大顶堆,一个小顶堆,且小顶堆中的数据都大于大顶堆。大顶堆元素个数大于等于小顶堆。

插入数据进行堆化:往小顶堆中插入数据,如果小顶堆元素个数等于大顶堆,则只需对小顶堆进行堆化;如果小顶堆元素个数大于大顶堆,则对小顶堆进行堆化后,移动堆顶元素进入大顶堆堆化。

插入数据因为需要涉及堆化,所以时间复杂度变成了 O(logn),但是求中位数我们只需要返回大顶堆的堆顶元素就可以了,所以时间复杂度就是 O(1)。