漫画:寻找无序数组的第k大元素(修订版)

简介: 题目是什么意思呢?比如给定的无序数组如下:如果 k=6,也就是要寻找第6大的元素,这个元素是哪一个呢?显然,数组中第一大的元素是24,第二大的元素是20,第三大的元素是17 ...... 第6大的元素是9。


本文修改了两个细节:

1.方法二中,插入数组A的条件是遍历到的元素“大于”数组A的最小元素,而非”小于”。

2.方法三中,节点24从小顶堆下沉的时候,应该和节点17交换,而不是和节点20交换。

在此感谢大家的指正。


640.jpg640.jpg

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


640.jpg640.jpg640.jpgimage.gif

image.gif


image.gif


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

640.png

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

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


image.gif640.png

image.gif640.jpg640.jpg

image.gif


方法一:排序法

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


image.gif640.png


640.jpg640.jpg640.jpg

image.gif

方法二:插入法


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

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

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

image.gif640.png


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

image.gif640.png

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

image.gif640.png


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

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

image.gif

640.png640.jpg640.jpg640.jpg

image.gif

image.gif

image.gif



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



image.gif640.jpg640.jpg640.jpg640.jpg

image.gif


image.gif


image.gif



什么是二叉堆?不太了解的小伙伴可以先看看这一篇:漫画:什么是二叉堆?(修正版)

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

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

640.jpg640.jpgimage.gif

image.gif



方法三:小顶堆法


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

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

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

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


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


640.pngimage.gif


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


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


640.pngimage.gif


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

640.png640.png

image.gif


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


image.gif

640.png640.png


image.gif


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

640.pngimage.gif


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

image.gif

640.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)image.gif

640.jpg640.jpgimage.gif

640.jpg640.jpg

/**
 * 寻找第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.length;i++){
if
(array[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
));
}

image.gif

640.jpg

方法四:分治法


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

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

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


image.gif640.png


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

640.png

image.gif


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

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



640.jpg

相关文章
|
6月前
|
算法 搜索推荐 数据可视化
【漫画算法】插入排序:插入宝石的传说
【漫画算法】插入排序:插入宝石的传说
|
算法 C++
【快乐手撕LeetCode题解系列】——移除元素
哈喽各位友友们😊,我今天又学到了很多有趣的知识,现在迫不及待的想和大家分享一下!😘我仅已此文,和大家分享【快乐手撕LeetCode题解系列】——移除元素~ 都是精华内容,可不要错过哟!!!😍😍😍
82 0
|
机器学习/深度学习 人工智能 搜索推荐
【数据结构】三万字图文讲解带你手撕八大排序(附源码)4
【数据结构】三万字图文讲解带你手撕八大排序(附源码)
110 0
【数据结构】三万字图文讲解带你手撕八大排序(附源码)4
|
机器学习/深度学习 算法 搜索推荐
【数据结构】三万字图文讲解带你手撕八大排序(附源码)3
【数据结构】三万字图文讲解带你手撕八大排序(附源码)
80 0
【数据结构】三万字图文讲解带你手撕八大排序(附源码)3
|
机器学习/深度学习 搜索推荐 算法
【数据结构】三万字图文讲解带你手撕八大排序(附源码)
【数据结构】三万字图文讲解带你手撕八大排序(附源码)
78 0
【数据结构】三万字图文讲解带你手撕八大排序(附源码)
|
机器学习/深度学习 人工智能 算法
【数据结构】三万字图文讲解带你手撕八大排序(附源码)2
【数据结构】三万字图文讲解带你手撕八大排序(附源码)
66 0
【数据结构】三万字图文讲解带你手撕八大排序(附源码)2
|
前端开发 容器
#yyds干货盘点# 前端歌谣的刷题之路-第一百四十三题-双列布局-浮动
#yyds干货盘点# 前端歌谣的刷题之路-第一百四十三题-双列布局-浮动
82 0
#yyds干货盘点# 前端歌谣的刷题之路-第一百四十三题-双列布局-浮动
|
算法 索引
【切图仔的算法修炼之旅】LeetCode1991:找到数组中的中间位置
【切图仔的算法修炼之旅】LeetCode1991:找到数组中的中间位置
115 0
|
搜索推荐 算法 Shell
【漫画】七种最常见的排序算法(动图版)(中)
【漫画】七种最常见的排序算法(动图版)
166 1
【漫画】七种最常见的排序算法(动图版)(中)
|
前端开发
一个元素即可实现 3D 插图效果
仅使用一个 <img> 标签,使用一个小技巧就可以创建一个 3D 插图,没有为伪元素,没有其他其他标签,没使用 SVG。仅仅是一个标签配合一些 CSS 样式。
一个元素即可实现 3D 插图效果