堆和堆傻傻分不清?一文告诉你 Java 集合中「堆」的最佳打开方式(下)

简介: 接上文。

heapify()


还有一个大名鼎鼎的非常重要的操作,就是 heapify() 了,它是一个很神奇的操作,


可以用 O(n) 的时间把一个乱序的数组变成一个 heap。


但是呢,heapify() 并不是一个 public API,看:


image.png


所以我们没有办法直接使用。


唯一使用 heapify() 的方式呢,就是使用


PriorityQueue(Collection<? extends E> c)


这个 constructor 的时候,人家会自动调用 heapify() 这个操作。


<span style="display:block;color:blue;">那具体是怎么做的呢?


哈哈源码已经暴露了:


从最后一个非叶子节点开始,从后往前做 siftDown().


因为叶子节点没必要操作嘛,已经到了最下面了,还能和谁 swap?


举个例子:


image.png


我们想把这个数组进行 heapify() 操作,想把它变成一个最小堆,拿到它的最小值。

那就要从 3 开始,对 3,7,5进行 siftDown().


Step 1.


image.png


尴尬 😅,3 并不用交换,因为以它为顶点的这棵小树已经满足了堆序性。


Step 2.


image.png


7 比它的两个孩子都要大,所以和较小的那个交换一下。


交换完成后;


image.png


Step 3.


最后一个要处理的就是 5 了,那这里 5 比它的两个孩子都要大,所以也和较小的那个交换一下。


image.png


换完之后结果如下,注意并没有满足堆序性,因为 4 还比 5 小呢。


image.png


所以接着和 4 换,结果如下:


image.png


这样整个 heapify() 的过程就完成了。


好了难点来了,为什么时间复杂度是 O(n) 的呢?


怎么计算这个时间复杂度呢?


其实我们在这个过程里做的操作无非就是交换交换。


那到底交换了多少次呢?


没错,交换了多少次,时间复杂度就是多少。


那我们可以看出来,其实同一层的节点最多交换的次数都是相同的。


那么这个总的交换次数 = 每层的节点数 * 每个节点最多交换的次数


image.png


这里设 k 为层数,那么这个例子里 k=3.


每层的节点数是从上到下以指数增长:


$$\ce{1, 2, 4, ..., 2^{k-1}}$$


每个节点交换的次数,


从下往上就是:


0, 1, ..., k-2, k-10,1,...,k2,k1


那么总的交换次数 S(k) 就是两者相乘再相加:


S(k) = \left(2^{0} *(k-1) + 2^{1} *(k-2) + ... + 2^{k-2} *1 \right)S(k)=(20(k1)+21(k2)+...+2k21)


这是一个等比等差数列,标准的求和方式就是错位相减法


那么


2S(k) = \left(2^{1} *(k-1) + 2^{2} *(k-2) + ... + 2^{k-1} *1 \right)2S(k)=(21(k1)+22(k2)+...+2k11)


两者相减得:


S(k) = \left(-2^{0} *(k-1) + 2^{1} + 2^{2} + ... + 2^{k-2} + 2^{k-1} \right)S(k)=(20(k1)+21+22+...+2k2+2k1)


化简一下:


(不好意思我实在受不了这个编辑器了。。。


image.png


所以 heapify() 时间复杂度是 O(n).


以上就是堆的三大重要操作,最后一个 heapify() 虽然不能直接操作,但是堆排序中用到了这种思路,之前的「选择排序」那篇文章里也提到了一些,感兴趣的同学可以后台回复「选择排序」获得文章~至于堆排序的具体实现和应用,以及为什么实际生产中并不爱用它,我们之后再讲。


最后再说一点题外话,最近发现了几篇搬运我的文章到其他平台的现象。每篇文章都是我精心打造的,都是自己的心肝宝贝,看到别人直接搬运过去也没有标明作者和来源出处实在是太难受了。。为了最好的阅读体验,文中的图片我都没有加水印,但这也方便了他人搬运。今天考虑再三,还是不想违背自己的本意,毕竟我的读者更为重要。


所以如果之后有小伙伴看到了,恳请大家后台或者微信告诉我一下呀,非常感谢!

我在各大平台同名,请认准「码农田小齐」~


如果你喜欢我的文章或者有收获的话,麻烦给我点个「赞」或者「在看」给我个小鼓励呀,会让我开心好久~


想跟我一起玩转算法和面试的小伙伴,记得关注我,我是小齐,我们下期见。

目录
相关文章
|
4月前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
295 100
|
4月前
|
Java API 数据处理
Java Stream API:现代集合处理新方式
Java Stream API:现代集合处理新方式
324 101
|
4月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
3月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
128 7
|
6月前
|
Oracle Java 关系型数据库
掌握Java Stream API:高效集合处理的利器
掌握Java Stream API:高效集合处理的利器
424 80
|
6月前
|
安全 Java API
Java 8 Stream API:高效集合处理的利器
Java 8 Stream API:高效集合处理的利器
332 83
|
5月前
|
存储 缓存 安全
Java集合框架(二):Set接口与哈希表原理
本文深入解析Java中Set集合的工作原理及其实现机制,涵盖HashSet、LinkedHashSet和TreeSet三大实现类。从Set接口的特性出发,对比List理解去重机制,并详解哈希表原理、hashCode与equals方法的作用。进一步剖析HashSet的底层HashMap实现、LinkedHashSet的双向链表维护顺序特性,以及TreeSet基于红黑树的排序功能。文章还包含性能对比、自定义对象去重、集合运算实战和线程安全方案,帮助读者全面掌握Set的应用与选择策略。
338 23
|
4月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
5月前
|
安全 Java 开发者
Java集合框架:详解Deque接口的栈操作方法全集
理解和掌握这些方法对于实现像浏览器后退功能这样的栈操作来说至关重要,它们能够帮助开发者编写既高效又稳定的应用程序。此外,在多线程环境中想保证线程安全,可以考虑使用ConcurrentLinkedDeque,它是Deque的线程安全版本,尽管它并未直接实现栈操作的方法,但是Deque的接口方法可以相对应地使用。
319 12
|
5月前
|
存储 缓存 安全
Java集合框架(三):Map体系与ConcurrentHashMap
本文深入解析Java中Map接口体系及其实现类,包括HashMap、ConcurrentHashMap等的工作原理与线程安全机制。内容涵盖哈希冲突解决、扩容策略、并发优化,以及不同Map实现的适用场景,助你掌握高并发编程核心技巧。