文章目录
🍋二分查找原理
定义
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
经典例题描述
input:[1,2,3,4,5]
key:3
return the index:2
代码实现
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (nums[m] == key) {
return m;
} else if (nums[m] > key) {
h = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
原理分析
时间复杂度:O(logN)
middle计算:
有两种计算中值m的方式:
-
m=(l+r)/2
缺点:l+r可能出现加法溢出的情况。
-
m=l+(r-l)/2
🍋二分查找拓展
经典例题描述
在一个有重复元素的数组中查找 key 的最左位置。
代码实现
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= key) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
原理分析
与正常版的不同:
- 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 时循环无法退出的情况:
nums = {0, 1, 2}, key = 1
l m h
0 1 2 nums[m] >= key
0 0 1 nums[m] < key
1 1 1 nums[m] >= key
...
当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
🍋1. 求开方
题目描述链接
https://leetcode.cn/problems/sqrtx/description/
题目特点
使用O(logN)的时间复杂度解决该问题,非递减的一维数组。
代码实现
使用二分查找(正常版本)。
public int mySqrt(int x) {
int l=0,r=x,ans=-1;
while(l<=r){
int mid=l+(r-l)/2;
if ((long)mid*mid<=x){
ans=mid;
l=mid+1;
}else{
r=mid-1;
}
}
return ans;
}
时间复杂度和空间复杂度
时间复杂度:O(logN);
空间复杂度:O(1)。
🍋2. 大于给定元素的最小元素
题目描述链接
https://leetcode.cn/problems/find-smallest-letter-greater-than-target/description/
题目特点
使用O(logN)的时间复杂度解决该问题,非递减的一维数组。
代码实现
使用二分查找(正常版本)。
public char nextGreatestLetter(char[] letters, char target) {
int left=0,right=letters.length-1,n=letters.length;
while(left<=right){
int mid=left+(right-left)/2;
if (letters[mid]<=target){
left=mid+1;
}else{
right=mid-1;
}
}
return left<n?letters[left]:letters[0];
}
时间复杂度和空间复杂度
时间复杂度:O(logN);
空间复杂度:O(1)。
🍋3. 有序数组的 Single Element
题目描述链接
https://leetcode.cn/problems/single-element-in-a-sorted-array/description/
题目特点
使用O(logN)的时间复杂度解决该问题,非递减的一维数组,找出分割点。
代码实现
使用二分查找(拓展版本)。
public int singleNonDuplicate(int[] nums) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (m % 2 == 1) {
m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
}
if (nums[m] == nums[m + 1]) {
l = m + 2;
} else {
h = m;
}
}
return nums[l];
}
时间复杂度和空间复杂度
时间复杂度:O(logN);
空间复杂度:O(1)。
🍋4. 第一个错误的版本题目描述链接
题目描述链接
https://leetcode.cn/problems/first-bad-version/description/
题目特点
使用O(logN)的时间复杂度解决该问题,找出两极分化的分割点。
代码实现
使用二分查找(拓展版本)。
public int firstBadVersion(int n) {
int left=0,right=n-1;
while(left<=right){
int mid=left+(right-left)/2;
if (isBadVersion(mid)){
right=mid-1;
}else{
left=mid+1;
}
}
return left;
}
时间复杂度和空间复杂度
时间复杂度:O(logN);
空间复杂度:O(1)。
🍋5. 旋转数组的最小数字
题目描述链接
https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/
题目特点
使用O(logN)的时间复杂度解决该问题,非递减的一维数组(类比),找出分割点。
代码实现
使用二分查找(拓展版本)。
public int findMin1(int[] nums) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] <= nums[h]) {
h = m;
} else {
l = m + 1;
}
}
return nums[l];
}
时间复杂度和空间复杂度
时间复杂度:O(logN);
空间复杂度:O(1)。
🍋6. 查找区间
题目描述链接
https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
题目特点
使用O(logN)的时间复杂度解决该问题,非递减的一维数组,找出分割点。
代码实现
使用二分查找(拓展版本)。
public int[] searchRange1(int[] nums, int target) {
int first = findFirst(nums, target);
int last = findFirst(nums, target + 1) - 1;
if (first == nums.length || nums[first] != target) {
return new int[]{-1, -1};
} else {
return new int[]{first, Math.max(first, last)};
}
}
private int findFirst(int[] nums, int target) {
int l = 0, h = nums.length; // 注意 h 的初始值
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= target) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
时间复杂度和空间复杂度
时间复杂度:O(logN);
空间复杂度:O(1)。
🍋题型总结
二分查找使用的条件:
-
题目数据特征有类一维数组或一维数组的数据结构;
解释:类一维数组,例如一个布尔数组,前面一部分都是false,后面一部分都是true,这样的数组默认叫它类一维数组。
-
一维数组的排序有非递减/非递增趋势。
注意点:
- 依赖数组的下标;
- 不适用数据量较少的情况,数据量少,耗费时间长;
- 不适用数据量较多的情况,数据量多,电脑存储可能就是离散的,不满足使用二分查找的条件。
优势:把时间复杂度从O(N)降低到O(log2N)。