求两个不等长、有序数组的中位数非常简洁的O(log(min(M,N)))迭代解详细解释

本文提供了一种寻找两个已排序数组中位数的高效算法,利用二分搜索思想,实现O(log(min(M,N)))的时间复杂度。

这个问题是众所周知的难以实现由于所有的角落情况。大多数实现将奇数和偶数数组视为两种不同的情况,并分别对待它们。事实上,有一点想法扭曲。这两种情况可以组合为一个,导致一个非常简单的解决方案,几乎不需要特殊的处理。

首先,让我们以一种非常规的方式看待“中值”的概念。那是:

“ 如果我们将排序数组切割为两半的EQUAL LENGTHS,则
中位数是Max(lower_half)和Min(upper_half)的AVERAGE,即
紧接着切割两个数字

例如,对于[2 3 5 7],我们在3和5之间切割:

[2 3 / 5 7]

那么中值=(3 + 5)/ 2。请注意,我将使用“/”表示剪切,并使用(数字/数字)来表示通过本文中的数字进行的剪切

对于[2 3 4 5 6],我们使切割通过4像这样:

[2 3(4/4)5 7]

由于我们将4分成了两半,我们现在说下层和上层子阵列包含4。这个概念也导致正确的答案:(4 + 4)/ 2 = 4;

为了方便起见,我们使用L表示紧接着切口的数字,R表示右边对应的数字。例如,在[2 3 5 7]中,我们分别有L = 3和R = 5。

我们观察到L和R的索引与数组N的长度有以下关系:

N        Index of L / R
1               0 / 0
2               0 / 1
3               1 / 1  
4               1 / 2      
5               2 / 2
6               2 / 3
7               3 / 3
8               3 / 4

不难得出L =(N-1)/ 2,R是N / 2的结论。因此,中值可以表示为

(L + R)/2 = (A[(N-1)/2] + A[N/2])/2

为了准备好两个数组的情况,让我们在数字之间添加一些虚数的“positions”(表示为#),并将数字视为“positions”。

[6 9 13 18]  ->   [# 6 # 9 # 13 # 18 #]    (N = 4)
position index     0 1 2 3 4 5  6 7  8     (N_Position = 9)
		  
[6 9 11 13 18]->   [# 6 # 9 # 11 # 13 # 18 #]   (N = 5)
position index      0 1 2 3 4 5  6 7  8 9 10    (N_Position = 11)

可以看出,无论长度N如何,总是有2 * N + 1'个位置。因此,中间切割应该始终在第N个位置(基于0)。由于在这种情况下索引(L)=(N-1)/ 2和索引(R)= N / 2,我们可以推断索引(L)=(CutPosition-1)/ 2,索引)/ 2


现在对于双阵列情况:

A1: [# 1 # 2 # 3 # 4 # 5 #]    (N1 = 5, N1_positions = 11)

A2: [# 1 # 1 # 1 # 1 #]     (N2 = 4, N2_positions = 9)

与单数组问题类似,我们需要找到一个将两个数组分成两半的剪切

“两个左半部分中的任何数字”<=“两个右半
部分中的任何数字”。

我们还可以提出以下意见:

  1. 有2个N1 + 2 N2 + 2位置。因此,在切口的每一侧必须有精确的N1 + N2位置,并且在切口的正上方必须有2个位置。

  2. 因此,当我们在A2中的位置C2 = K处切割时,A1中的切割位置必须为C1 = N1 + N2-k。例如,如果C2 = 2,则我们必须具有C1 = 4 + 5-C2 = 7。

     [# 1 # 2 # 3 # (4/4) # 5 #]    
    
     [# 1 / 1 # 1 # 1 #]   
    
  3. 当切割时,我们有两个L'和两个R。他们是

     L1 = A1[(C1-1)/2]; R1 = A1[C1/2];
     L2 = A2[(C2-1)/2]; R2 = A2[C2/2];
    

在上述示例中,

    L1 = A1[(7-1)/2] = A1[3] = 4; R1 = A1[7/2] = A1[3] = 4;
    L2 = A2[(2-1)/2] = A2[0] = 1; R2 = A1[2/2] = A1[1] = 1;

现在我们如何决定这个切口是否是我们想要的切口?因为L1,L2是左半边上最大的数字,R1,R2是右边最小的数字,我们只需要

L1 <= R1 && L1 <= R2 && L2 <= R1 && L2 <= R2

以确保下半部分中的任何数字<=上半部分中的任意数字。事实上,由于
L1 <= R1和L2 <= R2是自然保证的,因为A1和A2被排序,我们只需要确保:

L1 <= R2和L2 <= R1。

现在我们可以使用简单的二分搜索找出结果。

If we have L1 > R1, it means there are too many large numbers on the left half of A1, then we must move C1 to the left (i.e. move C2 to the right); 
If L2 > R1, then there are too many large numbers on the left half of A2, and we must move C2 to the left.
Otherwise, this cut is the right one. 
After we find the cut, the medium can be computed as (max(L1, L2) + min(R1, R2)) / 2;

两个侧面说明:

A.由于C1和C2可以彼此相互确定,我们可以选择较短的数组(例如A2),并且只移动C2,并相应地计算C1。这样我们可以实现O(log(min(N1,N2))的运行时复杂度

B.唯一的边缘情况是切割落在第0(第一)或第2 N(最后)位置。例如,如果C2 = 2N2,则R2 = A2 [2 * N2 / 2] = A2 [N2],其超过阵列的边界。为了解决这个问题,我们可以想象A1和A2实际上都有两个额外的元素,INT_MAX在A [-1],INT_MAX在A [N]。这些添加不改变结果,但是使实现更容易:如果任何L落在阵列的左边界之外,则L = INT_MIN,并且如果任何R落在右边界外,则R = INT_MAX。


我知道这不是很容易理解,但所有上述推理最终归结为以下简洁的代码:

 double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int N1 = nums1.size();
    int N2 = nums2.size();
    if (N1 < N2) return findMedianSortedArrays(nums2, nums1);	// Make sure A2 is the shorter one.
    
    if (N2 == 0) return ((double)nums1[(N1-1)/2] + (double)nums1[N1/2])/2;  // If A2 is empty
    
    int lo = 0, hi = N2 * 2;
    while (lo <= hi) {
        int mid2 = (lo + hi) / 2;   // Try Cut 2 
        int mid1 = N1 + N2 - mid2;  // Calculate Cut 1 accordingly
        
        double L1 = (mid1 == 0) ? INT_MIN : nums1[(mid1-1)/2];	// Get L1, R1, L2, R2 respectively
        double L2 = (mid2 == 0) ? INT_MIN : nums2[(mid2-1)/2];
        double R1 = (mid1 == N1 * 2) ? INT_MAX : nums1[(mid1)/2];
        double R2 = (mid2 == N2 * 2) ? INT_MAX : nums2[(mid2)/2];
        
        if (L1 > R2) lo = mid2 + 1;		// A1's lower half is too big; need to move C1 left (C2 right)
        else if (L2 > R1) hi = mid2 - 1;	// A2's lower half too big; need to move C2 left.
        else return (max(L1,L2) + min(R1, R2)) / 2;	// Otherwise, that's the right cut.
    }
    return -1;
} 
<think>首先,用户的问题是如何在O(log(m + n))时间复杂度内找到两个有序数组中位数。参考引用提供了线索。 关键点: - 中位数是合并后有序数组的中间值。如果总长度是偶数,则是中间两个数的平均值。 - 时间复杂度要O(log(m + n)),这暗示使用二分搜索。 - 引用[2]提到:二分搜索一个数组的切分位置,另一个数组的切分位置自动确定。为了最小化时间,搜索较短的数组。 - 引用[4]给出条件:i + j = (m + n + 1) / 2,其中i是第一个数组的切分位置,j是第二个数组的切分位置。这确保左右部分长度相等或差1。 - 还需要满足:A[i-1] <= B[j] 和 B[j-1] <= A[i],以确保切分点正确。 步骤: 1. 确保nums1是较短的数组。如果不是,交换它们。 2. 定义m = len(nums1), n = len(nums2)。 3. 计算总长度的一半:half = (m + n + 1) // 2。+1确保在奇数时取中间值。 4. 在nums1上二分搜索,定义左指针left = 0,右指针right = m。 5. while left <= right: - 计算i = (left + right) // 2 (nums1的切分点) - j = half - i (nums2的切分点) 6. 检查边界条件: - 如果i > 0 and j < n and nums1[i-1] > nums2[j]: 说明i太大,需要减小i,所以right = i - 1 - 如果j > 0 and i < m and nums2[j-1] > nums1[i]: 说明i太小,需要增大i,所以left = i + 1 - 否则,切分点正确 7. 找到正确切分点后,计算中位数: - 如果总长度奇数,中位数是maxLeft = max(nums1[i-1], nums2[j-1])(如果i或j为0,需处理边界) - 如果总长度偶数,中位数(maxLeft + minRight) / 2,其中minRight = min(nums1[i], nums2[j]) 边界处理: - 如果i == 0,则左部分最大值是nums2[j-1] - 如果j == 0,则左部分最大值是nums1[i-1] - 如果i == m,则右部分最小值是nums2[j] - 如果j == n,则右部分最小值是nums1[i] 在代码中,需要小心索引越界。 现在,回答用户问题: - 结构清晰:先解释思路,然后给出伪代码或详细步骤,最后讨论相关点。 - 使用中文。 - 添加引用标识:引用[^1]、[^2]等,基于提供的引用。 - 生成相关问题。 用户引用: - [^1]:问题描述和示例 - [^2]:二分搜索思路 - [^3]:PHP实现和优化 - [^4]:条件推导 在回答中,自然引用这些。 最终回答结构: 1. 问题重述 2. 决思路 3. 详细算法步骤 4. 代码示例(伪代码或Python) 5. 时间复杂度分析 6. 相关问题</think>### 如何在 O(log(m + n)) 时间复杂度内找到两个有序数组中位数决这个问题,核心思路是利用二分搜索来定位两个有序数组的“切分点”,确保左半部分元素数量等于或比右半部分多一个(用于处理奇偶长度),同时满足左半部分的最大值小于等于右半部分的最小值。这样,中位数可直接由切分点两侧的元素计算得出。以下是详细步骤: #### 1. **问题分析** - 给定两个有序数组 `nums1`(长度 `m`)和 `nums2`(长度 `n`),中位数是合并后有序数组的中间值: - 若总长度 `m + n` 为奇数,中位数是第 `(m + n + 1) / 2` 个元素。 - 若为偶数,中位数是第 `(m + n) / 2` 和 `(m + n) / 2 + 1` 个元素的平均值。 - 时间复杂度要 O(log(m + n)),需使用二分搜索。引用[2]指出:**应二分搜索较短的数组以最小化时间复杂度**,因为搜索范围由较短数组决定[^2]。 #### 2. **算法步骤** 假设 `nums1` 是较短的数组(若非,则交换 `nums1` 和 `nums2`)。定义: - `m = len(nums1)`, `n = len(nums2)` - `half = (m + n + 1) // 2`(确保左半部分元素数量 ≥ 右半部分) - 在 `nums1` 上二分搜索切分点 `i`(`0 ≤ i ≤ m`),则 `nums2` 的切分点 `j = half - i`。 步骤如下: 1. **初始化**:`left = 0`, `right = m`(二分搜索边界)。 2. **二分搜索**: - 计算 `i = (left + right) // 2`, `j = half - i`。 - 检查切分点是否满足条件: - 若 `i > 0 and j < n and nums1[i-1] > nums2[j]`:说明 `i` 过大,需减小(`right = i - 1`)。 - 若 `j > 0 and i < m and nums2[j-1] > nums1[i]`:说明 `i` 过小,需增大(`left = i + 1`)。 - 否则,切分点正确(满足 `nums1[i-1] ≤ nums2[j]` 且 `nums2[j-1] ≤ nums1[i]`)。 3. **计算中位数**: - **左半部分最大值 `maxLeft`**: - 若 `i == 0`:`maxLeft = nums2[j-1]` - 若 `j == 0`:`maxLeft = nums1[i-1]` - 否则:`maxLeft = max(nums1[i-1], nums2[j-1])` - **右半部分最小值 `minRight`**: - 若 `i == m`:`minRight = nums2[j]` - 若 `j == n`:`minRight = nums1[i]` - 否则:`minRight = min(nums1[i], nums2[j])` - **最终结果**: - 若 `(m + n) % 2 == 1`:中位数 = `maxLeft` - 否则:中位数 = `(maxLeft + minRight) / 2.0` #### 3. **代码实现(Python)** ```python def findMedianSortedArrays(nums1, nums2): # 确保 nums1 是较短的数组 if len(nums1) > len(nums2): nums1, nums2 = nums2, nums1 m, n = len(nums1), len(nums2) half = (m + n + 1) // 2 left, right = 0, m while left <= right: i = (left + right) // 2 # nums1 的切分点 j = half - i # nums2 的切分点 # 检查条件并调整二分边界 if i > 0 and j < n and nums1[i-1] > nums2[j]: right = i - 1 elif j > 0 and i < m and nums2[j-1] > nums1[i]: left = i + 1 else: # 处理边界计算 maxLeft if i == 0: maxLeft = nums2[j-1] elif j == 0: maxLeft = nums1[i-1] else: maxLeft = max(nums1[i-1], nums2[j-1]) # 若总长度为奇数,直接返回 if (m + n) % 2 == 1: return maxLeft # 处理边界计算 minRight if i == m: minRight = nums2[j] elif j == n: minRight = nums1[i] else: minRight = min(nums1[i], nums2[j]) return (maxLeft + minRight) / 2.0 return 0.0 # 默认返回值(实际不会执行) ``` #### 4. **时间复杂度分析** - **二分搜索过程**:每次迭代将搜索范围减半,范围为 `min(m, n)`,因此时间复杂度为 $O(\log(\min(m, n)))$,满足 $O(\log(m + n))$ 要。 - **边界条件检查**:每次迭代为 $O(1)$ 操作。 - 引用[4]强调:**通过统一使用 `half = (m + n + 1) // 2` 处理奇偶性,简化了逻辑**[^4]。 #### 5. **示例验证** - 示例1:`nums1 = [1, 3]`, `nums2 = [2]`(总长度3,奇数) - 切分点 `i=1`(`nums1` 左: `[1]`, 右: `[3]`),`j=1`(`nums2` 左: `[2]`, 右: `[]`) - `maxLeft = max(1, 2) = 2`,中位数 = 2.0 - 示例2:`nums1 = [1, 2]`, `nums2 = [3, 4]`(总长度4,偶数) - 切分点 `i=1`(`nums1` 左: `[1]`, 右: `[2]`),`j=2`(`nums2` 左: `[3]`, 右: `[4]`) - `maxLeft = max(1, 3) = 3`,`minRight = min(2, 4) = 2`,中位数 = $(3 + 2) / 2 = 2.5$ 此方法高效且符合时间复杂度要,更多细节可参考 LeetCode 原题描述[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值