数据结构和算法躬行记(4)——二分查找

简介:   二分查找(Binary Search)是对一种针对有序数据集合的查找算法,依赖数组,适合静态数据。通过 n/2^k=1(k 是比较次数),可以求得 k=log2^n,因此时间复杂度为高效地 O(logn)。

  二分查找(Binary Search)是对一种针对有序数据集合的查找算法,依赖数组,适合静态数据。通过 n/2^k=1(k 是比较次数),可以求得 k=log2^n,因此时间复杂度为高效地 O(logn)。

  其思路很简单,就是每次与区间的中间数据做比较,缩小查找范围,但是期间涉及到的细节很容易踩坑,例如比较时是否带等号、mid值是否要加一等。例题:704. 二分查找

  LeetCode的69. x 的平方根,x=sqrt(y); y=x^2,由于y和x是单调递增的,因此可用二分查找,每次取中值,不断逼近。另一种解题思路是牛顿迭代法。

  在《剑指offer》一书中曾提到,写出高质量的代码需要了解3个方面:

  (1)规范性,书写清晰、布局清晰和命名合理。

  (2)完整性,完成基本功能、考虑边界条件、做好错误处理。

  (3)鲁棒性,采取防御性编程、处理无效的输入。

  下面是二分查找最基本的代码实现,改编自《二分搜索》,为了防止mid的溢出,才设计成了 low + Math.floor((high - low) / 2)。


function binarySearch(nums, target) {
  let low = 0, high = ...;
  while(...) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      ...
    } else if (nums[mid] < target) {
      low = ...
    } else if (nums[mid] > target) {
      high = ...
    }
  }
  return ...;
}


  其中占位符“...”处就是容易出错的细节部分:

  (1)循环退出条件。

  (2)low 和 high 的更新。

  (3)找到目标值时的处理。

  (4)函数的返回值。


一、无重复数据


  在下面的binarySearch()函数中,nums是一个无重复数据的数组,需要注意:

  (1)while循环的判断条件是小于等于,因为high的取值是末尾索引,相当于两端闭区间 [low, high]。

  (2)当不匹配时,缩小查找区间,分成两块闭区间:[mid+1, high] 或 [low, mid-1]。

function binarySearch(nums, target) {
  let low = 0,
    high = nums.length - 1;      //注意
  while(low <= high) {           //注意
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      return mid;               //注意
    } else if (nums[mid] < target) {
      low = mid + 1;            //注意
    } else if (nums[mid] > target) {
      high = mid - 1;           //注意
    }
  }
  return -1;                    //注意
}


  面试题11 旋转数组的最小数字。二分查找的思路,用两个指针指向数组的第一个和最后一个元素,如果中间元素位于前面的递增子数组,那么它应该大于或等于第一个指针指向的元素。

  面试题53 在排序数组中查找数字。用二分查找锁定指定值,然后在左右两边顺序扫描,直至找出第一个和最后一个指定值。延伸题:0~n-1 中缺失的数字


二、含重复数据


  当查找的数组中包含重复数据时,二分查找需要做些处理。

  面试题3 不修改数组找出重复数字。二分查找思想,从中间数字m分成两部分,1~m和m+1~n,如果1~m的数字超过m,那么区间有重复数字。

1)查找第一个等于给定值的元素

  在下面的binarySearchLow()函数中,需要注意:

  (1)while循环的判断条件是小于,因为 low 和 high 相等会陷入死循环。

  (2)查找到匹配的数据不是立即返回,而是缩小范围,并且增加一次 high 的边界值判断。

  (3)在跳出循环后,需要最终判断是否与目标值匹配。


function binarySearchLow(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {                //注意
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      high = mid - 1;                //注意
      if (nums[high] != target)      //注意
        return mid;                  //注意
    } else if (nums[mid] < target) {
      low = mid + 1;
    } else if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  return nums[low] == target ? low : -1;    //注意
}


  另一个类似的问题是查找最后一个等于给定值的元素,修改匹配时的范围,如下所示。

function binarySearchHigh(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      low = mid + 1;                //注意
      if (nums[low] != target)      //注意
        return mid;                 //注意
    } else if (nums[mid] < target) {
      low = mid + 1;
    } else if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  return nums[low] == target ? low : -1;
}

  在下面的binarySearchLow()函数中,需要注意:

  (1)修改匹配给定值的条件,变为大于等于。

  (2)额外匹配一次 high 的边界值,成功时直接返回 mid。

  (3)修改跳出循环后的匹配条件。


function binarySearchLow(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] >= target) {       //注意
      high = mid - 1;                //注意
      if (nums[high] < target)       //注意
        return mid;                  //注意
    } else if (nums[mid] < target) {
      low = mid + 1;
    }
  }
  return nums[low] >= target ? low : -1;    //注意
}

  另一个类似的问题是查找最后一个小于等于给定值的元素,修改匹配时的范围,如下所示。


function binarySearchHigh(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] <= target) {        //注意
      low = mid + 1;                  //注意
      if (nums[low] > target)         //注意
        return mid;                   //注意
    } else if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  return nums[low] <= target ? low : -1;    //注意
}


  本文的这些示例都没有经过大量测试用例的严谨验证,欢迎指正其中的潜在错误。


相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
70 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
1月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
33 4
|
1月前
|
算法 C# 索引
C#二分查找算法
C#二分查找算法
|
1月前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
20 0
数据结构与算法学习十四:常用排序算法总结和对比
|
1月前
|
存储 算法 C语言
【C语言】二分查找算法
【C语言】二分查找算法
|
1月前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
1月前
|
消息中间件 存储 算法
一文搞懂二分查找算法!
一文搞懂二分查找算法!
|
1月前
|
算法 Java 索引
数据结构与算法学习十五:常用查找算法介绍,线性排序、二分查找(折半查找)算法、差值查找算法、斐波那契(黄金分割法)查找算法
四种常用的查找算法:顺序查找、二分查找(折半查找)、插值查找和斐波那契查找,并提供了Java语言的实现代码和测试结果。
21 0
|
30天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
7天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
下一篇
无影云桌面