Swift 数据结构与算法( 九) 数组 + Leetcode704. 二分查找

87 阅读2分钟

概念

注意 这三点是二分搜索最易写错的

  1. 计算中点:在计算中点时,我们通常使用 (left + right) / 2。然而,如果 leftright 都很大,这可能会导致整数溢出。一个更安全的做法是使用 left + (right - left) / 2
  2. 搜索区间的更新:在每一步中,我们都需要根据 nums[mid] 与目标值的比较结果来更新搜索区间。需要特别注意的是,我们应该将 left 更新为 mid + 1 或将 right 更新为 mid - 1,以确保搜索区间在每一步中都在缩小。
  3. 循环条件:循环应该在 left <= right 时继续,而不是 left < right。这是因为当 left == right 时,搜索区间仍然包含一个元素,我们需要检查这个元素是否是目标值。
1.0 二分搜索概念

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

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

image.png

摘自 labuladong

题目

704. 二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

 

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。

解题思路🙋🏻‍ ♀️

在数组 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 中查找目标值 7

使用公式 mid = left + (right - left) / 2 来计算中点,以防止可能的整数溢出。在这种情况下,每一步中 leftmidright 的值将如下:

  1. 初始状态:我们设置 left = 0right = nums.count - 1 = 9。计算得到 mid = left + (right - left) / 2 = 4
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        l           m               r
  1. 第一步:我们比较 nums[mid] = 5 和目标值 7。因为 5 < 7,我们知道 7 必须在数组的右半部分,所以我们将 left 移动到 mid + 1 = 5。此时,mid = left + (right - left) / 2 = 7
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
                       l     m      r
  1. 第二步:我们再次比较 nums[mid] = 87。因为 8 > 7,我们知道 7 必须在数组的左半部分,所以我们将 right 移动到 mid - 1 = 6。此时,mid = left + (right - left) / 2 = 6
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
                       l, r, 
                          m
  1. 第三步:我们再次比较 nums[mid] = 77。这次 7 == 7,所以我们找到了目标值,返回其索引 6
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
                       l, r,
                          m

所以,经过这三步,我们找到了目标值 7,并返回其在数组中的索引 6

边界思考🤔

2.0 二分搜索书写的时候易错点
  1. 计算中点:在计算中点时,我们通常使用 (left + right) / 2。然而,如果 leftright 都很大,这可能会导致整数溢出。一个更安全的做法是使用 left + (right - left) / 2
  2. 搜索区间的更新:在每一步中,我们都需要根据 nums[mid] 与目标值的比较结果来更新搜索区间。需要特别注意的是,我们应该将 left 更新为 mid + 1 或将 right 更新为 mid - 1,以确保搜索区间在每一步中都在缩小。
  3. 循环条件:循环应该在 left <= right 时继续,而不是 left < right。这是因为当 left == right 时,搜索区间仍然包含一个元素,我们需要检查这个元素是否是目标值。

假设我们有一个升序数组 [1, 2, 3, 4, 5, 6, 7, 8, 9],我们想要找到目标值 7

首先,我们初始化左右指针 leftright,并计算中间位置 mid

1 2 3 4 5 6 7 8 9
L         M       R
  • 易错点 1(初始化):左右指针的初始化应该为 left = 0right = nums.count - 1

接下来,我们看一下 nums[mid] 是否等于目标值。如果 nums[mid] < target,我们更新 left = mid + 1;如果 nums[mid] > target,我们更新 right = mid - 1。在这个例子中,nums[mid] = 5 < 7,所以我们更新 left = mid + 1

1 2 3 4 5 6 7 8 9
          L M     R
  • 易错点 2(边界处理):我们应该设置 left = mid + 1right = mid - 1,因为我们已经检查过 nums[mid],并确定它不等于目标值。

接下来,我们再次比较 nums[mid] 和目标值。现在,nums[mid] = 8 > 7,所以我们更新 right = mid - 1

1 2 3 4 5 6 7 8 9
          L R
          M

然后,我们再次比较 nums[mid] 和目标值。现在,nums[mid] = 6 < 7,所以我们更新 left = mid + 1

1 2 3 4 5 6 7 8 9
            L=R
            M
  • 易错点 3(溢出问题):在计算 mid 时,我们应该使用 mid = left + (right - left) / 2mid = right - (right - left) / 2 来防止溢出。

最后,我们发现 nums[mid] 等于目标值,所以我们返回 mid。如果我们没有找到目标值,我们会继续这个过程,直到 left > right

1 2 3 4 5 6 7 8 9
            M
  • 易错点 4(循环条件):循环的条件应该是 left <= right。只有当 left > right 时,我们才能确定搜索空间为空,也就是说我们已经检查过所有可能的元素。

  • 易错点 5(返回值):如果我们没有找到目标值,应该返回 -1 或其他特定的值。

代码

import Foundation

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

         var left = 0
         var right  = nums.count - 1

         // 当左边界小于等于右边界时, 执行循环
         while left <= right {
            // 计算中间元素索引 middle
            let middle = left + (right - left)/2

            // 如果中间元素等于目标值, 则返回其索引
            if nums[middle] == target  {
               return middle 
            // 如果中间元素小于目标值, 则目标值在数组的右半部分
            // 更新左边界为 middle + 1
            } else if nums[middle] < target {
                left = middle + 1
            // 如果中间元素大于目标值, 则目标值在数组的左半部分
            // 更新右边界为 middle - 1
            } else if nums[middle] > target {
                right = middle - 1
            }
         }
         // 如果在数组中找不到目标值, 则返回 -1
         return -1
    }
}

时空复杂度分析

O (logn)