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级。

剪枝优化

  1. 如果当前x与后两个数加起来都已经大于0,后续所有和都将大于0(已递增排序),退出循环

     if (x + nums[i + 1] + nums[i + 2] > 0) break;
    
  2. 如果x与最大的两个数加起来都小于0,直接让x向右移动,进入下一轮循环

    if (x + nums[n - 2] + nums[n - 1] < 0) continue;
    
  3. 如果数组第一个元素都大于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;
    }
};

25. K 个一组翻转链表 - 力扣(LeetCode)

posted @ 2025-04-09 22:09  七龙猪  阅读(2)  评论(0)    收藏  举报
-->