算法思想 - 二分查找算法
# 二分查找理论
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用 顺序存储结构 (opens new window),而且表中元素按关键字有序排列。
首先,假设表中元素是按升序排列,将表中间位置记录的 关键字 (opens new window) 与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置 记录 (opens new window)将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的 记录 (opens new window),使查找成功,或直到子表不存在为止,此时查找不成功。
二分查找是我们降低算法复杂度的主要手段之一,只要我们可以题目中存在:
- 有序
- 查找
两个因素,就可以用二分查找降低时间复杂度。
在最开始的阶段,二分查找的难点在于识别有序和查找。后面的难点在于如果通过建模手段把题目的数据变得存在有序和查找。
# 方式
二分查找的精髓就是 3 点:
- 目标值小于查找值时怎么办
- 目标值等于查找值时怎么办
- 目标值大于查找值时怎么办
二分查找这么看就成为了填空题。
二分查找的作用就是在 Logn 的时间复杂度内找到想要的数据。共有 5 种类型的二分查找方式:
- 数据无重复查找数据
- 数据有重复查找小于该数的最后一个数字的位置
- 数据有重复查找该数字第一次出现的位置
- 数据有重复查找该数字最后一次出现的位置
- 数据有重复查找第一个大于该数的数字的位置
下面针对这五类进行代码介绍。
# 分类
第一类:数据无重复查找数据
位置:0 1 2 3 4 5 6 7 8 9
数据:1 2 3 4 5 6 7 8 9 10
代码:
public int binarySearch(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int middle = left + (right - left) / 2;
if (numbers[middle] == target) {
return middle;
}else if (numbers[middle] > target) {
right = middle - 1;
}else {
left = middle + 1;
}
}
return -1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第二类:数据有重复查找小于该数的最后一个数字的位置
位置:0 1 2 3 4 5 6 7 8 9
数据:1 1 2 2 3 3 3 3 4 4
代码:
public int binarySearch(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int middle = left + (right - left + 1) / 2;
if (numbers[middle] == target) {
right = middle - 1;
}else if (numbers[middle] > target) {
right = middle - 1;
}else {
left = middle;
}
}
return left;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第三类:数据有重复查找该数字第一次出现的位置
位置:0 1 2 3 4 5 6 7 8 9
数据:1 1 2 2 3 3 3 3 4 4
代码:
public int binarySearch(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int middle = left + (right - left) / 2;
if (numbers[middle] == target) {
right = middle;
}else if (numbers[middle] > target) {
right = middle;
}else {
left = middle + 1;
}
}
return left;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第四类:数据有重复查找该数字最后一次出现的位置
位置:0 1 2 3 4 5 6 7 8 9
数据:1 1 2 2 3 3 3 3 4 4
代码:
public int binarySearch(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int middle = left + (right - left + 1) / 2;
if (numbers[middle] == target) {
left = middle;
}else if (numbers[middle] > target) {
right = middle - 1;
}else {
left = middle;
}
}
return left;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第五类:数据有重复查找第一个大于该数的数字的位置
位置:0 1 2 3 4 5 6 7 8 9
数据:1 1 2 2 3 3 3 3 4 4
代码:
public int binarySearch(int[] numbers, int target) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int middle = left + (right - left ) / 2;
if (numbers[middle] == target) {
left = middle + 1;
}else if (numbers[middle] > target) {
right = middle;
}else {
left = middle + 1;
}
}
return left;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 实战
题目来自力扣。
# 搜索插入位置
题目
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3
输入: nums = [1,3,5,6], target = 7
输出: 4
思路与算法
假设题意是叫你在排序数组中寻找是否存在一个目标值,那么训练有素的读者肯定立马就能想到利用二分法在 O(\log n)O(logn) 的时间内找到是否存在目标值。但这题还多了个额外的条件,即如果不存在数组中的时候需要返回按顺序插入的位置,那我们还能用二分法么?答案是可以的,我们只需要稍作修改即可。
考虑这个插入的位置 pos
,它成立的条件为:
nums[pos−1] < target ≤ nums[pos]
其中 nums 代表排序数组。由于如果存在这个目标值,我们返回的索引也是 pos,因此我们可以将两个条件合并得出最后的目标:「在一个有序数组中找第一个大于等于 target 的下标」。
问题转化到这里,直接套用二分法即可,即不断用二分法逼近查找第一个大于等于 target 的下标 。下文给出的代码是笔者习惯的二分写法,ans 初值设置为数组长度可以省略边界条件的判断,因为存在一种情况是 target 大于数组中的所有数,此时需要插入到数组长度的位置。
代码
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return mid;
}else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return left;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 有序数组中的单一元素
540. 有序数组中的单一元素 (opens new window)
题目
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
示例 1
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2
输入: nums = [3,3,7,7,10,11,11]
输出: 10
思路与算法
因为找出唯一一个没有重复的数,且其他相同的数只有两个,所以利用二分查找时,根据 mid 和它的前后来进行判断。
那么怎么知道不重复的在 mid 的左边还是右边呢?
根据 mid 的左侧和右侧个数来判断,因为 mid 指的是下标。
假设 mid 是偶数,则前面有
2 * n
的数,如果nums[mid] == nums[mid - 1]
,则代表左侧必有不重复的数,因为前面原本有2 * n
的数,则假设正好存在n / 2
个重复的数,但是显然nums[mid] == nums[mid - 1]
,所以左侧必有唯一的数。(0 - mid 之间不取 mid 是偶数,取了 mid 则是奇数,奇数之间肯定有一个唯一的数)假设 mid 是奇数,则前面有
2 * n - 1
个数,如果nums[mid] == nums[mid - 1]
,则代表右侧的必有不重复的数,因为前面原本有2 * n - 1
的数,则假设正好存在 2 个重复的数 + 一个不重复的数,但是显然nums[mid] == nums[mid - 1]
,所以右侧必有唯一的数。(0 - mid 之间不取 mid 是奇数,取了 mid 则是偶数,偶数之间不存在唯一的数,所以只有右侧有)
nums[mid] == nums[mid + 1]
同理 ......
如果最终 mid 是 0 或者 nums.length - 1
,则 0 或者 nums.length - 1
就是唯一数的下标。
代码
class Solution {
public int singleNonDuplicate(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if(mid == 0) {
return nums[0];
}else if(mid == nums.length - 1) {
return nums[nums.length - 1];
}
if (nums[mid] == nums[mid - 1]) {
// mid 指的是下标,如果 mid 是偶数(则 mid 前面一定有 2 * n 个数),则 mid 左边的数字一定是唯一的,如 nums = [1, 1, 2, 3, 3, 5, 5, 8, 8]
if (mid % 2 == 0) {
right = mid - 1;
}
// 如果 mid 是奇数(则 mid 前面一定有 2 * n - 1 个数),则 mid 右边的数字一定是唯一的
else {
left = mid + 1;
}
} else if (nums[mid] == nums[mid + 1]) {
// 如果 mid 是偶数(则 mid 前面一定有 2 * n 个数),则 mid 右边的数字一定是唯一的,因为满足了 nums[mid] == nums[mid + 1]
if (mid % 2 == 0) {
left = mid + 1;
}
// 如果 mid 是奇数(则 mid 前面一定有 2 * n - 1 个数),则 mid 左边的数字一定是唯一的
else {
right = mid - 1;
}
} else {
return nums[mid];
}
}
return nums[left];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36