4.18

136. 只出现一次的数字 - 力扣(LeetCode)

利用异或运算 a⊕a=0 的性质,我们可以用异或来「消除」所有出现了两次的元素,最后剩下的一定是只出现一次的元素。

例如 nums=[4,1,2,1,2],把所有元素异或:

4⊕1⊕2⊕1⊕2 = 4⊕(1⊕1)⊕(2⊕2) = 4⊕0⊕0 = 4

其中用到了异或运算的结合律,即 (a⊕b)⊕c=a⊕(b⊕c)(类比加法)。

代码中,初始化 ans = 0 是因为 0⊕a=a,相当于我们从第一个数开始,和其它数异或。

class Solution {
  public:
      int singleNumber(vector<int>& nums) {
        int ans = 0;
        for(int i : nums)  ans ^= i;
        return ans;
      }
  };

复杂度分析

  • 时间复杂度:O(n),其中 n 为 nums 的长度。
  • 空间复杂度:O(1)。
//自己的写法:两次遍历
class Solution {
  public:
      int singleNumber(vector<int>& nums) {
          unordered_map<int , int> mp;
          int res = -1;
          for(int i : nums){
            mp[i] ++;
          }
          for(auto& x : mp) {
            if(x.second == 1) {
                res = x.first;
                break;
            } 
          }  
          return res;                
      }
  };

异或题:1720. 解码异或后的数组 - 力扣(LeetCode)

class Solution {
  public:
      vector<int> decode(vector<int>& encoded, int first) {
          vector<int> res;
          res[0] = first;
          for (int i = 0; i < encoded.size(); i++) {
             res[i + 1] = encoded[i] ^ res[i];
          }
          return res;
      }
  };

这种写法是错误的:根本原因在于 vector 未初始化空间时直接通过下标访问元素。以下是详细分析和修复方案:

  1. vector 直接下标访问

    vector<int> res; 
    res[0] = first; // 错误!此时 res 是空容器,res[0] 是非法访问
    
    • vector 默认构造时是空的,size() == 0
    • 直接通过 res[0] 访问元素会触发越界,导致未定义行为(UB)。
  2. 循环中的越界访问

    for (int i = 0; i < encoded.size(); i++) {
        res[i + 1] = encoded[i] ^ res[i]; // 错误!i+1 超出当前 res 的大小
    }
    
    • res 初始为空,循环中 i+1 的索引必然越界。

修复后的代码

方案1:预先分配空间

class Solution {
public:
    vector<int> decode(vector<int>& encoded, int first) {
        int n = encoded.size() + 1;
        vector<int> res(n);  // 直接分配足够空间
        res[0] = first;      // 合法访问
        for (int i = 0; i < encoded.size(); i++) {
            res[i + 1] = encoded[i] ^ res[i];
        }
        return res;
    }
};

方案2:使用 push_back 动态扩展

class Solution {
public:
    vector<int> decode(vector<int>& encoded, int first) {
        vector<int> res;
        res.push_back(first); // 正确初始化第一个元素
        for (int i = 0; i < encoded.size(); i++) {
            res.push_back(encoded[i] ^ res[i]); // 动态添加
        }
        return res;
    }
};

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

二分法,细节:

  1. ‘=’加在 if(nums[mid] <= target) ll = mid;那么ll就是最后一个等于target的数(如果存在)
  2. ‘=’加在 if(nums[mid] >= target) r = mid;那么r就是第一个等于target的数(如果存在)
  3. 第一次二分之后判断是否存在target的条件是:if(r == nums.size() || nums[r] != target)
class Solution {
  public:
      vector<int> searchRange(vector<int>& nums, int target) {
          int l = -1 , r = nums.size();
          while(l + 1 < r){
            int mid = l + ((r - l) >> 1);
            if(nums[mid] < target) l = mid;
            else r = mid;//第一次出现的位置           
          }
          if(r == nums.size() || nums[r] != target)  return {-1 , -1};
          int ll = -1 , rr = nums.size();
          while(ll + 1 < rr){
            int mid = ll + ((rr - ll) >> 1);
            if(nums[mid] <= target)  ll = mid;
            else rr = mid;
          }
          return {r , ll};
      }
  };

611. 有效三角形的个数 - 力扣(LeetCode)

[!WARNING]

回溯法深搜会TLE,用户当前的代码思路是使用回溯法生成所有可能的三元组,然后判断是否满足条件。但显然,这种方法在时间效率上会有问题,因为数组长度可能很大,回溯的时间复杂度是O(n³),当n较大时会超时。比如,如果数组长度是1000,那么时间复杂度是10^9级别,显然无法通过。

image-20250418153521909

//回溯超时代码 
class Solution {
  public:
      int triangleNumber(vector<int>& nums) {
          int res = 0;
          vector<int> path;
          ranges::sort(nums);
                  
          auto dfs = [&](this auto&& dfs , int startIndex)->void{
              if(path.size() == 3 ){   
                if(path[0] + path[1] > path[2])   res ++;

                return;
              }  

              for (int i = startIndex; i < nums.size(); i++) {
                 if(path.size() < 3) {
                  path.push_back(nums[i]);
                  dfs(i + 1);
                  path.pop_back();
                 }
              }
          };
          dfs(0);
          return res;
      }
  };

分析

首先明确计算规则:从示例 1 可以知道,对于三元组 (2,3,4) 和 (4,3,2),我们只统计了其中的 (2,3,4),并没有把 (4,3,2) 也统计到答案中,所以题目意思是把这两个三元组当成是同一个三元组,我们不能重复统计。

既然有这样的规则,那么不妨规定三角形的三条边 a,b,c 满足:1≤a≤b≤c

这可以保证我们在统计合法三元组 (a,b,c) 的个数时,不会把 (c,b,a) 这样的三元组也统计进去。

那么问题变成,从 nums 中选三个数,满足 1≤a≤b≤c 且 a+b>c 的方案数。

方法一:枚举最长边 + 相向双指针

为了能够使用相向双指针,先对数组从小到大排序。

外层循环枚举最长边 c=nums[k],内层循环用相向双指针枚举 a=nums[i] 和 b=nums[j],具体如下:

  1. 初始化左右指针 i=0, j=k−1。
  2. 如果 nums[i]+nums[j]>c,由于数组是有序的,nums[j] 与下标 i′在 [i,j−1] 中的任何 nums[i′] 相加,都是 >c 的,因此直接找到了 j−i 个合法方案,加到答案中,然后将 j 减一。
  3. 如果 nums[i]+nums[j]≤c,由于数组是有序的,nums[i] 与下标 j′ 在 [i+1,j] 中的任何 nums[j′] 相加,都是 ≤c 的,因此后面无需考虑 nums[i],将 i 加一。
  4. 重复上述过程直到 i≥j 为止。

优化前

class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        ranges::sort(nums);
        int n = nums.size(), res = 0;
        
        for (int k = n - 1; k >= 2; k--) {
            int i = 0, j = k - 1;
            while (i < j) {
                if (nums[i] + nums[j] > nums[k]) {
                    res += j - i; // [i, j-1] 均满足条件
                    j--;
                } else {
                    i++;
                }
            }
        }
        return res;
    }
};

优化

类似 15. 三数之和的做法,本题也有两个优化。

首先把循环改成倒序枚举 k。可以剪枝优化:

第一个优化:在执行双指针之前,如果发现最小的 a 和 b 相加大于 c,也就是

nums\[0\]+nums\[1\]\>nums\[k\]

说明从 nums[0] 到 nums[k] 中任选三个数 a,b,c 都满足 a+b>c,那么直接把 \(C_{k+1}^3 =\frac{(k+1)k(k−1)}{6}\) 加入答案,退出外层循环。这是为什么要倒序枚举 k 的原因(正序枚举没法退出外层循环)。

第二个优化:在执行双指针之前,如果发现最大的 a 和 b 相加小于等于 c,也就是

nums[k−2]+nums[k−1]≤nums[k]

说明不存在 a+b>c,不执行双指针,继续外层循环。

注:由于数据原因,上述优化在本题可能并不明显。

class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        ranges::sort(nums);
        int ans = 0;
        for (int k = nums.size() - 1; k > 1; k--) {
            int c = nums[k];
            if (nums[0] + nums[1] > c) { // 优化一
                ans += (k + 1) * k * (k - 1) / 6;
                break;
            }
            if (nums[k - 2] + nums[k - 1] <= c) { // 优化二
                continue;
            }
            int i = 0; // a=nums[i]
            int j = k - 1; // b=nums[j]
            while (i < j) {
                if (nums[i] + nums[j] > c) {
                    ans += j - i;
                    j--;
                } else {
                    i++;
                }
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n^2),其中 n 为 nums 的长度。
  • 空间复杂度:O(1)。不计入排序的栈开销,仅用到若干额外变量。

方法二:枚举最短边 + 同向双指针

枚举最短边 a,问题变成计算满足 c−b < a 的 (b,c) 个数。

这个条件意味着,当 a 固定不变时,b 和 c 不能隔太远。

这可以用同向双指针解决,原理见 滑动窗口【基础算法精讲 03】

  1. 枚举 a=nums[i],其中 i=0,1,2,…,n−3。
  2. 如果 a=0,则跳过。
  3. 现在计算,对于 k=i+2,i+3,…,n−1,有多少个符合要求的 j?。
  4. 枚举 k 的同时,维护指针 j,初始值为 i+1。
  5. 如果发现 nums[k]−nums[j]≥a,说明 b 和 c 隔太远了,那么把 j 不断加一,直到 c−b<a,也就是 nums[k]−nums[j] < a 为止。
  6. 此时,对于固定的 a=nums[i] 和固定的 c=nums[k],nums[j] , nums[j+1] ,…, nums[k−1] 都可以作为 b,这一共有 k−j 个,加入答案。
class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        ranges::sort(nums);
        int n = nums.size(), ans = 0;
        for (int i = 0; i < n - 2; i++) {
            int a = nums[i];
            if (a == 0) { // 三角形的边不能是 0
                continue;
            }
            int j = i + 1;
            for (int k = i + 2; k < n; k++) {
                while (nums[k] - nums[j] >= a) {
                    j++;
                }
                // 如果 a=nums[i] 和 c=nums[k] 固定不变
                // 那么 b 可以是 nums[j],nums[j+1],...,nums[k-1],一共有 k-j 个
                ans += k - j;
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n^2),其中 n 为 nums 的长度。里面的二重循环,由于 j 一直在增大,所以里面的二重循环的时间复杂度是 O(n) 的。
  • 空间复杂度:O(1)。不计入排序的栈开销,仅用到若干额外变量。

673. 最长递增子序列的个数 - 力扣(LeetCode)

前置题:300. 最长递增子序列 - 力扣(LeetCode)

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;
      }
  };

思路

这道题可以说是 300.最长上升子序列 的进阶版本

1. 确定dp数组(dp table)以及下标的含义

这道题目我们要一起维护两个数组。

dp[i]:i之前(包括i)最长递增子序列的长度为dp[i]

count[i]:以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]

2. 确定递推公式

300.最长上升子序列 中,我们给出的状态转移是:

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

即:位置i的最长递增子序列长度 等于j从0到i-1各个位置的最长升序子序列 + 1的最大值

本题就没那么简单了,我们要考虑两个维度,一个是dp[i]的更新,一个是count[i]的更新。

那么如何更新count[i]呢?

以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]。

那么在nums[i] > nums[j]前提下

  1. 如果在[0, i-1]的范围内,找到了j,使得dp[j] + 1 > dp[i],说明找到了一个更长的递增子序列。

    那么以j为结尾的子串的最长递增子序列的个数,就是最新的以i为结尾的子串的最长递增子序列的个数,相当于在所有的递增子序列后加入一个nums[i] , 长度变了,个数不变。

    即:count[i] = count[j] , dp[i] = dp[j] + 1;

  2. 在nums[i] > nums[j]前提下,如果在[0, i-1]的范围内,找到了j,使得dp[j] + 1 == dp[i],说明找到了两个相同长度的递增子序列。

    那么以i为结尾的子串的最长递增子序列的个数 就应该加上以j为结尾的子串的最长递增子序列的个数,即:count[i] += count[j];

    [!NOTE]

    如何理解? 相当于把子序列 0 , 1 ,2 ···nums[j]换成了0 , 1 ,2···nums[i],这样子序列长度是相同的,那么count[i] 就应该加上count[j]的个数。

if (nums[i] > nums[j]) {
    if (dp[j] + 1 > dp[i]) {
        dp[i] = dp[j] + 1; // 更新dp[i]放在这里,就不用max了
        count[i] = count[j];
    } else if (dp[j] + 1 == dp[i]) {
        count[i] += count[j];
    }
}

题目要求最长递增序列的长度的个数,我们应该把最长长度记录下来。

代码如下:

for (int i = 1; i < nums.size(); i++) {
    for (int j = 0; j < i; j++) {
        if (nums[i] > nums[j]) {
            if (dp[j] + 1 > dp[i]) {
                count[i] = count[j];
              	dp[i] = dp[j] + 1;
            } else if (dp[j] + 1 == dp[i]) {
                count[i] += count[j];
            }           
        }
        if (dp[i] > maxCount) maxCount = dp[i]; // 记录最长长度
    }
}

3. dp数组如何初始化

再回顾一下dp[i]和count[i]的定义

  1. count[i]记录了以nums[i]为结尾的字符串,最长递增子序列的个数。那么最少也就是1个,所以count[i]初始为1。

  2. dp[i]记录了i之前(包括i)最长递增序列的长度。

最小的长度也是1,所以dp[i]初始为1。

代码如下:

vector<int> dp(nums.size(), 1);
vector<int> count(nums.size(), 1);

其实动规的题目中,初始化很有讲究,也很考察对dp数组定义的理解

4. 确定遍历顺序

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

j其实就是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]) {
            if (dp[j] + 1 > dp[i]) {
                count[i] = count[j];
                dp[i] = dp[j] + 1;
            } else if (dp[j] + 1 == dp[i]) {
                count[i] += count[j];
            }
           
        }
        if (dp[i] > maxCount) maxCount = dp[i];
    }
}

最后还有再遍历一遍dp[i],把最长递增序列长度对应的count[i]累计下来就是结果了。

代码如下:

for (int i = 1; i < nums.size(); i++) {
    for (int j = 0; j < i; j++) {
        if (nums[i] > nums[j]) {
            if (dp[j] + 1 > dp[i]) {
                count[i] = count[j];
            } else if (dp[j] + 1 == dp[i]) {
                count[i] += count[j];
            }
            dp[i] = max(dp[i], dp[j] + 1);
        }
        if (dp[i] > maxCount) maxCount = dp[i];
    }
}
int result = 0; // 统计结果
for (int i = 0; i < nums.size(); i++) {
    if (maxCount == dp[i]) result += count[i];
}

5. 举例推导dp数组

输入:[1,3,5,4,7]

673.最长递增子序列的个数

如果代码写出来了,怎么改都通过不了,那么把dp和count打印出来看看对不对!

以上分析完毕,C++整体代码如下:

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        vector<int> count(nums.size(), 1);
        int maxCount = 0;
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1;
                        count[i] = count[j];
                    } else if (dp[j] + 1 == dp[i]) {
                        count[i] += count[j];
                    }
                }
                if (dp[i] > maxCount) maxCount = dp[i];
            }
        }
        int result = 0;
        for (int i = 0; i < nums.size(); i++) {
            if (maxCount == dp[i]) result += count[i];
        }
        return result;
    }
};
  • 时间复杂度O(n^2)
  • 空间复杂度O(n)

还有O(nlogn)的解法,使用树状数组,今天有点忙就先不写了,感兴趣的同学可以自行学习一下,这里有我之前写的树状数组系列博客 (十年前的陈年老文了)

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

子序列系列力扣题目

按照如下顺序将力扣题目做完,相信会帮助你对动态规划之子序列问题有一个深刻的理解。以下每道题目在力扣题解区都有「代码随想录」的题解。

image.png


4. 寻找两个正序数组的中位数 - 力扣(LeetCode)

二、枚举:双指针做法

lc4-4-c2.png

目标:找第一组的最大值 < 第二组的最小值,即\(max(a_i,b_j)< min(a_{i+1},b_{j+1})\)
由于 \(a_i ≤ a_{i+1}\)\(b_j < b_{j+1}\),所以只需判断\(a_i ≤ b_{j+1}\)\(b_j< a_{i+1}\)是否同时成立。

下面来说具体做法。

设 a 的 b 的长度分别为 m 和 n,且 m≤n(如果不满足则交换两个数组)。

  • 为方便处理 i=−1,即 a 有 0 个数在第一组的情况,我们可以往 a 的最左边插入一个哨兵 −∞,这可以保证数组仍然是有序的。对于 j=−1 的情况也同理,往b的最左边插入一个−∞。
  • 为方便处理 i+1=m,即 a 有 m 个数在第一组的情况,我们可以往 a 的最右边插入一个哨兵 ∞,这可以保证数组仍然是有序的。对于 j+1=n 的情况也同理,往 b 的最右边插入一个 ∞。这可以避免 \(a_{i+1}\)\(b_ {j+1}\)下标越界。
  • 插入 −∞ 和 ∞ 后,便可保证无论 a 和 b 是什么样的,一定存在一个 i,满足 \(a_i≤b_ {j+1}\)\(a_{i+1}>=b_j\)
  • m 和 n 的值不变。

如此修改后,i 的含义变成了 a 有 i 个数在第一组,j 的含义变成了 b 有 j 个数在第一组。

初始化 i=0,那么 j 应该初始化成多少?

  • 如果 m+n 是偶数,那么每组的大小为\(\frac{m+n}{2}\),j 应当初始化成\(\frac{m+n}{2}\)
  • 如果 m+n 是奇数,我们规定第一组比第二组多一个数,第一组的大小为 \(\frac{m+n + 1}{2}\) ,j 应当初始化成 \(\frac{m+n + 1}{2}\)

两种情况可以合并为:j 初始化成\(⌊\frac{m+n + 1}{2}⌋\) , i + j = \(\frac{m+n + 1}{2}\)

为了保证组的大小不变,i 每增加 1,j 就要减少 1(i + + , j - -)。

根据图片中的结论,只要发现 \(a_i≤b_{ j+1}\)\(a_{i+1}>b_j\) ,那么:

  • 如果 m+n 是偶数,中位数为 \(max(a_i ,b_j)\) 和$ min(a_{i+1} ,b_{j+1})$ 的平均值。
  • 如果 m+n 是奇数,中位数为 \(max(a_i ,b_j)\)

双指针法:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& a, vector<int>& b) {
        if (a.size() > b.size()) {
            swap(a, b); // 保证下面的 i 可以从 0 开始枚举
        }
    int m = a.size(), n = b.size();
    a.insert(a.begin(), INT_MIN); // 最左边插入 -∞
    b.insert(b.begin(), INT_MIN);
    a.push_back(INT_MAX); // 最右边插入 ∞
    b.push_back(INT_MAX);

    // 枚举 nums1 有 i 个数在第一组
    // 那么 nums2 有 (m + n + 1) / 2 - i 个数在第一组
    int i = 0, j = (m + n + 1) / 2;
    while (true) {
        if (a[i] <= b[j + 1] && a[i + 1] > b[j]) { // 写 >= 也可以
            int max1 = max(a[i], b[j]); // 第一组的最大值
            int min2 = min(a[i + 1], b[j + 1]); // 第二组的最小值
            return (m + n) % 2 ? max1 : (max1 + min2) / 2.0;
        }
        i++; // 继续枚举
        j--;
    }
}
};

复杂度分析

  • 时间复杂度:O(m+n),其中 m 是 a 的长度,n 是 b 的长度。往 a 前面插入一个元素的时间复杂度是 O(m),往 b 前面插入一个元素的时间复杂度是 O(n),加起来是 O(m+n)。
  • 空间复杂度:O(m+n)。

三、优化:二分做法

由于 a 和 b 是有序数组,i 越小,\(a_i ≤b_{j+1}\) 越能成立;i 越大,\(a_i ≤b_{j+1}\)
越不能成立。所以可以二分最大的满足 \(a_i ≤b_{j+1}\) 的 i。二分结束后,我们有 a
\(a_i ≤b_{j+1}\)\(a _{i+1}>b_j\)

最后,讨论二分的上下界。本文用开区间二分,其他二分写法也是可以的。

  • 开区间二分左边界:0。在插入 −∞ 后, \(a_i ≤b_{j+1}\)在 i=0 时一定成立。
  • 开区间二分右边界:m+1。在插入 ∞ 后, \(a_i ≤b_{j+1}\) 在 i=m+1 时一定不成立。

答疑

问:能否二分红色折线图的最小值?

答:这种做法会在有重复元素时失效。试想一下,如果我们在折线图上二分,碰巧遇到了相邻且相同的元素,你要更新 left 还是更新 right 呢?

写法一

注意在数组前面插入元素的时间复杂度是线性的,所以和上面的复杂度分析一样,都是 O(n+m)。

真正满足题目时间复杂度要求的是后面的写法二。

class Solution {
public:
    double findMedianSortedArrays(vector<int>& a, vector<int>& b) {
        if (a.size() > b.size()) {
            swap(a, b); // 保证下面的 i 可以从 0 开始枚举
        }
    int m = a.size(), n = b.size();
    a.insert(a.begin(), INT_MIN);
    b.insert(b.begin(), INT_MIN);
    a.push_back(INT_MAX);
    b.push_back(INT_MAX);

    // 循环不变量:a[left] <= b[j+1]
    // 循环不变量:a[right] > b[j+1]
    int left = 0, right = m + 1;
    while (left + 1 < right) { // 开区间 (left, right) 不为空
        int i = (left + right) / 2;
        int j = (m + n + 1) / 2 - i;
        if (a[i] <= b[j + 1]) {
            left = i; // 缩小二分区间为 (i, right)
        } else {
            right = i; // 缩小二分区间为 (left, i)
        }
    }

    // 此时 left 等于 right-1
    // a[left] <= b[j+1] 且 a[right] > b[j'+1] = b[j],所以答案是 i=left
    int i = left;
    int j = (m + n + 1) / 2 - i;
    int max1 = max(a[i], b[j]);
    int min2 = min(a[i + 1], b[j + 1]);
    return (m + n) % 2 ? max1 : (max1 + min2) / 2.0;
}
};	

写法二

去掉插入的 −∞ 和 ∞,所有下标都减一。

开区间二分的左右边界改成 −1 和 m。i + j = ⌊(m+n+1)/2⌋−2

  • i=−1 时,j 的值为 ⌊(m+n+1)/2⌋−1。

  • i=0 时,j 的值为 ⌊(m+n+1)/2⌋−2。

一般地,j 和 i 的关系为 j = ⌊(m+n+1)/2⌋−2−i = ⌊(m+n-3)/2⌋−i。

答疑

问:当 m=0 时,是否会算出 i=0?

答:不会,m=0 不会进入二分循环,i=left=−1。

class Solution {
public:
    double findMedianSortedArrays(vector<int>& a, vector<int>& b) {
        if (a.size() > b.size()) {
            swap(a, b);
        }	   
		int m = a.size(), n = b.size();
    // 循环不变量:a[left] <= b[j+1]
    // 循环不变量:a[right] > b[j+1]
    int left = -1, right = m;
    while (left + 1 < right) { // 开区间 (left, right) 不为空
        int i = (left + right) / 2;
        int j = (m + n + 1) / 2 - 2 - i;
        if (a[i] <= b[j + 1]) {
            left = i; // 缩小二分区间为 (i, right)
        } else {
            right = i; // 缩小二分区间为 (left, i)
        }
    }

    // 此时 left 等于 right-1
    // a[left] <= b[j+1] 且 a[right] > b[j'+1] = b[j],所以答案是 i=left
    int i = left;
    int j = (m + n + 1) / 2 - 2 - i;
    int ai = i >= 0 ? a[i] : INT_MIN;
    int bj = j >= 0 ? b[j] : INT_MIN;
    int ai1 = i + 1 < m ? a[i + 1] : INT_MAX;
    int bj1 = j + 1 < n ? b[j + 1] : INT_MAX;
    int max1 = max(ai, bj);
    int min2 = min(ai1, bj1);
    return (m + n) % 2 ? max1 : (max1 + min2) / 2.0;
}
};

复杂度分析

  • 时间复杂度:O(log(min(m,n))),其中 m 是 a 的长度,n 是 b 的长度。注:这个复杂度比题目所要求的 O(log(m+n)) 更优。
  • 空间复杂度:O(1)。
posted @ 2025-04-22 21:09  七龙猪  阅读(3)  评论(0)    收藏  举报
-->