《数据结构与算法之美》专栏阅读笔记8——堆

本文介绍了堆的基本概念,包括大顶堆和小顶堆,并详细讲解了堆的实现,如插入、删除操作。接着,通过堆排序的建堆和排序过程,分析了其复杂度为O(nLogn)。最后,讨论了堆排序与快速排序的对比,并阐述了堆在优先级队列和求TopK问题中的应用。
摘要由CSDN通过智能技术生成

传说中的另一棵树

一种特殊的树。

基本概念

堆需要满足的两个要求:

  • 堆是一个完全二叉树
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

大顶堆:节点的值大于等于子树中节点的值
小顶堆:节点的值小于等于字数中节点的值

大顶堆
小顶堆
堆的实现

因为堆是完全二叉树,用数组来存储比较方便。
节点下标为i,则左子节点下标为2*i,右子节点下标为2*i+1,父节点下标为i/2

插入
堆中插入新数据后,需要满足堆的两个特性。调整的过程叫做“堆化”。
堆化分为两种:从上往下、从下往上。
思路:顺着节点所在的路径,向上,对比,交换。

public class Heap {
    private int[] storage;
    private int size;
    private int count;

    public Heap(int size) {
        storage = new int[size];
        this.size = size;
        count = 0;
    }

    public void insert(int data) {
        if (count >= size) {
            return;
        }
        storage[++count] = data;

        int idx = count;
        while(idx/2 != 0 && storage[idx] > storage[idx/2]) {
            int fIdx = idx/2;
            int tmp = storage[fIdx];
            storage[fIdx] = storage[idx];
            storage[idx] = tmp;
            idx = idx/2;
        }
    }
}

堆化虽说有两种方式,但是从上往下的堆化方式并不适合用在插入中。

删除
对于堆来说,因为堆结构由严格的要求(完全二叉树)限制,若是删除任意元素,需要重新构建堆。一般的操作需求都是删除堆顶元素(最大值或最小值),
删除,就是要减少一个,因为要保持完全二叉树的结构,用来补充删除的元素只能是从最后一个叶子节点来补。
补充之后需要再次进行堆化处理,此时就是从顶部开始(从上到下的方式)。

public void removeMax() {
        if (count == 0)
            return;

        storage[0] = storage[count--];
        heapify(storage, count);
    }

    private void heapify(int[] storage, int length) {
        int pIdx = 1;
        while(true) {
            int lIdx = pIdx * 2;
            int rIdx = pIdx * 2 + 1;
            int swapIdx = pIdx;
            if (lIdx <= length && storage[pIdx] < storage[lIdx]) {
                swapIdx = lIdx;
            }
            if (rIdx <= length && storage[pIdx] < storage[rIdx]) {
                swapIdx = rIdx;
            }
            if (swapIdx == pIdx) {
                break;
            }
            int tmp = storage[swapIdx];
            storage[swapIdx] = storage[pIdx];
            storage[pIdx] = tmp;
            pIdx = swapIdx;
        }
    }

一个需要注意的地方,使用数组存储二叉树的时候,左右子树的计算公式都是基于根节点的下标从1开始的前提,所以左右子结点的下标的判断时需要注意。程序中的count记录的是节点的个数,所以判断条件rIdx <= length

基于堆实现排序

堆排序分为建堆和排序两个步骤

建堆

建堆的可以有两种思路:

  • 按照插入的思路来建堆,一个一个往数组里放数据,每放一个数据都是从下往上堆化
  • 全部放进数组,然后从第一个非叶子节点开始,从后往前地进行向下堆化

第二种思路注意两点:

  • 堆化的思路还是从下往上,堆化的范围是当前处理节点的子树。
  • 处理的顺序是从后往前,先处理最后一个非叶子节点,依次直到根节点。

实现如下:

public void buildHeap(int[] storage, int size) {
        for (int i = size/2; i > 0; i--) {
            heapify(storage, size, i);
        }
    }

把写删除的时候的堆化函数用起来,添加一个参数:

private void heapify(int[] storage, int length, int i) {
        int pIdx = i;
        while(true) {
            int lIdx = pIdx * 2;
            int rIdx = pIdx * 2 + 1;
            int swapIdx = pIdx;
            if (lIdx <= length && storage[pIdx] < storage[lIdx]) {
                swapIdx = lIdx;
            }
            if (rIdx <= length && storage[pIdx] < storage[rIdx]) {
                swapIdx = rIdx;
            }
            if (swapIdx == pIdx) {
                break;
            }
            int tmp = storage[swapIdx];
            storage[swapIdx] = storage[pIdx];
            storage[pIdx] = tmp;
            pIdx = swapIdx;
        }
    }
复杂度分析

规律:

  • 每个节点堆化的过程中,需要比较和交换的节点个数,跟节点的高度成正比。
  • 叶子节点是不需要进行堆化的。

因此,复杂度的计算就是

每个节点堆化的复杂度是O(logn),因此总体复杂度是O(n)

排序

利用堆顶元素是最大值或最小值,以及每次堆化,处于堆顶的值都会是当前参加堆化的节点的极值这一点,来进行排序。
总结下来就是从每次参加堆化的节点中去掉堆顶,咋去掉呢,就是拿最后一个子元素来替换堆顶。

实现

public void sort(int[] storage, int size) {
        buildHeap(storage, size);
        int length = size;
        while(length > 1) {
            int tmp = storage[length];
            storage[length] = storage[1];
            storage[1] = tmp;

            heapify(storage, --length, 1);
        }
    }

总结下:堆化函数要好好写呢~
复杂度:O(nLogn)。

堆排序PK快速排序

快排在实际中应用比较多的原因:

  • 堆排序对数组元素的访问不是顺序的,对CPU缓存不友好
  • 堆排序过程中的交换次数多于快速排序,而且会打乱数据有序度
堆的应用
  • 优先级队列
    那个啥高性能定时器的例子在老白的代码里见到了呢
  • 求TopK
  • 求中位数:大顶堆和小顶堆配合用
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值