Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
For example,
Given [3,2,1,5,6,4]
and k = 2, return 5.
Note:
You may assume k is always valid, 1 ≤ k ≤ array's length.
题目的意思很明确,也很简单,就是要找出第k大的数。并且特意标注了,是在有序列中的第k大的元素,不是第k大的不重复的元素。
既然是找出第k大的元素,那么对这个数组进行排序,按照降序的话,返回a[k-1]就好了。这么做的时间复杂度是O(n*log(n)),其中n是数组的长度。虽然也是复杂度很好的一个算法了,但是还是有浪费的,我们只需要第k大的元素即可,不需要保证其他元素的有序性。那么,接下来就对排序算法进行一些改动,令其更加的适合这个题目,只需要找出第k大的元素就可以结束了。
首先来看第一种方法,对快排进行改动,在找到第k大的元素时就停止。
先来回顾下快排的思想:选取一个数,根据这个数,把数组分成三部分,第一部分是大于这个书的,第二部分是等于这个数的,第三部分是小于这个数的。然后继续把每个部分都进行分解直到每个部分的大小为一。
那么,如何将这种思想运用到这个题目上呢?依旧是划分,分成两部分,根据这两部分的大小来决定,第k大的元素是处在哪一部分。也就是选取一个数key,根据key把数组分成两部分S1 和S2,如果|S1| < k,那么第k大的元素必定在S2中,并且是S2中第(k-|S1|)大的元素;否则,第k大的元素在S1中,并且也是S1中第k大的元素。那么,在什么时候停止呢?那就是当|S1|+1 == k时,也就是我们选取的key刚好是第k大的元素时。这样思路就清晰了,这部分的代码如下:
int findKthLargest(vector<int>& nums, int s, int e, int k) {
int key = nums[s+k-1];
int p = divid(nums, s, e, k);
if (p+1 == k)
return key;
else if (p >= k) {
return findKthLargest(nums, s, s+p, k);
} else {
return findKthLargest(nums, s+p, e, k-p);
}
}
那么剩下的问题就是如何划分了。我们选定一个元素key,然后在有效范围内遍历整个数组,如果a[i] > key,那么就把a[i]放到数组的未排序部分的头部,这样,当遍历结束后,就找出了划分的边界。具体代码如下:
int divid(vector<int>& nums, int s, int e, int k) {
int key = nums[s+k-1];
int p = 0;
swap(nums, s+k-1, e-1);
for (int i = s; i < e-1; ++i) {
if (nums[i] >= key) {
swap(nums, i, s+p);
++p;
}
}
swap(nums, e-1, s+p);
return p;
}
需要注意的是对等于key值的处理,我这里是把除key以外的所有等于key的元素都放在第1部分,而把key放在了第2部分。这样做的理由如下:当数组的元素全都相同时,可以避免把全部的元素都放在一个部分而导致另一个部分为空,从而进一步避免了无限的函数调用。
这样下来,整个算法就完成了。和快排一样,这样算法也不是一个稳定的算法,算法的时间复杂度在很大程度上取决于key值的选择。如果按照平均效率来计算,即每次划分都能把数组的搜索范围减半,那么有递归方程:T(n) = T(n/2) + O(n)。 这样有T(n) = O(n)。当然这只是平均效率,最坏效率依然会有T(n) = T(n-1) + O(n) = O(n^2)。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
return findKthLargest(nums, 0, nums.size(), k);
}
int findKthLargest(vector<int>& nums, int s, int e, int k) {
int key = nums[s+k-1];
int p = divid(nums, s, e, k);
if (p+1 == k)
return key;
else if (p >= k) {
return findKthLargest(nums, s, s+p, k);
} else {
return findKthLargest(nums, s+p, e, k-p);
}
}
int divid(vector<int>& nums, int s, int e, int k) {
int key = nums[s+k-1];
int p = 0;
swap(nums, s+k-1, e-1);
for (int i = s; i < e-1; ++i) {
if (nums[i] >= key) {
swap(nums, i, s+p);
++p;
}
}
swap(nums, e-1, s+p);
return p;
}
void swap(vector<int>& nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
};
好了,基于快排的方法就到这里了。接下来,介绍基于堆排的算法。
堆排的重点在于建堆和维护堆。建堆是把整个数组的次序进行重整使其满足最大堆或者最小堆。而堆的维护是指,当把堆顶部的元素取走时,对剩下的元素进行调整,使其依然满足最大堆或者最小堆。
对于此题,我们需要建立并维护一个最大堆,即每个元素都比其子元素要大,然后通过k-1次的取出堆顶元素并进行堆的维护,那么在第k次,堆顶的元素,就是第k大的元素。
首先是对堆的重建的代码,因为每次最多有一半的元素需要调整,所以重建的时间复杂度是O(log(n)):
void rebulidHeap(vector<int>& nums, int s, int e) {
int left = 2*s+1;
int right = 2*s+2;
if (left >= e)
return ;
int big = left;
if (right < e && nums[right] > nums[big])
big = right;
if (nums[s] < nums[big]) {
swap(nums, s, big);
rebulidHeap(nums, big, e);
}
}
然后是建堆,时间复杂度是O(n):
void bulidHeap(vector<int>& nums) {
for (int i = nums.size()-1; i > 0; --i) {
int parent = (i-1)/2;
if (nums[i] > nums[parent]) {
swap(nums, i, parent);
rebulidHeap(nums, i, nums.size());
}
}
}
最后是获取第k大的元素,时间复杂度是O(k*log(n)):
for (int i = 0; i < k-1; ++i) {
swap(nums, 0, nums.size()-i-1);
rebulidHeap(nums, 0, nums.size()-i-1);
}
这样,总体的代码是,那么时间复杂度为建堆的O(n)加上获取目标的O(k*log(n)),所以总体的时间复杂度为O(n+k*log(k))。虽然在平均时间复杂度上比快排要高,但是它是一个稳定的算法,不会出现快排的那种复杂度上升到O(n^2)的情况:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
bulidHeap(nums);
for (int i = 0; i < k-1; ++i) {
swap(nums, 0, nums.size()-i-1);
rebulidHeap(nums, 0, nums.size()-i-1);
}
return nums[0];
}
void rebulidHeap(vector<int>& nums, int s, int e) {
int left = 2*s+1;
int right = 2*s+2;
if (left >= e)
return ;
int big = left;
if (right < e && nums[right] > nums[big])
big = right;
if (nums[s] < nums[big]) {
swap(nums, s, big);
rebulidHeap(nums, big, e);
}
}
void bulidHeap(vector<int>& nums) {
for (int i = nums.size()-1; i > 0; --i) {
int parent = (i-1)/2;
if (nums[i] > nums[parent]) {
swap(nums, i, parent);
rebulidHeap(nums, i, nums.size());
}
}
}
void swap(vector<int>& nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
};