这篇博客记录刷题第14天的学习体会与心得。
215. 数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
分析:最简单的思路是先排序,然后取倒数第
k
k
k 个元素,所以这道题其实是在考排序,直接调用C++ sort.()
函数也太对不起 medium级别了,下面我们用快速排序,以及最小顶堆取数组第K大元素。
- 快速排序[5]
- 快速排序包括将原数组 a [ l ⋯ r ] a[l \cdots r] a[l⋯r] 划分,递归调用快速排序对两个子数组 a [ l ⋯ q − 1 ] a[l \cdots q-1] a[l⋯q−1] 和 a [ q + 1 ⋯ r ] a[q+1 \cdots r] a[q+1⋯r] 排序这两步,保证 a [ l ⋯ q − 1 ] a[l \cdots q-1] a[l⋯q−1] 中每个元素小于等于 a [ q ] a[q] a[q],且 a [ q ] a[q] a[q]小于等于 a [ q + 1 ⋯ r ] a[q+1 \cdots r] a[q+1⋯r] 中每个元素。
- 当某次划分的 q q q 为倒数第 k k k 个下标,就直接返回 a [ q ] a[q] a[q];否则若 q q q 比倒数第 k k k 个下标小,就递归右区间,否则递归左区间。
- 快速排序的性能和「划分」出的子数组的长度密切相关,我们可以引入随机化加速这个过程,用partition之前加一步随机,将时间复杂度的期望降到 O ( n ) O(n) O(n).
class Solution {
public:
int quickSelect(vector<int>& a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
}
else {
return q < index ? quickSelect(a, q+1, r, index) : quickSelect(a, l, q-1, index);
}
}
inline int randomPartition(vector<int>& a, int l, int r) { //划分子数组前随机取右边界
int i = rand() % (r - l + 1) + 1;
swap(a[i], a[r]);
return partition(a, l, r);
}
inline int partition(vector<int>& a, int l, int r) { //划分
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) { //将小于a[r]元素挪到其左边
swap(a[++i], a[j]);
}
}
swap(a[i+1], a[r]); //a[r]位于i+1位置
return i + 1; //i+i即为分界点
}
int findKthLargest(vector<int>& nums, int k) {
srand()设置rand()产生随机数时的随机数种子
srand(time(0));
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
};
- 堆排序
先回顾一下堆的知识[6,7]。
堆(Heap)是一种数据结构,具有以下的特点:
1)完全二叉树;
2)heap中存储的值是偏序;Min-heap(最小堆/小顶堆/小根堆): 父节点的值小于或等于子节点的值; Max-heap(最大堆/大顶堆/大根堆): 父节点的值大于或等于子节点的值;
堆的存储:
一般都用数组来表示堆,i 结点的父结点下标就为 ( i – 1 ) / 2 (i–1)/2 (i–1)/2。它的左右子结点下标分别为 2 i + 1 2i+1 2i+1 和 2 i + 2 2i +2 2i+2。堆的创建
以某个元素为根结点向下调整为堆堆的插入
插入一个元素:先将结点插入到堆的尾部,再将该结点逐层向上调整,直到依旧构成一个堆。调整方法是看每一个子树是否符合大(小)根堆的特点,不符合的话则调整叶子和根的位置。堆的删除
按定义,堆中每次只能删除根节点。为了便于重建堆,实际的操作是将堆中最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整,使其仍然满足最大(小)堆特点,本质是一个交换排序的“下沉”过程。堆排序
以最大堆为例,堆建好之后堆中第0个数据(根节点)是堆中最大的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最大的数据,重复上述步骤取出一个排序数组。
因此,这道题我们构建一个最大堆,做 k − 1 k-1 k−1 次删除操作后堆顶元素就是第 k k k 大元素。
- 不用语言自带堆操作,实现最大堆排序,官方代码如下:
class Solution {
public:
void adjustmaxHeap(vector<int>& a, int i, int heapSize) {
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
if (l < heapSize && a[l] > a[largest]) {
largest = l;
}
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
if (largest != i) {
swap(a[i], a[largest]);
adjustmaxHeap(a, largest, heapSize);
}
}
void buildMaxHeap(vector<int>& a, int heapSize) {
for (int i = heapSize / 2; i >= 0; --i) {
adjustmaxHeap(a, i, heapSize);
}
}
int findKthLargest(vector<int>& nums, int k) {
int heapSize = nums.size();
buildMaxHeap(nums, heapSize);
for (int i = nums.size() - 1; i >= nums.size() - k + 1; --i) {
swap(nums[0], nums[i]);
--heapSize;
adjustmaxHeap(nums, 0, heapSize);
}
return nums[0];
}
};
- C++中有优先队列
priority_queue
[4],或者STL堆操作[7],可以直接使用。这里利用priority_queue
实现最小堆排序如下:当最小堆中不足 k k k 个元素时,不经选择压入元素;此后若nums
元素比最小堆堆顶元素大,则弹出堆顶并压入较大元素,如此筛选,最终最小堆中剩下的是前 k k k 个最大元素,此时堆顶元素即为所求,代码如下:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap; //greater对应从小到大出队列
for (int num : nums) {
if (minHeap.size() < k) {
minHeap.push(num);
} else if (minHeap.top() < num) {
minHeap.pop();
minHeap.push(num);
}
}
return minHeap.top();
}
};
217. 存在重复元素
给定一个整数数组,判断是否存在重复元素。
如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。
示例 1:
输入: [1,2,3,1]
输出: true示例 2:
输入: [1,2,3,4]
输出: false示例 3:
输入: [1,1,1,3,3,4,3,2,4,2]
输出: true
解题思路:
- 暴力搜索,将数组中每个元素减去其它元素,若存在差值 = 0,则返回 true。该方法时间复杂度为 O ( n 2 ) O(n^2) O(n2),会超时;
- 先对数组排序,则重复元素必定会出现在相邻位置,对排序后数组进行扫描,如果存在相邻的两个元素相等,则返回 true。该方法时间复杂度取决于排序算法,利用C++ 自带的
sort()
函数[1]可以实现 O ( n l o g n ) O(n logn) O(nlogn)的时间复杂度;
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
if (nums.empty())
return false;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size()-1; i++) {
if (nums[i] == nums[i+1]) {
return true;
}
}
return false;
}
};
- 利用哈希表,对于数组中每个元素,我们将它插入到哈希表中。如果插入一个元素时发现该元素已经存在于哈希表中,则说明存在重复的元素。哈希表查找可以实现 O ( n ) O(n) O(n) 的时间复杂度,考虑C++ undered_set是基于哈希表构建[2, 3],这里我们可以直接使用。
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> numSet;
numSet.reserve(nums.size()); //预分配空间
for (int i=0; i < nums.size(); ++i) {
//count( )方法--返回某个值元素个数
//也可以使用numSet.find(nums[i]) != s.end())判断
if (numSet.count(nums[i])) return true;
numSet.insert(nums[i]);
}
return false;
}
};
230. 二叉搜索树中第K小的元素
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明: 你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 1示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 3
进阶:
如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化kthSmallest
函数?
参考
[1] C++ 中的sort()排序函数用法
[2] C++ set与map、unordered_map、unordered_set与哈希表
[3] 用哈希表封装unordered_map/set
[4] c++优先队列(priority_queue)用法详解
[5] 数组中的第K个最大元素【官方题解】
[6] 数据结构——堆(Heap)大根堆、小根堆
[7] 堆,堆的创建,插入,删除,建立
[8] C++ STL堆操作