4.8

53. 最大子数组和 - 力扣(LeetCode)

思路1:前缀和

我们可以一边遍历数组计算前缀和pre_sum,一边维护前缀和的最小值min_pre_sum(相当于股票最低价格),用当前的前缀和(卖出价格)减去前缀和的最小值(买入价格),就得到了以当前元素结尾的子数组和的最大值(利润),用它来更新答案的最大值ans(最大利润)。

由于题目要求子数组不能为空,应当先计算前缀和-最小前缀和,再更新最小前缀和。相当于不能在同一天买入股票又卖出股票。

如果先更新最小前缀和,再计算前缀和-最小前缀和,就会把空数组的元素和 0 算入答案。

class Solution {
public:
   int maxSubArray(vector<int>& nums) {
      int ans = INT_MIN;
      int min_pre_sum = 0;
      int pre_sum = 0;

       for(int i : nums){
          pre_sum += x; // 当前的前缀和
         ans = max(ans, pre_sum - min_pre_sum); // 减去前缀和的最小值
         min_pre_sum = min(min_pre_sum, pre_sum); // 维护前缀和的最小值
       }
       return ans;
   }
};

复杂度分析

  • 时间复杂度:O(n),其中 nnums 的长度。
  • 空间复杂度:O(1)。仅用到若干额外变量。

思路二:DP

  1. 定义 f[i] 表示以 nums[i] 结尾的最大子数组和。

  2. 递推公式

    分类讨论:

  • nums[i] 单独组成一个子数组,那么 f[i]=nums[i]。
  • nums[i] 和前面的子数组拼起来,也就是在以 nums[i−1] 结尾的最大子数组和之后添加 nums[i],那么 f[i]=f[i−1]+nums[i]。

两种情况取最大值,得

f[i]= max(f[i−1],0)+nums[i], i≥1

简单地说,如果 nums[i] 左边的子数组元素和是负的,就不用和左边的子数组拼在一起了。

答案为 max(f)。

  1. 初始化

    vector<int> dp(nums.size() , 0)
      dp[0] = nums[0];
    
  2. 循环

    从左到右

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> f(nums.size());
        f[0] = nums[0];
        for (int i = 1; i < nums.size(); i++) {
            f[i] = max(f[i - 1], 0) + nums[i];
        }
        return ranges::max(f);
    }
};
//或者将dp数组向右平移一个单位
class Solution {
  public:
      int maxSubArray(vector<int>& nums) {
         int n = nums.size();
         int res = INT_MIN;
         vector<int> dp(n + 1 , INT_MIN);
         
         for (int i = 1; i <= n; i++) {
            dp[i] = max(dp[i - 1] , 0) + nums[i - 1];
            res = max(res , dp[i]);
         }
         return res;
      }
  };

空间优化

由于计算 f[i] 只会用到 f[i−1],不会用到更早的状态,所以可以用一个变量滚动计算。

状态转移方程简化为:f=max(f,0)+nums[i]

f 可以初始化成 0 或者任意负数。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int ans = INT_MIN; // 注意答案可以是负数,不能初始化成 0
        int f = 0;
        for (int x : nums) {
            f = max(f, 0) + x;
            ans = max(ans, f);
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n),其中 n 为 nums 的长度。
  • 空间复杂度:O(1)。仅用到若干额外变量。

21. 合并两个有序链表 - 力扣(LeetCode)

1.新建一个链表

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* dummy = new ListNode(0);
        ListNode* cur = dummy;

        while(list1 && list2){
          if(list1->val <= list2->val) {
            cur->next = list1;
            list1 = list1->next;
          }
          else {
            cur->next = list2;
            list2 = list2->next;
          }
          cur = cur->next;
        }
        cur->next = list1 ? list1 : list2;
        return dummy->next;
    }
};

2.递归

image-20250408204652765

//递归
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (list1 == nullptr) return list2; // 注:如果都为空则返回空
        if (list2 == nullptr) return list1;
        if (list1->val < list2->val) {
            list1->next = mergeTwoLists(list1->next, list2);
            return list1;
        }
        list2->next = mergeTwoLists(list1, list2->next);
        return list2;
    }
};

415. 字符串相加 - 力扣(LeetCode)

关键改进点

  1. 逐位计算:从字符串末尾开始逐位相加,避免数值溢出
  2. 进位处理:显式维护进位值 carry
  3. 动态构建结果:直接生成结果字符串,保留所有有效位
  4. 前导零兼容性:正确处理含前导零的输入(如 "000" + "00" = "0"

复杂度分析

  • 时间复杂度:O(max⁡(m,n)),其中 m,n 为输入字符串长度
  • 空间复杂度:O(max⁡(m,n)),存储结果字符串

此解法可处理任意长度的数字字符串相加,完全避免数值类型限制。

class Solution {
  public:
      string addStrings(string num1, string num2) {
        int carry = 0;
        int i = num1.size() - 1;
        int j = num2.size() - 1;
        string res;

        while(i >= 0 || j >= 0 || carry){
          int n1 = (i >= 0) ? (num1[i --] - '0') : 0;
          int n2 = (j >= 0) ? (num2[j --] - '0') : 0;
          int sum = n1 + n2 + carry;
          carry = sum / 10;
          res.push_back(sum % 10 + '0');
        }

        reverse(res.begin() , res.end());
        return res;
      }
  };

代码细节:

res.push_back(sum % 10 + '0');为什么不能写成res.push_back(sum % 10 - '0');

根本原因:ASCII编码规则

  1. 字符转数字时:
    '5' - '0' = 5(正确)
    通过减去字符'0'的ASCII值(十进制48),可以将字符数字转为实际数值。
  2. 数字转字符时:
    5 + '0' = '5'(正确)
    通过加上字符'0'的ASCII值(十进制48),可以将数值转为对应的字符。

错误修改后的行为

若将代码改为:

cpp

复制

res.push_back(sum % 10 - '0'); // 错误写法
  • 假设 sum % 10 = 5,则实际计算为:

    cpp

    复制

    5 - '0' = 5 - 48 = -43
    
  • -43 被当作ASCII码解释时,对应不可打印字符(如乱码或空字符)


示例分析

sum = 15 为例:

  1. 正确代码

    15 % 10 + '0' = 5 + 48 = 53 → ASCII字符 '5'
    

    结果字符串追加字符 '5'

  2. 错误代码

    15 % 10 - '0' = 5 - 48 = -43 → 非法字符
    

    结果字符串追加无效字符,最终输出完全错误。


关键结论

操作类型 正确表达式 错误表达式 结果
字符转数字 '5' - '0' - 数值 5
数字转字符 5 + '0' 5 - '0'(错误) 字符 '5'
  • 错误修改的本质:混淆了字符与数字转换的方向性规则。
  • 最终影响:生成的字符串包含非法字符,导致结果完全错误。

正确代码片段

int sum = 15;
char correct_char = sum % 10 + '0'; // '5'
char wrong_char = sum % 10 - '0';   // 非法字符(ASCII值为-43)

始终记住:

  • 数字 → 字符:加 '0'
  • 字符 → 数字:减 '0'

300. 最长递增子序列 - 力扣(LeetCode)

dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度

位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。所以:

if (nums[i] > nums[j])  dp[i] = max(dp[i], dp[j] + 1);

注意这里不是要dp[i]dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值

每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.

dp[i] 是有0到i-1各个位置的最长递增子序列推导而来,那么遍历i一定是从前向后遍历。

j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯从前向后遍历。

遍历i的循环在外层,遍历j则在内层,代码如下:

for (int i = 1; i < nums.size(); i++) {
 for (int j = 0; j < i; j++) {
     if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
 }
 if (dp[i] > result) result = dp[i]; // 取长的子序列
}
  1. 举例推导dp数组

输入:[0,1,0,3,2],dp数组的变化如下:

300.最长上升子序列

如果代码写出来,但一直AC不了,那么就把dp数组打印出来,看看对不对!

本题还需注意边界样例nums.size() == 0 || 1,最后输出的是dp数组的最大值,而不是dp[nums.size() - 1]!!

class Solution {
  public:
      int lengthOfLIS(vector<int>& nums) {
          int n = nums.size();
          if(n == 0 || n == 1)  return n;
          int res = 0;
          vector<int> dp(n , 1);
          for (int i = 1; i < n; i++) {
             for (int j = 0; j < i; j++) {
              if(nums[i] > nums[j])
                {
                  dp[i] = max(dp[i] , dp[j] + 1);
                }
             }
             res = max(res , dp[i]);
          }
          return res;
      }
  };

代码注意点:

  1. dp数组初始化: vector dp(n , 1);任何数自身序列长度都为1
  2. 边界条件:数组长度n <= 1时,return n
  3. 输出的是过程中统计的最大dp值。而不是输出dp[nums.size() - 1],得到的不一定是最大的,最大子序列不一定包括nums[n - 1]。

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