力扣第4题-Swift题解:寻找两个正序数组的中位数

·  阅读 302

双数组二分查找

题目描述

来源难度时间复杂度
力扣困难O( log(n) )

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

示例 1:

输入: nums1 = [1,3], nums2 = [2]
输出: 2.00000
解释: 合并数组 = [1,2,3] ,中位数 2

示例2:

输入: nums1 = [1,2], nums2 = [3,4]
输出: 2.50000
解释: 合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

示例3:

输入: nums1 = [0,0], nums2 = [0,0]
输出: 0.00000

示例 4:

输入: nums1 = [], nums2 = [1]
输出: 1.00000

示例5:

输入: nums1 = [2], nums2 = []
输出: 2.00000

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/me…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析

该问题如果不对时间复杂度和空间复杂度提出要求的话,通过最直接的方法可以通过。 考虑到问题规模,m + n <= 2000,我们设 N = m + n ,这个数据规模非常小。所以即使是 O(n*log(n)) 时间复杂度也可以顺利通过。

该问题按照时间复杂度,空间复杂度的优化程度分别有以下几种解法。

解决方法时间复杂度空间复杂度
直接合并排序 + 索引查找O(N*log(N))O(N)
归并合并排序 + 索引查找O(N)O(N)
双数组二分查找 方法 1O(log(N))O(1)
双数组二分查找 方法 2O(log(N))O(1)

解法 1: 直接合并排序 + 查找

这是最简单的思路。步骤非常的明确。

  1. 将数组nums1nums2合并为新的数组nums3
  2. 用系统函数为nums3进行排序。
  3. 寻找nums3中位数。

nums1nums2合并的时间复杂度为O(N),排序复杂度为O(N*log(N)),由于需要另外开辟一个空间进行合并和排序,所以空间复杂度为O(N)。 最终在已经排好序的nums3上查找中位数,只要简单的进行中位索引取值即可,这部分复杂度为O(1)。

综上,该算法的总时间复杂度为O(N*log(N)),空间复杂度为O(N)

解法2:归并合并排序 + 查找

解法2和解法1思路一样,也是合并nums1nums2形成新的排序数组nums3,区别在于合并的过程。

由于已知nums1nums2是各自已排序的数组,通过经典排序算法 归并排序 中的合并步骤就可以它们合并成新的排序数组。而该合并步骤的时间复杂度是O(N)

剩余步骤和解法1相同。

综上,该算法的总时间复杂度为O(N),空间复杂度为O(N)

解法 3:双数组二分查找 方法 1

已知数组nums1nums2都是已排序数组,自然不难考虑到可否在不合并的情况下,直接通过二分查找定位到合并后数组的中位数。

虽然算法不会合并nums1nums2,但是依然会假设合并后经过排序的数组为nums3

0作为数组下标的初始下标,那么数组nums1nums1[0..<m]组成,数组nums2nums2[0..<n]组成,其中mnnums1nums2的长度。

  • m + n为偶数时,中位数为nums3[(m+n)/2-1]nums3[(m+n)/2]的平均值。
  • m + n为奇数时,中位数为nums3[(m+n-1)/2]

因为不能直接合并nums3(这样会导致算法的复杂度变成O(N)),为了在不合并的情况下获取中位数,需要直接从nums1[0..<i]nums2[0..<j]查找。

可以计算出,对于nums3,中位数必然存在于nums3[0..<(m+n)/2+1]内,其中(m+n)/2取的是下整。

因为nums1nums2各自都是已排序的数组,可以证明nums3[0..<(m+n)/2+1]是由nums1[0..<i]nums2[0..<j]组成,其中i + j = (m+n)/2+1m >= i >= 0m >= i >= 0

那么,寻找中位数的问题就被转换为寻找ij的问题,且如果i已知,则j也已知,因为可以通过(m+n)/2+1-i获得。

那么接下去的问题就是,如何知道nums1[0..<i]nums2[0..<j]组成的数组正是nums3[0..<(m+n)/2+1]

分三种情况分析:

  1. m=0时,nums1为空数组,不难得出当i=0j=(m+n)/2+1时,nums1[0..<i]nums2[0..<j]就是我们要找到子数组。
  2. n=0时,同样可以得出当i=(m+n)/2+1j=0时,nums1[0..<i]nums2[0..<j]就是我们要找到子数组。
  3. m>0n>0时,当且仅当nums1[i-1]<=nums2[j-1]<=nums[i],或者nums1[j-1]<=nums2[i-1]<=nums[j]时,nums1[0..<i]nums2[0..<j]才是我们要找的子数组。

为了找到nums1[0..<i]nums2[0..<j],我们可以先任意指定一个i,因此j也得到了。然后我们通过以上分析法分析子数组是否找到,如果没找到,我们通过二分法移动i或者j,直到找到为止。

通过二分法移动i或者j时,移动的策略是:

  1. nums1[i]<nums2[j-1]时,说明nums1还可以向右移,则需要右移i,左移j
  2. nums2[j]<nums2[i-1]时,说明nums2还可以向右移,则需要右移j,左移i
  3. 移动的“步数”取决于nums1nums2移动范围,我们每次选择移动范围更小的那个进行二分,不然移动就会溢出,当确定了移动步数后,另一个数组只要朝相反的方向移动同样步数即可,这样i+j的结果始终恒定。

并且需要考虑如下边界情况:

  1. i需要左移但是i=0时,说明nums1的数据全部小于nums2,直接返回nums2的对应子数组。
  2. j需要左移但是j=0时,说明nums2的数据全部小于nums1,直接返回nums1的对应子数组。

nums1[0..<i]nums2[0..<j]顺利被找到时,接下去是解决如何得出中位数。 可以分如下情况:

  1. m+n结果为奇数时,中位数等于nums1[i-1]nums2[j-1]两个数(如果其中任何数因为边界问题不存在则忽略)中最大的值。
  2. m+n结果为偶数时,中位数等于nums1[i-1]nums1[i-2]nums2[j-1]nums2[j-2]四个数(如果因为边界问题不存在则忽略)中最大的两个值,取两个值的平均值。

基础思路说完,接下去就是上代码部分。

为了让算法思路更清晰,对代码进行适度的封装。首先因为涉及到“定制的”二分算法,所以封装一个结构体用来描述搜索范围。

struct RANGE {
    var begin = 0
    var end = 0
    var center = 0
    mutating func set(_ b: Int, _ e: Int, _ c: Int) {
        begin = b
        end = e
        center = c
    }
}
复制代码

定义一个cmp函数,用来描述需要左移、需要右移还是已经找到子数组的情况。

func cmp(_ a: Int, _ b: Int) -> Int {
    guard a != -1 && b != -1 else { return 0 }
    if nums1[a] >= nums2[b] {
        return b + 1 >= nums2.count || nums2[b+1] >= nums1[a] ? 0 : 1
    }
    return a + 1 >= nums1.count || nums1[a+1] >= nums2[b] ? 0 : -1
}
复制代码

定义一个mov函数,处理子数组移动的情况,当一个子数组范围往一边移动时,必然会让另一个子数组往相反方向移动。

func mov(_ r1: inout RANGE, _ r2: inout RANGE, _ mv: Int) {
    r1.end = r1.center
    r1.center -= mv
    r2.begin = r2.center + 1
    r2.center += mv
}
复制代码

接下去就是最重要的搜索部分。搜索search()函数将返回一个范围值,返回的结果就是nums1nums2各自的子数组结果。和之前描述的略有差异的是,返回的结果下标为子数组最后一个数字的下标。如果其中一个子数组不存在,则下标返回-1

func search() -> (Int, Int) {
   let n = (nums1.count + nums2.count) / 2 + 1
   if nums1.isEmpty {
       return (-1, n - 1)
   } else if nums2.isEmpty {
       return (n-1, -1)
   }
   var range1 = RANGE()
   var range2 = RANGE()
   if nums1.count < nums2.count {
       range1.set(0, nums1.count, nums1.count / 2)
       range2.set(0, nums2.count, n - 2 - range1.center)
   } else {
       range2.set(0, nums2.count, nums2.count / 2)
       range1.set(0, nums1.count, n - 2 - range2.center)
   }
   var t = cmp(range1.center, range2.center)
   while t != 0 {
       if t > 0 {
           var mv = 0
           if range1.center - range1.begin < range2.end - 1 - range2.center {
               mv = (range1.center - range1.begin + 1) / 2
           } else {
               mv = (range2.end - range2.center) / 2
           }
           guard mv != 0 else { return (-1, n-1) }
           mov(&range1, &range2, mv)
       } else {
           var mv = 0
           if range1.end - 1 - range1.center < range2.center - range2.begin {
               mv = (range1.end - range1.center) / 2
           } else {
               mv = (range2.center - range2.begin + 1) / 2
           }
           if mv == 0 {
               return (n-1, -1)
           }
           mov(&range2, &range1, mv)
       }
       t = cmp(range1.center, range2.center)
   }
   return (range1.center, range2.center)
}
复制代码

最后,函数answer用来计算已知返回了两个子数组的情况下,计算中位数的结果。

func answer(_ s: (Int, Int)) -> Double {
      var m1 = Int(Int32.min)
      var m2 = Int(Int32.min)
      if (nums1.count + nums2.count) % 2 == 0 {
          if s.0 != -1 {
              m1 = nums1[s.0]
              if s.0 > 0 { m2 = nums1[s.0-1] }
              if m2 > m1 { swap(&m1, &m2) }
          }
          if s.1 != -1 {
              if nums2[s.1] > m2 { m2 = nums2[s.1] }
              if m2 > m1 { swap(&m1, &m2) }
              if s.1 > 0 && nums2[s.1-1] > m2 { m2 = nums2[s.1-1] }
              if m2 > m1 { swap(&m1, &m2) }
          }
      } else {
          if s.0 != -1 { m1 = nums1[s.0] }
          if s.1 != -1 { m1 = max(m1, nums2[s.1]) }
          m2 = m1
      }
      return (Double(m1) + Double(m2)) / 2.0
  }
  let s = search()
  return answer(s)
}
复制代码

整体代码如下:

class Solution {
    func findMedianSortedArrays(_ nums1: [Int], _ nums2: [Int]) -> Double {
        struct RANGE {
            var begin = 0
            var end = 0
            var center = 0
            mutating func set(_ b: Int, _ e: Int, _ c: Int) {
                begin = b
                end = e
                center = c
            }
        }
        func cmp(_ a: Int, _ b: Int) -> Int {
            guard a != -1 && b != -1 else { return 0 }
            if nums1[a] >= nums2[b] {
                return b + 1 >= nums2.count || nums2[b+1] >= nums1[a] ? 0 : 1
            }
            return a + 1 >= nums1.count || nums1[a+1] >= nums2[b] ? 0 : -1
        }
        func mov(_ r1: inout RANGE, _ r2: inout RANGE, _ mv: Int) {
            r1.end = r1.center
            r1.center -= mv
            r2.begin = r2.center + 1
            r2.center += mv
        }
        func search() -> (Int, Int) {
            let n = (nums1.count + nums2.count) / 2 + 1
            if nums1.isEmpty {
                return (-1, n - 1)
            } else if nums2.isEmpty {
                return (n-1, -1)
            }
            var range1 = RANGE()
            var range2 = RANGE()
            if nums1.count < nums2.count {
                range1.set(0, nums1.count, nums1.count / 2)
                range2.set(0, nums2.count, n - 2 - range1.center)
            } else {
                range2.set(0, nums2.count, nums2.count / 2)
                range1.set(0, nums1.count, n - 2 - range2.center)
            }
            var t = cmp(range1.center, range2.center)
            while t != 0 {
                if t > 0 {
                    var mv = 0
                    if range1.center - range1.begin < range2.end - 1 - range2.center {
                        mv = (range1.center - range1.begin + 1) / 2
                    } else {
                        mv = (range2.end - range2.center) / 2
                    }
                    guard mv != 0 else { return (-1, n-1) }
                    mov(&range1, &range2, mv)
                } else {
                    var mv = 0
                    if range1.end - 1 - range1.center < range2.center - range2.begin {
                        mv = (range1.end - range1.center) / 2
                    } else {
                        mv = (range2.center - range2.begin + 1) / 2
                    }
                    if mv == 0 {
                        return (n-1, -1)
                    }
                    mov(&range2, &range1, mv)
                }
                t = cmp(range1.center, range2.center)
            }
            return (range1.center, range2.center)
        }
        func answer(_ s: (Int, Int)) -> Double {
            var m1 = Int(Int32.min)
            var m2 = Int(Int32.min)
            if (nums1.count + nums2.count) % 2 == 0 {
                if s.0 != -1 {
                    m1 = nums1[s.0]
                    if s.0 > 0 { m2 = nums1[s.0-1] }
                    if m2 > m1 { swap(&m1, &m2) }
                }
                if s.1 != -1 {
                    if nums2[s.1] > m2 { m2 = nums2[s.1] }
                    if m2 > m1 { swap(&m1, &m2) }
                    if s.1 > 0 && nums2[s.1-1] > m2 { m2 = nums2[s.1-1] }
                    if m2 > m1 { swap(&m1, &m2) }
                }
            } else {
                if s.0 != -1 { m1 = nums1[s.0] }
                if s.1 != -1 { m1 = max(m1, nums2[s.1]) }
                m2 = m1
            }
            return (Double(m1) + Double(m2)) / 2.0
        }
        let s = search()
        return answer(s)
    }
}
复制代码

解法 3:双数组二分查找 方法 2

双数组二分查找的方法2和方法1策略类似,唯一不同的是该方法定义了的搜索函数findValueAtIndex搜索nums1nums2组成的nums3中的第k个元素 。而该函数的实现策略选用了双数组二分。

借助该函数,我们就能轻松的获得中位数。

源代码如下。

// O(log(n))复杂度解法 2
class Solution {
    class Scope {
        var begin = 0
        var end = 0
        var center: Int { !valid ? -1 : (begin + end) / 2 }
        var valid: Bool { end > begin }
        var count: Int { end - begin }
        func moveRight() { begin = center }
        func moveLeft() { if valid { end = center } }
    }
    
    func findMedianSortedArrays(_ nums1: [Int], _ nums2: [Int]) -> Double {
        func findValueAtIndex(_ index: Int) -> Int {
            let scope1 = Scope()
            let scope2 = Scope()
            scope1.end = nums1.count
            scope2.end = nums2.count
            
            while scope1.valid || scope2.valid {
                let v1 = scope1.valid ? nums1[scope1.center] : Int.min
                let v2 = scope2.valid ? nums2[scope2.center] : Int.min
                let currentIndex = scope1.center + scope2.center + 1
                
                if v1 < v2 {
                    if currentIndex == index {
                        if scope1.valid && scope1.center + 1 < scope1.end && nums1[scope1.center+1] < nums2[scope2.center] {
                            scope2.moveLeft()
                        } else {
                            return nums2[scope2.center]
                        }
                    } else if currentIndex < index {
                        scope1.count <= 1 ? scope2.moveRight() : scope1.moveRight()
                    } else if currentIndex > index {
                        scope2.moveLeft()
                    }
                } else {
                    if currentIndex == index {
                        if scope2.valid && scope2.center + 1 < scope2.end && nums2[scope2.center+1] < nums1[scope1.center] {
                            scope1.moveLeft()
                        } else {
                            return nums1[scope1.center]
                        }
                    } else if currentIndex < index {
                        scope2.count <= 1 ? scope1.moveRight() : scope2.moveRight()
                    } else {
                        scope1.moveLeft()
                    }
                }
            }
            return 0
        }
        
        let c1 = nums1.count
        let c2 = nums2.count
        if (c1 + c2) % 2 == 0 {
            let i1 = (c1 + c2) / 2
            let i2 = i1 - 1
            let v1 = findValueAtIndex(i1)
            let v2 = findValueAtIndex(i2)
            return (Double(v1) + Double(v2)) / 2.0
        } else {
            let i = (c1 + c2) / 2
            let value = findValueAtIndex(i)
            return Double(value)
        }
    }
}
复制代码

总结

这是道个人认为非常经典考察对二分查找理解的题,该题目提供了两个已排序数组,如果要在O(1)空间和O(log(n))时间内解决该问题,就非要对两个数组进行二分查找而不进行重排不可。

力扣的题解里有非常精巧的代码解法,但是看了下基本思路无外乎我提供了第3和第4种解法,加上语言上的技巧可以让代码更精简。


关注我的公众号风海铜锣技术君点击加群按钮即可加入力扣解题交流群。


源代码可以到我的github下载: github.com/FengHaiTong…

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改