【Leetocde数组题目合集】
基本知识点:数组在内存上是连续的;数组内的元素无法删除,只能进行覆盖;
一、双指针求解数组
1. 知识点
- 可以使用双指针求解数组的前提是该数组为有序数组;
2. 模版总结
(1) 快慢指针
适用场景: 原地修改数组,如去重、删除元素
求解模板
slow = 0;
while (fast < nums.size()) {
if (满足保留条件) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
典型题目
- leetcode27 删除元素
数组元素不能删除只能被覆盖,如果没有=val的元素,快慢指针始终指向同一位置,如果有则val位于快慢指针之间;本题的原理就是用非val元素依次覆盖val元素
保留条件:快指针指向元素不等于val
快指针始终指向未处理的元素,慢指针之前为已经处理好的数组,两个指针中间夹着的就是已经被处理过的数组(这个区间内的数字要不值为val,要不就是已经被拿来覆盖之前出现过的val的位置),最后返回慢指针即可
int removeElement(vector<int>& nums, int val) {
int s = 0, f = 0;
while (f < nums.size()) {
if (nums[f] != val) {
nums[s] = nums[f];
s++;
}
f++;
}
return s;
}
类似题目:
题目 | 特点 | 思路 | 是否掌握 |
---|---|---|---|
leetcode26 删除重复元素 | 删除重复元素 | 保留条件:快指针和慢指针前一位相比,不一样才保留 | 已掌握 |
leetcode80 删除元素II | 相比20,这里是要删除多余的重复元素,使得每个重复元素只会出现2次 | 如果没有重复2次以上的元素,快慢指针始终指向同一位置,快指针指向还未处理的元素;保留条件:快指针的值不等于慢指针前2位元素 | 还未掌握 |
JZ21 调整数组的顺序使奇数位于偶数前 | 涉及到数组内插入元素,插入前,需要所有元素后移挪出位置 | 保留条件: 如果快指针所指的该元素为奇数,就需要插入到慢指针之前;快指针指向未处理元素,慢指针指向偶数区间第一位 | 还未掌握 |
leetcode 75 颜色分类 | 三指针,左右指针作为三种颜色的隔开边界;左右指针之间为1,左指针左边都为0,右指针右边都为2 | 保留条件:如果当前元素为0,就和l交换位置;如果为2则和r交换位置并且i位置不变(这是为了防止换过来的还是2) | 未掌握,需要先判断是不是为2,且要用while一直判断,因为i只会在0-r之间移动,所以对于和r交换过来的元素还要再判断,直到不为2;然后还要再判断一下是不是为0 |
(2) 左右指针
适用场景:有序数组的搜索(如两数之和)、反转数组、回文判断
求解模板
int l = 0, r = nums.size();
while (l < r) {
if (满足条件) {
return 结果;
} else if (nums[l]+nums[r] < target) {
l++;
} else {
r--;
}
}
典型题目
- leetcode 167 两数之和II-输入有序数组
满足条件:左右指针所指向元素之和为target,因为数组有序,和小于target就右移左指针;大于target就左移右指针
vector<int> twoSum(vector<int>& nums, int target) {
int l = 0, r = nums.size()-1;
while (l < r) {
if (nums[l] + nums[r] == target) return vector<int> {l+1, r+1};
else if (nums[l] + nums[r] < target) l++;
else r--;
}
return vector<int> {};
}
类似题目:
题目 | 特点 | 思路 | 是否掌握 |
---|---|---|---|
leetcode344 反转字符串 | 交换左右指针元素,防止覆盖 | 满足条件:左右指针依次交换即可,注意引入临时变量防止覆盖 | 已掌握 |
leetcode977 有序数组的平方 | 最大的平方一定出现在绝对值最大的数字,也就是首尾元素上 | 满足条件:如果左指针所指元素平方更大,则平方数组当前位置放置左指针元素的平方(平方数组为倒序遍历) | 已掌握 |
leetcode88 合并两个有序数组 | 直接在数组1上操作,所以只能从末尾开始先确定大的元素,以防覆盖数组1 | 满足条件:如果右指针(数组2)元素更大,则放在tail所指位置,否则放左指针所指向数组1的元素 | 已掌握 |
leetcode15 三数之和 | 除了左右指针还需要一个指针遍历整个数组,左右指针则寻找指针i右边和为target-nums[i]的数字 | **满足条件:nums[l]+nums[r] = target - nums[i]**注意移动左右指针的时候要进行重复元素的剪枝 | 已掌握 |
(3) 二分查找
使用场景1:有序数组中的元素查找
求解模版
边界条件:
1)左闭右闭:l = 0, r = nums.size()-1, 下次搜索不包含mid,也就是while (l <= r), 下一次更新指针的时候要不是l = mid+1, 要不是r = mid-1;
2)左闭右开:l = 0, r = nums.size(), 下次搜索还要包含mid,也就是while(l < r), 下一次更新指针的时候要不是l = mid+1, 要不是r = mid;
int l = 0, r = nums.size();
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) r = mid;
else l = mid + 1;
}
return -1;
- leetcode 704 二分查找
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size();
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) l = mid+1;
else r = mid;
}
return -1;
}
使用场景2:旋转数组二分
求解模版
int l = 0, r = nums.size()-1;
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] > nums[r]) l = mid+1;
else r = mid;
}
return nums[l]; // 返回最小值
- leetcode 153 寻找旋转数组中的最小值
不是全程有序,而是变成两段有序数组
target:右边界,确定出当前较小数字组成的有序段是在mid左边还是右边;如果mid比r小,说明较小元素肯定在mid左边,右边界左移;反之,左边界右移
int findMin(vector<int>& nums) {
int l = 0;
int r = nums.size()-1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] > nums[r]) l = mid+1;
else r = mid;
}
return nums[l];
}
类似题目
题目 | 特点 | 思路 | 是否掌握 |
---|---|---|---|
leetcode33 搜索旋转数组 | 相比153不是寻找最小值,而是寻找目标值是否存在,所以条件就会变得更加复杂 | 判断条件:target比mid大还是小、target比右边界大还是小、mid比右边界大还是小 | 未掌握 |
leetcode34 在排序数组中查找第一个和最后一个位置 | 要查到第一个和最后一个位置,就可以转变为寻找第一个为target的位置,和第一个为target+1的位置;要找到左边界,就需要如果遇到大于等于target的都去移动右边界 | target值:target && target+1 | 未掌握,在计算end边界前先需要判断一下是否存在end |
leetcode 4 | 两个数组直之间比较大小 | 判断条件:数组1和数组2第k/2个数谁更小,则说明谁的前k/2个元素肯定填充在0-k-1之间,不会出现第k个,所以直接可以排除 | 未掌握 |
(4) 滑动窗口
- 可以用滑动窗口求解的题目中都包含着==“连续”、“最xxx”==这种字眼
- 求解时要注意先考虑清楚这仨个问题:窗口内是什么?左边界如何移动?右边界如何移动?
适用场景1:固定窗口;
求解模版
分为未达到固定长度前和达到固定长度后分别如何处理
for (int i = 0; i < nums.size(); i++) {
if (i < k) 如何处理;
else {
去掉越界的第i-k个元素
再处理第i个元素
维护全局最大
}
}
- leetcode 643 子数组的平均数I
double findMaxAverage(vector<int>& nums, int k) {
int max_s = INT_MIN; // 窗口元素和的最大值
int s = 0; // 维护窗口元素和
for (int i = 0; i < nums.size(); i++) {
// 1. 进入窗口
s += nums[i];
if (i < k - 1) { // 窗口大小不足 k
continue;
}
// 2. 更新答案
max_s = max(max_s, s);
// 3. 离开窗口
s -= nums[i - k + 1];
}
return (double) max_s / k;
}
适用场景2:可变窗口;求最小/最长子数组长度(如无重复字符、覆盖子串)
求解模版
int l = 0, res = 0;
unordered_set<int> set; // set充当可变滑动窗口
for (int i = 0; i < nums.size(); i++) {
加入nums[r];
while (l <= r && 不满足条件) {
set.erase(nums[l]);//窗口内移除l所指向元素
l++;
}
set.insert(s[r]);// 窗口内塞入r对应的元素
res = max(res, r-l+1);
r++;
}
- leetcode3 无重复字符的最长子串
需要满足的窗口条件:无重复字符
int lengthOfLongestSubstring(string s) {
unordered_set<char> set;//去重
int res= 0;
for (int l = 0, r = 0; r < s.length(); r++) {
char ch = s[r];
while (l <= r && set.count(ch)) {
set.erase(s[l]);
l++;
}
set.insert(s[r]);
res = max(res, r-l+1);
}
return res;
}
类似题目
题目 | 特点 | 思路 | 是否掌握 |
---|---|---|---|
leetcode209 长度最小的子数组 | 需要维护窗口长度最小 | 窗口内需要满足的条件:窗口内和不超过target; 否则就先更新区间长度,再移动左边界 | 未掌握 |
二、前缀和求数组
(1)一维前缀和
使用场景:快速求子数组的和、差分数列区间更新
求解模版
前缀和数组长度和原数组长度一致,且每个元素表示从0到当前索引位置的子区间和
vector<int> prefix (0, nums.size()+1);
for (int i = 1; i <= nums.size(); i++) {
prefix[i+1] = prefix[i] + nums[i-1];
}
int sum_ij = prefix[j] - prefix[i]; // 计算区间[i,j]的和
Leetcode 560 和为k的子数组
prefix[presum]表示从0累加到该位置和为presum的数字有多少个,prefix[presum-k]表示前缀和为presum-k的有多少个;那么这些数字到当前索引的区间和就为k。注意有一种情况那就是当前位置的前缀和就为k,即区间和为0,所以需要令prefix[0] = 1;
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> prefix;
prefix[0] = 1;
int presum = 0, count = 0;
for (int i = 1; i <= nums.size(); i++) {
presum += nums[i-1];
count += prefix[presum-k];
prefix[presum]++;
}
return count;
}
(2)二维前缀和
求解模版
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> prefix (n+1, vector<int> (m+1, 0));
for (int i = 0; i <= m; i++) {
for (int j = 0; i <= n; j++) {
prefix[i+1][j+1] = matrix[i][j] + prefix[i][j+1] + prefix[i+1][j] - prefix[i][j]
}
}
# 计算子矩阵和: prefix[x2][y2] - prefix[x1-1][y2] - prefix[x2][y1-1] + prefix[x1-1][y1-1]
三、矩阵相关
使用场景:矩阵遍历、螺旋打印
求解模版
int t = 0, l = 0;
int b = matrix.size(), r = matrix[0].size();
while (l < r && t < b) {
// 行顶部
for (int j = l; j < r; j++) {
matrix[t][j];
}
// 最右边界
for (int i = t; i < b; i++) {
matrix[i][r];
}
// 最下边
for (int j = r; j > l; j--) {
matrix[b][j];
}
// 最左边
for (int i = b; i > t; i--) {
matrix[i][l];
}
l++;
t++;
r--;
b--;
}
if (t == b) {
// 最后一行,左闭右闭
for (int i = l; i <= r; i++) {
matrix[b][i];
}
} else if (l == r) {
// 剩的是最后一列
for (int j = t; j <= b; j++) {
matrix[j][l];
}
}
典型题目
- leetcode54 螺旋矩阵
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector <int> res;
int l = 0, r = matrix[0].size()-1, t = 0, b = matrix.size()-1;
while (l < r && t < b) {
// 左闭右开,以防重复
// 首行
for (int j = l; j < r; j++) {
res.push_back(matrix[t][j]);
}
// 最右边界
for (int i = t; i < b; i++) {
res.push_back(matrix[i][r]);
}
// 最下边
for (int j = r; j > l; j--) {
res.push_back(matrix[b][j]);
}
// 最左边
for (int i = b; i > t; i--) {
res.push_back(matrix[i][l]);
}
l++;
r--;
t++;
b--;
}
if (t == b) {
// 最后一行,左闭右闭
for (int i = l; i <= r; i++) {
res.push_back(matrix[b][i]);
}
} else if (l == r) {
// 剩的是最后一列
for (int j = t; j <= b; j++) {
res.push_back(matrix[j][l]);
}
}
return res;
}
- leetcode 48 旋转图像
先上下调换,再沿着主对角线翻折
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++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n-i-1][j];
matrix[n-i-1][j] = temp;
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
四、哈希表优化
适用场景:快速查找元素是否存在、统计频率
求解模版
unordered_map<int, int> map;
for (auto num : nums) {
if (map.count(target-num)) return map[target-num];
else map[target-num] = 索引或者频率;
}
典型题目
- leetcode1 两数之和
哈希表里记录的是nums[i]的索引,满足条件为target-当前元素差刚好在map里
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp;
for(int i = 0; i < nums.size(); i++) {
if (mp.count(target - nums[i])) {
return vector<int> {i, mp[target - nums[i]]};
} else {
mp[nums[i]] = i;
}
}
return vector<int> {};
}
-leetcode128 最长连续序列
先确定连续序列最左边界,再确定最右边界;维护一个全局变量记录当前最长连续序列长度
int longestConsecutive(vector<int>& nums) {
int ans = 0;
unordered_set<int> map(nums.begin(), nums.end());
for (int n : map) {
if (map.contains(n-1)) continue;
int y = n+1;
while (map.contains(y)) y++;
ans = max(ans, y-n);
}
return ans;
}
五、单调栈
适用场景:解决”下一个更大/更小元素”、区间极值、维持有序性等
求解模版
维护一个单调递增或单调递减的栈,在遍历数组的时候动态调整栈结构,以便快速找到目标值
单减栈是比栈顶元素大则弹出栈顶元素,以快速找到下一个更大的元素;单增栈是比栈顶元素小的时候就弹出栈顶元素,以快速找到下一个更小的元素;
- leetcode 496 下一个更大的元素I
递减栈+哈希表映射; 维护一个针对nums2的单减栈,然后再判断nums1的数字是否在nums2里,如果在就填入map里记下来的数否则填-1;
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
unordered_map<int, int> map;
for (int num : nums2) {
while (!st.empty() && num > st.top()) {
map[st.top()] = num;
st.pop();
}
st.push(num);
}
vector<int> res;
for (int num : nums1) {
res.push_back(map.count(num) ? map[num] : -1);
}
return res;
}
-Leetcode 503 下一个更大的元素II
把数组拼接2次,模拟循环
注意的是这个索引需要还原为idx = i%n; 同时为了保证每个索引只入栈一次,只记录i在第一个数组的边界索引,第二个数组纯属是为了判断尾部元素的边界、
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> st;
int n = nums.size();
vector<int> res (n, -1);
for (int i = 0; i < 2 * n; i++) {
int idx = i % n;
while (!st.empty() && nums[idx] > nums[st.top()]) {
int top = st.top();
st.pop();
res[top] = nums[idx];
}
if (i < n) st.push(idx);
}
return res;
}
- leetcode 739 每日温度
维护的还是一个单减栈,只不过结果里记录的是下一个最大值和当前栈顶元素的间隔
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st; // 递增栈
vector<int> result(temperatures.size(), 0); // 长度和温度一样,初始化为0
// 这样如果有右边没有比该元素大的,就直接为0
for (int i = 0; i < temperatures.size(); i++) {
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
int idx = st.top();
st.pop();
result[idx] = i - idx ;
}
st.push(i);
}
return result;
}
- leetcode 84 柱状图中的最大矩形
维护一个单增栈,从而确定当前高度可以扩展的左右边界
注意要在栈内先增加一个哨兵,以免出现整个高度数组是单减数列,这样的话栈内永远只有一个数字,没有左边界,这个时候算宽度会得到-1;为了避免这种情况所以先在栈内增加一个哨兵
同时在数组尾部也增加一个0,这是为了确保栈内所有元素都会被弹出算面积
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
int max_s = 0;
st.push(-1);
heights.push_back(0);
for (int i = 0; i < heights.size(); i++) {
while (st.top() != -1 && heights[i] < heights[st.top()]) {
int h = heights[st.top()];
st.pop();
int w = i - st.top() -1;
max_s = max(max_s, h * w);
}
st.push(i);
}
return max_s;
}
- leetcode 316 去除重复字母
用String模拟一个单增栈,使得下一个加入栈的字母只会让整个字符更小
已经加入栈的要进行标记,同时统计每个字母出现的次数;在弹出的时候相比之前的题还需要判断一下该字母后面是不是还有,如果没有了则不能弹出
string removeDuplicateLetters(string s) {
vector<int> count(256, 0); // 统计每个字母出现的次数
vector<bool> visited(256, false); // 统计每个字母是否在栈内
string st;
for (char ch : s) count[ch]++;
for (char ch : s) {
count[ch]--;
if (visited[ch]) continue;
while (!st.empty() && ch < st.back() && count[st.back()]) {
visited[st.back()] = false;
st.pop_back();
}
st.push_back(ch);
visited[ch] = true;
}
return st;
}
- leetcode 155 最小栈
维护一个栈,栈内存的是当前数和当前最小值的差值
如果栈顶元素小于0,说明当前差值对应的元素为最小值,所以弹出它的时候最小值也要随着更新。否则说明最小值还在它前面
void push(int val) {
if (st.size() == 0) {
st.emplace(0LL); //栈内无元素时,存放差值0
minVal = val; //当前入站元素为最小值
} else {
st.emplace((long long)val - minVal); //记录当前元素与最小值的差值
minVal =min(minVal, (long long)val); //更新最小值
}
}
void pop() {
//栈顶元素为正值,直接弹出,若为负值,表明在这个为最小值发生改变,需要还原
minVal -= min(st.top(), 0LL);
st.pop();
}
int top() {
//用最小值还原栈顶元素
return (int)(minVal + max(st.top(), 0LL));
}
六、模拟
1. 位运算
确定只出现一次的数字:异或
- Leetcode 136 只出现一次的数字
int singleNumber(vector<int>& nums) {
int x = 0;
for (int num : nums) x ^= num;
return x;
}
2. 多数投票法
- leetcode 169
维护一个候选数和计数器,如果遇到的数字和候选数一样则count++, 否则count–;如果count变为负则换候选数
int majorityElement(vector<int>& nums) {
int count = 1;
int result = nums[0];
for (int i = 1; i < nums.size(); i++) {
if (result == nums[i]) count++;
else {
count--;
if (count < 0) {
count = 1;
result = nums[i];
}
}
}
return result;
}