二分查找实现

非常详细的解释,简单但是细节很重要

https://www.cnblogs.com/kyoner/p/11080078.html

正常实现

  1. Input : [1,2,3,4,5]
  2. key : 3
  3. return the index : 2
  1. public int binarySearch(int[] nums, int key) {
  2. int l = 0, h = nums.length - 1;
  3. while (l <= h) {
  4. int m = l + (h - l) / 2;
  5. if (nums[m] == key) {
  6. return m;
  7. } else if (nums[m] > key) {
  8. h = m - 1;
  9. } else {
  10. l = m + 1;
  11. }
  12. }
  13. return -1;
  14. }

时间复杂度

二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。

m 计算

有两种计算中值 m 的方式:

  • m = (l + h) / 2
  • m = l + (h - l) / 2

l + h 可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围。但是 l 和 h 都为正数,因此 h - l 不会出现加法溢出问题。所以,最好使用第二种计算法方法。

未成功查找的返回值

循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:

  • -1:以一个错误码表示没有查找到 key
  • l:将 key 插入到 nums 中的正确位置

变种

二分查找可以有很多变种,实现变种要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:

  1. public int binarySearch(int[] nums, int key) {
  2. int l = 0, h = nums.length - 1;
  3. while (l < h) {
  4. int m = l + (h - l) / 2;
  5. if (nums[m] >= key) {
  6. h = m;
  7. } else {
  8. l = m + 1;
  9. }
  10. }
  11. return l;
  12. }

该实现和正常实现有以下不同:

  • h 的赋值表达式为 h = m
  • 循环条件为 l < h
  • 最后返回 l 而不是 -1

在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。

在 h 的赋值表达式为 h = m 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:

  1. nums = {0, 1, 2}, key = 1
  2. l m h
  3. 0 1 2 nums[m] >= key
  4. 0 0 1 nums[m] < key
  5. 1 1 1 nums[m] >= key
  6. 1 1 1 nums[m] >= key
  7. ...

当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。

1. 求开方

69. Sqrt(x) (Easy)

Leetcode / 力扣

  1. public int mySqrt(int x) {
  2. if (x <= 1) {
  3. return x;
  4. }
  5. int l = 1, h = x;
  6. while (l <= h) {
  7. int mid = l + (h - l) / 2;
  8. int sqrt = x / mid;
  9. if (sqrt == mid) {
  10. return mid;
  11. } else if (mid > sqrt) {
  12. h = mid - 1;
  13. } else {
  14. l = mid + 1;
  15. }
  16. }
  17. return h;
  18. }

2. 大于给定元素的最小元素

744. Find Smallest Letter Greater Than Target (Easy)

Leetcode / 力扣

  1. public char nextGreatestLetter(char[] letters, char target) {
  2. int n = letters.length;
  3. int l = 0, h = n - 1;
  4. while (l <= h) {
  5. int m = l + (h - l) / 2;
  6. if (letters[m] <= target) {
  7. l = m + 1;
  8. } else {
  9. h = m - 1;
  10. }
  11. }
  12. return l < n ? letters[l] : letters[0];
  13. }

3. 有序数组的 Single Element

540. Single Element in a Sorted Array (Medium)

Leetcode / 力扣

这个题目细节很恶心,就是要注意右边界,在 h 的赋值表达式为 h = m 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:

  1. nums = {0, 1, 2}, key = 1
  2. l m h
  3. 0 1 2 nums[m] >= key
  4. 0 0 1 nums[m] < key
  5. 1 1 1 nums[m] >= key
  6. 1 1 1 nums[m] >= key

令 index 为 Single Element 在数组中的位置。在 index 之后,数组中原来存在的成对状态被改变。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。

从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。

因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。

取得是nums[m]和nums[m+1],因此不能取 l == h的 情况会出现越界异常

  1. public int singleNonDuplicate(int[] nums) {
  2. int l = 0, h = nums.length - 1;
  3. while (l < h) {
  4. int m = l + (h - l) / 2;
  5. if (m % 2 == 1) {
  6. m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
  7. }
  8. if (nums[m] == nums[m + 1]) {
  9. l = m + 2;
  10. } else {
  11. h = m;
  12. }
  13. }
  14. return nums[l];
  15. }

4. 第一个错误的版本

278. First Bad Version (Easy)

Leetcode / 力扣

题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,在第 x 位置开始出现错误版本,导致后面的版本都错误。可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。

如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。

因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。

  1. public class Solution extends VersionControl {
  2. public int firstBadVersion(int n) {
  3. int lo=0,hi=n;
  4. while(lo<hi){
  5. int m=lo+(hi-lo)/2;
  6. if(isBadVersion(m)){
  7. hi=m;
  8. }else{
  9. lo=m+1;
  10. }
  11. }
  12. return lo;
  13. }
  14. }

5. 旋转数组的最小数字

153. Find Minimum in Rotated Sorted Array (Medium)

Leetcode / 力扣

  1. class Solution {
  2. public int findMin(int[] nums) {
  3. int lo=0,hi=nums.length-1;
  4. while(lo<hi){
  5. int m=lo+(hi-lo)/2;
  6. if(nums[m]>nums[hi])lo=m+1;
  7. else hi=m;
  8. }
  9. return nums[lo];
  10. }
  11. }

6. 查找区间

34. Find First and Last Position of Element in Sorted Array

Leetcode / 力扣

先找最左侧的节点,然后再找有边界

  1. class Solution {
  2. public int[] searchRange(int[] nums, int target) {
  3. if(nums.length==0)return new int[]{-1,-1};
  4. int lo=0,hi=nums.length-1;
  5. while(lo<hi){
  6. int m=lo+(hi-lo)/2;
  7. if(nums[m] >= target){
  8. hi=m;
  9. }else if(nums[m]<target){
  10. lo=m+1;
  11. }
  12. }
  13. int cur=lo;
  14. if(nums[lo]!=target)return new int[]{-1,-1};
  15. while(cur<nums.length){
  16. if(nums[cur]==target)cur++;
  17. else break;
  18. }
  19. cur--;
  20. return new int[]{lo,cur};
  21. }
  22. }

可以用二分查找找出第一个位置和最后一个位置,但是寻找的方法有所不同,需要实现两个二分查找。我们将寻找 target 最后一个位置,转换成寻找 target+1 第一个位置,再往前移动一个位置。这样我们只需要实现一个二分查找代码即可。

  1. public int[] searchRange(int[] nums, int target) {
  2. int first = findFirst(nums, target);
  3. int last = findFirst(nums, target + 1) - 1;
  4. if (first == nums.length || nums[first] != target) {
  5. return new int[]{-1, -1};
  6. } else {
  7. return new int[]{first, Math.max(first, last)};
  8. }
  9. }
  10. private int findFirst(int[] nums, int target) {
  11. int l = 0, h = nums.length; // 注意 h 的初始值
  12. while (l < h) {
  13. int m = l + (h - l) / 2;
  14. if (nums[m] >= target) {
  15. h = m;
  16. } else {
  17. l = m + 1;
  18. }
  19. }
  20. return l;
  21. }

Leedcode算法专题训练(二分查找)的更多相关文章

  1. Leedcode算法专题训练(数组与矩阵)

    1. 把数组中的 0 移到末尾 283. Move Zeroes (Easy) Leetcode / 力扣 class Solution { public void moveZeroes(int[] ...

  2. Leedcode算法专题训练(搜索)

    BFS 广度优先搜索一层一层地进行遍历,每层遍历都是以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点.需要注意的是,遍历过的节点不能再次被遍历. 第一层: 0 -> {6,2,1,5} ...

  3. Leedcode算法专题训练(双指针)

    算法思想 双指针 167. 两数之和 II - 输入有序数组 双指针的典型用法 如果两个指针指向元素的和 sum == target,那么得到要求的结果: 如果 sum > target,移动较 ...

  4. Leedcode算法专题训练(哈希表)

    Java 中的 HashSet 用于存储一个集合,可以查找元素是否在集合中.如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在.例如对于只有小写字符的元素,就可以用一个长度为 2 ...

  5. Leedcode算法专题训练(树)

    递归 一棵树要么是空树,要么有两个指针,每个指针指向一棵树.树是一种递归结构,很多树的问题可以使用递归来处理. 1. 树的高度 104. Maximum Depth of Binary Tree (E ...

  6. Leedcode算法专题训练(分治法)

    归并排序就是一个用分治法的经典例子,这里我用它来举例描述一下上面的步骤: 1.归并排序首先把原问题拆分成2个规模更小的子问题. 2.递归地求解子问题,当子问题规模足够小时,可以一下子解决它.在这个例子 ...

  7. Leedcode算法专题训练(排序)

    排序 快速排序 用于求解 Kth Element 问题,也就是第 K 个元素的问题. 可以使用快速排序的 partition() 进行实现.需要先打乱数组,否则最坏情况下时间复杂度为 O(N2). 堆 ...

  8. Leedcode算法专题训练(贪心)

    1. 分配饼干 455. 分发饼干 题目描述:每个孩子都有一个满足度 grid,每个饼干都有一个大小 size,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足.求解最多可以获得满足的孩子数 ...

  9. Leedcode算法专题训练(位运算)

    https://www.cnblogs.com/findbetterme/p/10787118.html 看这个就完事了 1. 统计两个数的二进制表示有多少位不同 461. Hamming Dista ...

随机推荐

  1. NGK DeFi Baccarat怎么玩能赚钱?

    市面上大多数DeFi项目都是基于以太坊来开发的,除了吞吐量低.存储量小以及交易速度慢等问题以外,高额的Gas手续费将不少终端用户拒之门外. 基于此NGK.IO推出了低门槛的DeFi项目-- Bacca ...

  2. 为什么说NGK的去中心化预言机越来越受欢迎?

    2020年区块链市场非常火热,从年初的交易所杠杆,到Defi热潮,一波连着一波,风向不断切换,很多人无奈感叹跟不上时代,很多人欢欣雀跃登上了早班车.随着Defi的不断火热,预言机也进入了大众视野.NG ...

  3. 五分钟快速上手MyBatis

    MyBatis 是一款优秀的持久层框架,它支持自定义 SQL.存储过程以及高级映射. 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作. 可以通过简单的 XML 或注解来配置和映射,Ja ...

  4. golang操作redis/go-redis库

    目录 Redis介绍 Redis支持的数据结构 Redis应用场景 准备Redis环境 go-redis库 安装 连接 普通连接 V8新版本相关 连接Redis哨兵模式 连接Redis集群 基本使用 ...

  5. 【DP】斜率优化初步

    向y总学习了斜率优化,写下这篇blog加深一下理解. 模板题:https://www.acwing.com/problem/content/303/ 分析 因为本篇的重点在于斜率优化,故在此给出状态转 ...

  6. Android+Chrome 真机调试H5页面实践

    前言 使用weinre在真机上调试H5页面,有一个突出的缺点,就是无法调试真机上的样式,真机上页面动态创建的dom在weinre的Elements面板显示不出来,所以调试真机上的页面样式也就无从谈起. ...

  7. 还在用crontab? 分布式定时任务了解一下

    前言 日常任务开放中,我们会有很多异步.批量.定时.延迟任务要处理,go-zero中有 go-queue,推荐使用 go-queue 去处理,go-queue 本身也是基于 go-zero 开发的,其 ...

  8. JVM笔记 -- JVM的生命周期介绍

    Github仓库地址:https://github.com/Damaer/JvmNote 文档地址:https://damaer.github.io/JvmNote/ JVM生命周期 启动 执行 退出 ...

  9. 从一个想法看 FreeBSD 是商业化还是学院派

    在某知名计算机网络论坛上我看到一个帖子,说自己想根据 FreeBSD 做一个移动的终端操作系统,就像安卓,苹果的 IOS 一样的. 逆向思维当初开发安卓的时候不可能没有考虑过 FreeBSD,因为无论 ...

  10. LiberOJ #124. 除数函数求和 【整除分块】

    一.题目 #124. 除数函数求和 二.分析 比较好的一题,首先我们要对题目和样例进行分析,明白题目的意思. 由于对于每一个$d$,它所能整除的数其实都是定的,且数量是$ \lfloor \frac{ ...