《数据结构与算法之美》笔记之 堆及其应用


堆排序时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)

一、定义

定义只有两条:

  1. 堆是一个完全二叉树;
    除了最后一层外,其它节点都是满的,最后一层节点全部靠左排列,完全二叉树非常适合使用数组存储
    图片来自百度百科
  2. 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
    等价于“堆中每个节点的值都大于等于(或小于等于)其左子节点和右子节点的值”,大于等于的叫大顶堆,小于等于的叫小顶堆。

二、如何实现一个堆

首先确定如何存储一个堆 和 堆支持哪些操作

存储堆

使用数组存储完全二叉树,不需要存储左右子节点的指针,只需要通过数组的下标就可以找到左右子节点和父节点。所以用数组存储堆。

假设堆顶元素存储在数组下标为 1 的位置,那么下标为 i i i的节点的相关节点位置为:

  • 左子节点为 2 ∗ i 2*i 2i
  • 右子节点为 2 ∗ i + 1 2*i+1 2i+1
  • 父节点为 i 2 \frac i 2 2i

堆的基本操作

主要有插入元素,删除堆顶元素

插入元素

假设我们将新插入的元素放在堆的最后,插入元素后需要继续满足堆的定义,就需要对堆进行调整,这个过程叫堆化
堆化分为自上而下和自下而上
自下而上即让新插入的节点与父节点比对大小,如果不满足子节点小于等于父节点的大小关系就互换两个节点,一直重复,直到满足关系。

删除堆顶元素

假设堆为大顶堆,删除之后就需要将左右子节点较大的元素放入父节点,迭代删除,但是这样会出现空洞,所以我们可以先将堆的最后一个元素放入堆顶,然后利用同样的父子节点比对方法进行互换,这就是自上而下的的堆化方法。

那堆的这两个基本操作的时间复杂度是多少?堆化是顺着节点所在路径比较交换,时间复杂度与树的高度成正比,树的高度不超过 log ⁡ 2 n \log_2 n log2n,所以时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

三、堆排序

堆排序的步骤为建堆排序

1.建堆

将数组原地建成一个堆
两种思路:

  1. 借助堆的插入方法,假设起初堆中只包含一个元素,下标为1,之后插入下标为2的元素,再插入下标为3的元素…直到将包含n个元素的数组组成了堆,这样就是从前往后处理数组元素,自下而上堆化。
  2. 另外的思路是从后往前处理数组元素,自上而下堆化
    因为叶子节点往下堆化只能与自己比较,所以我们从第一个非叶子节点(下标为 n 2 \frac n 2 2n)开始依次堆化,下标大于 n 2 \frac n 2 2n的是叶子节点,不需要堆化。建堆过程的时间复杂度是O(n)

2. 排序

建堆之后,数据就是按照大顶堆的特性来组织的,我们将堆顶元素与堆最后一个元素交换,即将最大元素放到堆的最后,类似于删除了堆顶元素,将剩下n-1个重新建堆,再取堆顶元素,放到n-1个元素的最后,直到堆中只剩下标为1的一个元素。排序每次维护堆的时间复杂度为O(logn),进行了n-1次,所以时间复杂度为O(nlogn)。

堆排序是原地排序,并不是稳定的排序算法,排序过程中堆的每一个节点要与堆顶节点互换。

如果堆在数组中从0开始存储,那么相关节点的下标为:

  1. 左节点下标为 2 ∗ i + 1 2*i+1 2i+1
  2. 右节点下标为 2 ∗ i + 2 2*i+2 2i+2
  3. 父节点下标为 i − 1 2 \frac {i-1} 2 2i1

3. 堆排序的缺点

实际开发中快排要比堆排序好,有以下原因:

  1. 快排局部顺序访问,而堆排序依次访问的下标是指数增长的,如1,2,4,8,。。。这样对CPU缓存不友好
  2. 对于同样的顺序,堆排序的数据交换次数多于快速排序。快排的数据交换次数不会比逆序度多,而快排第一步的建堆过程会打乱数据的先后顺序,导致原有数据有序度降低。

堆的应用

主要有:优先队列,求Top K 和求中位数

优先队列

堆和优先队列非常相似,一个堆就可以看做一个优先队列
优先队列的具体应用有合并有序小文件和高性能定时器

求TOP K

针对静态数据集合,可以维护一个大小为K的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较,如果比堆顶大,则把堆顶元素删除,并把这个元素插入堆,如果比堆顶元素小,则不作处理。时间复杂度是O(nlogK)。
针对动态数据集合,我们同样维护一个大小为K的小顶堆,当数据被添加到集合时,与堆顶元素比较,决定是非删除堆顶元素并入堆,

利用堆求中位数

对于静态数据,先排序再确定中位数,但是对于动态数据集合,每次排序的成本太高。

我们可以维护两个堆,一个大顶堆,一个小顶堆,大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,小顶堆中数据都大于大顶堆的数据,这样堆顶元素就有一个是中位数,当n是奇数时,大顶堆存储 2 n + 1 2n+1 2n+1个数据,这样大顶堆堆顶就是中位数
数据是动态变化,当添加一个元素a时,当a小于大顶堆堆顶我们就将a插入大顶堆,否则插入小顶堆,这样就有可能出现两个堆中数据不符合前面约定,我们将一个堆的堆顶插入另一个堆直到满足要求即可。此时插入数据的时间复杂度为O(logn),取出中位数的时间复杂度为O(1),求其它百分位数据是同样道理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值