二分查找
69.求开方
给你一个非负整数 x
,计算并返回 x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 231 - 1
思路
- 二分查找
- 第一次使用left < right这种循环条件,之前一直不知道什么区别
- 最终循环退出后,这种的循环条件的left和right是一定重合的
- 用之前的left<=right这种循环条件,有时出现left出现或者right出现
- 那么这个时候就要对循环退出后的结果进行额外判断,所以最好用left < right
- 同时注意下如果搜索区间是left=mid的转化的话,mid要向上取整,目前先记住这个点
极力推荐看b站liweiwei1419的二分查找专栏
class Solution {
public int mySqrt(int x) {
if(x == 0) return 0;
if(x < 4) return 1;
int left = 2, right = x / 2;
while(left < right) {
int mid = (left + right + 1) / 2;
if(mid <= x / mid) {
// 搜索区间 [mid, right],mid有可能是最大的小于等于根号x的数下标
left = mid;
}else {
// 搜索区间[left, mid - 1],mid是大于根号x的,不可能是答案
right = mid - 1;
}
}
// 最终left和right会重合
return left;
}
}
744.寻找比目标字母大的最小字母
给你一个字符数组 letters
,该数组按非递减顺序排序,以及一个字符 target
。letters
里至少有两个不同的字符。
返回 letters
中大于 target
的最小的字符。如果不存在这样的字符,则返回 letters
的第一个字符。
示例 1:
输入: letters = ["c", "f", "j"],target = "a"
输出: "c"
解释:letters 中字典上比 'a' 大的最小字符是 'c'。
示例 2:
输入: letters = ["c","f","j"], target = "c"
输出: "f"
解释:letters 中字典顺序上大于 'c' 的最小字符是 'f'。
示例 3:
输入: letters = ["x","x","y","y"], target = "z"
输出: "x"
解释:letters 中没有一个字符在字典上大于 'z',所以我们返回 letters[0]。
提示:
2 <= letters.length <= 104
letters[i]
是一个小写字母letters
按非递减顺序排序letters
最少包含两个不同的字母target
是一个小写字母
思路
- 很容易想到,二分查找
- 类似于这种查找比目标的最大或者最小的值,都是在对mid判断的时候做一些特殊处理
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
int left = 0, right = letters.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
// 这样的特殊处理,会使得left在最终指向的就是第一个大于target的下标
if(letters[mid] <= target) {
left = mid + 1;
}else {
right = mid - 1;
}
}
// 如果left都越界了,那就说明没有大于target的字母,就按题目返回letters[0]
return left == letters.length ? letters[0] : letters[left];
}
}
540.有序数组中的单一元素
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 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
提示:
1 <= nums.length <= 105
0 <= nums[i] <= 105
思路
- 数组中每个元素都是成对出现,那么正常情况下都是偶数下标为第一个,奇数下标是第二个
- 若此时出现了单一元素,那么单一元素之前的元素依旧满足上面的结论,但是之后的元素结论相反
- 所以通过这个来判断left动还是right动
class Solution {
public int singleNonDuplicate(int[] nums) {
int len = nums.length;
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = (left + right) / 2;
if(mid % 2 == 1) { // m为奇数下标
if(mid - 1 >= 0 && nums[mid] == nums[mid - 1]) {
left = mid + 1;
}else {
right = mid - 1;
}
}else { // m为偶数下标
if(mid + 1 < len && nums[mid] == nums[mid + 1]) {
left = mid + 1;
}else {
right = mid - 1;
}
}
}
/**
为什么是return nums[left]?
如果此时mid已经达到了那个唯一元素
那么不管唯一元素下标是奇数还是偶数,他只会执行right = mid - 1
那么right最后会到mid前面一个数,而left总是会到达mid
*/
return nums[left];
}
}
153.寻找旋转排序数组的最小值
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums
中的所有整数 互不相同nums
原来是一个升序排序的数组,并进行了1
至n
次旋转
思路
- 见代码
class Solution {
public int findMin(int[] nums) {
int len = nums.length;
int left = 0, right = len - 1;
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] < nums[right]) {
// nums[mid]可能是最小值,right缩小到mid
right = mid;
}else {
// nums[mid] >= nums[right],最小值就肯定在[mid+1,right]区间
left = mid + 1;
}
}
// 最终left和right重合
return nums[left];
}
}
34. 在排序数组中查找元素的第一个和最后一个位置下标
给你一个按照非递减顺序排列的整数数组 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
思路
- 针对代码两个方法为什么一个是nums[mid] >= target,一个是nums[mid]<= target 做解释
- nums[mid] >= target用来查找第一个位置,right=mid再向左收缩,最终收缩到第一个元素
- 如果你是用nums[mid] <= target来查找第一个元素的位置,向右收缩只会取到最后一个元素位置
class Solution {
public int[] searchRange(int[] nums, int target) {
if(nums.length == 0) return new int[]{-1, -1};
int first = findFirstPosition(nums, target);
if(first == -1) {
return new int[]{-1, -1};
}
int last = findLastPosition(nums, target);
return new int[]{first, last};
}
private int findFirstPosition(int[] nums, int target) {
int len = nums.length;
int left = 0, right = len - 1;
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] >= target) {
// nums[mid]有可能是第一个元素,而且mid之后不可能是第一个元素
// 区间缩小到[left, mid]
right = mid;
}else {
left = mid + 1;
}
}
if(nums[left] != target) return -1;
return left;
}
private int findLastPosition(int[] nums, int target) {
int len = nums.length;
int left = 0, right = len - 1;
while(left < right) {
int mid = (left + right + 1) / 2;
if(nums[mid]<= target) {
// nums[mid]有可能是第最后元素,而且mid之前不可能是最后元素
// 区间缩小到[mid, right]
left = mid;
}else {
right = mid - 1;
}
}
return left;
}
}
35.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 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
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
为 无重复元素 的 升序 排列数组-104 <= target <= 104
class Solution {
// 题目的最终意思就是说查找第一个大于等于目标元素的值的下标位置
public int searchInsert(int[] nums, int target) {
if(target > nums[nums.length - 1]) {
return nums.length;
}
int left = 0, right = nums.length - 1;
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] < target) { // 根据题意来写if else,这个很重要
left = mid + 1;
}else {
right = mid;
}
}
return left;
}
}