在一个无序的int数组上构建一个最小堆的时间复杂度_漫画:寻找无序数组的第k大元素(修订版)...

488ac2179df3dcf491edde4e28cf46d0.png
657d9b23baf43b193e01a9dc6e998b85.png

————— 第二天 —————

bcaf4085ac5a07e406b6454586ce0a92.png
7caa32f92e16fead257d6d11984653ad.png
51bcf8a9449bc20714645fc2956bd36d.png

题目是什么意思呢?比如给定的无序数组如下:

676d893d2fea09489a4f30d8ab758a30.png

如果 k=6,也就是要寻找第6大的元素,这个元素是哪一个呢?

显然,数组中第一大的元素是24,第二大的元素是20,第三大的元素是17 ...... 第6大的元素是9

722da15a5bef5c7f0944da6a4ba2f8fc.png
59213ccf2407e75ace7b45f8bfb14502.png
526f9916de64d444136151157ab828e5.png

方法一:排序法

这是最容易想到的方法,先把无序数组从大到小进行排序,排序后的第k个元素,自然就是数组中的第k大元素。

af051f5034b6d65d76f7c3bf9daf36ca.png
2871352f78591ea0234921f91dbd4b37.png
6f7aaa2891f016db2ad03922695e6aa5.png
6d5b52b693f3c2bc5487fa82e9fb2a4d.png

方法二:插入法

维护一个长度为k的数组A的有序数组,用于存储已知的k个较大的元素。

接下来遍历原数组,每遍历到一个元素,和数组A中最小的元素相比较,如果小于等于数组A的最小元素,继续遍历;如果大于数组A的最小元素,则插入到数组A中,并把曾经的最小元素“挤出去”。

比如k=3,先把最左侧的7,5,15三个数有序放入数组A当中,代表当前最大的三个数。

985367172080812509812e3ab2676c70.png

这时候,遍历到3, 由于3<5,继续遍历。

17173fefa648c6ec64a244d3e7f3fc75.png

接下来遍历到17,由于17>5,插入到数组A的合适位置,类似于插入排序,并把原先最小的元素5“挤出去”。

a97197231205e71b00a79fcb4b022e2f.png

继续遍历原数组,一直遍历到数组的最后一个元素......

最终,数组A中存储的元素是24,20,17,代表着整个数组中最大的3个元素。此时数组A中的最小的元素17就是我们要寻找的第k大元素。

86cf8df785a8f4d3f969da1702a127ad.png
311f9471a6a2dff50f82b577daf68f55.png
46942d74b0590ad6a74f2bd25756b757.png
96c3f23242fb0c53094616e5fec941a7.png

————————————

2ed9048854a20af0027e5eaba12ec808.png
e5537bd46ba2f82d36256dd08d21fe03.png
36a08c8b10ec2595dc7f38ffba8e61e5.png
b4f5aa68b273a23ce0bac0c5e2b0ad30.png

什么是二叉堆?不太了解的小伙伴可以先看看这一篇:

漫画:什么是二叉堆?(修正版)

简而言之,二叉堆是一种特殊的完全二叉树,它包含大顶堆和小顶堆两种形式。

其中小顶堆的特点,是每一个父节点都小于等于自己的子节点。要解决这个算法题,我们可以利用小顶堆的特性。

a3145a04e118173566687bb7c7cb384c.png
e5a4522abd331880af1d0c426b9ba2f8.png

方法三:小顶堆法

维护一个容量为k的小顶堆,堆中的k个节点代表着当前最大的k个元素,而堆顶显然是这k个元素中的最小值

遍历原数组,每遍历一个元素,就和堆顶比较,如果当前元素小于等于堆顶,则继续遍历;如果元素大于堆顶,则把当前元素放在堆顶位置,并调整二叉堆(下沉操作)。

遍历结束后,堆顶就是数组的最大k个元素中的最小值,也就是第k大元素

假设k=5,具体的执行步骤如下:

1.把数组的前k个元素构建成堆。

2828fbd205c3643fe782867226d74d42.png

2.继续遍历数组,和堆顶比较,如果小于等于堆顶,则继续遍历;如果大于堆顶,则取代堆顶元素并调整堆。

遍历到元素2,由于 2<3,所以继续遍历。

4c80505446cb6dd84019908c414e674f.png

遍历到元素20,由于 20>3,20取代堆顶位置,并调整堆。

106b55b2a3184ea0b255520adc143c77.png
2df511c92ad04df048d2a180f67318e1.png

遍历到元素24,由于 24>5,24取代堆顶位置,并调整堆。

b77ff40b3b5f91d4516d22af949ffbbd.png
6396523d3b671d9b1532c680ad39f649.png

以此类推,我们一个一个遍历元素,当遍历到最后一个元素8的时候,小顶堆的情况如下:

1a614eaf2aa1c32b6058c44e64dd6d2a.png

3.此时的堆顶,就是堆中的最小值,也就是数组中的第k大元素。

34ac84a7ead4948f5fa68f25a15bdcda.png

这个方法的时间复杂度是多少呢?

1.构建堆的时间复杂度是 O(k)

2.遍历剩余数组的时间复杂度是O(n-k)

3.每次调整堆的时间复杂度是 O(logk)

其中2和3是嵌套关系,1和2,3是并列关系,所以总的最坏时间复杂度是O((n-k)logk + k)。当k远小于n的情况下,也可以近似地认为是O(nlogk)

这个方法的空间复杂度是多少呢?

刚才我们在详细步骤中把二叉堆单独拿出来演示,是为了便于理解。但如果允许改变原数组的话,我们可以把数组的前k个元素“原地交换”来构建成二叉堆,这样就免去了开辟额外的存储空间。

因此,方法的空间复杂度是O(1)

94bc40c701b9fad87fde0718cb66ff58.png
e9425a48c04028e5a78615d921bfe433.png
/** * 寻找第k大的元素 * @param array 待调整的堆 * @param k 第几大 */public static int findNumberK(int[] array, int k){ //1.用前k个元素构建小顶堆 buildHeap(array, k); //2.继续遍历数组,和堆顶比较 for(int i=k; i array[0]){ array[0] = array[i]; downAdjust(array, 0, k); } } //3.返回堆顶元素 return array[0];}/** * 构建堆 * @param array 待调整的堆 * @param length 堆的有效大小 */private static void buildHeap(int[] array, int length) { // 从最后一个非叶子节点开始,依次下沉调整 for (int i = (length-2)/2; i >= 0; i--) { downAdjust(array, i, length); }}/** * 下沉调整 * @param array 待调整的堆 * @param index 要下沉的节点 * @param length 堆的有效大小 */private static void downAdjust(int[] array, int index, int length) { // temp保存父节点值,用于最后的赋值 int temp = array[index]; int childIndex = 2 * index + 1; while (childIndex < length) { // 如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子 if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) { childIndex++; } // 如果父节点小于任何一个孩子的值,直接跳出 if (temp <= array[childIndex]) break; //无需真正交换,单向赋值即可 array[index] = array[childIndex]; index = childIndex; childIndex = 2 * childIndex + 1; } array[index] = temp;}public static void main(String[] args) { int[] array = new int[] {7,5,15,3,17,2,20,24,1,9,12,8}; System.out.println(findNumberK(array, 5));}
34efd172a97cebce5a468a27171ee87d.png

方法四:分治法

大家都了解快速排序,快速排序利用分治法,每一次把数组分成较大和较小的两部分。

我们在寻找第k大元素的时候,也可以利用这个思路,以某个元素A为基准,把大于于A的元素都交换到数组左边,小于A的元素都交换到数组右边。

比如我们选择以元素7作为基准,把数组分成了左侧较大,右侧较小的两个区域,交换结果如下:

67f5b6a5f28cdef54bcd3769ebb03ed6.png

包括元素7在内的较大元素有8个,但我们的k=5,显然较大元素的数目过多了。于是我们在较大元素的区域继续分治,这次以元素12位基准:

26602cac0f915283d8b0d0efba7bf197.png

这样一来,包括元素12在内的较大元素有5个,正好和k相等。所以,基准元素12就是我们所求的。

这就是分治法的大体思想,这种方法的时间复杂度甚至优于小顶堆法,可以达到O(n)。有兴趣的小伙伴可以尝试用代码实现一下。

307468a0717e0023a05bd85655ecfd19.png

喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值