堆排序终极解析

895 阅读4分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是大于(大顶堆)或者小于(小顶堆)它的左右子节点值。它的最坏、最好、平均时间复杂度均为O(nlogn),同时它也是一个不稳定排序。

算法基本思想

  1. 将待排序序列构成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点
  2. 将其与末尾元素进行交换,此时末尾元素就是最大值
  3. 然后将剩余n-1个元素重新构成一个堆,就会得到n个元素的次小值,如此反复执行,便能得到一个有序序列

数据结构解析

堆是用完全二叉树的结构来维护的一组数据

完全二叉树:设二叉树的深度为h,除第h层外,其他各层(1~h-1)的结点数都达到最大个数,第h层所有的节点都连续集中在最左边的树。

小根堆:对于堆中的每个结点的左右子节点的值都要小于该结点的堆称为最小堆(很多时候都看作为小于等于)

heap[i]< heap[2i+1] && heap[i]< head[2i+2]
大根堆:对于堆中的每个结点的左右子结点的值都要大于该结点的堆称为最小堆(很多时候都看作为大于等于)
heap[i]>heap[2i+1] && heap[i]> head[2i+2]

复杂度解析

堆排序本质是一种选择排序,整体主要由构建初始堆交换堆顶元素并重建堆两部分组成。
其中构建初始堆经推到复杂度为O(n)
交换堆顶元素并重建堆,将循环n - 1次,每次都是从根节点往下循环查找,所以每一次时间是logn,总时间:logn(n-1) = nlogn-logn;
时间复杂度为O(nlogn)
因为堆排序是就地排序,没有递归没有占据多余空间,所以空间复杂度是O(1)

稳定性解析

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,经过排序过后,这些记录的相对次序保持不变

即在原序列中,Ri = Rj, 且Ri在Rj之前,而在排序后 的序列中,Ri仍在Rj之前则称这种排序算法是稳定的,否则则称其为不稳定的
考虑序列{9, 5a, 7, 5b },按照堆排序的算法走一遍,很快就能发现,输出序列为{5b, 5a, 7, 9},而且与等号无关,显然可见堆排序是不稳定的

使用场景解析

堆排序相较于快速排序、归并排序更适合于数据量特别大的场合,比如要处理超过数百万条记录的某些场景
堆排序不需要大量的递归或者多维的暂存数组,这对于数据量特别巨大的序列是非常合适的。而快速排序、归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误

代码实现

JAVA实现

/**
* 选择排序-堆排序
* @param array 待排序数组
* @return 已排序数组
*/
public static int[] heapSort(int[] array) {
    //这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1
    for (int i = array.length / 2 - 1; i >= 0; i--) {  
        adjustHeap(array, i, array.length);  //调整堆
    }

    // 上述逻辑,建堆结束
    // 下面,开始排序逻辑
    for (int j = array.length - 1; j > 0; j--) {
        // 元素交换,作用是去掉大顶堆
        // 把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
        swap(array, 0, j);
        // 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
        // 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
        // 而这里,实质上是自上而下,自左向右进行调整的
        adjustHeap(array, 0, j);
    }
    return array;
}

/**
* 整个堆排序最关键的地方
* @param array 待组堆
* @param i 起始结点
* @param length 堆的长度
*/
public static void adjustHeap(int[] array, int i, int length) {
    // 先把当前元素取出来,因为当前元素可能要一直移动
    int temp = array[i];
    for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {  //2*i+1为左子树i的左子树(因为i是从0开始的),2*k+1为k的左子树
        // 让k先指向子节点中最大的节点
        if (k + 1 < length && array[k] < array[k + 1]) {  //如果有右子树,并且右子树大于左子树
            k++;
        }
        //如果发现结点(左右子结点)大于根结点,则进行值的交换
        if (array[k] > temp) {
            swap(array, i, k);
            // 如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,循环对子节点所在的树继续进行判断
                i  =  k;
                    } else {  //不用交换,直接终止循环
            break;
        }
    }
}

/**
* 交换元素
* @param arr
* @param a 元素的下标
* @param b 元素的下标
*/
public static void swap(int[] arr, int a, int b) {
    int temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}

JavaScript实现

var len;
function buildMaxHeap(arr) { //建立大顶堆
  len = arr.length;
  for (let i = Math.floor(len/2); i >= 0 ; i--) {
    heapify(arr, i);
  }
}
function heapify(arr, i) {
  var left = 2 * i + 1,
    right = 2 * i + 2,
    largest = i;
  
  if (left < len && arr[left] > arr[largest]) {
    largest = left;
  }

  if (right < len && arr[right] > arr[largest]) {
    largest = right;
  }

  if (largest != i) {
    swap(arr, i, largest);
    heapify(arr, largest);
  }
}
function swap(arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
function heapSort(arr) {
  buildMaxHeap(arr);
  for (var i = arr.length - 1; i > 0; i--) {
    swap(arr, 0, i);
    len--;
    heapify(arr, 0);
  }
  return arr;
}