给一个有序数组和目标值,找第一次/最后一次/任何一次出现的索引,如果没有出现返回-1
模板四点要素
- 1、初始化:start=0、end=len-1
- 2、循环退出条件:start + 1 < end
- 3、比较中点和目标值:A[mid] ==、 <、> target
- 4、判断最后两个元素是否符合:A[start]、A[end] ? target
时间复杂度 O(logn),使用场景一般是有序数组的查找
典型示例
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
C++版本
// 二分搜索最常用模板
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
int left = 0, right = nums.size() - 1;
//处理循环
while(left + 1 < right){
int middle = left + (right - left) / 2;
if(nums[middle] == target) return middle;
else if(nums[middle] < target) left = middle; //比较middle跟target的值
else right = middle;
}
//最后还剩下left right两个值,做单独判断
if(nums[left] == target) return left;
if(nums[right] == target) return right;
return -1;
}python3版本
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] == target: return mid
elif nums[mid] < target: left = mid
else: right = mid
if nums[left] == target: return left
if nums[right] == target: return right
return -1大部分二分查找类的题目都可以用这个模板,然后做一点特殊逻辑即可
另外二分查找还有一些其他模板如下图,大部分场景模板#3 都能解决问题,而且还能找第一次/最后一次出现的位置,应用更加广泛
所以用模板#3 就对了,详细的对比可以这边文章介绍:二分搜索模板
如果是最简单的二分搜索,不需要找第一个、最后一个位置、或者是没有重复元素,可以使用模板#1,代码更简洁
// 无重复元素搜索时,更方便
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
int left = 0, right = nums.size() - 1;
//处理循环
while(left <= right){
int middle = left + (right - left) / 2;
if(nums[middle] == target) return middle;
else if(nums[middle] < target) left = middle-1; //比较middle跟target的值
else right = middle+1;
}
// 如果找不到,start 是第一个大于target的索引
// 如果在B+树结构里面二分搜索,可以return start
// 这样可以继续向子节点搜索,如:node:=node.Children[start]
return -1
}给你一个非负整数 x ,计算并返回 x 的 算术平方根 。 由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。 注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
Python版本
class Solution:
def mySqrt(self, x: int) -> int:
if x == 0: return 0
left, right = 1, x//2 + 1
while left + 1 < right:
mid = left + (right - left) // 2
if mid * mid == x: return mid
elif mid * mid < x: left = mid
else: right = mid
if left * left > x: return left - 1
elif right * right <= x: return right
else: return left给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。 进阶:不要 使用任何内置的库函数,如 sqrt 。
思路:只要num大于4,其平方数肯定不会超过num//2,这样可以减少一次二分运算,提升速度.
Python版本
class Solution:
def isPerfectSquare(self, num: int) -> bool:
left, right = 1, num // 2 + 1
while left + 1 < right:
mid = left + (right - left) // 2
if mid * mid == num: return True
elif mid * mid < num: left = mid
else:
right = mid
if left * left == num or right * right == num: return True
else: return False实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,x^n )。
思路:使用快速幂思想,二分法解决,需要注意奇偶数的不同
Python版本
class Solution:
def myPow(self, x: float, n: int) -> float:
if x == 0: return 0
res = 1.0
m = abs(n)
while m > 0:
if m % 2: res = res * x
x = x * x
m = m // 2
if n < 0: return 1. / res
return res给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母 target,请你寻找在这一有序列表里比目标字母大的最小字母。
在比较时,字母是依序循环出现的。举个例子:
- 如果目标字母 target = 'z' 并且字符列表为 letters = ['a', 'b'],则答案返回 'a'
Python版本
class Solution:
def nextGreatestLetter(self, letters: List[str], target: str) -> str:
if letters[0] > target or target >= letters[-1]: return letters[0]
left, right = 0, len(letters) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if letters[mid] <= target: left = mid #尽量往左移动
else: right = mid
if letters[left] > target: return letters[left]
return letters[right]给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序。
Python版本
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1, nums2 = sorted(set(nums1)), sorted(set(nums2))
if len(nums1) <= len(nums2): return inter(nums1, nums2)
else: return inter(nums2, nums1)
def inter(nums1, nums2):
res = list()
for i, num in enumerate(nums1):
if find(num, nums2): res.append(num)
return res
def find(num, nums):
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] == num: return True
elif nums[mid] < num: left = mid
else: right = mid
if nums[left] != num and nums[right] != num: return False
return True给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
// 思路:需要考虑找到的数量
Python版本
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1, nums2 = sorted(nums1), sorted(nums2)
if len(nums1) <= len(nums2): return inter(nums1, nums2)
else: return inter(nums2, nums1)
def inter(nums1, nums2):
res = list()
i = 0
while i < len(nums1):
num = nums1[i]
cnt1 = 1
while i+1 < len(nums1) and nums1[i+1] == num:
cnt1 += 1
i += 1
cnt2 = findNum(num, nums2)
res.extend([num] * min(cnt1, cnt2))
i += 1
return res
def findNum(num, nums):
left = find(num, nums, find_left=True)
if left == -1: return 0
right = find(num, nums, find_left=False)
return right - left + 1
def find(num, nums, find_left):
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] < num: left = mid
elif nums[mid] > num: right = mid
else:
if find_left: right = mid
else: left = mid
if nums[left] != num and nums[right] != num: return -1
if find_left:
if nums[left] == num: return left
else: return right
else:
if nums[right] == num: return right
else: return left给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。你所设计的解决方案必须只使用常量级的额外空间。
//思路: 两种解题思路,一是二分,第二种是双指针,肯定双指针耗时更优 Python版本一:二分查找
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
'''
这里使用二分法来找; 也可以使用双指针法
'''
for i, num in enumerate(numbers):
right = find(target - num, i + 1, len(numbers) - 1, numbers)
if right != -1: return [i+1, right+1]
return [-1, -1]
def find(num, lo, hi, nums):
while lo + 1 < hi:
mid = lo + (hi - lo) // 2
if nums[mid] >= num: hi = mid
else: lo = mid
if nums[lo] == num: return lo
elif nums[hi] == num: return hi
else: return -1Python版本二:双指针
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers) - 1
while left < right:
if numbers[left] + numbers[right] == target: return [left + 1, right + 1]
elif numbers[left] + numbers[right] > target: right = right - 1
else: left = left + 1
return [-1, -1]猜数字游戏的规则如下:
每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):
- -1:我选出的数字比你猜的数字小 pick < num
- 1:我选出的数字比你猜的数字大 pick > num
- 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num
Python版本
# The guess API is already defined for you.
# @param num, your guess
# @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
# def guess(num: int) -> int:
class Solution:
def guessNumber(self, n: int) -> int:
left, right = 1, n
while left + 1 < right:
mid = left + (right - left) // 2
if guess(mid) == 0: return mid
elif guess(mid) == -1: right = mid
else: left = mid
if guess(left) == 0: return left
else: return right给定一个包含 n 个整数的排序数组,找出给定目标值 target 的起始和结束位置。 如果目标值不在数组中,则返回
[-1, -1]
思路:核心点就是找第一个 target 的索引,和最后一个 target 的索引,所以用两次二分搜索分别找第一次和最后一次的位置
Python3版本
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
if len(nums) == 0: return [-1, -1]
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] < target: left = mid
elif nums[mid] > target: right = mid
else:
left = mid
right = mid
while left > 0 and nums[left-1] == target: left = left - 1
while right < len(nums) - 1 and nums[right+1] == target: right = right + 1
return [left, right]
if nums[left] == target:
if nums[right] == target: return [left, right]
else: return [left, left]
else:
if nums[right] == target: return [right, right]
else: return [-1, -1]class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
if len(nums) == 0: return [-1, -1]
'''
分解成找两次,找最左边一次,找最右边一次
'''
left = findTarget(nums, target, True)
if left == -1: return [-1, -1]
right = findTarget(nums, target, False)
return [left, right]
def findTarget(nums, target, find_left):
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] < target: left = mid
elif nums[mid] > target: right = mid
else:
if find_left: right = mid
else: left = mid
if find_left:
if nums[left] == target: return left
elif nums[right] == target: return right
else: return -1
else:
if nums[right] == target: return right
elif nums[left] == target: return left
else: return -1
C++版本
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return {-1, -1};
int low = findTarget(nums, target, true);
if(low == -1) return {-1, -1};
int high = findTarget(nums, target, false);
return {low, high};
}
int findTarget(vector<int>& nums, int target, bool findLeft){
int left = 0, right = nums.size() - 1;
while(left + 1 < right){
int middle = left + (right - left)/2;
if(nums[middle] < target) left = middle;
else if(nums[middle] > target) right = middle;
else{
if(findLeft) right = middle;
else left = middle;
}
}
if(findLeft) {
if(nums[left] == target) return left;
else if(nums[right] == target) return right;
else return -1;
}
else{
if(nums[right] == target) return right;
else if(nums[left] == target) return left;
else return -1;
}
}给定一个 排序好 的数组 arr ,两个整数 k 和 x ,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。
思路有好几种:
- 找到最小值,然后双指针
- 直接找k长度的子序列
- 删去最远的剩下的就是最近的长度为k的子数组
Python版本
### 方法一
class Solution:
def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
if len(arr) == k: return arr
left, right = 0, len(arr) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if arr[mid] >= x: right = mid
else: left = mid
res = list()
while len(res) < k:
if right >= len(arr) or (left >= 0 and abs(arr[left]-x) <= abs(arr[right]-x)):
res.insert(0, arr[left])
left = left - 1
else:
res.append(arr[right])
right = right + 1
return res### 方法二
class Solution:
def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
'''
直接定位mid为左边界;
查找mid+k的结果是否满足要求;
不满足的话二分法左右调整mid.
'''
if len(arr) == k: return arr
left, right = 0, len(arr) - k
while left + 1 < right:
mid = left + (right - left) // 2
if x - arr[mid] <= arr[mid + k] - x:
right = mid
else:
left = mid
if abs(arr[left] - x) <= abs(arr[right + k - 1] - x): return arr[left: left + k]
else: return arr[right: right + k]给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
int searchInsert(vector<int>& nums, int target) {
if(nums.empty()) return 0;
int left = 0, right = nums.size() - 1;
while(left + 1 < right){
int middle = left + (right - left) / 2;
if(nums[middle] == target) return middle;
else if(nums[middle] < target) left = middle;
else right = middle;
}
if(target <= nums[left]) return left;
else if(target <= nums[right]) return right;
else return right + 1;
}编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
- 每行中的整数从左到右按升序排列。
- 每行的第一个整数大于前一行的最后一个整数。
C++版本
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty() || matrix[0].empty()) return false;
int rows = matrix.size();
int cols = matrix[0].size();
int left = 0, right = rows * cols - 1;
while(left + 1 < right){
int mid = left + (right - left) / 2;
int valM = matrix[mid/cols][mid%cols];
if(target == valM) return true;
else if(target < valM) right = mid;
else left = mid;
}
if(matrix[left/cols][left%cols] == target || matrix[right/cols][right%cols] == target) return true;
return false;
}Python版本
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
rows = len(matrix)
cols = len(matrix[0])
left, right = 0, rows * cols - 1
while left + 1 < right:
mid = left + (right - left) // 2
val = matrix[mid//cols][mid%cols]
if val == target: return True
elif val < target: left = mid
else: right = mid
if matrix[left//cols][left%cols] == target or matrix[right//cols][right%cols] == target:
return True
else: return False假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
Python3版本
# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:
class Solution:
def firstBadVersion(self, n: int) -> int:
left, right = 1, n
while left + 1 < right:
mid = left + (right - left) // 2
if not isBadVersion(mid): left = mid
else: right = mid
if isBadVersion(left): return left
else: return rightC++版本
int firstBadVersion(int n) {
int left = 1, right = n;
while(left + 1 < right){
int mid = left + (right - left) / 2;
if(isBadVersion(mid)) right = mid;
else left = mid;
}
if(isBadVersion(left)) return left;
return right;
}假设按照升序排序的数组在预先未知的某个点上进行了旋转( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请找出其中最小的元素。 Python版本
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
if nums[left] <= nums[right]: return nums[left] # 说明还是升序排列
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] >= nums[right]: left = mid
else: right = mid
return min(nums[left], nums[right])版本二
class Solution:
def findMin(self, nums: List[int]) -> int:
if nums[0] < nums[-1]: return nums[0] ## 说明顺序没变
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] > nums[left]:
left = mid
else:
right = mid
return min(nums[left], nums[right])C++版本
int findMin(vector<int>& nums) {
if(nums.empty()) return 0;
int left = 0, right = nums.size() - 1;
if(nums[right] > nums[left]) return nums[left]; //说明没有旋转
while(left + 1 < right){
int mid = left + (right - left) / 2;
if(nums[mid] > nums[left]) left = mid;
else right = mid;
}
return min(nums[left], nums[right]);
}假设按照升序排序的数组在预先未知的某个点上进行了旋转 ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请找出其中最小的元素。(包含重复元素)
// 思路:跳过重复元素,mid值和end值比较,分为两种情况进行处理
Python版本
class Solution:
def findMin(self, nums: List[int]) -> int:
if nums[0] < nums[-1]: return nums[0]
left, right = 0, len(nums) - 1
while left + 1 < right:
while left < right and nums[left] == nums[left+1]: left = left + 1
while left < right and nums[right] == nums[right-1]: right = right - 1
mid = left + (right - left) // 2
if nums[mid] >= nums[right]: left = mid
else: right = mid
return min(nums[left], nums[right])C++版本
int findMin(vector<int>& nums) {
if(nums.empty()) return 0;
int left = 0, right = nums.size() - 1;
if(nums[left] < nums[right]) return nums[left]; // 说明没旋转
while(left + 1 < right){
//去重
while(left < right && nums[left + 1] == nums[left]) left++;
while(left < right && nums[right - 1] == nums[right]) right--;
int mid = left + (right - left) / 2;
if(nums[mid] >= nums[left]) left = mid;
else right = mid;
}
return min(nums[left], nums[right]);
}假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。 你可以假设数组中不存在重复的元素。
Python版本
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] == target: return mid
if nums[mid] > nums[left]:
if target >= nums[left] and target <= nums[mid]: right = mid
else: left = mid
else:
if target <= nums[right] and target >= nums[mid]: left = mid
else: right = mid
if nums[left] == target: return left
elif nums[right] == target: return right
else: return -1C++版本
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
int left = 0, right = nums.size() - 1;
while(left + 1 < right){
int mid = left + (right - left) / 2;
if(nums[mid] == target) return mid;
if(nums[mid] > nums[left]){
if(target >= nums[left] && target <= nums[mid]) right = mid;
else left = mid;
}
else{
if(target <= nums[right] && target >= nums[mid]) left = mid;
else right = mid;
}
}
if(nums[left] == target) return left;
else if(nums[right] == target) return right;
else return -1;
}注意点
面试时,可以直接画图进行辅助说明,空讲很容易让大家都比较蒙圈
峰值元素是指其值严格大于左右相邻值的元素。 给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。 你可以假设 nums[-1] = nums[n] = -∞ 。 你必须实现时间复杂度为 O(log n) 的算法来解决此问题。 思路:规定了算法复杂度,那就是二分匹配法
Python3版本
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left + 1 < right:
mid = left + (right - left) // 2
if nums[mid] < nums[mid+1]: left = mid
else: right = mid
if nums[left] < nums[right]: return right
else: return left前面几道题说明了,二分匹配中,不一定直接对比mid和target, left和mid, mid本身, mid和相邻(mid+1),都是能做compare的。
假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。 编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。(包含重复元素)
bool search(vector<int>& nums, int target) {
if(nums.empty()) return false;
int left = 0, right = nums.size() - 1;
while(left + 1 < right){
while(left < right && nums[left+1] == nums[left]) left++;
while(left < right && nums[right-1] == nums[right]) right--;
int mid = left + (right - left) / 2;
if(nums[mid] == target) return true;
if(nums[mid] >= nums[left]){ //注意是等号
if(target >= nums[left] && target <= nums[mid]) right = mid;
else left = mid;
}
else{
if(target >= nums[mid] && target <= nums[right]) left = mid;
else right = mid;
}
}
if(nums[left] == target || nums[right] == target) return true;
else return false;
}二分搜索核心四点要素(必背&理解)
- 1、初始化:start=0、end=len-1
- 2、循环退出条件:start + 1 < end
- 3、比较中点和目标值:A[mid] ==、 <、> target
- target不一定是既定的数字,也可能是start/end/mid+1对应的值,灵活应用
- 4、判断最后两个元素是否符合:A[start]、A[end] ? target
- x的平方根
- 有效的完全平方数
- Pow(x, n)
- 寻找比目标字母大的最小字母
- 两个数组的交集
- 两个数组的交集II
- 两数之和II-输入有序数组
- 猜数字大小
- search-for-range
- 找到k个最接近的元素
- search-insert-position
- search-a-2d-matrix
- first-bad-version
- find-minimum-in-rotated-sorted-array
- find-minimum-in-rotated-sorted-array-ii
- search-in-rotated-sorted-array
- 寻找峰值
- search-in-rotated-sorted-array-ii
