Skip to content

堆结构和堆排序


堆结构

基本介绍

堆结构是一种特殊的完全二叉树,分为大根堆和小根堆

大根堆:任何一颗子树的根节点都大于子节点

小根堆:任何一颗子树的根节点都小于子节点

以大根堆为例



相关性质

堆结构是一种特殊的完全二叉树,可以用数组实现,通过调整数组中元素的位置,维持堆的结构,用数组的大小 size 来控制堆的大小

堆中元素位置与数组中元素位置的关系:从索引 0 开始,在堆中从上到下,从左到右,依次排列 0 - n-1 位置的元素


对于任意索引 i 位置的元素,由如下性质

(1)i 的父亲节点:(i - 1) / 2

(2)i 的左孩子:i * 2 + 1

(3)i 的右孩子:i * 2 + 2

堆调整

堆是一种特殊的完全二叉树,n 个节点,树的高度为 log n(以 2 为底)

堆的调整过程最差情况是从顶到低,调整的路径怎么也不会大于树的高度,即堆调整的时间复杂度为 O(log n)

heapInsert

以大根堆为例,这里表示上调整,插入一个元素或者元素变大了,调整堆的结构,维持大根堆的性质

java
// i位置的数,向上调整大根堆
// arr[i] = x,x是新来的!往上看,直到不比父亲大,或者来到 0 位置(顶节点)
public static void heapInsert(int[] arr, int i) {
    // 对于索引 i 位置的元素,其父节点的索引为 (i - 1) / 2
    // 元素交换后,i 来到父节点的位置,继续判断是否需要向上调整
    while (arr[i] > arr[(i - 1) / 2]) {
        swap(arr, i, (i - 1) / 2);
        i = (i - 1) / 2;
    }
}

heapify

以大根堆为例,这里表示下调整,删除一个元素或者元素变小了,调整堆的结构,维持大根堆的性质

java
// i位置的数,变小了,又想维持大根堆结构,向下调整大根堆
// 当前堆的大小为size
public static void heapify(int[] arr, int i, int size) {
    int l = i * 2 + 1;
    // 有左孩子
    while (l < size) {
        // 有左孩子,l
        // 右孩子,l + 1
        // 找出左右孩子中最大的孩子下标
        int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
        // 将最大的孩子和当前元素相比,找出最大的元素,并更新最大下标
        best = arr[best] > arr[i] ? best : i;
        // 如果 best == i,说明最强的元素是 i 自己,不需要继续调整
        if (best == i) {
            break;
        }
        swap(arr, best, i);
        i = best;
        l = i * 2 + 1;
    }
}

堆排序

题目链接

https://leetcode.cn/problems/sort-an-array/

思路分析

建立大根堆,将堆顶元素和最后一个元素交换,size--(表示元素排好了,从堆中移除),向下调整堆维持大根堆的结构,依次循环

有两种方式建堆,从顶到底建大根堆,从底到顶建大根堆(时间复杂度为 O(n),推荐使用

堆排序的整体时间复杂度是 O(n * log n)

从顶到底建大根堆

堆排序时间复杂度:O(n * log n)

构建堆的时间复杂度:O(n * log n)

每来一个元素,就 heapInsert 一次,log1 + log2 + log3 + … + logn 收敛于 O(n*logn)

java
class Solution {
    public int[] sortArray(int[] nums) {
        if (nums.length > 1) {
            heapSort(nums);
        }
        return nums;
    }

    // 从顶到底建堆
    public void heapSort(int[] arr) {
        int n = arr.length;
        // 建立大根堆
        for (int i = 0; i < n; i++) {
            heapInsert(arr, i);
        }
        // 堆顶元素和最后一个元素交换位置
        int size = n;
        while (size > 0) {
            swap(arr, 0, size - 1);
            size--;
            heapify(arr, 0, size);
        }
    }

    public void heapify(int[] arr, int i, int size) {
        int l = 2 * i + 1;
        // 有左孩子
        while (l < size) {
            // 不一定有右孩子,要判断
            // 找出两个孩子中最大的那个,并记录下标
            int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
            // 将最大的孩子与 i 位置的比较
            best = arr[best] > arr[i] ? best : i;
            // 最大的就是 i 自己,不用调整
            if (best == i) {
                break;
            }
            swap(arr, best, i);
            // 更新 i 位置
            i = best;
            // 继续下一轮的往下调整
            l = 2 * i + 1;
        }

    }

    public void heapInsert(int[] arr, int i) {
        // 维持大根堆的性质
        // 父节点的索引 (i -1) / 2
        while (arr[i] > arr[(i - 1) / 2]) {
            swap(arr, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }

    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

从底到顶建大根堆

构建堆的时间复杂度:O(n)

堆排序时间复杂度:O(n * log n)

从底到顶建大根堆,每加入一个元素就需要往下调整,根据如下对元素的规模分析可以推算建堆的时间复杂度


java
class Solution {
    public int[] sortArray(int[] nums) {
        if (nums.length > 1) {
            heapSort(nums);
        }
        return nums;
    }

    // 从低到顶建堆
    public void heapSort(int[] arr) {
        int n = arr.length;
        for (int i = n - 1; i >= 0; i--) {
            heapify(arr, i, n);
        }
        int size = n;
        while (size > 0) {
            swap(arr, 0, size - 1);
            size--;
            heapify(arr, 0, size);
        }
    }

    public void heapify(int[] arr, int i, int size) {
        // 向下调整
        int l = i * 2 + 1;
        while (l < size) {
            // 找出两个孩子中较大的那个
            // 要考虑是否有右孩子
            int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
            // 最大的孩子跟父节点比较
            best = arr[best] > arr[i] ? best : i;
            // 说明最大的节点是自己
            if (best == i) {
                break;
            }
            // 否则就向下调整堆的结构
            swap(arr, i, best);
            // i 来到 best 的位置,继续下一轮的向下调整
            i = best;
            l = i * 2 + 1;
        }
    }

    public void heapInsert(int[] arr, int i) {
        // 向上调整,父节点的索引:(i - 1) / 2
        while (arr[i] > arr[(i - 1) / 2]) {
            swap(arr, i, (i - 1) / 2);
            // i 来到父节点的位置
            i = (i - 1) / 2;
        }
    }

    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

⭐ 增倍分析法

Tip

已知以 2 为底的 log 1 + log 2 + log 3 + ... + log n --> O(n * logn)

(1)时间复杂度分析

从顶到底建堆,每加入一个元素就要调整堆,而调整堆的时间复杂度是 O(logn),则堆排序的整体时间复杂度以 2 为底的 log 1 + log 2 + log 3 + ... + log n

(2)增倍分析法

n 个数依次进堆,数量级的上限也就是 log n,log n < n * log n,也就是说此时的上限是 n * log n

增一倍,2n 个数字的时候,前 n 个数的复杂度如上分析,由于后 n 个数的复杂度是基于前 n 个数的,则后 n 个数的复杂度的规模肯定大于 log n,而已知 n 个数的时候复杂度为 n * log n,也即是说此时的下限是 n * log n

对于复杂度来说,只看规模,常熟可以忽略,上限和下限都是 n * log n

那复杂度只能是 O(n * log n)