Swift 数据结构与算法( 八) 数组 + 剑指 Offer 53 - I. 在排序数组中查找数字 I(二分查找)

52 阅读2分钟

概念

二分搜索概念

二分查找并不简单,Knuth 大佬(发明 KMP 算法的那位)都说二分查找:思路很简单,细节是魔鬼。很多人喜欢拿整型溢出的 bug 说事儿,但是二分查找真正的坑根本就不是那个细节问题,而是在于到底要给 mid 加一还是减一,while 里到底用 <= 还是 <

你要是没有正确理解这些细节,写二分肯定就是玄学编程,有没有 bug 只能靠菩萨保佑(谁写谁知道)。我特意写了一首诗来歌颂该算法,概括本文的主要内容,建议保存(手动狗头):

image.png

摘自 labuladong

题目

剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

 

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

 

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

 

注意: 本题与主站 34 题相同(仅返回值不同):leetcode-cn.com/problems/fi…

解题思路🙋🏻‍ ♀️

  1. 首先,我们初始化 left 为 0,rightnums.count - 1,即 5。我们还初始化 firstend 为 0。在进入第一次的 while 循环之前,变量的状态如下:
nums = [5,7,7,8,8,10]
        l          r
  1. 我们进入第一次的 while 循环,计算 middle = left + (right - left)/2 = 2。因为 nums[middle] = 7 < 8,所以我们将 left 更新为 middle + 1 = 3。循环继续,现在的状态如下:
nums = [5,7,7,8,8,10]
              l    r
              M
  1. 继续第一次的 while 循环,计算 middle = left + (right - left)/2 = 3。因为 nums[middle] = 8,所以我们将 right 更新为 middle = 3。现在 left == right,所以第一次的 while 循环结束,我们更新 first = left = 3。现在的状态如下:
nums = [5,7,7,8,8,10]
              l,
              r
              M
  1. 我们开始第二次的 while 循环,计算 middle = left + (right - left + 1)/2 = 4。因为 nums[middle] = 8,所以我们将 left 更新为 middle = 4。循环继续,现在的状态如下:
nums = [5,7,7,8,8,10]
                l  r
  1. 继续第二次的 while 循环,计算 middle = left + (right - left + 1)/2 = 4。因为 nums[middle] = 8,所以我们将 right 更新为 middle = 4。现在 left == right,所以第二次的 while 循环结束,我们更新 end = right = 4。现在的状态如下:
nums = [5,7,7,8,8,10]
                l,
                r
                M
  1. 最后,我们返回 end - first + 1 = 2,这就是目标值 8 在数组中出现的次数。

边界思考🤔

 // 如果数组中没有目标值,直接返回 0
         if nums[left] != target  {
             return 0
         }

代码

import Foundation

class Solution {
    func search(_ nums: [Int], _ target: Int) -> Int {
         
         // 如果数组为空,直接返回 0
         if nums.count <= 0 {
             return 0
         }

         // 初始化左右指针和首尾位置
         var left = 0
         var right  = nums.count - 1
         var first = 0
         var end = 0

         // 第一次二分查找,找到目标值的首次出现位置
         while left < right {
            let middle = left + (right - left)/2
            if nums[middle] < target { // 如果中间值小于目标值,目标值在右侧,移动左指针
                left = middle + 1
            } else { // 如果中间值大于或等于目标值,目标值在左侧或就是中间值,移动右指针
                right = middle
            }
         }

         // 如果数组中没有目标值,直接返回 0
         if nums[left] != target  {
             return 0
         }

         // 记录目标值的首次出现位置
         first = left
         
         // 重新设置右指针,进行第二次二分查找,找到目标值的末次出现位置
         right = nums.count - 1
         while left < right {
            let middle = left + (right - left + 1)/2
            if nums[middle] <= target { // 如果中间值小于或等于目标值,目标值在右侧或就是中间值,移动左指针
                left = middle
            } else { // 如果中间值大于目标值,目标值在左侧,移动右指针
                right = middle - 1
            }
         }

         // 记录目标值的末次出现位置
         if nums[right] == target  {
            end = right 
         }

         // 返回目标值的出现次数
         return end - first + 1
    }
}

时空复杂度分析

O (logn)