目录
二分法
二分搜索是通过不断划分取中间点划分区间,以此在极大程度上减少查找的次数。
二分查找的前提,是整个数组是有序的,并且数组无重复元素
有序很好理解,对于无重复元素的条件而言,如果一旦有重复元素,使用二分查找法法返回的元素下标就可能不是唯一的。
第一种写法 全闭合区间 [left, right]
def find_target(nums: list, tgt: int):
left = 0
right = len(nums) -1
while left <= right:
mid = (left + right)//2
if nums[mid] == tgt: return mid
elif num[mid] > tgt:
## 说明tgt在左边区间 即 [left, mid -1]
right = mid - 1
else:
## 说明tgt在右边区间 即 [mid +1, right]
left = mid + 1
return -1
##没有找到,返回-1
第二种写法
def find_target(nums: list, tgt: int):
left = 0
right = len(nums) #由于是左闭右开的区间,因此right需要取到最右的开区间位置
while left < right:
mid = (left + rigth) //2
if nums[mid] == tgt: return mid
elif nums[mid] > tgt:
## 说明tgt在左边区间,即[left, mid]
right = mid
else:
## 说明tgt在右边区间,即[mid+1, right]
left = mid + 1
return -1
而针对二分法的时间复杂度解析:
假设有N个元素
每次查找剩下的区间是:
N
/
2
1
,
N
/
4
2
2
,
N
/
2
3
,
N
/
2
4
,
N
/
2
5
,
,
,
N
/
2
k
N/2^1, N/42^2, N/2^3, N/2^4,N/2^5,,,N/2^k
N/21,N/422,N/23,N/24,N/25,,,N/2k
【k表示共进行了k次搜索】
所以,
N
/
2
k
=
1
N/2^k = 1
N/2k=1
所以k=logN
所以二分法的时间复杂度是logN
涉及二分法题目
题目一:LT_35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
输入: nums = [1,3,5,6], target = 7
输出: 4
二分法
(1)方法一
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
## 左闭右闭
left = 0
right = len(nums) -1
while left <= right:
mid = (left + right)//2
if nums[mid] == target: return mid
elif nums[mid] > target:
##说明tgt在左区间, [left, mid-1]
right = mid -1
elif nums[mid] < target:
##说明tgt在右区间,[mid + 1, rigth]
left = mid + 1
return left
(2)方法二
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
##左闭右开
left = 0
right = len(nums)
while left < right:
mid = (left + right)//2
if nums[mid] == target: return mid
elif nums[mid] > target:
##说明tgt在左区间,[left, mid)
right = mid
elif nums[mid] < target:
##说明tgt在右区间,[mid+1, right)
left = mid + 1
return left
题目二:LT_14. 最长公共前缀
找到几个字符串中最长公共前缀
Input: ["flower","flow","flight"]
Output: "fl"
Input: ["dog","racecar","car"]
Output: ""
class Solution:
def checkCommon(self, strs, tmp):
for s in strs:
if s.find(str1)!=0: return False
return True
def longestCommonPrefix(self, strs: List[str]) -> str:
str1 = strs[0]
left = 0
right = len(str1) -1
while left <= right:
mid = (left + right ) //2
tmp = str1[0:mid+1]
if self.checkCommon(strs,tmp):
##说明ans需要再往右边一点
left = mid + 1
else:
##说明ans在左边一点
right = mid - 1
return str1[0:left] if right>=0 else ""
题目三:LT_69. x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5
例子
输入:x = 4
输出:2
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
class Solution:
def mySqrt(self, x: int) -> int:
##区间是[0, x]之间寻找
left, right = 0, x
while left <= right:
mid = (left + right)//2
val = mid * mid
if val == x : return mid
elif val > x:
##说明ans在左区间里找 即[left, mid-1]
right = mid -1
else:
##说明ans在右区间 即 [mid+1, right]
left = mid + 1
return right
题目四:LT_287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间
输入:nums = [1,3,4,2,2]
输出:2
输入:nums = [3,1,3,4,2]
输出:3
改编后的二分法
抽屉式的二分法
nums = [1,3,4,2,2]
cnt表示比当前value小或者等于的有多少个
比如比2小或者等于的在nums数组有3个
比如 在这个例子中,left=1, right = 4 所以mid = (left + right) //2 = (1+4)//2 = 2
所以在nums中,小于或者等于mid的有3个。3 > mid = 2 所以答案是 在 left ~ mid 之间。
如果大于mid的话,则在mid+1 ~ right之间。 最后返回left 或者right都行.
class Solution:
def countNums(self, nums, mid):
cnt = 0
for num in nums:
if num <= mid: cnt += 1
return cnt
def findDuplicate(self, nums: List[int]):
left = 1
right = len(nums)-1
while left < right:
mid = (left + right )//2
if self.countNums(nums, mid)>mid:
##说明ans在左区间,而且大于的时候,mid也在候选区范围内 所以right = mid
right = mid
else:
left = mid+1
return left
【总结】
其实两种写法的关键区别点在 当nums[mid] > tgt的时候,这个时候mid有没有可能是潜在的答案,如果肯定mid不可能是答案,则right = mid-1;如果mid有可能是潜在的答案,则right=mid。
对于方法一:
比如题目 LT_35. 搜索插入位置中,输入: nums = [1,3,5,6], target = 5
如果nums[mid] = 6 > target =5 则这个时候 mid不可能是潜在的答案,所以right = mid -1
又比如题目寻找平方根,如果mid=3 则 mid=3的平方为9 >tgt=8, 这个时候mid=3 不可能是答案,所以right=mid-1
对于方法二:
比如题目 LT_287. 寻找重复数中,输入:nums = [1,3,4,2,2]
如果nums[mid=2] = 3 > mid = 2, 这个时候,mid=2 是潜在可能的答案,如果right = mid 需要取到这个值。
题目五:剑指 Offer II 006_排序好的twoSum问题
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target
输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。
思路:
如果要使用二分法的话,则可以先固定一个值,然后在剩下的数组中,寻找另一个值。
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
N = len(numbers)
for i in range(N):
tgt = target - numbers[i]
left = i+1
right = N-1
while left <= right:
mid = (left + right)//2
if numbers[mid] == tgt: return [i,mid]
elif numbers[mid] > tgt:
##说明在左区间,且mid不可取
right = mid - 1
else:
left = mid + 1
return []
时间复杂度: Nlogn (N次的二分法)
题目六:LT_378. 有序矩阵中第 K 小的元素
给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一个内存复杂度优于 O(n2) 的解决方案
matrix = [
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
],
k = 8,
返回 13。
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13
方法一:暴力法
直接将二维数组转换成一维数组,进行排序,然后取第K个元素即可
class Solution:
def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
##把二维数组转换成一维数组
array = sum(matrix, [])
##排序
ans = sorted(array)
return ans[k-1]
空间复杂度:O(n^2)
排序时间复杂度:O(n^2logn)
方法二:二分法(to be continued)
方法三:并归排序(to be continued)
题目七:LT_34. 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
输入:nums = [], target = 0
输出:[-1,-1]
这道题有点不一样的是 有重复的数,所幸还是有序的。只要我们使用两次二分法,分别寻找左边界和右边界。
class Solution:
def searchLeft(self, nums, target):
left = 0
right = len(nums)-1
while left <= right:
mid = (left + right)//2
if nums[mid] == target:
##如果相等,需要再往左边探测看看
right = mid -1
elif nums[mid] > target:
##说明在左区间,且mid不可能 [left, mid-1]
right = mid -1
else:
##说明在右区,且mid不可能 [mid -1, right]
left = mid+1
if nums[left] == target: return left
else: return -1
def searchRight(self, nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right)//2
if nums[mid] == target:
##说明需要往右边再看看
left = mid + 1
elif nums[mid] > target:
##说明在左边区间,且mid不可能
right = mid -1
else:
left = mid + 1
if nums[right]==target: return right
else: return -1
def searchRange(self, nums: List[int], target: int) -> List[int]:
##special case
if len(nums) ==0 or nums[-1] <target: return [-1,-1]
left = self.searchLeft(nums, target)
right = self.searchRight(nums, target)
return [left, right]
题目八:LT_540. 有序数组中的单一元素
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
输入: nums = [3,3,7,7,10,11,11]
输出: 10
思路:
这里的思路是
如果都是成双成对的偶数的话,比如nums= [1,1,2,2,3,3,4,4,8,8],那么必定会有
如果index是偶数,则nums[index] = nums[index + 1]
如果index是奇数,则nums[index -1] = nums[index]
而题目中,出现有一个数是只出现一次,比如nums = [1,1,2,3,3,4,4,8,8]
我们需要找出这个单数,而这个单数的出现必定会打破上面的规则,即
如果index是偶数,则nums[index] = nums[index + 1]
如果index是奇数,则nums[index -1] = nums[index]
所以当索引 mid是偶数的时候,且nums[mid] = nums[mid + 1],则说明在[left, mid]这个区间是成双的偶数,ans在右区间;如果nums[mid] != nums[mid + 1], 则说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间
同理如果index是奇数,则nums[index -1] = nums[index], 则说明在[left, mid]这个区间是成双的偶数,ans在右区间。 如果nums[mid-1] != nums[mid ], 则说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间。
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
##二分法
left = 0
right = len(nums)-1
while left < right:
mid = (left + right)//2
##如果mid 是偶数,则
if mid%2==0:
if nums[mid] == nums[mid+1]:
##说明在[left, mid+1]这个区间是成双的偶数,ans在右区间 [mid+2,right]
left = mid + 2
else:
##说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间即 [left, mid]
right = mid
else:
##如果mid是奇数,则
if nums[mid-1] == nums[mid]:
##说明在[left, mid]这个区间是成双的偶数,ans在右区间 [mid+1,right]
left = mid + 1
else:
##说明在左区间出现了这个单数,并打破了这个规则。所以ans在左区间即 [left, mid-1]
right = mid -1
return nums[right]
【特别注意这里用的是 while left < right, 而不是 while left <= right, 是因为这里有right=mid,如果用while left <= right,则会出现无限循环。】