leetcode 100热题数组篇
力扣31
“下一个排列” 的定义是:给定数字序列的字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
我们可以将该问题形式化地描述为:给定若干个数字,将其组合为一个整数。如何将这些数字重新排列,以得到下一个更大的整数。如 123
下一个更大的数为 132
。如果没有更大的整数,则输出最小的整数。
以 1,2,3,4,5,6
为例,其排列依次为:
123456
123465
123546
...
654321
可以看到有这样的关系:123456 < 123465 < 123546 < ... < 654321
。
算法推导
如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:
-
我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如
123456
,将5
和6
交换就能得到一个更大的数123465
。 -
我们还希望下一个数
增加的幅度尽可能的小
,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
- 在 尽可能靠右的低位 进行交换,需要 从后向前 查找
- 将一个 尽可能小的「大数」 与前面的「小数」交换。比如
123465
,下一个排列应该把5
和4
交换而不是把6
和4
交换 - 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以
123465
为例:首先按照上一步,交换5
和4
,得到123564
;然后需要将5
之后的数重置为升序,得到123546
。显然123546
比123564
更小,123546
就是123465
的下一个排列
以上就是求 “下一个排列” 的分析过程。
算法过程
标准的 “下一个排列” 算法可以描述为:
- 从后向前 查找第一个 相邻升序 的元素对
(i,j)
,满足A[i] < A[j]
。此时[j,end)
必然是降序 - 在
[j,end)
从后向前 查找第一个满足A[i] < A[k]
的k
。A[i]
、A[k]
分别就是上文所说的「小数」、「大数」 - 将
A[i]
与A[k]
交换 - 可以断定这时
[j,end)
必然是降序,逆置[j,end)
,使其升序 - 如果在步骤 1 找不到符合的相邻元素对,说明当前
[begin,end)
为一个降序顺序,则直接跳到步骤 4
该方法支持数据重复,且在 C++ STL 中被采用。
可视化
以求 12385764
的下一个排列为例:
首先从后向前查找第一个相邻升序的元素对 (i,j)
。这里 i=4
,j=5
,对应的值为 5
,7
:
然后在 [j,end)
从后向前查找第一个大于 A[i]
的值 A[k]
。这里 A[i]
是 5
,故 A[k]
是 6
:
将 A[i]
与 A[k]
交换。这里交换 5
、6
:
这时 [j,end)
必然是降序,逆置 [j,end)
,使其升序。这里逆置 [7,5,4]
:
因此,12385764
的下一个排列就是 12386457
。
最后再可视化地对比一下这两个相邻的排列(橙色是蓝色的下一个排列):
首先我们从序列的从后往前找,找到第一个逆序的地方,也就是nums[k - 1] < nums[k]
然后再从k开始,找到第一个不大于nums[k - 1]的数,然后交换位置,再将整个的后面序列翻转
代码
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int k = nums.size() - 1;
while(k > 0 && nums[k - 1] >= nums[k]) k--;
if(k <= 0) reverse(nums.begin(),nums.end());
else {
int p = k;
//找到第一个不大于nums[k - 1]的数
while(p < nums.size() && nums[p] > nums[k - 1]) p ++;
swap(nums[p - 1],nums[k - 1]);
reverse(nums.begin() + k,nums.end());
}
}
};
【两步翻转搞定】48. 旋转图像
解题思路
(数组翻转) O(n2)
- 水平翻转,第一行和最后一行交换,第二行和倒数第二行交换
- 对角线翻转
时间复杂度
O(n2)
空间复杂度
O(1)
代码
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
//水平翻转
for(int i = 0;i < n / 2;i++)
for(int j = 0;j < n;j++)
swap(matrix[i][j],matrix[n - i - 1][j]);
//对角线翻转
for(int i = 0;i < n;i++)
for(int j = 0;j < i;j++)
swap(matrix[i][j],matrix[j][i]);
}
};
【模拟】169. 多数元素
解题思路
(模拟) O(n)
性质:出现次数超过一半
假设这个数是 A,其他数是 B,根据这个性质,我们可以做一个比喻,数 A 比做是钱,出现几次就是几块钱,数 B 比做商品,一件商品消费一块钱,那么遍历完整个数组,消费完全部商品,钱肯定会剩下。
代码实现:
-
用 val 存储重复出现的数,cnt 记录 val 出现的次数
-
遍历整个数组,如果 val 出现次数为 0,就从当前数 nums[i]开始计数,
val ==nums[i], cnt = 1
表示当前数出现一次,如果当前数 nums[i] 等于 valnums[i] == val
,出现次数加 1cnt ++
,否则出现次数减 1cnt --
-
遍历完成后 val 中存储的就是答案‘
其实也是摩尔投票法 找出众数
时间复杂度
遍历一遍数组,时间复杂度为 O(n)
空间复杂度
O(1)
C++ 代码
class Solution {
public:
int majorityElement(vector<int>& nums) {
int val = -1,cnt = 0;
for(int i = 0;i < nums.size();i++)
{
if(cnt == 0) val = nums[i],cnt++;
else
{
if(val == nums[i]) cnt++;
else cnt--;
}
}
return val;
}
};
【小根堆、快速选择算法双解法】215. 数组中的第K个最大元素
算法 1
(小根堆) O(n)
利用优先队列(堆),由于题目要我们求的是第 k 大的元素,因此我们建立一个小根堆。
根据当前队列元素个数或当前元素与栈顶元素的大小关系进行分情况讨论:
- 当优先队列元素不足 k 个,可将当前元素直接放入队列中;
- 当优先队列元素达到 k 个,并且当前元素大于栈顶元素(栈顶元素必然不是答案),可将当前元素放入队列中。
时间复杂度
O(n),每个元素遍历一遍
空间复杂度
O(k)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int,vector<int>,greater<int>> pq;//小顶堆
for(int i = 0;i < nums.size();i++)
{
if(pq.size() < k){
pq.push(nums[i]);
}
else if(pq.top() < nums[i]){
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};
算法 2
(快速选择算法) O(n)
按照快速排序的思想,将数组按照从大到小排序,如果第 K 大的数的下标 k 在 [l,j] 区间 (k <= j
) 那么答案就递归在左部分区间中寻找,否则从右部分区间 [j+1,r] 查询。
递归出口:当 l == r
时,nums[l] 就是第 k 大的数
时间复杂度
O(n),每个元素最多遍历一遍
空间复杂度
O(1)
代码
class Solution {
public:
int quick_sort(vector<int>& nums,int l,int r,int k)
{
// 递归出口:第 K 大的数
if(l == r) return nums[l];
int x = nums[l],i = l - 1,j = r + 1;
while(i < j)
{
do i++;while(nums[i] > x);
do j--;while(nums[j] < x);
if(i < j) swap(nums[i],nums[j]);
}
//如果 k <= j 说明第 K 大的数在左半部分,那就递归从左边找;否则从右边找
if(k <= j) return quick_sort(nums,l,j,k);
else return quick_sort(nums,j + 1,r,k);
}
int findKthLargest(vector<int>& nums, int k) {
// k 传的是下标
return quick_sort(nums,0,nums.size() - 1,k - 1);
}
};
238.除自身以外数组的乘积
解题思路
(前后缀分解) O(n)
对于本题目每一个数的结果都可以分成两部分的乘积,左边所有数的乘积 * 右边所有数的乘积
- 从前往后遍历数组,使用一个临时变量 p 记录当前位置前的所有数的乘积,同时记录此时的结果 res[i]
- 从后往前遍历数组,使用一个临时变量 s 记录当前位置后的所有数的乘积,将 s∗res*[*i] 就是当前位置左右两部分乘积的结果。
代码
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> res(n,1);
//计算前缀积
for(int i = 1;i < n;i++) res[i] = res[i - 1] * nums[i - 1];
//s这个数表示后缀积
for(int j = n - 1,s = 1;j >= 0;j--)
{
res[j] *= s;
//计算右边所有数的乘积
s *= nums[j];
}
return res;
}
};
【原地交换 O(1) 空间复杂度】448. 找到所有数组中消失的数字
解题思路 **原地修改数组 ** 背过就行了
题目更高阶的要求不使用额外的空间。这增加了难度。
如果题目是「每个数字都出现了 2 次,只有一个数字出现了 1 次」,你会做吗?这是题目136. 只出现一次的数字。朋友们应该都知道可以用异或。而本题中每个数字出现的次数可以是 0/1/2 次,已经无法用异或了。
真正求解本题需要用到一个奇技淫巧:原地修改数组。
这个思想来自于长度为 N 的数组可以用来统计 1 N 各数字出现的次数;题目给出的数组的长度正好为 N,所以可以原地修改数组实现计数。
当前元素是 num**s[i],那么我们把第 num**s[i]−1 位置的元素 乘以 -1,表示这个该位置出现过。当然如果 第 num**s[i]−1 位置的元素已经是负数了,表示 num**s[i] 已经出现过了,就不用再把第 num**s[i]−1 位置的元素乘以 -1。最后,对数组中的每个位置遍历一遍,如果 i 位置的数字是正数,说明 i 未出现过。
代码
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
for(int i = 0;i < nums.size();i++)
{
int x = abs(nums[i]);
//标记这个数字出现过了
if(nums[x - 1] > 0) nums[x - 1] *= -1;
}
vector<int> res;
for(int i = 0;i < nums.size();i++)
{
//nums[i] 大于 0 表示 i+1这个数字未出现过
if(nums[i] > 0) res.push_back(i + 1);
}
return res;
}
};