二分查找详解.md 36.8 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '二分查找算法详解'
L
labuladong 已提交
3
tags: ['数组', '双指针', '二分查找', '核心框架']
L
labuladong 已提交
4
---
L
labuladong 已提交
5

6 7
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
8
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
9 10 11 12
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
13
![](https://labuladong.github.io/pictures/souyisou1.png)
14

L
labuladong 已提交
15
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
16 17 18



L
labuladong 已提交
19 20 21 22 23 24 25
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [34. Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/) | [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) | 🟠
| [704. Binary Search](https://leetcode.com/problems/binary-search/) | [704. 二分查找](https://leetcode.cn/problems/binary-search/) | 🟢
| - | [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/) | 🟢
26 27 28

**-----------**

L
labuladong 已提交
29
> tip:本文有视频版:[二分搜索核心框架套路](https://www.bilibili.com/video/BV1Gt4y1b79Q/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。
L
labuladong 已提交
30

L
labuladong 已提交
31 32
本文是前文 [二分搜索详解](https://mp.weixin.qq.com/s/uA2suoVykENmCQcKFMOSuQ) 的修订版,添加了对二分搜索算法更详细的分析。

L
labuladong 已提交
33 34 35 36
先给大家讲个笑话乐呵一下:

有一天阿东到图书馆借了 N 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗?于是保安把书分成两堆,让第一堆过一下报警器,报警器响;于是再把这堆书分成两堆…… 最终,检测了 logN 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。

L
labuladong 已提交
37
从此,图书馆丢了 N - 1 本书(手动狗头)。
L
labuladong 已提交
38

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

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

L
labuladong 已提交
43
![](https://labuladong.github.io/pictures/二分查找/poem.png)
L
labuladong 已提交
44

L
labuladong 已提交
45
本文就来探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。而且,我们就是要深入细节,比如不等号是否应该带等号,`mid` 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。
L
labuladong 已提交
46

L
labuladong 已提交
47
另外再声明一下,对于二分搜索的每一个场景,本文还会探讨多种代码写法,目的是为了让你理解出现这些细微差异的本质原因,最起码你看到别人的代码时不会懵逼。实际上这些写法没有优劣之分,你喜欢哪种就用哪种好了。
L
labuladong 已提交
48 49 50

### 零、二分查找框架

L
labuladong 已提交
51
<!-- muliti_language -->
L
labuladong 已提交
52 53 54 55 56
```java
int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
57
        int mid = left + (right - left) / 2;
L
labuladong 已提交
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}
```

**分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节**。本文都会使用 else if,旨在讲清楚,读者理解后可自行简化。

其中 `...` 标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先注意这几个地方。后文用实例分析这些地方能有什么样的变化。

L
labuladong 已提交
74
**另外提前说明一下,计算 `mid` 时需要防止溢出**,代码中 `left + (right - left) / 2` 就和 `(left + right) / 2` 的结果相同,但是有效防止了 `left``right` 太大,直接相加导致溢出的情况。
L
labuladong 已提交
75 76 77

### 一、寻找一个数(基本的二分搜索)

L
fixbug  
labuladong 已提交
78
这个场景是最简单的,可能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 -1。
L
labuladong 已提交
79

L
labuladong 已提交
80
<!-- muliti_language -->
L
labuladong 已提交
81 82 83 84 85 86
```java
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意

    while(left <= right) {
87
        int mid = left + (right - left) / 2;
L
labuladong 已提交
88 89 90 91 92 93
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
94
    }
L
labuladong 已提交
95 96 97 98
    return -1;
}
```

L
labuladong 已提交
99 100
这段代码可以解决力扣第 704 题「二分查找」,但我们深入探讨一下其中的细节。

101
**1、为什么 while 循环的条件中是 <=,而不是 <**
L
labuladong 已提交
102

103
答:因为初始化 `right` 的赋值是 `nums.length - 1`,即最后一个元素的索引,而不是 `nums.length`
L
labuladong 已提交
104

105
这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 `[left, right]`,后者相当于左闭右开区间 `[left, right)`,因为索引大小为 `nums.length` 是越界的。
L
labuladong 已提交
106

107
我们这个算法中使用的是前者 `[left, right]` 两端都闭的区间。**这个区间其实就是每次进行搜索的区间**
L
labuladong 已提交
108 109 110 111 112 113 114 115 116 117

什么时候应该停止搜索呢?当然,找到了目标值的时候可以终止:

```java
    if(nums[mid] == target)
        return mid; 
```

但如果没找到,就需要 while 循环终止,然后返回 -1。那 while 循环什么时候应该终止?**搜索区间为空的时候应该终止**,意味着你没得找了,就等于没找到嘛。

118
`while(left <= right)` 的终止条件是 `left == right + 1`,写成区间的形式就是 `[right + 1, right]`,或者带个具体的数字进去 `[3, 2]`,可见**这时候区间为空**,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
L
labuladong 已提交
119

L
fixbug  
labuladong 已提交
120
`while(left < right)` 的终止条件是 `left == right`,写成区间的形式就是 `[right, right]`,或者带个具体的数字进去 `[2, 2]`**这时候区间非空**,还有一个数 2,但此时 while 循环终止了。也就是说这区间 `[2, 2]` 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。
L
labuladong 已提交
121

122
当然,如果你非要用 `while(left < right)` 也可以,我们已经知道了出错的原因,就打个补丁好了:
L
labuladong 已提交
123 124 125 126 127 128 129 130 131

```java
    //...
    while(left < right) {
        // ...
    }
    return nums[left] == target ? left : -1;
```

132
**2、为什么 `left = mid + 1`,`right = mid - 1`?我看有的代码是 `right = mid` 或者 `left = mid`,没有这些加加减减,到底怎么回事,怎么判断**
L
labuladong 已提交
133 134 135

答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。

136
刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 `[left, right]`。那么当我们发现索引 `mid` 不是要找的 `target` 时,下一步应该去搜索哪里呢?
L
labuladong 已提交
137

L
labuladong 已提交
138
当然是去搜索区间 `[left, mid-1]` 或者区间 `[mid+1, right]` 对不对?**因为 `mid` 已经搜索过,应该从搜索区间中去除**
L
labuladong 已提交
139

140
**3、此算法有什么缺陷**
L
labuladong 已提交
141 142 143

答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。

144
比如说给你有序数组 `nums = [1,2,2,2,3]``target` 为 2,此算法返回的索引是 2,没错。但是如果我想得到 `target` 的左侧边界,即索引 1,或者我想得到 `target` 的右侧边界,即索引 3,这样的话此算法是无法处理的。
L
labuladong 已提交
145

L
labuladong 已提交
146
这样的需求很常见,**你也许会说,找到一个 `target`,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了**
L
labuladong 已提交
147 148 149 150 151

我们后续的算法就来讨论这两种二分查找的算法。

### 二、寻找左侧边界的二分搜索

152
以下是最常见的代码形式,其中的标记是需要注意的细节:
L
labuladong 已提交
153

L
labuladong 已提交
154
<!-- muliti_language -->
L
labuladong 已提交
155 156 157 158 159 160
```java
int left_bound(int[] nums, int target) {
    int left = 0;
    int right = nums.length; // 注意
    
    while (left < right) { // 注意
L
labuladong 已提交
161
        int mid = left + (right - left) / 2;
L
labuladong 已提交
162 163 164 165 166 167 168 169 170 171 172 173
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    return left;
}
```

174 175 176
**1、为什么 while 中是 `<` 而不是 `<=`**?

答:用相同的方法分析,因为 `right = nums.length` 而不是 `nums.length - 1`。因此每次循环的「搜索区间」是 `[left, right)` 左闭右开。
L
labuladong 已提交
177

178
`while(left < right)` 终止的条件是 `left == right`,此时搜索区间 `[left, left)` 为空,所以可以正确终止。
L
labuladong 已提交
179

L
labuladong 已提交
180
> info:这里先要说一个搜索左右边界和上面这个算法的一个区别,也是很多读者问的:**刚才的 `right` 不是 `nums.length - 1` 吗,为啥这里非要写成 `nums.length` 使得「搜索区间」变成左闭右开呢**?
L
labuladong 已提交
181 182
>
> 因为对于搜索左右侧边界的二分查找,这种写法比较普遍,我就拿这种写法举例了,保证你以后遇到这类代码可以理解。你非要用两端都闭的写法反而更简单,我会在后面写相关的代码,把三种二分搜索都用一种两端都闭的写法统一起来,你耐心往后看就行了。
183 184

**2、为什么没有返回 -1 的操作?如果 `nums` 中不存在 `target` 这个值,怎么办**
L
labuladong 已提交
185

L
labuladong 已提交
186
答:其实很简单,在返回的时候额外判断一下 `nums[left]` 是否等于 `target` 就行了,如果不等于,就说明 `target` 不存在。
L
labuladong 已提交
187

L
labuladong 已提交
188
不过我们得考察一下 `left` 的取值范围,免得索引越界。假如输入的 `target` 非常大,那么就会一直触发 `nums[mid] < target` 的 if 条件,`left` 会一直向右侧移动,直到等于 `right`,while 循环结束。
L
labuladong 已提交
189

L
labuladong 已提交
190
由于这里 `right` 初始化为 `nums.length`,所以 `left` 变量的取值区间是闭区间 `[0, nums.length]`,那么我们在检查 `nums[left]` 之前需要额外判断一下,防止索引越界:
L
labuladong 已提交
191 192 193 194 195

```java
while (left < right) {
    //...
}
L
labuladong 已提交
196
// 此时 target 比所有数都大,返回 -1
L
labuladong 已提交
197
if (left == nums.length) return -1;
L
labuladong 已提交
198
// 判断一下 nums[left] 是不是 target
L
labuladong 已提交
199 200 201
return nums[left] == target ? left : -1;
```

202
**3、为什么 `left = mid + 1`,`right = mid` ?和之前的算法不一样**
L
labuladong 已提交
203

L
labuladong 已提交
204
答:这个很好解释,因为我们的「搜索区间」是 `[left, right)` 左闭右开,所以当 `nums[mid]` 被检测之后,下一步应该去 `mid` 的左侧或者右侧区间搜索,即 `[left, mid)``[mid + 1, right)`
L
labuladong 已提交
205

206
**4、为什么该算法能够搜索左侧边界**
L
labuladong 已提交
207

208
答:关键在于对于 `nums[mid] == target` 这种情况的处理:
L
labuladong 已提交
209 210 211 212 213 214

```java
    if (nums[mid] == target)
        right = mid;
```

215 216 217 218 219 220 221 222 223 224 225 226
可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 `right`,在区间 `[left, mid)` 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。

**5、为什么返回 `left` 而不是 `right`**

答:都是一样的,因为 while 终止的条件是 `left == right`

**6、能不能想办法把 `right` 变成 `nums.length - 1`,也就是继续使用两边都闭的「搜索区间」?这样就可以和第一种二分搜索在某种程度上统一起来了**

答:当然可以,只要你明白了「搜索区间」这个概念,就能有效避免漏掉元素,随便你怎么改都行。下面我们严格根据逻辑来修改:

因为你非要让搜索区间两端都闭,所以 `right` 应该初始化为 `nums.length - 1`,while 的终止条件应该是 `left == right + 1`,也就是其中应该用 `<=`

L
labuladong 已提交
227
<!-- muliti_language -->
228 229 230 231 232 233 234 235 236 237 238
```java
int left_bound(int[] nums, int target) {
    // 搜索区间为 [left, right]
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        // if else ...
    }
```

因为搜索区间是两端都闭的,且现在是搜索左侧边界,所以 `left``right` 的更新逻辑如下:
L
labuladong 已提交
239

L
labuladong 已提交
240
<!-- muliti_language -->
241 242 243 244 245 246 247 248 249 250 251 252
```java
if (nums[mid] < target) {
    // 搜索区间变为 [mid+1, right]
    left = mid + 1;
} else if (nums[mid] > target) {
    // 搜索区间变为 [left, mid-1]
    right = mid - 1;
} else if (nums[mid] == target) {
    // 收缩右侧边界
    right = mid - 1;
}
```
L
labuladong 已提交
253

L
labuladong 已提交
254
和刚才相同,如果想在找不到 `target` 的时候返回 -1,那么检查一下 `nums[left]``target` 是否相等即可:
255

L
labuladong 已提交
256
<!-- muliti_language -->
257
```java
L
labuladong 已提交
258 259 260 261
// 此时 target 比所有数都大,返回 -1
if (left == nums.length) return -1;
// 判断一下 nums[left] 是不是 target
return nums[left] == target ? left : -1;
262 263 264 265
```

至此,整个算法就写完了,完整代码如下:

L
labuladong 已提交
266
<!-- muliti_language -->
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
```java
int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
L
labuladong 已提交
284 285 286 287 288
    // 判断 target 是否存在于 nums 中
    // 此时 target 比所有数都大,返回 -1
    if (left == nums.length) return -1;
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
289 290 291 292
}
```

这样就和第一种二分搜索算法统一了,都是两端都闭的「搜索区间」,而且最后返回的也是 `left` 变量的值。只要把住二分搜索的逻辑,两种形式大家看自己喜欢哪种记哪种吧。
L
labuladong 已提交
293 294 295

### 三、寻找右侧边界的二分查找

L
labuladong 已提交
296
类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同:
L
labuladong 已提交
297

L
labuladong 已提交
298
<!-- muliti_language -->
L
labuladong 已提交
299 300 301 302 303
```java
int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length;
    
    while (left < right) {
L
labuladong 已提交
304
        int mid = left + (right - left) / 2;
L
labuladong 已提交
305 306 307 308 309 310 311 312 313 314 315 316
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    return left - 1; // 注意
}
```

317
**1、为什么这个算法能够找到右侧边界**
L
labuladong 已提交
318 319 320 321

答:类似地,关键点还是这里:

```java
322 323
if (nums[mid] == target) {
    left = mid + 1;
L
labuladong 已提交
324 325
```

L
labuladong 已提交
326
`nums[mid] == target` 时,不要立即返回,而是增大「搜索区间」的左边界 `left`,使得区间不断向右靠拢,达到锁定右侧边界的目的。
L
labuladong 已提交
327

328
**2、为什么最后返回 `left - 1` 而不像左侧边界的函数,返回 `left`?而且我觉得这里既然是搜索右侧边界,应该返回 `right` 才对**
L
labuladong 已提交
329

330
答:首先,while 循环的终止条件是 `left == right`,所以 `left``right` 是一样的,你非要体现右侧的特点,返回 `right - 1` 好了。
L
labuladong 已提交
331

L
labuladong 已提交
332
至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在锁定右边界时的这个条件判断:
L
labuladong 已提交
333 334

```java
L
labuladong 已提交
335
// 增大 left,锁定右侧边界
336 337 338
if (nums[mid] == target) {
    left = mid + 1;
    // 这样想: mid = left - 1
L
labuladong 已提交
339 340
```

L
labuladong 已提交
341
![](https://labuladong.github.io/pictures/二分查找/3.jpg)
L
labuladong 已提交
342

343
因为我们对 `left` 的更新必须是 `left = mid + 1`,就是说 while 循环结束时,`nums[left]` 一定不等于 `target` 了,而 `nums[left-1]` 可能是 `target`
L
labuladong 已提交
344

L
labuladong 已提交
345
至于为什么 `left` 的更新必须是 `left = mid + 1`,当然是为了把 `nums[mid]` 排除出搜索区间,这里就不再赘述。
L
labuladong 已提交
346

347
**3、为什么没有返回 -1 的操作?如果 `nums` 中不存在 `target` 这个值,怎么办**
L
labuladong 已提交
348

L
labuladong 已提交
349 350 351
答:只要在最后判断一下 `nums[left-1]` 是不是 `target` 就行了。

类似之前的左侧边界搜索,`left` 的取值范围是 `[0, nums.length]`,但由于我们最后返回的是 `left - 1`,所以 `left` 取值为 0 的时候会造成索引越界,额外处理一下即可正确地返回 -1:
L
labuladong 已提交
352

L
labuladong 已提交
353
<!-- muliti_language -->
L
labuladong 已提交
354 355 356 357
```java
while (left < right) {
    // ...
}
L
labuladong 已提交
358 359 360 361 362
// 判断 target 是否存在于 nums 中
// 此时 left - 1 索引越界
if (left - 1 < 0) return -1;
// 判断一下 nums[left] 是不是 target
return nums[left - 1] == target ? (left - 1) : -1;
L
labuladong 已提交
363 364
```

365 366 367 368
**4、是否也可以把这个算法的「搜索区间」也统一成两端都闭的形式呢?这样这三个写法就完全统一了,以后就可以闭着眼睛写出来了**

答:当然可以,类似搜索左侧边界的统一写法,其实只要改两个地方就行了:

L
labuladong 已提交
369
<!-- muliti_language -->
370 371 372 373 374 375 376 377 378 379 380 381 382 383
```java
int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
        }
    }
L
labuladong 已提交
384 385 386
    // 最后改成返回 left - 1
    if (left - 1 < 0) return -1;
    return nums[left - 1] == target ? (left - 1) : -1;
387 388 389
}
```

L
labuladong 已提交
390
当然,由于 while 的结束条件为 `right == left - 1`,所以你把上述代码中的 `left - 1` 都改成 `right` 也没有问题,这样可能更有利于看出来这是在「搜索右侧边界」。
391 392 393 394

至此,搜索右侧边界的二分查找的两种写法也完成了,其实将「搜索区间」统一成两端都闭反而更容易记忆,你说是吧?

### 四、逻辑统一
L
labuladong 已提交
395

L
labuladong 已提交
396 397 398
有了搜索左右边界的二分搜索,你可以去解决力扣第 34 题「在排序数组中查找元素的第一个和最后一个位置」,

接下来梳理一下这些细节差异的因果逻辑:
L
labuladong 已提交
399

400
**第一个,最基本的二分查找算法**
L
labuladong 已提交
401 402 403 404 405 406 407 408 409 410 411

```python
因为我们初始化 right = nums.length - 1
所以决定了我们的搜索区间 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1  right = mid-1

因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回
```

412
**第二个,寻找左侧边界的二分查找**
L
labuladong 已提交
413 414 415 416 417 418 419 420 421 422 423 424

```python
因为我们初始化 right = nums.length
所以决定了我们的搜索区间 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1  right = mid

因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界
```

425
**第三个,寻找右侧边界的二分查找**
L
labuladong 已提交
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440

```python
因为我们初始化 right = nums.length
所以决定了我们的搜索区间 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1  right = mid

因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界

又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right必须减一
```

441 442
对于寻找左右边界的二分搜索,常见的手法是使用左闭右开的「搜索区间」,**我们还根据逻辑将「搜索区间」全都统一成了两端都闭,便于记忆,只要修改两处即可变化出三种写法**

L
labuladong 已提交
443
<!-- muliti_language -->
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
```java
int binary_search(int[] nums, int target) {
    int left = 0, right = nums.length - 1; 
    while(left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1; 
        } else if(nums[mid] == target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}

int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
L
labuladong 已提交
475 476 477 478 479
    // 判断 target 是否存在于 nums 中
    // 此时 target 比所有数都大,返回 -1
    if (left == nums.length) return -1;
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
}

int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1;
        }
    }
L
labuladong 已提交
495 496 497 498
    // 此时 left - 1 索引越界
    if (left - 1 < 0) return -1;
    // 判断一下 nums[left] 是不是 target
    return nums[left - 1] == target ? (left - 1) : -1;
499 500 501
}
```

L
labuladong 已提交
502
如果以上内容你都能理解,那么恭喜你,二分查找算法的细节不过如此。通过本文,你学会了:
L
labuladong 已提交
503 504 505 506 507

1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。

2、注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。

508
3、如需定义左闭右开的「搜索区间」搜索左右边界,只要在 `nums[mid] == target` 时做修改即可,搜索右侧时需要减一。
L
labuladong 已提交
509

L
labuladong 已提交
510
4、如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 `nums[mid] == target` 条件处的代码和返回的逻辑即可,推荐拿小本本记下,作为二分搜索模板。
L
labuladong 已提交
511

L
labuladong 已提交
512
最后我想说,以上二分搜索的框架属于「术」的范畴,如果上升到「道」的层面,**二分思维的精髓就是:通过已知信息尽可能多地收缩(折半)搜索空间**,从而增加穷举效率,快速找到目标。
L
labuladong 已提交
513

L
labuladong 已提交
514
理解本文能保证你写出正确的二分查找的代码,但实际题目中不会直接让你写二分代码,我会在 [二分查找的变体](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a07736e4b01a485209b0b4/1)[二分查找的运用](https://labuladong.github.io/article/fname.html?fname=二分运用) 中进一步讲解如何把二分思维运用到更多算法题中。
L
labuladong 已提交
515

L
labuladong 已提交
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534


<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [base case 和备忘录的初始值怎么定?](https://labuladong.github.io/article/fname.html?fname=备忘录等基础)
 - [丑数系列算法详解](https://labuladong.github.io/article/fname.html?fname=丑数)
 - [二分搜索怎么用?我又总结了套路](https://labuladong.github.io/article/fname.html?fname=二分运用)
 - [二分搜索怎么用?我和快手面试官进行了深度探讨](https://labuladong.github.io/article/fname.html?fname=二分分割子数组)
 - [二分搜索算法经典习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a07736e4b01a485209b0b4/1)
 - [二分查找高效判定子序列](https://labuladong.github.io/article/fname.html?fname=二分查找判定子序列)
 - [动态规划设计:最长递增子序列](https://labuladong.github.io/article/fname.html?fname=动态规划设计:最长递增子序列)
 - [双指针技巧秒杀七道数组题目](https://labuladong.github.io/article/fname.html?fname=双指针技巧)
 - [存储系统设计之 LSM 树原理](https://labuladong.github.io/article/fname.html?fname=LSM树)
 - [带权重的随机选择算法](https://labuladong.github.io/article/fname.html?fname=随机权重)
 - [快速排序详解及应用](https://labuladong.github.io/article/fname.html?fname=快速排序)
 - [我写了首诗,把滑动窗口算法变成了默写题](https://labuladong.github.io/article/fname.html?fname=滑动窗口技巧进阶)
 - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
L
labuladong 已提交
535
 - [番外:用算法打败算法](https://labuladong.github.io/article/fname.html?fname=PDF中的算法)
L
labuladong 已提交
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
 - [讲两道常考的阶乘算法题](https://labuladong.github.io/article/fname.html?fname=阶乘题目)

</details><hr>




<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>

<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>

| LeetCode | 力扣 |
| :----: | :----: |
| [1201. Ugly Number III](https://leetcode.com/problems/ugly-number-iii/?show=1) | [1201. 丑数 III](https://leetcode.cn/problems/ugly-number-iii/?show=1) |
| [162. Find Peak Element](https://leetcode.com/problems/find-peak-element/?show=1) | [162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/?show=1) |
| [240. Search a 2D Matrix II](https://leetcode.com/problems/search-a-2d-matrix-ii/?show=1) | [240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/?show=1) |
| [33. Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/?show=1) | [33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/?show=1) |
| [35. Search Insert Position](https://leetcode.com/problems/search-insert-position/?show=1) | [35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/?show=1) |
| [658. Find K Closest Elements](https://leetcode.com/problems/find-k-closest-elements/?show=1) | [658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/?show=1) |
| [74. Search a 2D Matrix](https://leetcode.com/problems/search-a-2d-matrix/?show=1) | [74. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/?show=1) |
| [793. Preimage Size of Factorial Zeroes Function](https://leetcode.com/problems/preimage-size-of-factorial-zeroes-function/?show=1) | [793. 阶乘函数后 K 个零](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/?show=1) |
| [81. Search in Rotated Sorted Array II](https://leetcode.com/problems/search-in-rotated-sorted-array-ii/?show=1) | [81. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/?show=1) |
| [852. Peak Index in a Mountain Array](https://leetcode.com/problems/peak-index-in-a-mountain-array/?show=1) | [852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/?show=1) |
| - | [剑指 Offer 04. 二维数组中的查找](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/?show=1) |
| - | [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/?show=1) |
| - | [剑指 Offer 53 - II. 0~n-1中缺失的数字](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/?show=1) |
| - | [剑指 Offer II 068. 查找插入位置](https://leetcode.cn/problems/N6YdxV/?show=1) |
| - | [剑指 Offer II 069. 山峰数组的顶部](https://leetcode.cn/problems/B1IidL/?show=1) |

</details>



571 572
**_____________**

L
labuladong 已提交
573
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
574

L
labuladong 已提交
575
![](https://labuladong.github.io/pictures/souyisou2.png)
L
labuladong 已提交
576 577


B
brucecat 已提交
578 579 580 581 582 583 584 585 586
======其他语言代码====== 

[704.二分查找](https://leetcode-cn.com/problems/binary-search)

[34.在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)



### python
L
labuladong 已提交
587

M
MarineJoker 已提交
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
[MarineJoker](https://github.com/MarineJoker) 提供 Python3 代码  

```python
# 基本二分搜索
def binarySearch(nums, target):
    left = 0
    right = len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            # 直接返回
            return mid
        elif nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid - 1
    # 直接返回
    return -1


# 寻找左侧边界的二分搜索,开区间写法
def left_bound(nums, target):
    left, right = 0, len(nums)
    if right == 0:
        return -1
    while left < right:
        mid = (left + right) // 2
        if nums[mid] == target:
M
MarineJoker 已提交
616
            # 锁定左侧边界
M
MarineJoker 已提交
617 618 619 620 621
            right = mid
        elif nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid
M
MarineJoker 已提交
622
    # 检查left越界情况
M
MarineJoker 已提交
623 624 625 626 627
    if left >= len(nums) or nums[left] != target:
        return -1
    return left


M
MarineJoker 已提交
628
# 寻找右侧边界的二分搜索,开区间写法
M
MarineJoker 已提交
629 630 631 632 633 634 635
def right_bound(nums, target):
    left, right = 0, len(nums)
    if right == 0:
        return -1
    while left < right:
        mid = (left + right) // 2
        if nums[mid] == target:
M
MarineJoker 已提交
636
            # 锁定右侧边界
M
MarineJoker 已提交
637 638 639 640 641 642
            left = mid + 1
        elif nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid
    # 检查越界情况
M
MarineJoker 已提交
643
    if left == 0 or nums[left - 1] != target:
M
MarineJoker 已提交
644 645 646 647
        return -1
    return left - 1


M
MarineJoker 已提交
648
# 寻找左侧边界的二分搜索,闭区间写法
M
MarineJoker 已提交
649 650 651 652 653
def left_bound(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
M
MarineJoker 已提交
654
            # 锁定左侧边界
M
MarineJoker 已提交
655 656 657 658 659
            right = mid - 1
        elif nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid - 1
M
MarineJoker 已提交
660
    # 检查left越界情况
M
MarineJoker 已提交
661 662 663 664
    if left >= len(nums) or nums[left] != target:
        return -1
    return left

M
MarineJoker 已提交
665
# 寻找右侧边界的二分搜索,闭区间写法
M
MarineJoker 已提交
666 667 668 669 670
def right_bound(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
M
MarineJoker 已提交
671
            # 锁定右侧边界
M
MarineJoker 已提交
672 673 674 675 676
            left = mid + 1
        elif nums[mid] < target:
            left = mid + 1
        elif nums[mid] > target:
            right = mid - 1
M
MarineJoker 已提交
677
    # 检查right越界情况
M
MarineJoker 已提交
678 679 680
    if right < 0 or nums[right] != target:
        return -1
    return right
M
MarineJoker 已提交
681
```
B
brucecat 已提交
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885



### javascript

寻找左、右侧边界的二分搜索,两端闭合

```js
let binary_search = function (nums, target) {
    if (nums.length === 0) return -1;
    let left = 0, right = nums.length - 1;
    while (left <= right) {
        let mid = Math.floor(left + (right - left) / 2);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] === target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}


let left_bound = function (nums, target) {
    if (nums.length === 0) return -1;

    let left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        let mid = left + Math.floor((right - left) / 2);
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] === target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 检查出界情况
    if (left >= nums.length || nums[left] !== target)
        return -1;
    return left;
}

// 两端都闭
let right_bound = function (nums, target) {
    if (nums.length === 0) return -1;

    let left = 0, right = nums.length - 1;
    while (left <= right) {
        let mid = left + (right - left) / 2;

        if (nums[mid] < target) {
            // 区间变到[mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 区间变到[left, mid-1]
            right = mid - 1;
        } else if (nums[mid] === target) {
            // 别返回,锁定右侧边界
            // 区间变到[mid+1, right]
            left = mid + 1;
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] !== target)
        return -1;
    return right;
}
```

[704.二分查找](https://leetcode-cn.com/problems/binary-search)

```js
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    if (nums.length === 0) return -1;
    let left = 0, right = nums.length - 1;
    while (left <= right) {
        let mid = Math.floor(left + (right - left) / 2);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] === target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
};
```



[34.在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)

按照本文的思路,可以很容易就写出答案,但会超时。

```js
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    let left = left_bound(nums, target);
    let right = right_bound(nums, target)
    return [left]
};


let left_bound = function (nums, target) {
    if (nums.length === 0) return -1;

    let left = 0, right = nums.length - 1;
    // 搜索区间为 [left, right]
    while (left <= right) {
        let mid = left + Math.floor((right - left) / 2);
        if (nums[mid] < target) {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
        } else if (nums[mid] === target) {
            // 收缩右侧边界
            right = mid - 1;
        }
    }
    // 检查出界情况
    if (left >= nums.length || nums[left] !== target)
        return -1;
    return left;
}

// 两端都闭
let right_bound = function (nums, target) {
    if (nums.length === 0) return -1;

    let left = 0, right = nums.length - 1;
    while (left <= right) {
        let mid = left + (right - left) / 2;

        if (nums[mid] < target) {
            // 区间变到[mid+1, right]
            left = mid + 1;
        } else if (nums[mid] > target) {
            // 区间变到[left, mid-1]
            right = mid - 1;
        } else if (nums[mid] === target) {
            // 别返回,锁定右侧边界
            // 区间变到[mid+1, right]
            left = mid + 1;
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] !== target)
        return -1;
    return right;
}
```

现通过lower变量来确定,当前的二分法是向左找还是向右找。

```js
const binarySearch = (nums, target, lower) => {
    let left = 0, right = nums.length - 1, ans = nums.length;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        if (nums[mid] > target || (lower && nums[mid] >= target)) {
            right = mid - 1;
            ans = mid;
        } else {
            left = mid + 1;
        }
    }
    return ans;
}

var searchRange = function(nums, target) {
    let ans = [-1, -1];
    const leftIdx = binarySearch(nums, target, true);
    const rightIdx = binarySearch(nums, target, false) - 1;
    if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
        ans = [leftIdx, rightIdx];
    } 
    return ans;
};

```