算法导论-第6章-堆排序

6.1 堆及堆的性质

(二叉)堆可以看作完全二叉树,其存储结构通常是数组,表示堆的数组A中有两个重要属性: A . l e n g t h A.length A.length表示数组元素的个数; A . h e a p − s i z e A.heap-size A.heapsize表示有多少个堆元素在数组中, 0 ≤ A . h e a p − s i z e ≤ A . l e n g t h 0 \le A.heap-size \le A.length 0A.heapsizeA.length

Figure 6.1.png

假设树的根结点为 A [ 1 ] A[1] A[1],给定一个结点的下标 i i i,可以得到其父结点、左孩子和右孩子的下标:

Parent.png

PARENT(i) return i >> 1
LEFT(i) return i << 1
RIGHT(i) return i << 1 | 1
// 在堆排序好的实现中,这三个函数通常是以宏或者内联函数实现的。

二叉堆的两种形式:大根堆小根堆

  • 大根堆:最大元素在堆顶,除根结点外的所有结点 i i i满足 A [ P A R E N T ( i ) ] ≥ A [ i ] A[PARENT(i)] \ge A[i] A[PARENT(i)]A[i]
  • 小根堆:最小元素在堆顶,除根结点外的所有结点 i i i满足 A [ P A R E N T ( i ) ] ≤ A [ i ] A[PARENT(i)] \le A[i] A[PARENT(i)]A[i]

本章中的堆排序以大根堆为例,优先队列以小根堆为例。

6.2 维护堆的性质

维护堆的性质(又称整堆):设有数组 A A A和结点 i i i,假定根结点为 L E F T ( i ) LEFT(i) LEFT(i) R I G H T ( i ) RIGHT(i) RIGHT(i)的二叉树都是大根堆,但 A [ i ] A[i] A[i]可能小于其孩子结点,因此要对结点 A [ i ] A[i] A[i]进行整堆,使其重新满足大根堆的性质。

Max-Heapify.png

上面算法的思想是:找出结点 i i i和左右孩子结点中的最大值,交换结点 i i i和最大值结点,然后递归地向下调用方法,传入下方被交换位置的结点索引。

下图展示了MAX-HEAPIFY的执行过程。

Figure 6.2.png

MAX-HEAPIFY的时间复杂度是 O ( h ) \Omicron(h) O(h) h h h为树高。

6.3 建堆

我们可以自下而上的调用MAX-HEAPIFY方法将数组 A [ 1.. n ] A[1..n] A[1..n]构建成大根堆。子数组 A [ ⌊ n 2 ⌋ + 1.. n ] A[\lfloor \frac{n}{2} \rfloor + 1..n] A[⌊2n+1..n]中的元素都是叶结点,每个叶结点都可以看成包含一个元素的堆,只需对 A [ 1.. ⌊ n 2 ⌋ ] A[1..\lfloor \frac{n}{2} \rfloor] A[1..2n⌋]的结点进行整堆操作。

Build-Max-Heap.png

Figure 6.3.jpg

我们可以在线性时间 O ( n ) \Omicron(n) O(n)内,把一个无序数组构造称为一个大根堆。

6.4 堆排序

算法思想:初始时,利用BUILD-MAX-HEAP将输入数组 A [ 1.. n ] A[1..n] A[1..n]建成大根堆,其中 n = A . l e n g t h n=A.length n=A.length。因为数组中的最大元素总在根结点 A [ 1 ] A[1] A[1]中,通过把它与 A [ n ] A[n] A[n]进行互换,从堆中去掉结点 n n n(这一操作可以通过减少 A . h e a p − s i z e A.heap-size A.heapsize的值来实现),在剩余的结点中,因为互换,新的结点可能会违背最大堆的性质,因此需要进行整堆操作,调用MAX-HEAPIFY(A, 1),从而在 A [ 1.. n − 1 ] A[1..n-1] A[1..n1]上构造一个新的大根堆。不断重复这一过程,知道堆的大小降到2。

Heapsort.png

Figure 6.4.jpg

package ch06;

import java.util.Arrays;

public class HeapSort {
    private static int heap_size; // 当前堆中的元素

    public static int PARENT(int i) {
        return ((i + 1) >> 1) - 1;
    }

    public static int LEFT(int i) {
        return ((i + 1) << 1) - 1;
    }

    public static int RIGHT(int i) {
        return ((i + 1) << 1);
    }

    public static void exchange(int[] arr, int a, int b) { // 交换数组中两个位置的元素
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void MAX_HEAPIFY(int[] A, int i) { // 整堆操作
        int l = LEFT(i);
        int r = RIGHT(i);
        int largest;
        if (l + 1 <= heap_size && A[l] > A[i]) {
            largest = l;
        } else {
            largest = i;
        }
        if (r + 1 <= heap_size && A[r] > A[largest]) {
            largest = r;
        }
        if (largest != i) {
            exchange(A, i, largest);
            MAX_HEAPIFY(A, largest);
        }
    }

    public static void BUILD_MAX_HEAP(int[] A) { // 建堆操作
        heap_size = A.length;
        for (int i = A.length / 2 - 1; i >= 0; i--) {
            MAX_HEAPIFY(A, i);
        }
    }

    public static void HEAP_SORT(int[] A) { // 堆排序
        BUILD_MAX_HEAP(A);
        for (int i = A.length - 1; i >= 0; i--) {
            exchange(A, 0, i);
            heap_size--;
            MAX_HEAPIFY(A, 0);
        }
    }

    public static void main(String[] args) {
        int[] A = {4, 1, 3, 2, 16, 9, 10, 14, 8, 7};
        HEAP_SORT(A);
        System.out.println(Arrays.toString(A));
    }
}

6.5 优先队列

尽管快速排序的一般性能要优于堆排序,但堆这一数据结构仍然有很多应用。堆的一个常见应用:优先队列。和堆一样,优先队列也有两种形式:最大优先队列、最小优先队列。

优先队列(priority queue)是一种用来维护由一组元素构成的集合 S S S的数据结构,其中的每个元素都有一个相关的值,称为关键字(key)。一个最大优先队列支持如下操作:

  • INSERT(S, x):把元素 x x x插入集合 S S S中。
  • MAXIMUM(S):返回 S S S中具有最大关键字的元素。
  • EXTRACT-MAX(S):去掉并返回 S S S中具有最大关键字的元素。
  • INCREASE-KEY(S, x, k):将元素 x x x的关键字值增加到 k k k,这里假设 k k k的值不小于 x x x的原关键字值。

最大优先队列的一个应用:基于优先级的共享计算机系统的作业调度。最大优先队列记录将要执行的作业以及他们的优先级。当一个作业完成或者被中断后,调度器调用EXTRACT-MAX(S)从优先队列中选出最高优先级的作业执行。此外,还可以调用INSERT(S, x)把一个新作业加入到队列中。

最小优先队列支持的操作如下:

  • INSERT(S, x):把元素 x x x插入集合 S S S中。
  • MINIMUM(S):返回 S S S中具有最小关键字的元素。
  • EXTRACT-MIN(S):去掉并返回 S S S中具有最小关键字的元素。
  • DECREASE-KEY(S, x, k):将元素 x x x的关键字值减小到 k k k,这里假设 k k k的值不大于 x x x的原关键字值。

最小优先队列的一个应用:基于事件驱动的模拟器。发生时间作为关键字,事件必须按照发生的时间顺序进行模拟。

下面是最大优先队列的实现:

MAX-HEAP-MAXIMUM(A)
    if A.heap-size < 1
        error "heap underflow"
    return A[1]

MAX-HEAP-EXTRACT-MAX(A)
    max = MAX-HEAP-MAXIMUM(A)
    A[1] = A[A.heap-size]
    A.heap-size = A.heap-size - 1
    MAX-HEAPIFY(A, 1)
    return max

MAX-HEAP-INCREASE-KEY(A, x, k)
    if k < x.key
        error "new key is smaller than current key"
    x.key = k
    find the index i in array A where object x occurs
    while i > 1 and A[PARENT(i)].key < A[i].key
        exchange A[i] with A[PARENT(i)], updating the information that maps priority queue objects to array indices
        i = PARENT(i)

MAX-HEAP-INSERT(A, x, n)
    if A.heap-size == n
        error "heap overflow"
    A.heap-size = A.heap-size + 1
    k = x.key
    x.key = -A[A.heap-size] = x
    map x to index heap-size in the array
    MAX-HEAP-INCREASE-KEY(A, x, k)

下面是MAX-HEAP-INCREASE-KEY的操作过程

Figure 6.5.png

在一个包含 n n n个元素的堆中,所有优先队列的操作都可以在 O ( log ⁡ n ) \Omicron(\log n) O(logn)时间内完成。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值