4. 寻找两个正序数组的中位数

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

class Solution {
  int KthSmallest(vector<int> &V1, vector<int> &V2, int K) {
    int Size1 = V1.size(), Size2 = V2.size();
    int I = 0, J = 0;
    while (true) {
      // To get Kth smallest means that K is ORDER, not INDEX, so in fact 1 is
      // the identity here. And if we want to use K to INDEX, we need to convert
      // it to 0-indexed, i.e. minus by one.
      if (I == Size1)
        return V2[J + (K - 1)];
      if (J == Size2)
        return V1[I + (K - 1)];
      if (K == 1)
        return std::min(V1[I], V2[J]);

      int Half = K / 2;
      // Comparison happens INSIDE the to-be-eliminated part.
      int NewI = std::min(I + Half, Size1) - 1;
      int NewJ = std::min(J + Half, Size2) - 1;
      if (V1[NewI] < V2[NewJ]) {
        // Eliminate elements at the front. Keep in mind that comparison happens
        // INSIDE, so we need to add one here to convert INDEX to SIZE.
        K -= NewI - I + 1;
        I = NewI + 1;
      } else {
        K -= NewJ - J + 1;
        J = NewJ + 1;
      }
    }
  }
public:
  double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int Total = nums1.size() + nums2.size();
    if (Total % 2 == 0)
      return (KthSmallest(nums1, nums2, Total / 2) +
              KthSmallest(nums1, nums2, Total / 2 + 1)) / 2.;
    else
      return KthSmallest(nums1, nums2, Total / 2 + 1);
  }
};

这个题最关键的点在于两个部分。第一,是把「求中位数」转化为「求第 K 小的数,其中 K 为中位数的 1-indexed 序数」;第二,是在待排序列内部进行的比较策略。这两个策略都涉及到大量基数、1-indexed 序数和 0-indexed 序数的转换。

注意官解对于 I 和 J 的步进是「懒汉式」的,也即最开始大家都是 0,直到 NewI 和 NewJ 被确定之后再开始步进。实际上这道题确实需要记录新旧两种 I,但是记录 OldI 我试过,很混乱,而且上来就要设定 I 和 J 不优雅,所以还是官解这种 NewI 比较好。

注意设置 NewI 的时候那个 std::min() 很关键,没有它的话边界条件就不是严格等于而是大于等于了,说实话我感觉不设这个可能都不太能跑起来。

二刷

class Solution {
  int KthSmallest(vector<int> &A, vector<int> &B, int K) {
    int SizeA = A.size(), SizeB = B.size();
    // i and j are 0-indexed; K is 1-indexed.
    int i = 0, j = 0;
    while (true) {
      // Check for corner cases.
      if (i == SizeA)
        return B[j + (K - 1)];
      if (j == SizeB)
        return A[i + (K - 1)];
      // Check if we have counted to the last number.
      if (K == 1)
        return std::min(A[i], B[j]);

      // Make Next pointers no larger than end().
      int NextI = std::min(SizeA, i + K / 2);
      int NextJ = std::min(SizeB, j + K / 2);
      // Compare INCLUSIVE ranges, and update K correspondingly.
      if (A[NextI - 1] < B[NextJ - 1]) {
        K -= NextI - i;
        i = NextI;
      } else {
        K -= NextJ - j;
        j = NextJ;
      }
    }
  }
public:
  double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int TotalSize = nums1.size() + nums2.size();
    if (TotalSize % 2 == 0)
      return (KthSmallest(nums1, nums2, TotalSize / 2) +
              KthSmallest(nums1, nums2, TotalSize / 2 + 1)) / 2.;
    else
      return KthSmallest(nums1, nums2, TotalSize / 2 + 1);
  }
};

这次是用左闭右开的语义写的,i 和 j 仍然是 0-indexed,但是关于 NextI 和 NextJ 的处理就清爽了很多,「在待排序列内部比较」的策略也明确了,就是那个在下标当中 -1 的操作。

但是说实话,这个 KthSmallest() 的写法还是有点环环相扣的感觉。你可以看到,我们既然把不动点设置在开头、只考察 i 和 j 而非 Next,并且条件写得非常严格(要求严格等于 Size 而不是大于等于),那么就必须要在循环的最后、下一次循环开始之前把 Next 指针的范围控制住,以及相对应地更新 K 的值。

说实话,这样写应该确实是最好的。不动点的判定本就应该尽量简单。这道题可以理解,而且也应该背下来。