算法基础-堆排序

堆排序(heapsort) 和 归并排序 一样, 它的时间复杂度也是 O(nlgn).

回忆一下, 归并排序 使用的 算法设计方法: 分治法.

堆排序 使用另一种算法设计技术: 使用 堆(heap) 的数据结构进行信息管理.

虽然 这一词源自 堆排序, 但是目前它已经被引申为 “垃圾收集存储机制”, 比如 Java 中内存模型的 结构.

1 堆

在介绍堆之前, 需要了解 的基础知识. 若已了解, 直接看 1.2.

1.1 树的基础知识

对于一棵有根树 T(根为r) 中的 结点x, x孩子个数 称之为 度(degree), 从 rx 的简单路径 称之为 深度(depth). 树T 的最大深度 也是 树的高度(height).

满二叉树(full binary tree):
每个结点 是 叶子结点(leaf) 或者 度(degree)为 2.
对于 满二叉树 中的任一一个结点, 要么度是0, 要么度是1, 不存在度为1的结点.

完全k叉树(complete k-ary tree):
所有 叶子结点深度 相同, 且 所有 内部结点 都是 k.

看下图:
heap_tree_1
图 (1) 满二叉树(除叶子结点外, 内部结点的度都为2), 但 不是 完全二叉树(叶子结点的深度不同).
图 (2) 既不是 满二叉树, 也不是 完全二叉树.
图 (3) 既是 满二叉树, 也是 完全二叉树.

以上 满二叉树完全二叉树 的定义是 <算法导论> 中的定义.

国内的教材和 <算法导论> 中的定义不一样.
国内 “满二叉树” 和 <算法导论> 中的 “完全二叉树” 定义是一样的.
国内 “完全二叉树” 定义:
叶子结点只能出现在最下层和次下层, 且最下层的叶子结点集中在树的左部.

上图 (2) 中的 二叉树 符合 国内 “完全二叉树” 的定义.

1.2 堆的性质

如下图所示, (binary) heap, (二叉)堆 是一个数组A[0..9], 它可以被看成一个 近似的完全二叉树(nearly complete binary tree).
heap_tree_3

树中的每一个结点 对应 数组中的一个元素. 除了最顶层外, 该树是完全充满的, 而且 从左向右 填充.
通常, 数组的长度 A.length 并一定是 堆 的大小, 堆 的大小我们用 heapSize 表示. 其中, 1<= heapSize <= A.length.
虽然 A[0..length-1] 都有数据, 但只有 A[0..heapSize-1] 中存放的事堆的有效元素.

对于一个堆(近似完全二叉树)来说, 给定一个结点的下标 i , 我们很容易能得到 它 的 父节点, 左孩子, 和 右孩子 的下标.
i 的父节点的下标:

parent(i)
    return (i - 1)/2

i 的左孩子的下标:

left(i)
    return 2i + 1

i 的右孩子的下标:

right(i)
    return 2*(i + 1)

二叉堆有两种形式:

  1. 最大堆(max-heap) 也叫 大根堆, 满足 A[parent(i)] >= A[i], 即满足 结点x的值 大于等于 其孩子结点的值.
  2. 最小堆(min-heap) 也叫 小根堆, 满足 A[parent(i)] <= A[i], 即满足 结点x的值 小于等于 其孩子结点的值.

在 堆排序算法中, 我们使用的是 最大堆.

2 维护堆的性质

我们假定 根结点 为 left(i)right(i) 的二叉树都是 最大堆.
但这时 A[i] 可能小于其孩子(A[left(i)]A[right(i)]), 这就违背了 最大堆 的性质.

我们使用 maxHeapify 来维护最大堆性质.
maxHeapify 通过让 A[i] 的值在最大堆中"逐级下降", 从而使下标为 i 的根结点的子树 重新遵循 最大堆 的性质.

考虑下图情况 (a):
A[4] 为根的堆是 最大堆, 以 A[5] 为根的堆也是 最大堆. 但以 A[i] (i=2) 为根的堆 不是最大堆.
由于 A[i] 违反了 最大堆 的性质, 所以需要对 A[i] 结点进行调整以使其满足最大堆性质.
(备注: 这个图是我从书上剪切过来的, 下标是从1开始的, 但这并不妨碍理解算法.)
heap_tree_4
为了把 A[i] 放到合适合适的位置, 我们需要用 A[i]A[left(i)], A[right(i)] 这三者做对比, 用最大的结点做 根.
如上图(b)所示, 144, 7 都要大, 因此用 14 作为 根, 把 414 互换位置, 并将 i 指向 left(i).
此时, i=4, 但 A[i] 仍然不满足最大堆性质. 再用 A[i]A[left(i)], A[right(i)] 这三者做对比, 用最大的结点做 根.
如上图(c)所示, 82, 4都要大, 因此用 8 作为 根, 把48互换位置, 并将 i 指向 right(i).
此时, 整个堆已经满足 最大堆性质, maxHeapify 完成.

这个过程用代码描述:

private int left(int i) {
    return 2 * i + 1;
}

private int right(int i) {
    return 2 * (i + 1);
}

public void maxHeapify(int[] a, int i, int heapSize) {
    int l = left(i);
    int r = right(i);
    int largestIndex;
    if (l < heapSize && a[l] > a[i]) {
        largestIndex = l;
    } else {
        largestIndex = i;
    }

    if (r < heapSize && a[r] > a[largestIndex]) {
        largestIndex = r;
    }
    if (largestIndex != i) {
        int temp = a[i];
        a[i] = a[largestIndex];
        a[largestIndex] = temp;

        maxHeapify(a, largestIndex, heapSize);
    }
}

对于n个结点, 树高h的堆来说, maxHeapify 的时间复杂度是O(h)O(lgn).

3 建堆

我们可以 自底向上 利用 maxHeapify 把数组转换成 最大堆.

具体操作步骤:
从倒数第二层的最右边的非叶子结点的下标(heapSize / 2 - 1)开始遍历, 每次下标减一, 遍历到下标为 0 后结束. 每次迭代时调用 maxHeapify 把当前堆构造成最大堆.

heapSize / 2 - 1 在数组a[0..heapSize] 中的位置 是 最后一个非叶子结点的位置;
heapSize/2 , heapSize/2 +1 …, heapSize 这些下标都是叶子结点.

构建 最大堆 的代码实现:

public void buildMaxHeap(int[] a) {
    int heapSize = a.length;
    // i 从倒数第二层最右边的 非叶子结点开始构造
    for (int i = (heapSize / 2 - 1); i >= 0; i--) {
        maxHeapify(a, i, heapSize);
    }
}

buildMaxHeap需要O(n)时间, maxHeapify需要O(lgn)时间, 因此buildMaxHeap 的时间复杂度是 O(nlgn).
O(nlgn) 只是一个粗略的上界, 根据<算法导论>严格的推导, buildMaxHeap 更精确的时间复杂度是 O(n).

4 堆排序算法

堆排序算法思想:
先构建一个最大堆;
i = heapSize-1 遍历到 i = 1, 每次迭代: 先交换 a[0]a[i], 再 heapSize--, 再用 maxHeapify(a, i, heapSize) 重新构建堆.
每次迭代都会把最大元素放在 有效堆 的最后, 同时把 有效堆 的大小(heapSize)缩小 1. 这样得到的效果是每次迭代把 有效堆 中 一个最大的元素排好序.

堆排序 的代码实现:

public void heapSort(int[] a) {
    buildMaxHeap(a);
    int heapSize = a.length;
    for (int i = a.length - 1; i >= 1; i--) {
        int temp = a[i];
        a[i] = a[0];
        a[0] = temp;

        heapSize = heapSize - 1;
        maxHeapify(a, 0, heapSize);
    }
}

heapSort 的时间复杂度是 O(nlgn).

堆排序代码的实现很简单. 主要是要理解 最大堆结构和数组的关系 以及 维护最大堆性质建堆 这两个操作.

附录: 堆排序代码

public class HeapSort2 {
    private int parent(int i) {
        return (i - 1) / 2;
    }

    private int left(int i) {
        return 2 * i + 1;
    }

    private int right(int i) {
        return 2 * (i + 1);
    }

    public void maxHeapify(int[] a, int i, int heapSize) {
        int l = left(i);
        int r = right(i);
        int largestIndex;
        if (l < heapSize && a[l] > a[i]) {
            largestIndex = l;
        } else {
            largestIndex = i;
        }

        if (r < heapSize && a[r] > a[largestIndex]) {
            largestIndex = r;
        }
        if (largestIndex != i) {
            int temp = a[i];
            a[i] = a[largestIndex];
            a[largestIndex] = temp;

            maxHeapify(a, largestIndex, heapSize);
        }
    }

    public void buildMaxHeap(int[] a) {
        int heapSize = a.length;
        // i 从第一个非叶子结点开始构造
        for (int i = (heapSize / 2 - 1); i >= 0; i--) {
            maxHeapify(a, i, heapSize);
        }
    }

    public void heapSort(int[] a) {
        buildMaxHeap(a);
        int heapSize = a.length;
        for (int i = a.length - 1; i >= 1; i--) {
            int temp = a[i];
            a[i] = a[0];
            a[0] = temp;

            heapSize = heapSize - 1;
            maxHeapify(a, 0, heapSize);
        }
    }

    public static void main(String[] args) {
        int[] a = {4, 5, 1, 2, 7, 11, 3};
        HeapSort2 heapSort = new HeapSort2();
        heapSort.heapSort(a);
        System.out.println(Arrays.toString(a));
    }
}

Reference

[1]. 算法导论(第三版)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值