1. 堆排序
解题思路:时间复杂度O(
n
∗
l
o
g
2
n
n*log_2n
n ∗ l o g 2 n ),使用小根堆会达到近乎O(n),空间复杂度O(
l
o
g
2
n
log_2{n}
l o g 2 n )
使用小根堆,建堆时间复杂度O(k),调整堆(删除堆顶并插入新元素)O(
n
∗
l
o
g
2
k
n*log_2k
n ∗ l o g 2 k ),其中k是题目要求的返回第k最大元素。因此小根堆大小为k,故建堆为O(k). 共计O(
k
+
n
∗
l
o
g
2
k
k+n*log_2k
k + n ∗ l o g 2 k ) = O(n) 不断地将元素插入到小根堆(根最小,其它元素都比根大)中,当堆中有k个元素,此时还需要往堆中插入元素时,需要进行判断。 因为此时堆顶元素正好是堆中倒数第k大元素。如果新插入元素比堆顶大。证明当前堆顶不是倒数第k大 则堆顶删除,并将新元素插入。此时调整堆,新的堆顶元素为第k大。以此类推。直到所有元素入堆后。 最终返回堆顶即可。
代码:当前官方增加了很多测试用例,已经无法超越100%的用户了,目前最快的算法,只能达到17ms,进行优化后,也只到了15ms。我查看2021年提交时的记录,是3ms超越100%。目前已经无法达到了。
使用Java提供的优先级队列实现小根堆(面试时候肯定不让你用。因此这个代码帮你理解整体的思路。然后第二个实现方法,我们需要自己实现小根堆)
class Solution {
public int findKthLargest ( int [ ] nums, int k) {
PriorityQueue < Integer > queue = new PriorityQueue < > ( new Comparator < Integer > ( ) {
@Override
public int compare ( Integer o1, Integer o2) {
return o1 - o2;
}
} ) ;
for ( int num: nums) {
if ( queue. size ( ) == k) {
if ( queue. peek ( ) < num) {
queue. poll ( ) ;
queue. offer ( num) ;
}
} else {
queue. offer ( num) ;
}
}
return queue. poll ( ) ;
}
}
自己实现小根堆,因为Java自带容器加了很多健壮性和线程安全的逻辑,所以效率较慢,我们自己实现小根堆就会快很多。
class Solution {
public int findKthLargest ( int [ ] nums, int k) {
int [ ] minHeap = new int [ k] ;
for ( int i = 0 ; i < k; i++ ) {
minHeap[ i] = nums[ i] ;
}
for ( int i = k / 2 - 1 ; i >= 0 ; i-- ) {
adjustHeap ( minHeap, i) ;
}
for ( int i = k; i < nums. length; i++ ) {
if ( nums[ i] > minHeap[ 0 ] ) {
minHeap[ 0 ] = nums[ i] ;
adjustHeap ( minHeap, 0 ) ;
}
}
return minHeap[ 0 ] ;
}
private void adjustHeap ( int [ ] array, int root) {
while ( true ) {
int left = 2 * root + 1 ;
int right = left + 1 ;
int min = root;
if ( left < array. length && array[ left] < array[ min] ) min = left;
if ( right < array. length && array[ right] < array[ min] ) min = right;
if ( min == root) break ;
swap ( array, root, min) ;
root = min;
}
}
private void swap ( int [ ] array, int i, int j) {
int temp = array[ i] ;
array[ i] = array[ j] ;
array[ j] = temp;
}
}
2. 快速选择
堆排序方便将K个数全找出来,例如数组中最大的k个数。如果只是找第k大数,有点大材小用。因为堆排序需要操作很多与第k大数无关的k-1,k-2…这些数。 而快速选择是快速排序的创始人针对解决单独只想找到某一个数字的问题而创作的算法,它不需要类似快速排序每一趟确定一个数字的最终位置。这个算法只关心第K个元素的位置。因此它的时间复杂度来到了O(n)
我们每次都找到区间中间位置的数x 以x为基枢,保证当前区域,所有小于x的放在x左边,大于x的放在x的右边 如果x左边够k个数,就继续去左边处理,如果左边不够k个数,就去右边处理
如果左边部分没有第k大数,那就去右边找,此时左边部分的 leftNums个数,都已经确定都比第k大数小,所以k - leftNums为下轮右边部分该找的值。这一点很重要,会节省很多操作。
这也是为什么我这个算法在官方测试用例增加后依然可以跑进3ms。而官方的写法只能在11ms。
class Solution {
public int findKthLargest ( int [ ] _nums, int k) {
int n = _nums. length;
return quickselect ( _nums, 0 , n - 1 , n- k+ 1 ) ;
}
void swap ( int [ ] a, int i, int j) {
int t = a[ i] ;
a[ i] = a[ j] ;
a[ j] = t;
}
int quickselect ( int [ ] a, int left, int right, int k) {
if ( left >= right) return a[ left] ;
int x = a[ left + right >> 1 ] ;
int leftIndex = left - 1 , rightIndex = right + 1 ;
while ( leftIndex < rightIndex) {
do leftIndex++ ; while ( a[ leftIndex] < x) ;
do rightIndex-- ; while ( a[ rightIndex] > x) ;
if ( leftIndex < rightIndex) {
swap ( a, leftIndex, rightIndex) ;
}
}
if ( k <= ( rightIndex - left + 1 ) )
return quickselect ( a, left, rightIndex, k) ;
else
return quickselect ( a, rightIndex+ 1 , right, k- ( rightIndex- left+ 1 ) ) ;
}
}