堆和堆傻傻分不清?一文告诉你 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() 虽然不能直接操作,但是堆排序中用到了这种思路,之前的「选择排序」那篇文章里也提到了一些,感兴趣的同学可以后台回复「选择排序」获得文章~至于堆排序的具体实现和应用,以及为什么实际生产中并不爱用它,我们之后再讲。


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


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

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


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


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

目录
相关文章
|
2月前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
42 6
|
2月前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
41 3
|
2月前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
33 2
|
2月前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
34 3
|
1月前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
19天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
25 2
|
23天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
19天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
23天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
23天前
|
Java 开发者