题目
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
|
|
示例 2:
|
|
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
题解
这道题的直接思路是排序,但排序并不是最优的,在排序的基础上可以对算法进行优化,从而使得复杂度更低。由于自己已经好久没有自实现过排序算法,基本上已经忘完,因此也打算趁这个机会温习下排序相关的算法。
快排
首先尝试使用快排解决,快排算法的说明参考文章白话经典算法系列之六 快速排序 快速搞定。
快排的主要思想就是 挖坑 + 分治,以题目中给的例子作为说明。
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
3 | 2 | 1 | 5 | 6 | 4 |
首先我们将v[0]中的3拿出来,存到tmp变量中。这样我们就可以认为v[0]处留下了一个坑,如下图。
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
? | 2 | 1 | 5 | 6 | 4 |
那么我们需要填坑。该怎么填呢?我们从数组的最后往前遍历,如果某个元素小于tmp,那么就填到坑里。
显然,直到v[2]时,我们才找到了值1是小于tmp的,那么我们将其填到v[0]中,对应的结构如下:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | ? | 5 | 6 | 4 |
这个时候,我们再从v[0]的右边开始往后找,找比tmp大的元素,填到v[2]的坑中,在这里我们发现v[1]是小于tmp的,因此不用填坑,同时这个时候我们已经走到了中间,那么我们把tmp填到v[2]中,结构如下:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 3 | 5 | 6 | 4 |
接下来我们对v[0]-v[1]和v[3]-v[5]这两个区段做递归的填坑操作。在v[0]-v[1]这个区段中,我们人眼看到他们是已经有序的了,所以这里不再说明,继续以v[3]-v[5]区段来说。
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 3 | 5 | 6 | 4 |
我们取v[3]到tmp中,留下一个坑,结构如下:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 3 | ? | 6 | 4 |
这个时候我们依旧从后往前找比tmp(5)小的元素填到坑里,这里我们找到了4,填进去,结构如下,之后留下一个新的坑。
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 6 | ? |
之后我们再从v[4]往后找比tmp(5)大的元素,这里我们找到了v[4],即6,把它填到坑里,结构如下:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 3 | 4 | ? | 6 |
最后没有其他元素了,我们把tmp填到最后留下的坑里,结构如下:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 |
接下来再往下递归,不过这个时候只剩v[3]和v[5]这两个单独的元素了,已经到底了,所以就可以结束递归了。这个时候,我们看到整个数组是已经有序的了。
按照以上的逻辑,实现快排代码如下,在实现过程中注意对边界的处理。
|
|
堆排
所谓的堆是一个完全二叉树,且满足父节点总是大于(小于)子节点的性质。
首先我们看数组{3, 2, 1, 5, 6, 4}
,它对应的堆的结构如下图(这个时候还不能称之为堆,因为不满足父节点总是大于(小于)子节点的性质)。在数组中,如果我们当前值的index是i,那么它的左子节点的index是2 * i + 1
,而其右子节点的index是2 * i + 2
。以数值3为例,它的index是0,那么其左子节点的index是2 * 0 + 1
,即1,对应数值2。而其右子节点的index是2 * 0 + 2
,即2,对应数值1。
堆排序算法中,主要有2个步骤,
- 首先将数组堆化,即让数组满足堆的性质(父节点总是大于(小于)子节点)。
- 将堆的顶点与末尾元素交换,之后对除末尾元素外剩下的元素继续重复该步骤,直到待堆化的元素数量为0.
下面我们用图示详细的说明上面的过程,首先对数组进行堆化,这里我们让结果为升序,既然是升序,那么堆应该为大顶堆,根据上面的步骤很容易想到原因。我们要让根节点与最后一个元素交换位置,那么根节点显然应该为最大的节点,那自然是大顶堆。
堆化过程首先从最后一个非叶节点开始,这里是值1,将其与其子节点比较,显然其小于4,那么将他们两个交换位置,交换后结果如下。
之后往前到上一个非叶节点,即值2,同样将其与其节点比较,其最大的子节点是6,大于2,将他们交换位置,交换后结果如下。
再往前对根节点进行该操作,3显然小于6,将他们交换位置,交换后结果如下。
交换后,虽然根节点是最大的值,但交换后的3并不大于它的子节点,需要重新对其调整,将3与5交换位置,交换后结果如下。
至此,数组已经堆化完成,这已经是个大顶堆了,接下来我们将开始进行步骤2,将根节点换到最后,之后重复堆化过程。首先我们将根节点6与末尾节点1交换位置,交换后结果如下。
接下来对剩下的元素继续进行堆化过程,抛开6,最后一个非叶节点是5,满足条件,之后再看根节点1,小于5,与5交换,结果如下。
交换后节点1与其子节点不满足堆的性质,将1与最大的3交换,交换后结果如下。
这个时候堆化完成,继续将根节点5与最后一个元素2交换位置,交换后结果如下。
继续按照上面的流程操作,细节就不再画图了,直到所有的元素按照升序排列,最终的结构如下。
该堆对应的数组为{1, 2, 3, 4, 5, 6}
,排序完成。
按照上面的逻辑,我们实现代码如下。
|
|
解答
在了解了快排和堆排这两种排序算法周,就可以来解决这个问题了。我们可以直接将整个数组排好序,之后拿到结果,但这道题要的是第K大的数,而堆排刚好是每次会选择一个较大的元素排到末尾,那么我们在实际的循环中,压根不需要将整个数组排好序,只需要排到第N个数就行了。
对应的实现如下:
|
|
参考讨论,发现更多的思路是这样的,定义一个小顶堆,我们向里面添加元素,在添加时会对其中的元素进行排序。当元素超过k个时,把顶部的元素移出来,当全部的元素都添加完毕后,顶部的元素就是我们要的第K大的元素。
参考文章
https://songlee24.github.io/2014/04/02/heap-sort-implementation/