一、数组
第1部分
- 明确变量含义
- 循环不变量
- 小数据量调试
- 大数据量调试
1.1 二分查找
方法一:闭区间查找
class Solution {
public:
int binarySearch1(vector<int> &nums, int n, int target) {
int l = 0, r = n - 1;//闭区间查找
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
return mid;
if (nums[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
return -1;
}
};
方法二:开区间查找
class Solution {
public:
int binarySearch2(vector<int> &nums, int n, int target) {
int l = 0, r = n;//开区间查找
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
return mid;
if (nums[mid] > target)
r = mid;
else
l = mid + 1;
}
return -1;
}
};
1.2 LeetCode 283. 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
方法一:辅助空间移动
class Solution {
public:
void moveZeroes(vector<int> &nums) {
vector<int> nonZeroEle;
for (int i = 0; i < nums.size(); i++)
if (nums[i] != 0) nonZeroEle.push_back(nums[i]);
for (int i = 0; i < nonZeroEle.size(); i++)
nums[i] = nonZeroEle[i];
for (int i = nonZeroEle.size(); i < nums.size(); i++)
nums[i] = 0;
}
};
方法二:原地移动
class Solution {
public:
void moveZeroes(vector<int> &nums) {
int k = 0; // [0..k) / [0..k-1] 区间中都是非零元素
for (int i = 0; i < nums.size(); i++) { //循环不变,依旧保证[0..k)区间中都是非零元素
if(nums[i]!=0) {
nums[k]=nums[i];
k++;
}
}
for (int i = k; i < nums.size(); i++)
nums[i]=0;
}
};
方法三:交换移动
class Solution {
public:
void moveZeroes(vector<int> &nums) {
int k = 0; // [0..k) / [0..k-1] 区间中都是非零元素
for (int i = 0; i < nums.size(); i++) { //循环不变,依旧保证[0..k)区间中都是非零元素
if(nums[i]!=0)
swap(nums[k++],nums[i]);
}
}
};
1.3 LeetCode 27. 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
代码:
class Solution {
public:
int removeElement(vector<int> &nums, int val) {
int i = 0;//[0,i) / [0,i-1] 不包含val
int n = nums.size() - 1; //查找区间[i,n]
while (i <= n) {
while (i <= n&&nums[i] != val) i++;
while (i <= n&&nums[n] == val) n--;
if (i <= n) swap(nums[i++], nums[n--]);
}
return i;
}
};
1.4 LeetCode 26. 删除排序数组中的重复项
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
代码:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.size() == 0) return {};
int j = 1;
for (int i = 1; i < nums.size(); i++)
{
if (nums[i] != nums[i-1])
{
nums[j] = nums[i];
j++;
}
}
return j ;
}
};
1.5 LeetCode 80. 删除排序数组中的重复项 II
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定 nums = [1,1,1,2,2,3],
函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,1,2,3,3],
函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
代码:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n=nums.size();
if (n<=2) return n;
int j=2;
for (int i = 2; i < n; i++) {
if(nums[i]!=nums[j-2]){
nums[j] = nums[i];
j++;
}
}
return j;
}
};
2.1 LeetCode 75. 颜色分类
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。
示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
进阶:
- 一个直观的解决方案是使用计数排序的两趟扫描算法。
- 首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
你能想出一个仅使用常数空间的一趟扫描算法吗?
class Solution {
public:
void sortColors(vector<int>& nums) {
int zero = -1;
int tow = nums.size();
for (int i = 0; i < tow; ) {
if(nums[i]==1){
i++;
} else if (nums[i]==0){
zero++;
swap(nums[i], nums[zero]);
i++;
} else{
__glibcxx_assert(nums[i]==2);
tow--;
swap(nums[i], nums[tow]);
}
}
}
};
2.2 LeetCode 88. 合并两个有序数组
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明:
- 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
- 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
class Solution {
public:
void merge(vector<int> &nums1, int m, vector<int> &nums2, int n) {
int i = m + n - 1;
m--;
n--;
while (n >= 0 && m >= 0) {
if(nums1[m]>nums2[n]) {
nums1[i] = nums1[m];
m--;
} else{
nums1[i] = nums2[n];
n--;
}
i--;
}
while (n >= 0) {
nums1[i] = nums2[n];
n--;
i--;
}
}
};
2.3 LeetCode 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
解法一:划分区域+二分查找
class Solution {
public:
int findKthLargest(vector<int> &nums, int k) {
int n = nums.size();
__glibcxx_assert(k < 0 && k < n);
return findK(nums, 0, n - 1, n - k + 1);
}
private:
//二分查找K
int findK(vector<int> &nums, int l, int r, int k) {
int index = partition(nums, l, r);
int Kth = index - l + 1;
if (k == Kth) {
return nums[index];
} else if (k < Kth) {
return findK(nums, l, index - 1, k);
} else {
return findK(nums, index + 1, r, k - Kth);
}
}
//划分区域
int partition(vector<int> &nums, int l, int r) {
int e = nums[l];
int front = l + 1; //[l+1,front)
int rear = r; //(rear,r]
while (front <= rear) {
while (front <= rear && nums[front] <= e)
front++;
while (front <= rear && nums[rear] > e)
rear--;
if (front < rear)
swap(nums[front], nums[rear]);
}
swap(nums[l], nums[rear]);
return rear;
}
};
第2部分 双指针(对撞指针)
- 对撞指针
3.1 LeetCode 167. 两数之和 II - 输入有序数组
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
- 返回的下标值(index1 和 index2)不是从零开始的。
- 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
解法一:双指针法
class Solution {
public:
vector<int> twoSum(vector<int> &numbers, int target) {
int low = 0; //[0, low)
int high = numbers.size() - 1;//(high, numbers.size() - 1]
int ans[2] = {-1, -1};
while (high > low) { //high==low 时指向同一个元素
int res = numbers[high] + numbers[low];
if (res == target) {
return {low + 1, high + 1};
} else if (res < target) {
low++;
} else {
high--;
}
}
return {-1, -1};
}
};
3.2 LeetCode 125. 验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
**说明:**本题中,我们将空字符串定义为有效的回文串。
示例 1:
输入: "A man, a plan, a canal: Panama"
输出: true
示例 2:
输入: "race a car"
输出: false
解法一:双指针法
class Solution {
public:
bool isPalindrome(string s) {
int low = 0;
int high = s.size() - 1;
while (low < high) { // 当 low == high 时不用考虑
while (low < high && !isalnum(s[low]))
low++;
while (low < high && !isalnum(s[high]))
high--;
//if (low < high && tolower(s[low]) != tolower(s[high])) 亦可以
if (tolower(s[low]) != tolower(s[high]))
return false;
low++;
high--;
}
return true;
}
};
3.2 LeetCode 334. 递增的三元子序列
给定一个未排序的数组,判断这个数组中是否存在长度为 3 的递增子序列。
数学表达式如下:
-
如果存在这样的 i, j, k, 且满足 0 ≤ i < j < k ≤ n-1,
-
使得 arr[i] < arr[j] < arr[k] ,返回 true ; 否则返回 false 。
说明: 要求算法的时间复杂度为 O(n),空间复杂度为 O(1) 。
示例 1:
输入: [1,2,3,4,5]
输出: true
示例 2:
输入: [5,4,3,2,1]
输出: false
解法一:双指针法
class Solution {
public:
bool increasingTriplet(vector<int> &nums) {
int n = nums.size();
if (n < 3) return false;
int first = INT_MAX;
int second = INT_MAX;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] <= first) {
first = nums[i];
} else if (nums[i] <= second) {
second = nums[i];
} else if (nums[i] > second) {
return true;
}
}
return false;
}
};
3.3 LeetCode 345. 反转字符串中的元音字母
编写一个函数,以字符串作为输入,反转该字符串中的元音字母。
示例 1:
输入:"hello"
输出:"holle"
示例 2:
输入:"leetcode"
输出:"leotcede"
解法一:对撞指针
class Solution {
public:
void reverseString(vector<char> &s) {
int r = s.size() - 1;
int l = 0;
while (r > l) {
swap(s[l], s[r]);
r--;
l++;
}
}
};
3.4 LeetCode 345. 反转字符串中的元音字母
编写一个函数,以字符串作为输入,反转该字符串中的元音字母。
示例 1:
输入:"hello"
输出:"holle"
示例 2:
输入:"leetcode"
输出:"leotcede"
提示:
- 元音字母不包含字母 “y” 。
解法一:对撞指针
class Solution {
public:
unordered_set<char> flag{'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
string reverseVowels(string s) {
int r = s.size() - 1;
int l = 0;
while (l < r) {
while (l < r && flag.find(s[l]) == flag.end()) {
l++;
}
while (l < r && flag.find(s[r]) == flag.end()) {
r--;
}
if (l < r){
swap(s[l], s[r]);
l++;
r--;
}
}
return s;
}
};
3.5 LeetCode 11. 盛最多水的容器
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
**说明:**你不能倾斜容器,且 n 的值至少为 2。
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解法一:对撞指针
class Solution {
public:
int maxArea(vector<int> &height) {
int m = 0;
int l = 0;
int r = height.size() - 1;
while (l < r) {
if (height[l] < height[r]){
m = max(m, (r - l) * height[l]);
l++;
}else{
m = max(m, (r - l) * height[r]);
r--;
}
}
return m;
}
};
第3部分 滑动窗口
4.1 LeetCode 209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
进阶:
- 如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
解法一:滑动窗口
class Solution {
public:
int minSubArrayLen(int s, vector<int> &nums) {
int l = 0;
int r = -1;//滑动窗口[l,r]
int sum = 0;
int length = nums.size() + 1;
while (l < nums.size()) {
if (r + 1 < nums.size() && sum < s) {
r++;
sum += nums[r];
} else {
sum -= nums[l];
l++;
}
if (sum >= s)
length = min(length, r - l + 1);
}
if (length == nums.size() + 1)
return 0;
return length;
}
};
4.2 LeetCode 3. 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
解法一:滑动窗口
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int freq[256] = {0};
int m = -1;
int l = 0;
int r = -1;//滑动窗口[l,r]
while (l < s.length()) {
if (r + 1 < s.length() && freq[s[r + 1]] == 0) {
r++;
freq[s[r]]++;
} else {
freq[s[l]]--;
l++;
}
m = max(m, r - l + 1);
}
if (m == -1) return 0;
return m;
}
};
解法二:滑动窗口
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int freq[256] = {0};
int m = 0;
int l = 0;
int r = -1;//滑动窗口[l,r]
while (l < s.length()) {
if (r + 1 < s.length() && freq[s[r + 1]] == 0) {
r++;
freq[s[r]]++;
} else {
freq[s[l]]--;
l++;
}
m = max(m, r - l + 1);
}
return m;
}
};
解法三:滑动窗口(遍历字符数组)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if (s.length() == 0)
return 0;
int freq[256] = {0};
int m = -1;
int l = 0;
int r = -1;//滑动窗口[l,r]
for (int i = 0; i < s.length();) {//遍历字符数组
if (freq[s[i]] == 0) {
r++;
freq[s[i]]++;
i++;
} else {
freq[s[l]]--;
l++;
}
m = max(m, r - l + 1);
}
return m;
}
};
4.3 LeetCode 438. 找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。
- 不考虑答案输出的顺序。
示例 1:
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
示例 2:
输入:
s: "abab" p: "ab"
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的字母异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的字母异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的字母异位词。
解法一:滑动窗口
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int freq[256] = {0};
for (int i = 0; i < p.length(); ++i) {
freq[p[i]]++;
}
vector<int> res;
int l = 0;
int r = -1;
while (l < s.length()) {
if (r + 1 < s.length() && freq[s[r + 1]] != 0) {
r++;
freq[s[r]]--;
} else {
freq[s[l]]++;
l++;
}
if (r - l + 1 == p.size())
res.push_back(l);
}
return res;
}
};
4.4 LeetCode *76. 最小覆盖子串
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
提示:
- 如果 S 中不存这样的子串,则返回空字符串 “”。
- 如果 S 中存在这样的子串,我们保证它是唯一的答案。
解法一:滑动窗口
class Solution {
public:
string minWindow(string s, string t) {
int winFreq[128] = {0};
int tFreq[128] = {0};
for (int i = 0; i < t.size(); ++i) {
tFreq[t[i]]++;
}
int ans[2] = {-1, -1};
int l = 0;
int r = -1;
int distance = 0;
int len = s.size() + 1;
while (l < s.size()) {
if (distance == t.size()) {//移动左端点
if (len > r - l + 1) {
ans[0] = l;
ans[1] = r;
len = r - l + 1;
}
if (winFreq[s[l]] <= tFreq[s[l]]) {
distance--;
}
winFreq[s[l]]--;
l++;
} else if (r + 1 < s.size()) {//移动右端点
r++;
if (winFreq[s[r]] < tFreq[s[r]]) {
distance++;
}
winFreq[s[r]]++;
}
//distance < t.size() && r == s.size() - 1 滑动窗口中不可能符合条件
if (distance < t.size() && r == s.size() - 1) {
break;
}
}
if (len == s.size() + 1)
return "";
return s.substr(ans[0], ans[1] - ans[0] + 1);
}
};
解法二:滑动窗口
class Solution {
public:
string minWindow(string s, string t) {
int winFreq[128] = {0};
int tFreq[128] = {0};
for (int i = 0; i < t.size(); ++i) {
tFreq[t[i]]++;
}
int ans[2] = {0, -1};//滑动窗口区间
int l = 0;
int r = -1;
int distance = 0;
int len = s.size() + 1;
while (l < s.size()) {
if (distance == t.size()) {//移动左端点
if (len > r - l + 1) {
ans[0] = l;
ans[1] = r;
len = r - l + 1;
}
if (winFreq[s[l]] <= tFreq[s[l]]) {
distance--;
}
winFreq[s[l]]--;
l++;
} else if (r + 1 < s.size()) {//移动右端点
r++;
if (winFreq[s[r]] < tFreq[s[r]]) {
distance++;
}
winFreq[s[r]]++;
} else //已不能满足条件
break;
}
return s.substr(ans[0], ans[1] - ans[0] + 1);
}
};