二分查找(binary search)是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。二分查找算法在情况下的复杂度是对数时间,进行次比较操作。
二分查找有很多等价写法,判断搜索循环条件,左右边界更新方式的选择需要配合,否则容易出现死循环或者遗漏区域。在这里给出两个框架,根据具体问题选择适当的框架解决。
常见的二分查找通常在左闭右闭区间[left,right]范围内进行搜索,循环条件为,左边界更新为left = mid + 1,右边界更新为right = mid - 1。
class Solution:
def searchIndex(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2 # 防止(left+right)太大而溢出
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
上面的做法当mid没有找到恰好满足条件的解,需要对左右边界进行判断时,左右边界不等,使判断变得复杂。而使用左闭右开区间[left,right)时,使用下述算法框架可保证left==right,只需判断一个位置即可。
class Solution:
def searchIndex(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while left < right:
mid = left + (right - left) // 2 # 防止(left+right)太大而溢出
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid
return -1
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
输入: [1,3,5,6], 5
输出: 2
输入: [1,3,5,6], 2
输出: 1
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid
return left
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
输入: 4
输出: 2
输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
x的平方根不会超过x的一半,所以将右边界设置为x / 2 + 1
class Solution:
def mySqrt(self, x: int) -> int:
if x==0 or x==1:
return x
left = 1
right = x // 2 + 1
while left < right:
mid = left + (right - left) // 2
if mid * mid == x:
return mid
elif mid * mid < x:
left = mid + 1
else:
right = mid
return left - 1
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
一开始的想法使用二分查找找出目标值所在的任意一个索引,再向前后分别遍历。但这种算法在最坏情况下时间复杂度为O(n),即整个数组内值全为target。因此使用两个二分查找,分别查找目标值的左边界和右边界。
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
ret = [-1,-1]
ret[0] = self.searchLeft(nums,target)
ret[1] = self.searchRight(nums,target)
return ret
def searchLeft(self,nums,target):
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
if mid == 0 or nums[mid-1] != target:
return mid
else:
right = mid - 1 # 不是左边界,继续二分查找
elif target > nums[mid]:
left = mid + 1
else:
right = mid - 1
return -1
def searchRight(self,nums,target):
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
if mid == len(nums) - 1 or nums[mid+1] != target:
return mid
else:
left = mid + 1 # 不是右边界,继续二分查找
elif target > nums[mid]:
left = mid + 1
else:
right = mid - 1
return -1
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
输入: [3,4,5,1,2]
输出: 1
输入: [4,5,6,7,0,1,2]
输出: 0
如果mid处值小于前一个值,则mid处值为解返回,若mid==0,则判断其与nums[1]值的大小关系。判断区间内数组是否为有序数组,若有序,返回区间的第一个值nums[left],若仍为旋转数组,通过mid处的值与left,right处值比较,判断继续在左区间搜索or右区间搜索。
class Solution:
def findMin(self, nums: List[int]) -> int:
if len(nums)==1:
return nums[0]
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if mid == 0 and nums[1] < nums[0]:
return nums[1]
elif mid != 0 and nums[mid-1] > nums[mid]:
return nums[mid]
else:
if nums[left] > nums[right]:
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid - 1
else:
return nums[left]
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
此题为部分有序的数组,且要求复杂度为O(log n),暗示需使用二分搜索算法。由于数组部分有序,需尤其注意如何更新左右边界。有序数组当target>nums[mid]时,应继续搜索右半部分,即更新左边界。但有一种情况需要在左半部分进行搜索,把当前的数组看作两个有序数组的拼接,当mid落在第二个有序数组内,且target比第二个有序数组的最大值还大,此时要查找的值在第一个有序数组内,此时,更新右边界。当target 注:当然也可先使用上一题的方式找到旋转位置,再在两个有序的数组内进行二分查找 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。 编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。 这是搜索旋转排序数组的延伸题目,本题中的nums可能包含重复元素。 输入: nums = [2,5,6,0,0,1,2], target = 0 输入: nums = [2,5,6,0,0,1,2], target = 3 继续按照上题的思路解决本题,由于有重复数字,使得target在第一个有序数组还是第二个的边界判断更加复杂。在边界处跳过所有重复数字,然后进行判断。最坏情况下有很多重复数字,遍历数组,时间复杂度为O(n) 编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: 每行中的整数从左到右按升序排列。 输入: 两个二分搜索,先找出target可能在的list,再在这个list中查找是否存在target。 峰值元素是指其值大于左右相邻值的元素。 给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。 数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。 你可以假设 nums[-1] = nums[n] = -∞。 输入: nums = [1,2,3,1] 输入: nums = [1,2,1,3,5,6,4] 解法比较简单,主要是需要想到只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值(想象山峰的形状)。比较mid与mid+1处值,如果mid较大,则左侧存在峰值,如果 mid + 1 较大,则右侧存在峰值。 给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。 注意: 1 ≤ n ≤ 1000 输入: 输出: 解释: 在max(nums)和sum(nums)之间进行二分查找,子数组和的范围在这二者之间。对于一个mid,计算根据这个mid能将数组分为几个子数组。如小于m,则还可以继续分组,mid大了,在左侧继续搜索。若大于m证明分组多了,mid小了,在右侧继续搜索。最后返回交点。 class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif target > nums[mid]:
# 在左边搜索,需要满足两个条件
if nums[mid] < nums[left] and nums[right] < target:
right = mid - 1
else:
left = mid + 1
else:
# 在右边搜索,需要满足两个条件
if nums[mid] > nums[right] and target < nums[left]:
left = mid + 1
else:
right = mid - 1
return -1
81.搜索旋转排序数组2
输出: true
输出: falseclass Solution:
def search(self, nums: List[int], target: int) -> bool:
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return True
elif target > nums[mid]:
while nums[mid] == nums[left] and mid > left: # 不超过mid
left += 1
if nums[mid] < nums[left] and target >= nums[left]: # 在左边搜索
right = mid - 1
else:
left = mid + 1
else:
while nums[mid] == nums[right] and mid < right:
right -= 1
if nums[mid] > nums[right] and target <= nums[right]: # 在右边搜索
left = mid + 1
else:
right = mid - 1
return False
74.搜索二维矩阵
每行的第一个整数大于前一行的最后一个整数。
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 3
输出: true
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 13
输出: falseclass Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if len(matrix) == 0 or len(matrix[0]) == 0:
return False
left = 0
right = len(matrix)
while left < right:
mid = left + (right - left) // 2
if matrix[mid][0] == target:
return True
elif target < matrix[mid][0]:
right = mid
else:
left = mid + 1
nums = matrix[left-1]
left = 0
right = len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return True
elif target < nums[mid]:
right = mid
else:
left = mid + 1
return False
162.寻找峰值
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。
说明:你的解法应该是 O(logN) 时间复杂度的。class Solution:
def findPeakElement(self, nums: List[int]) -> int:
left = 0
right = len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < nums[mid+1]:
left = mid + 1
else:
right = mid
return left
410.分割数组的最大值
数组长度 n 满足以下条件:
1 ≤ m ≤ min(50, n)
nums = [7,2,5,10,8]
m = 2
18
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。class Solution {
public int splitArray(int[] nums, int m) {
// 在 max(nums) 和 sum(nums)中二分 子数组最大值是有范围的
int left = 0;
int right = 0;
for(int num:nums) {
right += num;
left = num > left ? num : left;
}
while(left < right) {
int mid = left + (right - left) / 2;
int cnt = 1; //记录分成的组数
int sum = 0;
for(int num:nums) {
if(sum + num > mid) {
sum = num;
cnt++;
} else {
sum += num;
}
}
if(cnt > m) { //组数大于规定组数,证明mid小了,在右侧继续搜索
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
}