目录
零、二分查找介绍
0.1 折半查找时间复杂度
二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(log n)。
0.2 mid 计算
有两种计算中值 m 的方式:
- m = (left + right) / 2
- m = left + (right - left) / 2
left + right可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围。但是left和 right 都为正数,因此 right - left 不会出现加法溢出问题。所以,最好使用第二种计算法方法。
若有一方赋值条件为 left=mid,或者right=mid,则循环判断条件就为while(left<right),即去掉等于号。
0.3二分查找的三种形式
如果判断条件时while(left<right),则终止状况为left=right
一、69. x 的平方根
1.1 题目描述
实现 int sqrt(int x) 函数。(难度简单)
计算并返回 x 的平方根,其中 x 是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:输入: 4
输出: 2示例 2:输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
1.2 代码
1.2.1 二分法
由于 x 平方根的整数部分ans 是满足 k^2 ≤x 的最大 k 值,因此我们可以对 k 进行二分查找,从而得到答案。
二分查找的下界为 0,上界可以粗略地设定为 x。在二分查找的每一步中,我们只需要比较中间元素mid 的平方与 x 的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案ans 后,也就不需要再去尝试ans+1 了。
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/26 10:46
* @description :69. x 的平方根
* 二分法
**/
public class mySqrt_02 {
public static void main(String[] args) {
int x=8;
//int x=1;
//int x=2147483647;//发生了溢出
mySqrt(x);
}
public static int mySqrt(int x) {
int left = 0;
int right = x;
int ans = -1;//记录<=x的最大mid值
while (left <=right) {
int mid = left + (right - left); //推荐使用
if ((long) mid * mid <= x) {
ans=mid;
left = mid + 1;
} else
right = mid - 1;
}
System.out.println(ans);
return ans;
}
}
复杂度分析
时间复杂度:O(logx),即为二分查找需要的次数。
空间复杂度:O(1)。
1.2.2 牛顿法
牛顿法思路:https://leetcode-cn.com/problems/sqrtx/solution/er-fen-cha-zhao-niu-dun-fa-python-dai-ma-by-liweiw/
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/26 14:00
* @description :69. x 的平方根
* 直接用根号下x,发生了溢出
* 现在使用的牛顿法
*
**/
public class mySqrt_01 {
public static void main(String[] args) {
//int x=8;
//int x=1;
int x=2147483647;//发生了溢出
System.out.println(mySqrt(x));
}
public static int mySqrt(int x) {
long ans = x;
while (ans * ans > x) {
ans = (ans + x / ans) / 2;
}
return (int) ans;
}
}
二、744. 寻找比目标字母大的最小字母
2.1 题目描述
给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。(难度简单)
在比较时,字母是依序循环出现的。举个例子:如果目标字母 target = 'z' 并且字符列表为 letters = ['a', 'b'],则答案返回 'a'
示例:
输入:letters = ["c", "f", "j"]
target = "a"
输出: "c"输入:letters = ["c", "f", "j"]
target = "c"
输出: "f"输入:letters = ["c", "f", "j"]
target = "d"
输出: "f"输入:letters = ["c", "f", "j"]
target = "g"
输出: "j"输入:letters = ["c", "f", "j"]
target = "j"
输出: "c"输入:letters = ["c", "f", "j"]
target = "k"
输出: "c"
提示:letters长度范围在[2, 10000]区间内。
letters 仅由小写字母组成,最少包含两个不同的字母。
目标字母target 是一个小写字母。
2.2 代码
2.2.1 二分法
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/27 9:15
* @description :744. 寻找比目标字母大的最小字母(难度简单)
* //字母是依次循环出现的
**/
public class nextGreatestLetter_01 {
public static void main(String[] args) {
char[] letters = {'c','f','j'};
char target = 'z';
nextGreatestLetter(letters,target);
}
public static char nextGreatestLetter(char[] letters, char target) {
int left= 0;int n=letters.length;
int right =n-1;
while (left<=right){
int mid = left+(right-left)/2;
if (letters[mid]<target){
left=mid+1;
}
else right=mid-1;
}
return letters[left%letters.length];
//return left < n ? letters[left] : letters[0];
}
}
复杂度分析
- 时间复杂度:O(log n)。n 指的是 letters 的长度,我们只查看数组中的logn 个元素。
- 空间复杂度:O(1)。只使用了常数个指针。
三、540. 有序数组中的单一元素
3.1 题目描述
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。(难度中等)
示例 1:输入: [1,1,2,3,3,4,4,8,8]
输出: 2示例 2:输入: [3,3,7,7,10,11,11]
输出: 10
注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
3.2 代码
3.2.1 二分法
1.只查偶数位,若偶数位的后方还为自己,则说明单个元素在mid之后,否则,单个元素在mid之前,保证mid为偶数。
2.若mid为奇数,则需要将mid变为偶数,于是mid--,若改为mid++,则nums[mid] == nums[mid + 1]中mid+1可能会越界。
3. 如果 nums[mid] == nums[mid + 1],那么 index 所在的数组位置为 [mid + 2, right],此时令 left = mid + 2;如果 nums[mid] != nums[mid + 1],那么 index 所在的数组位置为 [left, mid],此时令 right = mid。(例如[1,1,2,3,3]的mid为2,此位置也可能是单个元素的位置)
4.因为 right 的赋值表达式为 right = mid,那么循环条件也就只能使用 left < right 这种形式。
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/27 10:05
* @description :540. 有序数组中的单一元素(难度中等)
* 只查偶数位,若偶数位的后方还为自己,则说明单个元素在mid之后,否则,单个元素在mid之前,保证mid为偶数
*
* 如果 nums[mid] == nums[mid + 1],那么 index 所在的数组位置为 [mid + 2, right],
* 此时令 left = mid + 2;如果 nums[mid] != nums[mid + 1],那么 index 所在的数组位置为 [left, mid],此时令 right= mid。
* 因为 right 的赋值表达式为 right= mid,那么循环条件也就只能使用 left < right这种形式。
**/
public class singleNonDuplicate_01 {
public static void main(String[] args) {
//int nums[]={1,1,2,3,3,4,4,8,8};
int nums[]={1,1,2,3,3};
singleNonDuplicate(nums);
}
public static int singleNonDuplicate(int[] nums) {
int left=0;
int right= nums.length-1;
while(left<right){
//只查偶数位,若偶数位的后方还为自己,则说明单个元素在mid之后,否则,单个元素在mid之前,保证mid为偶数
int mid = left+(right-left)/2;
if (mid%2==1) mid--; //++有可能越界
if (nums[mid]==nums[mid+1]){
left =mid+2;
}
else right=mid;
}
return nums[left];
}
}
复杂度分析
- 时间复杂度:O(log n)。我们仅对元素的一半进行二分搜索。
- 空间复杂度:O(1),仅用了常数的空间。
四、278. 第一个错误的版本
4.1 题目描述
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。(难度简单)
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
示例: 给定 n = 5,并且 version = 4 是第一个错误的版本。
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true所以,4 是第一个错误的版本。
4.2 代码
4.2.2 二分法
注意:isBadVersion(mid)==ture 说明为错误版本,返回第一个为true的版本
若mid版出错,则说明出错的在[left,mid],令right=mid。
若mid版正确,则说明出错的在[mid+1,right],令left=md+1。
因为 right 的赋值表达式为right=mid,因此循环条件为 left < right。
/**
* @author : lkw
* @data : 2021/3/29 8:26
* @description :278. 第一个错误的版本(简单)
**/
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left=1;
int right=n;
int mid;
//若mid版出错,则说明出错的在[l,mid]
//若mid版正确,则说明出错的在[mid+right]
while(left<right){
mid = left+(right-left)/2;
//isBadVersion(mid)==ture 说明为错误版本
if (!isBadVersion(mid)){
//如果是正确版本,说明错版本在后面
left=mid+1;
}
else right=mid;
}
return left;
}
}
- 时间复杂度:O(logn)。搜索空间每次减少一半,因此时间复杂度为O(logn)。
- 空间复杂度:O(1)。
五、153. 寻找旋转排序数组中的最小值
5.1 题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] 。(难度中等)
请找出其中最小的元素。
示例 1:输入:nums = [3,4,5,1,2]
输出:1
示例 2:输入:nums = [4,5,6,7,0,1,2]
输出:0示例 3:输入:nums = [1]
输出:1
5.2 代码
5.2.1 数组遍历
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/29 8:58
* @description :153. 寻找旋转排序数组中的最小值(难度中等)
**/
public class findMin_01 {
public static void main(String[] args) {
int nums[]={3,4,5,1,2};
findMin(nums);
}
public static int findMin(int[] nums) {
int len= nums.length;
int m=nums[0];
for (int i = 0; i <len-1 ; i++) {
if (nums[i]>nums[i+1]){
m=nums[i+1];
}
}
return m;
}
}
- 时间复杂度:O(n),n为数组长度
- 空间复杂度:O(1)
5.2.2 二分法
若mid的值小于等于右边界值,则旋转最小数字应在mid左侧区域,并且包含mid,[left,mid],因此right=mid;
若mid的值大于右边界值,则旋转最小数字在mid右侧区域,并且肯定不是mid(因为mid的值大,不是最小数组),因此区间调整为[mid+1,right],因此left=mid+1;
因为 right 的赋值表达式为right=mid,因此循环条件为 left < right。
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/29 9:26
* @description :153. 寻找旋转排序数组中的最小值(难度中等)
* 二分法
**/
public class findMin_02 {
public static void main(String[] args) {
int nums[]={3,4,5,1,2};
System.out.println(findMin(nums));
}
public static int findMin(int[] nums) {
int len= nums.length;
int left=0;
int right=len-1;
while(left<right){
int mid = left+(right-left)/2;
//mid的值小于等于右边界值,在其左侧,并且包含mid,[left,mid]
if (nums[mid]<=nums[right]){
right=mid;
}//mid的值大于右边界值,则旋转最小数字在mid右侧区域,并且肯定不是mid(因为mid大,不是最小数组),因此区间调整为[mid+1,right]
else left=mid+1;
}
return nums[left];
}
}
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
六、34. 在排序数组中查找元素的第一个和最后一个位置
6.1 题目描述
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。(难度中等)
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
示例 1:输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]示例 2:输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]示例 3:输入:nums = [], target = 0
输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109
6.2 代码
6.2.1 二分法
二分法的三种写法:
1. 第一次出现的位置和最后出现的位置由两个函数书写:
import java.util.Arrays;
public class Solution {
public int[] searchRange(int[] nums, int target) {
if (nums.length == 0) {
return new int[]{-1, -1};
}
int firstPosition = findFirstPosition(nums, target);
int lastPosition = findLastPosition(nums, target);
return new int[]{firstPosition, lastPosition};
}
private int findFirstPosition(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// ① 不可以直接返回,应该继续向左边找,即 [left, mid - 1] 区间里找
right = mid - 1;
} else if (nums[mid] < target) {
// 应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
} else {
// 此时 nums[mid] > target,应该继续向左边找,即 [left, mid - 1] 区间里找
right = mid - 1;
}
}
// 此时 left 和 right 的位置关系是 [right, left],注意上面的 ①,此时 left 才是第 1 次元素出现的位置
// 因此还需要特别做一次判断
if (left != nums.length && nums[left] == target) {
return left;
}
return -1;
}
private int findLastPosition(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 只有这里不一样:不可以直接返回,应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
} else if (nums[mid] < target) {
// 应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
} else {
// 此时 nums[mid] > target,应该继续向左边找,即 [left, mid - 1] 区间里找
right = mid - 1;
}
}
if (right != -1 && nums[right] == target) {
return right;
}
return -1;
}
}
找第一个位置和最后一个位置的区别仅仅在于nums[mid] == target:
找第一个位置
if (nums[mid] == target) {
// ① 不可以直接返回,应该继续向左边找,即 [left..mid - 1] 区间里找
right = mid - 1;
最后一个位置:
if (nums[mid] == target) {
// 只有这里不一样:不可以直接返回,应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
因此可以将两个函数进行合并。
2.合并两个函数:
package BinarySearch;
/**
* @author : lkw
* @data : 2021/3/29 9:53
* @description :34. 在排序数组中查找元素的第一个和最后一个位置(难度中等)
**/
public class searchRange_01 {
public static void main(String[] args) {
int[] nums = {5,7,7,8,8,10};
int tatget= 8;
System.out.println(searchRange(nums, tatget));
}
public static int[] searchRange(int[] nums, int target) {
int leftIdx = binarySearch(nums, target, true);//找第一个出现的
int rightIdx = binarySearch(nums, target, false) - 1;//找最后一位
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
//判断找的的是否在数组内,找到的是否是目标元素
return new int[]{leftIdx, rightIdx};
}
return new int[]{-1, -1};
}
public static int binarySearch(int[] nums, int target, boolean lower) {
int left = 0, right = nums.length - 1;
int ans = nums.length;//??
while (left <= right) {
int mid = left+(right-left ) / 2;
if (nums[mid] > target || (lower && nums[mid]== target)) {
//nums[mid] > target或者lower==true且nums[mid]= target进入循环(即找左边界)
right = mid - 1;
ans = mid;
} else {
//nums[mid] < target的情况,或者找lower==false且nums[mid]= target(即找右边界)
left = mid + 1;
}
}
return ans;
}
}