LeetCode-Kth Largest Element in an Array

来源:互联网 发布:淘宝seo常见的问题 编辑:程序博客网 时间:2024/06/01 09:08
算法分析与设计,第九周博客
215. Kth Largest Element in an Array
Description

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;    }};