leetcode 链接:https://leetcode-cn.com/problems/kth-largest-element-in-an-array/

题目

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例:

  1. 输入: [3,2,1,5,6,4] k = 2
  2. 输出: 5
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

解答& 代码

解法一:先排序

快速排序参考:快速排序 - 菜鸟教程,时间复杂度 O(Nlog(N))

  • 先将数组从大到小排序
  • 取第 k-1 个数

    class Solution {
    private:
      // 快排,将数组 num 的下标 left 到 right 的元素排序(包含 left 和 right)
      void QuickSort(vector<int>& num, int left, int right)
      {
          if(left < right)
          {
              // 随机选择第 randIdx 个数作为基准数
              int randIdx = rand() % (right - left + 1) + left;
              swap(num[randIdx], num[left]);    // 将这个基准数交换到数组最左边 left 的位置
    
              int i = left;        // 用于从左往右找
              int j = right;        // 用于从右往左找
              int x = num[left];    // 基准数
              while(i < j)
              {
                  while(i < j && num[j] <= x)        // 从右往左找第一个比基准数 x 大的数
                      --j;
                  if(i < j)
                  {
                      num[i] = num[j];            // 将这个数填到左边的坑 num[i] 中
                      ++i;
                  }
    
                  while(i < j && num[i] >= x)        // 从左往右找第一个比基准数 x 小的数
                      ++i;
                  if(i < j)
                  {
                      num[j] = num[i];            // 将这个数填到右边的坑 num[j] 中
                      --j;
                  }
              }
              num[i] = x;        // 将基准数放置到正确的位置,其左边的数都比它大,右边的数都比它小
              QuickSort(num, left, i - 1);    // 递归对基准数左边部分进行排序
              QuickSort(num, i + 1, right);    // 递归对基准数右边部分进行排序
          }
      }
    
      // 从大到小排序
      void sort(vector<int>& nums)
      {
          QuickSort(nums, 0, nums.size() - 1);
      }
    public:
      int findKthLargest(vector<int>& nums, int k) {
          sort(nums);                // 排序
          return nums[k - 1];        // 取第 k 大的元素,因为下标是从 0 开始,所以取第 k-1 个元素 
      }
    };
    

    删除第 8-10 行代码,即每次将最左边的元素作为基准数的执行结果: ``` 执行结果:通过

执行用时:192 ms, 在所有 C++ 提交中击败了 7.58% 的用户 内存消耗:10 MB, 在所有 C++ 提交中击败了 9.75% 的用户

加了第 8-10 行代码,随机选择基准数后,执行结果变好了:

执行结果:通过

执行用时:12 ms, 在所有 C++ 提交中击败了 60.95% 的用户 内存消耗:9.8 MB, 在所有 C++ 提交中击败了 52.28% 的用户


<a name="rkdKs"></a>
## 解法二:快速选择(O(N))
使用快排从大到小排序,但不需要进行完整排序。快排过程中,

- 如果分区时基准元素最终的位置为 k-1,那么这个基准元素就是第 k 大的元素,不用继续排序,直接返回
- 如果分区时基准元素最终的位置大于 k-1,那么第 k 大的元素在其左半边,只需排序左半边
- 如果分区时基准元素最终的位置小于 k-1,那么第 k 大的元素在其右半边,只需排序右半边

- 时间复杂度:**O(n)**,参考「《算法导论》9.2:期望为线性的选择算法」
   - 注意:**快速排序的期望运行时间为 O(nlogn),但快速选择的期望运行时间为 O(n)** 
   - 粗略估计:假设每次选取的基准元素位置正好都在区间的中间位置,那么恰好能排除一半的元素,那么第一次调用 partition 时需要遍历 n 个元素,第二次调用 partition 时需要 n/2 个元素... ... 那么 T(n) = n + n/2 + n/4 + ... < 2n,因此时间复杂度为 O(n)
- 空间复杂度:O(logn),递归使用的栈空间
```cpp
class Solution {
private:
    // 快排分割函数
    int partition(vector<int>& nums, int left, int right)
    {
        int randomIdx = rand() % (right - left + 1) + left;
        swap(nums[left], nums[randomIdx]);
        int pivot = nums[left];
        while(left < right)
        {
            while(left < right && nums[right] <= pivot)
                --right;
            nums[left] = nums[right];

            while(left < right && nums[left] >= pivot)
                ++left;
            nums[right] = nums[left];
        }
        nums[left] = pivot;
        return left;
    }

    // 快排主函数(从大到小排序)
    void quickSort(vector<int>& nums, int left, int right, int k)
    {
        if(left >= right)
            return;

        int pivot = partition(nums, left, right);
        if(pivot == k - 1)        // 如果基准元素最终被排序到第 k-1 个位置,就是第 k 大的元素
            return;
        else if(pivot > k - 1)    // 如果基准元素的位置大于 k-1,那么第 k 大元素在其左半边,只需对左半边排序
            quickSort(nums, left, pivot - 1, k);
        else                    // 如果基准元素的位置小于 k-1,那么第 k 大元素在其右半边,只需对右半边排序
            quickSort(nums, pivot + 1, right, k);
    }

public:
    int findKthLargest(vector<int>& nums, int k) {
        quickSort(nums, 0, nums.size() - 1, k);
        return nums[k - 1];
    }
};

执行结果:

执行结果:通过

执行用时:8 ms, 在所有 C++ 提交中击败了 87.84% 的用户
内存消耗:9.8 MB, 在所有 C++ 提交中击败了 36.52% 的用户

解法三:堆排序(大根堆 O(NlogN))

class Solution {
private:
    // 交换数组中两个元素
    void swap(vector<int>& nums, int idx1, int idx2)
    {
        int temp = nums[idx1];
        nums[idx1] = nums[idx2];
        nums[idx2] = temp;
    }

    // 调整堆
    void adjustHeap(vector<int>& nums, int start, int end)
    {
        int dadIdx = start;
        int sonIdx = dadIdx * 2 + 1;
        while(sonIdx <= end)
        {
            if(sonIdx + 1 <= end && nums[sonIdx + 1] > nums[sonIdx])
                sonIdx += 1;
            if(nums[sonIdx] > nums[dadIdx])
            {
                swap(nums, sonIdx, dadIdx);
                dadIdx = sonIdx;
                sonIdx = dadIdx * 2 + 1;
            }
            else
                return;
        }
    }

    // 堆排序(大根堆)
    void maxHeapSort(vector<int>& nums, int k)
    {
        int listLen = nums.size();
        if(listLen <= 1)
            return;

        // 初始化建立堆
        for(int i = listLen / 2 - 1; i >= 0; --i)
            adjustHeap(nums, i, listLen - 1);

        // 每次将堆首(最大值)移动到堆尾,重新调整剩下堆元素
        // 因为取第 k 大,所以不需要进行完整排序,k 次即可
        for(int i = 0; i < k; ++i)
        {
            swap(nums, 0, listLen - i - 1);
            adjustHeap(nums, 0, listLen - i - 2);
        }
    }

public:
    int findKthLargest(vector<int>& nums, int k) {
        maxHeapSort(nums, k);
        return nums[nums.size() - k];
    }
};

执行结果:

执行结果:通过

执行用时:4 ms, 在所有 C++ 提交中击败了 98.74% 的用户
内存消耗:9.7 MB, 在所有 C++ 提交中击败了 72.34% 的用户

解法四:堆排序(小根堆 O(NlogK))

class Solution {
private:
    void adjustHeap(vector<int>& nums, int start, int end)
    {
        int dadIdx = start;
        int sonIdx = dadIdx * 2 + 1;
        while(sonIdx <= end)
        {
            if(sonIdx + 1 <= end && nums[sonIdx + 1] < nums[sonIdx])
                sonIdx += 1;
            if(nums[dadIdx] <= nums[sonIdx])
                return;
            else
            {
                swap(nums[sonIdx], nums[dadIdx]);
                dadIdx = sonIdx;
                sonIdx = dadIdx * 2 + 1;
            }
        }
    }

    void buildMinHeap(vector<int>& nums)
    {
        int size = nums.size();
        for(int i = size / 2 - 1; i >= 0; --i)
            adjustHeap(nums, i, size - 1);
    }

public:
    int findKthLargest(vector<int>& nums, int k) {
        vector<int> minHeap;
        // 先将 nums 的前 k 个数建立为小根堆
        for(int i = 0; i < k; ++i)
            minHeap.push_back(nums[i]);
        buildMinHeap(minHeap);

        // 遍历 nums 剩余元素,如果当前元素大于小根堆堆顶,则替换堆顶,重新调整堆
        for(int i = k; i < nums.size(); ++i)
        {
            if(nums[i] > minHeap[0])
            {
                minHeap[0] = nums[i];
                adjustHeap(minHeap, 0, k - 1);
            }
        }

        // 最后小根堆的堆顶就是第 K 大的元素
        return minHeap[0];
    }
};

执行结果:

执行结果:通过

执行用时:12 ms, 在所有 C++ 提交中击败了 55.64% 的用户
内存消耗:9.8 MB, 在所有 C++ 提交中击败了 28.22% 的用户

扩展:求 const top k

如果传入的数组是 const vector<int> nums,也就是不能修改原数组的元素值,那么如何在 O(1) 的空间复杂度内实现查找第 k 大的元素?

参考:教娃编程 - 二分查找求第K大数组元素的算法

解法:二分

  • 时间复杂度 O(NlogN):二分查找的时间复杂度是 O(logN),而对于每次二分的 mid,调用 count 查找数组 nums 中大于等于 mid 的元素个数,时间复杂度为 O(N)
  • 空间复杂度 O(1) ```cpp class Solution { private: // 返回在 nums 数组中大于等于 x 的元素个数 int countLargerThanX(vector nums, int x) {
      int size = nums.size();
      int cnt = 0;
      for(int i = 0; i < size; ++i)
      {
          if(nums[i] >= x)
              ++cnt;
      }
      return cnt;
    
    }

public: int findKthLargest(vector& nums, int k) { int left = min_element(nums.begin(), nums.end()); // 数组的最小元素值 int right = max_element(nums.begin(), nums.end()); // 数组的最大元素值 // 二分查找 while(left <= right) { int mid = left + (right - left) / 2; int cnt = countLargerThanX(nums, mid); if(cnt >= k) // 需要让 mid 更大使得其计数更小,因此收缩左边界 left = mid + 1; else // 需要让 mid 更小使得其计数更大,因此收缩右边界 right = mid - 1; } return right; } };

```
执行结果:通过

执行用时:4 ms, 在所有 C++ 提交中击败了 98.36% 的用户
内存消耗:11.7 MB, 在所有 C++ 提交中击败了 5.07% 的用户