LeetCode系列:4 Median of Two Sorted Arrays

版权声明:书写博客,不为传道受业,只为更好的梳理更好的记忆,欢迎转载与分享,更多博客请访问:http://blog.csdn.net/myinclude 和 http://www.jianshu.com/u/b20be2dcb0c3 https://blog.csdn.net/myinclude/article/details/84774513

Q:There are two sorted arrays nums1 and nums2 of size m and n respectively. Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

You may assume nums1 and nums2 cannot be both empty.

Example 1:

nums1 = [1, 3]
nums2 = [2]

The median is 2.0
Example 2:

nums1 = [1, 2]
nums2 = [3, 4]

The median is (2 + 3)/2 = 2.5

E:这里有一个长度为m的有序数组nums1和长度为n的有序数组nums2。找到这两个有序数组的中位数,并且让时间复杂度为O(log(m+n))。

你可以假设两个数组不可能同时为空。

e.g.
Example 1:

nums1 = [1, 3]
nums2 = [2]

The median is 2.0
Example 2:

nums1 = [1, 2]
nums2 = [3, 4]

The median is (2 + 3)/2 = 2.5

A:

Approach 1:

先不考虑时间复杂度,既然两个数组都是有序数组,那我们不就可以把两个数组合起来,然后取中位数吗,两个有序数组合并,其实就是归并排序的一个部分。

class Solution {
    public func findMedianSortedArrays(_ nums1 : [Int], _ nums2 : [Int]) -> Double {
        
        let m = nums1.count
        let n = nums2.count
        
        var nums = [Int]()
        
        if m == 0 {	//num1为空数组
            if n % 2 == 0 {	//num元素为偶数个
                return Double(nums2[n/2 - 1] + nums2[n/2]) / 2.0
            } else {
                return Double(nums2[n/2])
            }
        }
        if n == 0 {	//num2为空数组
            if m % 2 == 0 {
                return Double(nums1[m/2 - 1] + nums1[m/2]) / 2.0
            } else {
                return Double(nums1[m/2])
            }
        }
        var count : Int = 0
        var i : Int = 0
        var j : Int = 0
        while count < (m + n) {	
            if i == m {	//num1遍历完毕
                while j != n { 	//num2未遍历完毕
                    nums.append(nums2[j])
                    count += 1
                    j += 1
                }
                break
            }
            
            if j == n { //num2遍历完毕
                while i != m {	//num1未遍历完毕
                    nums.append(nums1[i])
                    count += 1
                    i += 1
                }
                break
            }
            
            if nums1[i] < nums2[j] {
                nums.append(nums1[i])
                count += 1
                i += 1
            } else {
                nums.append(nums2[j])
                count += 1
                j += 1
            }
        }
        
        if count % 2 == 0 {
            return Double(nums[count/2 - 1] + nums[count/2]) / 2.0
        } else {
            return Double(nums[count/2])
        }
    }
}
Complexity Analysis:
  • 时间复杂度: O(m+n)。两个数组每个元素都要遍历一遍(m+n)
  • 空间复杂度:O(m+n)。临时数组大小为(m+n)

Approach 2:

方法二是基于方法一的改进,从时间和空间上分别考虑并改进。

时间上,我们要找的是中位数,根据中位数的定义,我们没有必要将nums1和nums2两个数组整个遍历一遍,只要遍历两个数组长度总和一般的位置就可以结束,遍历次数可以少一半,但是时间复杂度还是和方法一一样。

空间上,我们需要找中位数,那对于两个数组总和为奇数时,我们需要获取(m+n+1)/ 2的那个元素,对于两个数组为偶数时,我们需要获取最后一次遍历的那个元素(m+n)/ 2 +1和他前面那个元素(m+n)/ 2。所以看看算法空间复杂度能不能保持在常量级。

首先明确一下遍历次数,假设m+n = len,对于两个数组和为奇数的时候,我们需要获取序号为(len+1)/2的元素,我们需要遍历len / 2+1次(Int计算,会抹去小数部分);对于两个数组和为偶数的时候,我们需要获取len / 2的元素和len / 2 +1的两个元素,所以遍历次数也是len / 2 + 1次。所以不管是奇数还是偶数,我们的遍历次数都是(m+n)/ 2 + 1次。

算法实现:
既然我们不需要保存两个数组的所有元素,而是最多保存两个元素,那我们用两个变量currentNum和preNum来分别表示当前位置的数字和前一个数字,每次循环,将最新的结果赋值给currentNum,将currentNum赋值给preNum。
循环遍历思路还是和方法一一样,但是遍历次数变成len/2 + 1,不用完整遍历。用aIndex和bIndex分别表示num1和num2遍历的当前位置,进行比较,将最新的结果赋值给currentNum,将currentNum赋值给preNum。

考虑所有可能的情况包括某个数组已经没有元素:
若num1已经没有元素了,那可以继续遍历num2的元素,更新currentNum和preNum;即aIndex≥mbIndex++;
当num1还有元素,num2没有元素,继续遍历num1的元素,更新currentNum和preNum;即bIndex≥naIndex++;
当num1还有元素,num2也还有元素,则需要做num1[aIndex]num2[bIndex]的比较。即aIndex<mbIndex<n比较num1[aIndex]num2[bIndex]的大小。

所以总结遍历条件就是:

aIndex<bIndex && (bIndex>=n || num1[aIndex]<num2[bIndex])

完整代码:

class Solution {
    func findMedianSortedArrays(_ nums1: [Int], _ nums2: [Int]) -> Double {
    
        var currentNum = 0, preNum = 0, aIndex = 0, bIndex = 0
        let m = nums1.count, n = nums2.count, len = m + n
        for _ in 0...len/2 {
            preNum = currentNum
            if aIndex < m && (bIndex >= n || nums1[aIndex] < nums2[bIndex]) {
                currentNum = nums1[aIndex]
                aIndex += 1
            }  else {
                currentNum = nums2[bIndex]
                bIndex += 1
            }
        }
        
        if len & 1 == 0 {
            return Double(preNum + currentNum) / 2.0
        } else {
            return  Double(currentNum)
        }
    }
}
Complexity Analysis:
  • 时间复杂度: O(m+n)。两个数组总元素个数的一半(m+n)/ 2
  • 空间复杂度:O(1)。共申请了7个临时变量,所以是常量级

Approach 3:

以上两个方式都不能达到时间复杂度为O(log(m+n)),时间复杂度都是O(m+n),要让时间复杂度为O(log(m+n)),那我们可以想到二分法,前面两个方法循环方式是一个一个遍历,一个一个排除,那我们可不可以批量排除,跳跃遍历呢,当然是可以的。假设我们现在要找第k小的数字,我们可以每次循环排除掉k / 2个数。

假设我们现在要找第7小的数字,num1和num2如下图:
k = 7
我们比较两个数组的第k / 2个数字,如果k是奇数,向下取整,也就是比较第三个数字,上边数组中的4和下边数组中的3,如果哪个数字小,就表明该数组的前k / 2个数字都不可能是第k小的数字,可以排除掉,也就是说num2的[1,2,3]三个元素都不能是第7小的数字,我们可以把他们排除掉。将[1,3,4,9]和[4,5,6,7,8,9,10]组成两个新的数组,重新进行比较,如下图,其中橙色为已经去掉的数字。

我们现在可以总结一下:
数组A[1],A[2],A[3],A[4]…A[k / 2]…A[m],其中2k不一定等于m
数组B[1],B[2],B[3],B[4]…B[k / 2]…B[n],其中2k不一定等于n
假设A[k / 2] < B[k / 2],那么我们可以得出:
A[1],A[2],A[3],A[4]…A[k / 2]都不可能是第k小的数。

而A数组中,比A[k / 2]小的数有(k / 2 - 1)个,B数组中,比B[k / 2]小的数有(k / 2 - 1)个,而A[k / 2] < B[k / 2],那么比B[k / 2]小的数有(k / 2 - 1 + k / 2 - 1 + 1)= (k - 1)个,那么B[k / 2]有可能是第k小的数,但是也有可能A数组中还有比B[k / 2]小的数。
k = 4
上图中橙色为第一次比较就排除了的数字。现在进行第二轮比较,一开始k = 7,而我们已经排除了3个数字,所以这次 k = 4, k / 2 = 2,进行第二轮比较,因为3小于5,所以我们可以剔除num1中的[1,3]两个元素,得到结果如下图:
k = 2
第一次我们排除了3个数字,第二次我们排除了2个数字,所以我们剩下需要找到最小的7 - 3 - 2 = 2个数字,即k = 2。现在当前查找的数字都为4,我们可以假设4≥4 也可以假设4 ≤ 4,因为我们总要排除一个4而保留一个4,所以对结果没有影响,下图是假设4 ≥ 4的结果,排除掉num2中的[4],结果如下图:
k = 1
此时我们已经排除了3 + 2 + 1 = 6个数字,所以剩下7 - 3 - 2 - 1 = 1,即k = 1。那我们只需要获取两个数组剩下元素中的第一次数字再进行一次比较,就可以获取到我们所要的结果。因为4<5,所以第7小的数字为4。


我们每次都是去k / 2的数进行比较,有时候可能会遇到数组长度小于k / 2的时候,如下图:
k = 7
由于k / 2 = 3大于num1的长度2,所以我们将箭头指向num1的末尾进行比较,如果num1[m - 1]小于num2[k / 2],那么直接排除掉num1数组,剩下就不用比较了,直接取num2[k - m ]即为第k小的数字,如下图:
k = 5
在第一次比较中k = 7 ,k / 2 = 3,且num[1] = 2 < num[2] = 3,所以直接排除掉整个num1数组[1,2],剩下k = k - m = 7 - 2 = 5,即取num2[5-1]则为第k小的数字。

在上面的示例中,不论k为奇数还是偶数,对于算法都没有影响,而且,在找k的过程中,k的值都有可能奇数变偶数,偶数变奇数,最终当k=1或者某个数组为空时,停止遍历。

这里采用递归的思路,为了防止数组长度小于k / 2,所以每次比较min(k / 2 , len(数组)),把长度小于k / 2的那个数组排除掉。其他情况,将两数组进行递归并更新k的值直到k=1或者数组长度小于k/2。

class Solution {

    
    func findMedianSortedArrays(_ nums1: [Int], _ nums2: [Int]) -> Double {
        let m = nums1.count, n = nums2.count
        //若m+n为偶数,则计算两个k值,(m+n)/ 2以及(m+n)/ 2 +1,若m+n为奇数,则计算一个k值(m+n+1)/ 2,为统一计算,不管是奇数还是偶数,我们都计算两个k,为奇数时,计算的是相同的两个k,偶数时,计算(m+n)/ 2和(m+n)/ 2 +1,而Int计算,(m+n)/ 2 == (m+n+1) / 2,所以综合起来有下面的结果。
        return (findKth(nums1,nums2, (m+n+1)/2) + findKth(nums1, nums2, (m+n+2)/2)) * 0.5
        
    }
    
    private func findKth(_ nums1 : [Int], _ nums2 : [Int], _ k : Int) -> Double {
        let m = nums1.count, n = nums2.count
        
        guard m <= n else {
            return findKth(nums2, nums1, k)
        }
        
        guard m != 0 else {
            return Double(nums2[k - 1])
        }
        guard k != 1 else {
            return Double(min(nums1[0], nums2[0]))
        }
        
        let i = min(k / 2, m)
        let j = min(k / 2, m)
        
        if nums1[i - 1] < nums2[j - 1] {
            return findKth(Array(nums1[i..<m]),nums2, k - i)
        } else {
            return findKth(nums1, Array(nums2[j..<n]), k - j)
        }
        
    }
    
}
Complexity Analysis:
  • 时间复杂度: O(log(m+n))。每进行一次循环,我们就减少 k / 2 个元素,所以时间复杂度是 O(log(k)),而k = (m + n)/ 2 ,所以最终的复杂也就是 O(log(m + n))。
  • 空间复杂度:O(1)。虽然我们用到了递归,但是可以看到这个递归属于尾递归,所以编译器不需要不停地堆栈,所以空间复杂度为 O(1)。

Approach 4:

首先看看中位数的定义:
中位数(又称中值,英语:Median),统计学中的专有名词,代表一个样本、种群或概率分布中的一个数值,其可将数值集合划分为相等的上下两部分。
对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均数作为中位数。 (from: 百度百科)

我们将数组进行划分,假设数组长度为m,如下图,我们有0到m共m+1个位置可以进行切割。
在这里插入图片描述

现在我们分别使用i和j对A,B两个数组进行分割,将A,B两个数组左边的分割结果组合成左半部分(绿色),将右边分割结果组合成右半部分(黄色)。

当A数组和B数组的总长度为偶数时,我们能够得到下图所示的两部分。

若我们能够保证:

  • 左半部分长度等于右半部分长度:
    i + j = m - i + n - j
    即:
    j = (m + n) / 2 - i
  • 左半部分最大值小于等于右半部分最小值:
    max( A [ i - 1 ] , B [ j - 1 ] ) ≤ min ( A [ i ] , B [ j ] )

那么,中位数就可以表示为如下:
(左半部分最大值+右半部分最小值)* 0.5
即:
(max ( A [ i - 1 ] , B [ j - 1 ] ) + min ( A [ i ],B [ j ] ) ) * 0.5
偶数
当A数组和B数组的总长度为奇数时(假设分割使B数组左边多出一个元素),我们能够得到下图所示的两部分。

若我们能够保证:

  • 左半部分长度比右半部分长度大1:
    i + j = m - i + n - j + 1
    即:
    j = (m + n + 1) / 2 - i
  • 左半部分最大值小于等于右半部分最小值:
    max( A [ i - 1 ] , B [ j - 1 ] ) ≤ min ( A [ i ] , B [ j ] )

那么,中位数就是左半部分最大的值,
即:
max ( A [ i - 1 ] , B [ j - 1 ] )
奇数


对于第一个条件:
我们对总长度为偶数和奇数进行了分类讨论,其实因为Int运算的特性,我们可以将条件合并起来,得出:
j = ( m + n + 1 ) / 2 - i

  • m+n为偶数时,m + n能把2整除,Int(1/2) = 0
  • m+n为奇数时,m + n + 1能把2整除

所以对于偶数的情况,将j = ( m + n ) / 2 - i变成j = ( m + n + 1 ) / 2 - i对整个结果是没有影响的。
由于0 ≤ i ≤ m,为保证0 ≤ j ≤ n,我们需要保证m ≤ n

  • m ≤ n ,i ≤ m,j = (m + n + 1) / 2 - i ≥ (m + m + 1) / 2 - m = 0
  • m ≤ n ,i ≥ 0 , j = (m + n + 1) / 2 - i ≤ (n + n + 1) / 2 - i ≤ (n + n + 1) / 2 = n

对于第二个条件:
奇数和偶数的情况是一样的,为了保证max( A [ i - 1 ] , B [ j - 1 ] ) ≤ min ( A [ i ] , B [ j ] ),而A[ i - 1 ] < A [ i ]B[ j - 1 ] < B [ j ],所以只需要保证B [ j - 1 ] ≤ A[ i ]A [ i - 1 ] ≤ B[ j ]即可。

现在我们来看,如何保证:B [ j - 1 ] ≤ A[ i ]A [ i - 1 ] ≤ B[ j ]

  • 如果B [ j - 1 ] ≤ A [ i ]A [ i - 1 ] ≤ B[ j ],那么我们可以直接得到结果;
  • 如果B [ j - 1 ] > A [ i ],意味着A [ i ]过小,因为有序,我们只需要将i增加,而i的增减和j的增减是相反的,i变大,j就会变小,而i变大,A[ i ]也会跟着变大,j减小,B[ j ]也会减小;
  • 如果A [ i - 1 ] > B [ i ],意味着A [ i ]过大,因为有序,我们只需要将i减小,i减小,j就会变大;
    重复2,3步直到满足条件。

现在,让我们来考虑一下边界问题,即当 i = 0 ,i = m ,j = 0, j = n的情况:

  • 当i = 0 或者 j = 0 即切在了最前面,如下图:
    在这里插入图片描述
    这时当 i = 0,左半部分最大值即为B [ j - 1] , 当j = 0 时,左半部分最大值是A [ i - 1];右半部分最小值不变。
  • 当i = m 或者 j = n时,即切在了最后面,如下图:
    在这里插入图片描述
    这时当 i = m,右半部分最小值即为B [ j ] , 当j = n 时,右半部分最小值是A [ i];左半部分最大值不变。

所以总结起来就是:

  • (j = 0 or i = m or B[ j − 1 ] ≤ A[ i ]) 或是
    (i = 0 or j = n or A[ i − 1 ] ≤ B [ j ] )
    这意味着 i 是完美的,我们可以停止搜索。
  • j > 0 and i < m and B[ j − 1 ] > A [ i ]
    这意味着 i 太小,我们必须增大它。
  • i > 0 and j < n and A[ i − 1 ] > B [ j ]
    这意味着 i 太大,我们必须减小它。
class Solution {

    func findMedianSortedArrays(_ nums1: [Int], _ nums2: [Int]) -> Double {
        let m = nums1.count, n = nums2.count
        if m > n {
            return findMedianSortedArrays(nums2, nums1)
        }
        
        var halfLength : Int = (m + n + 1) >> 1
        var begin = 0, end = m
        var maxLeft = 0, minRight = 0
        while begin <= end {
            let mid1 = (begin + end) >> 1
            let mid2 = halfLength - mid1
            if mid1 > 0 && mid2 < n && nums1[mid1 - 1] > nums2[mid2] {
                end = mid1 - 1
            } else if mid2 > 0 && mid1 < m && nums1[mid1] < nums2[mid2 - 1] {
                begin = mid1 + 1
            } else {
                if mid1 == 0 {
                    maxLeft = nums2 [mid2 - 1]
                } else if mid2 == 0 {
                    maxLeft = nums1[mid1 - 1]
                } else {
                    maxLeft = max(nums1[mid1 - 1], nums2[mid2 - 1])
                }
                
                if (m + n) % 2 == 1 {
                    return Double(maxLeft)
                }
                
                if mid1 == m {
                    minRight = nums2[mid2]
                } else if mid2 == n {
                    minRight = nums1[mid1]
                } else {
                    minRight = min(nums1[mid1], nums2[mid2])
                }
                break
            }
        }
        return Double(maxLeft + minRight) * 0.5
    }
}
Complexity Analysis:
  • 时间复杂度: O(log(min(m,n)) )。我们对较短的数组进行了二分查找,所以时间复杂度是 O(log(min(m,n)))。
  • 空间复杂度:O(1)。只使用了固定变量,与数组长度无关,所以空间复杂度是O(1)。

猜你喜欢

转载自blog.csdn.net/myinclude/article/details/84774513