• 堆排序基本原理及实现


    一、堆的概念

    我们一般提到堆排序里的堆指的是二叉堆(binary heap),是一种完全二叉树,二叉堆有两种:最大堆和最小堆,特点是父节点的值大于(小于)两个小节点的值。

    二、基础知识

    完全二叉树有一个性质是,除了最底层,每一层都是满的,这使得堆可以利用数组来表示,每个结点对应数组中的一个元素,如下图所示

    heap-and-array

    对于给定的某个结点的下标 i(从1开始),可以很容易的计算出这个结点的父结点、孩子结点的下标:

    • Parent(i) = floor(i/2),i 的父节点下标
    • Left(i) = 2i,i 的左子节点下标
    • Right(i) = 2i + 1,i 的右子节点下标

    但是数组都是0基的,所以调整下标之后,对应关系如下图所示:

    heap-and-array-zero-based

    因此前面说到的关系也要随之调整:

    • Parent(i) = floor((i-1)/2),i 的父节点下标
    • Left(i) = 2i + 1,i 的左子节点下标
    • Right(i) = 2(i + 1),i 的右子节点下标

    三、堆的基本操作

    1. 最大堆调整(Max-Heapify)

    该操作主要用于维持堆的基本性质。假设数组A和下标i,假定以Left(i)和Right(i)为根结点的左右两棵子树都已经是最大堆,节点i的值可能小于其子节点。调整节点i的位置,使得子节点永远小于父节点,过程如下图所示:

    MAX‐HEAPIFY-Procedure

    由于一次调整后,堆仍然违反堆性质,所以需要递归的测试,使得整个堆都满足堆性质,用 JavaScript 可以表示如下:

    /**
     * 从 index 开始检查并保持最大堆性质
     *
     * @array
     *
     * @index 检查的起始下标
     *
     * @heapSize 堆大小
     *
     **/
    function maxHeapify(array, index, heapSize) {
      var iMax = index,
          iLeft = 2 * index + 1,
          iRight = 2 * (index + 1);
      if (iLeft < heapSize && array[index] < array[iLeft]) {
        iMax = iLeft;
      }
      if (iRight < heapSize && array[iMax] < array[iRight]) {
        iMax = iRight;
      }
      if (iMax != index) {
        swap(array, iMax, index);
        maxHeapify(array, iMax, heapSize); // 递归调整
      }
    }
    function swap(array, i, j) {
      var temp = array[i];
      array[i] = array[j];
      array[j] = temp;
    }

    2. 创建最大堆(Build-Max-Heap)

    创建最大堆(Build-Max-Heap)的作用是将一个数组改造成一个最大堆,接受数组和堆大小两个参数,Build-Max-Heap 将自下而上的调用 Max-Heapify 来改造数组,建立最大堆。因为 Max-Heapify 能够保证下标 i 的结点之后结点都满足最大堆的性质,所以自下而上的调用 Max-Heapify 能够在改造过程中保持这一性质。如果最大堆的数量元素是 n,那么 Build-Max-Heap 从 Parent(n) 开始,往上依次调用 Max-Heapify。流程如下:

    building-a-heap

    用 JavaScript实现:

    function buildMaxHeap(array, heapSize) {
      var i,
          iParent = Math.floor((heapSize - 1) / 2);
          
      for (i = iParent; i >= 0; i--) {
        maxHeapify(array, i, heapSize);
      }
    }

    3. 堆排序(Heap-Sort)

    堆排序(Heap-Sort)是堆排序的接口算法,Heap-Sort先调用Build-Max-Heap将数组改造为最大堆,然后将堆顶和堆底元素交换,之后将底部上升,最后重新调用Max-Heapify保持最大堆性质。由于堆顶元素必然是堆中最大的元素,所以一次操作之后,堆中存在的最大元素被分离出堆,重复n-1次之后,数组排列完毕。如果是从小到大排序,用大顶堆;从大到小排序,用小顶堆。流程如下:

    HeapSort

    用 JavaScript实现:

    function heapSort(array, heapSize) {
      buildMaxHeap(array, heapSize);
      for (int i = heapSize - 1; i > 0; i--) {
        swap(array, 0, i);
        maxHeapify(array, 0, i);
      }  
    }

    四、时间复杂度与排序稳定性

    我们知道n个元素的完全二叉树的深度h=floor(logn),分析各个环节的时间复杂度如下。

    1. 堆调整时间复杂度

    从堆调整的代码可以看到是当前节点与其子节点比较两次,交换一次。父节点与哪一个子节点进行交换,就对该子节点递归进行此操作,设对调整的时间复杂度为T(k)(k为该层节点到叶节点的距离),那么有:

    • T(k)=T(k-1)+3, k∈[2,h]
    • T(1)=3

    迭代法计算结果为:

    • T(h)=3h=3floor(log n)

    所以堆调整的时间复杂度是O(log n) 。

    2. 建堆的时间复杂度

    n个节点的堆,树高度是h=floor(log n)。

    对深度为于h-1层的节点,比较2次,交换1次,这一层最多有2^(h-1)个节点,总共操作次数最多为3(12^(h-1));对深度为h-2层的节点,总共有2^(h-2)个,每个节点最多比较4次,交换2次,所以操作次数最多为3(22^(h-2))……
    以此类推,从最后一个父节点到根结点进行堆调整的总共操作次数为:

    s=3*[2^(h-1) + 2*2^(h-2) + 3*2^(h-3) + … + h*2^0]       a
    2s=3*[2^h + 2*2^(h-1) + 3*2(h-2) + … + h*2^1]           b
    b-a,得到一个等比数列,根据等比数列求和公式
    s = 2s - s = 3*[2^h + 2^(h-1) + 2^(h-2) + … + 2 - h]=3*[2^(h+1)-  2 - h]≈3*n

    所以建堆的时间复杂度是O(n)。

    3. 堆排序时间复杂度

    从上面的代码知道,堆排序的时间等于建堆和进行堆调整的时间之和,所以堆排序的时间复杂度是O(nlog n + n) =O(nlog n)。

    4. 稳定性

    堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。

    五、参考

    1. 堆排序

    2. 常见排序算法 - 堆排序 (Heap Sort)

    3. 堆排序(Heap Sort)算法学习

    4. 堆排序的时间复杂度

    (完)

  • 相关阅读:
    多重背包POJ1276不要求恰好装满 poj1014多重背包恰好装满
    哈理工1053完全背包
    求最小公倍数与最大公约数的函数
    Bus Pass ZOJ 2913 BFS 最大中取最小的
    POJ 3624 charm bracelet 01背包 不要求装满
    HavelHakimi定理(判断一个序列是否可图)
    z0j1008Gnome Tetravex
    ZOJ 1136 Multiple BFS 取模 POJ 1465
    01背包 擎天柱 恰好装满 zjut(浙江工业大学OJ) 1355
    zoj 2412 水田灌溉,求连通分支个数
  • 原文地址:https://www.cnblogs.com/harrymore/p/9121886.html
Copyright © 2020-2023  润新知