4.9
704. 二分查找 - 力扣(LeetCode)
TLE了,超时了往while循环陷入死循环方面思考。
对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。区间的定义就是不变量,在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。
针对数组无重复元素且有序的前提:
二分法的前提条件:有序数组、数组中无重复元素
//超时代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int l = -1 , r = n ;
while(l < r){ //该行错误
int mid = (l + r) / 2;
if(nums[mid] < target) l = mid;
else if(nums[mid] > target) r = mid;
else return mid;
}
return nums[r] == target ? r : -1;//如果找到了会返回mid,循环内没找到直接返回-1
}
};
用统一的优雅代码:定义左右边界为-1、n
由于该法定义的是开区间(l , r) 因此循环结束条件应该是l + 1 == r
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = -1;
int r = nums.size();
while(l + 1 != r){
int mid = l + ((r - l) >> 1);
if(target < nums[mid]) r = mid;
else if(target > nums[mid]) l = mid ;
else return mid ;
}
return -1;
}
};
8. 字符串转换整数 (atoi) - 力扣(LeetCode)
解题思路:
- 根据示例 1,需要去掉前导空格;
- 根据示例 2,需要判断第 1 个字符为
+和-的情况,因此,可以设计一个变量sign,初始化的时候为1,如果遇到-,将sign修正为-1;- 判断是否是数字,可以使用字符的 ASCII 码数值进行比较,即
0 <= c <= '9';- 根据示例 3 和示例 4 ,在遇到第 1 个不是数字的字符的情况下,转换停止,退出循环;
- 根据示例 5,如果转换以后的数字超过了
int类型的范围,需要截取。这里不能将结果res变量设计为long类型,注意:由于输入的字符串转换以后也有可能超过long类型,因此需要在循环内部就判断是否越界,只要越界就退出循环,这样也可以减少不必要的计算;- 由于涉及下标访问,因此全程需要考虑数组下标是否越界的情况。
特别注意:
1、由于题目中说「环境只能保存 32 位整数」,因此这里在每一轮循环之前先要检查乘以 10 以后是否溢出,具体细节请见编码。
2、Java 、Python 和 C++ 字符串的设计都是不可变的,即使用
trim()会产生新的变量,因此我们尽量不使用库函数,使用一个变量index去做遍历,这样遍历完成以后就得到转换以后的数值。
class Solution {
public:
int myAtoi(string str) {
int n = str.size();
// 去除前导空格
int index = 0;
while(index < n && str[index] == ' ') index ++;
//排除极端情况
if(index == n) return 0;
// 处理符号
int sign = 1;
if (str[index] == '+' || str[index] == '-') {
sign = (str[index] == '-') ? -1 : 1;
index++;
}
// 符号后必须紧跟数字
if (index >= n || !isdigit(str[index])) return 0;
// 计算数值并处理溢出
long res = 0;
while (index < n && isdigit(str[index])) {
int digit = str[index] - '0';
res = res * 10 + digit;
if (res * sign > INT_MAX) return INT_MAX;
if (res * sign < INT_MIN) return INT_MIN;
index++;
}
return res * sign;
}
};
1. 两数之和 - 力扣(LeetCode)
哈希表key存值,value存下标,因为要快速找key = target - nums[i]
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int , int> mp;
for (int i = 0; i < nums.size(); i++) {
auto it = mp.find(target - nums[i]);
if(it != mp.end()) return {it->second , i};
mp[nums[i]] = i;
}
return {};
}
};
15. 三数之和 - 力扣(LeetCode)
思路:双向双指针
前置题:167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)
class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { int l = 0 , r = nums.size() - 1; while(1){//题目保证答案存在,直接写while(true)即可,写while(l < r)过不了 int s = nums[l] + nums[r]; if(s == target) return {l + 1 , r + 1}; s > target ? r -- : l ++; } } };这道题的启发是:如果数组有序(从小到大),则可以用双向双指针优化,通过比较s与target大小进行控制l、r指针移动,从而一次遍历数组即可。
运用到三数之和中,也先对数组进行排序。因为题目说任意顺序返回,我们自己就规定x<y<z,然后用x遍历nums,y,z分别指向x+1 , n-1,用双向双指针解决。总时间即达到n^2级。
剪枝优化:
如果当前x与后两个数加起来都已经大于0,后续所有和都将大于0(已递增排序),退出循环
if (x + nums[i + 1] + nums[i + 2] > 0) break;如果x与最大的两个数加起来都小于0,直接让x向右移动,进入下一轮循环
if (x + nums[n - 2] + nums[n - 1] < 0) continue;如果数组第一个元素都大于0,则三数和必不能等于0
if(x > 0) return res;去重逻辑的思考
说到去重,其实主要考虑三个数的去重。 a, b ,c, 对应的就是
nums[i],nums[left],nums[right]a的去重
a 如果重复了怎么办,a是
nums里遍历的元素,那么应该直接跳过去(continue)。但这里有一个问题,是判断
nums[i] 与 nums[i + 1]是否相同,还是判断nums[i] 与 nums[i-1]是否相同。如果我们的写法是这样:
if (nums[i] == nums[i + 1]) { // 去重操作 continue; }那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断下一个也是-1,那这组数据就pass了。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以这里是有两个重复的维度。
那么应该这么写:
if (i > 0 && nums[i] == nums[i - 1]) { continue; }这么写就是当前使用
nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。这是一个非常细节的思考过程。
b与c的去重
很多同学写本题的时候,去重的逻辑多加了对right 和left 的去重:(代码中注释部分)
while (right > left) { if (nums[i] + nums[left] + nums[right] > 0) { right--; // 去重 right while (left < right && nums[right] == nums[right + 1]) right--; } else if (nums[i] + nums[left] + nums[right] < 0) { left++; // 去重 left while (left < right && nums[left] == nums[left - 1]) left++; } else { } }但细想一下,这种去重其实对提升程序运行效率是没有帮助的。
拿right去重为例,即使不加这个去重逻辑,依然根据
while (right > left)和if (nums[i] + nums[left] + nums[right] > 0)去完成right-- 的操作。多加了
while (left < right && nums[right] == nums[right + 1]) right--;这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少判断的逻辑。最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的。
所以这种去重是可以不加的。 仅仅是把去重的逻辑提前了而已。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
ranges::sort(nums);
vector<vector<int>> ans;
int n = nums.size();
for (int i = 0; i < n - 2; i++) {
int x = nums[i];
if(x > 0) return res; //优化三: 如果数组第一个元素都大于0,则三数和必不能等于0
if (i && x == nums[i - 1]) continue; // 跳过重复数字
if (x + nums[i + 1] + nums[i + 2] > 0) break; // 优化一:如果当前x与后两个数加起来都已经大于0,后续所有和都将大于0,退出循环
if (x + nums[n - 2] + nums[n - 1] < 0) continue; // 优化二:如果x与最大的两个数加起来都小于0,直接让x向右移动,进入下一轮循环
int j = i + 1, k = n - 1;
while (j < k) {
int s = x + nums[j] + nums[k];
if (s > 0) {
k--;
} else if (s < 0) {
j++;
} else { // 三数之和为 0
ans.push_back({x, nums[j], nums[k]});
for (j++; j < k && nums[j] == nums[j - 1]; j++); // 跳过重复数字
for (k--; k > j && nums[k] == nums[k + 1]; k--); // 跳过重复数字
/*这两行写成do{}while();形式更好理解:
do {
j ++;
}while(j < k && nums[j] == nums[j - 1]) ;
do {
k --;
}while(j < k && nums[k] == nums[k + 1]); */
}
}
}
return ans;
}
};
454. 四数相加 II - 力扣(LeetCode)
假设四个数a + b + c + d ==0
本题最后返回可能的组数,因此用一个哈希表freq统计a + b的和出现的频率,然后二重循环遍历c 、d,如果找到freq.find(0 - a - b),则在结果cnt加上该频率
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
18. 四数之和 - 力扣(LeetCode)
四数之和,和15.三数之和 (opens new window)是一个思路,都是使用双指针法, 基本解法就是在15.三数之和 (opens new window)的基础上再套一层for循环。
但是有一些细节需要注意,例如: 不要判断
nums[k] > target就返回了,三数之和 可以通过nums[i] > 0就返回了,因为 0 已经是确定的数了。四数之和这道题目
target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。四数之和的双指针解法是
两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。那么一样的道理,五数之和、六数之和等等都采用这种解法。
剪枝、去重的操作都是类似的:
从前往后循环的就与前一个数比。
i循环里:
if(nums[i] > target && nums[i] > 0) break; 对nums[i]去重:
if(i > 0 && nums[i] == nums[i - 1]) continue;j循环里:
if(nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break; 对nums[j]去重:
if(j > i + 1 && nums[j] == nums[j - 1]) continue;对nums[left]和nums[right]去重(因为找到四元组之后需要将l++ , r- -),所以初始化先做这一步。
for(l ++ ; l < r && nums[l] == nums[l - 1] ; l ++);
for(r -- ; r > l && nums[r] == nums[r + 1] ; r --);AC代码:
class Solution { public: vector<vector<int>> fourSum(vector<int>& nums, int target) { vector<vector<int>> res; sort(nums.begin() , nums.end()); for (int i = 0; i < nums.size(); i++) { //剪枝处理 if(nums[i] > target && nums[i] > 0) break;// 这里使用break,统一通过最后的return返回 // 对nums[i]去重 if(i > 0 && nums[i] == nums[i - 1]) continue; for (int j = i + 1; j < nums.size(); j++) { // 二级剪枝处理 if(nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break; // 对nums[j]去重 if(j > i + 1 && nums[j] == nums[j - 1]) continue; int l = j + 1 , r = nums.size() - 1; while(r > l){ // nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出 long sum = (long)nums[i] + nums[j] + nums[l] + nums[r]; if(sum > target) r --; else if(sum < target) l ++; else{ res.push_back({nums[i] , nums[j] , nums[l] , nums[r]}); // 对nums[left]和nums[right]去重 for(l ++ ; l < r && nums[l] == nums[l - 1] ; l ++); for(r -- ; r > l && nums[r] == nums[r + 1] ; r --); } } } } return res; } };
- 时间复杂度: O(n^3)
- 空间复杂度: O(1)
70. 爬楼梯 - 力扣(LeetCode)
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1);
if(n <= 2) return n;
dp[1] = 1 , dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
/* 也可不用特判:
int climbStairs(int n) {
int dp[n + 1];
dp[0] = 1 , dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
空间优化:
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) return n;
int f0 = 1 , f1 = 2 , f2;
for (int i = 3; i <= n; i++) {
f2 = f1 + f0;
f0 = f1;
f1 = f2;
}
return f1;
}
};
/*本题 1 <= n <= 45,所以f1可以初始化为1 , i可以从2开始:
int climbStairs(int n) {
int f0 = 1 , f1 = 1;
for (int i = 2; i <= n; i++) {
int new_f = f1 + f0;
f0 = f1;
f1 = new_f;
}
return f1;
}
234. 回文链表 - 力扣(LeetCode)
找中点+翻转链表
注意头插法翻转的赋值:
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;//最后两步不要反,从前往后覆盖赋值
class Solution {
// 876. 链表的中间结点
ListNode* middleNode(ListNode* head) {
ListNode* slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 206. 反转链表
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr, *cur = head;
while (cur) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
public:
bool isPalindrome(ListNode* head) {
ListNode* mid = middleNode(head);
ListNode* head2 = reverseList(mid);
while (head2) {
if (head->val != head2->val) { // 不是回文链表
return false;
}
head = head->next;
head2 = head2->next;
}
return true;
}
};

浙公网安备 33010602011771号