二分查找:二分查找与搜索树高频问题
基于二分查找的拓展问题
山脉数组的峰顶索引
LeetCode 852 山脉数组的峰顶索引
https://leetcode.cn/problems/peak-index-in-a-mountain-array/
思路分析
方法1:直接遍历数组
注:时间复杂度不满足要求
找到下标i,满足 arr[i-1]<arr[i]>arr[i+1]
优化:找到第一个下标i,满足 arr[i]>arr[i+1]
方法2:二分查找
对于二分的某一个位置 mid,mid可能的位置有3种情况
- mid在上升阶段,arr[mid-1]<arr[mid]<arr[mid+1]
- mid在顶峰,arr[mid-1]<arr[mid]>arr[mid+1]
- mid在下降阶段,arr[mid-1]>arr[mid]>arr[mid+1]
根据mid当前所在的位置,调整二分的左右指针,就能找到峰顶
代码实现
方法1:直接遍历数组
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
for i in range(len(arr)):
if arr[i] > arr[i+1]:
return i
方法2:二分查找
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
low = 1
high = len(arr)-2
while low<=high:
mid = (low+high)//2
# 上升
if arr[mid-1]<arr[mid]<arr[mid+1]:
low = mid+1
# 顶峰
elif arr[mid-1]<arr[mid]>arr[mid+1]:
return mid
# 下降
elif arr[mid-1]>arr[mid]>arr[mid+1]:
high = mid-1
else:
return -1
return -1
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
# 方法2:二分查找,简化写法,没看懂,todo
n = len(arr)
left, right, ans = 1, n-2, 0
# 不断缩小[left, right]的范围
while left <= right:
mid = (left+right)//2
if arr[mid] > arr[mid+1]:
ans = mid
right = mid-1
else:
left = mid+1
return ans
旋转数字的最小数字
LeetCode153 寻找旋转排序数组中的最小值
https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/
思路分析
一个不包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图:
其中,横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要查找的目标。
假设数组的最后一个元素为x,最小值min
最小值min右侧的元素(不包括x本身),小于x;
最小值min左侧的元素,大于x;
可通过这一特性,通过二分法来查找最小值min
左边界为low,右边界为high,区间中点pivot,比较nums[pivot]与nums[high],存在以下三种情况
情况1:nums[pivot] < nums[high],说明nums[pivot] 在最小值右侧(包含最小值),去除右半区间
情况2:nums[pivot] > nums[high],说明nums[pivot] 在最小值左侧(不包含最小值),去除左半区间
情况3:nums[pivot] == nums[high],由于数组不包含重复元素,只要区间长度不为1,pivot就不会与high重合;如果区间长度为1,就可以结束二分查找了,此种情况不存在
代码实现
class Solution:
def findMin(self, nums: List[int]) -> int:
# 方法一:二分查找
left=0
right=len(nums)-1
mid = 0
while left<=right:
mid = (left+right)//2
if nums[mid] < nums[right]:
right = mid
elif nums[mid] > nums[right]:
left = mid+1
else:
break
return nums[mid]
class Solution:
def findMin(self, nums: List[int]) -> int:
# 方法一:二分查找,代码精简
left=0
right=len(nums)-1
while left<right:
mid = (left+right)//2
if nums[mid] < nums[right]:
right = mid
else:
left = mid+1
return nums[left]
题目拓展
数组中存在重复元素
LeetCode 154. 寻找旋转排序数组中的最小值 II
https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/
找缺失数字
剑指 Offer 53 - II. 0~n-1中缺失的数字
https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/description/
剑指offer题目:一个长度为n-1的递增排序数组,所有数字都是唯一的,每个数字范围在0~n-1。在0-n-1内的n个数字中有且只有一个数字不在该数组中,找出这个数字
思路分析
二分查找,缺失数字之前 nums[i] = i,缺失数字之后 nums[i] != i
代码实现
class Solution:
def missingNumber(self, nums: List[int]) -> int:
left = 0
right = len(nums) - 1
while left <= right:
mid = (right + left) // 2
if nums[mid] == mid:
left = mid + 1
else:
right = mid - 1
return left
优化求平方根
LeetCode69. x 的平方根
https://leetcode.cn/problems/sqrtx/
思路分析
用最快的方式找到 n*n=x 中的n。
优化思想:凡是在有序区间查找的场景,都可以用二分查找来优化速度。如果有序区间是变化的,那就每次都针对这个变化的区间进行二分查找。
代码实现
class Solution:
def mySqrt(self, x: int) -> int:
left, right = 0, x
ans = None
while left <= right:
mid = (left+right)//2
if mid*mid <= x:
ans = mid
left = mid+1
else:
right = mid-1
return ans
中序与搜索树原理
中序遍历与前后序遍历相比有不一样的特征,例如中序遍历可以和搜索树结合在一起,前后序不行
概念理解:二叉搜索树
简单来说,如果一棵二叉树是搜索树,则按照中序遍历其序列正好是一个递增序列
比较规范的定义:
- 若它的左子树不为空,则左子树上所有节点的值均小于它根节点的值
- 若它的右子树不为空,则右子树上所有节点的值均大于它根节点的值
- 它的左子树、右子树也分别为二叉排序树
如:
这两棵树中序遍历分别为
[ 3, 6, 9, 10, 14, 16, 19] [ 3, 6, 9, 10 ]
剪枝:搜索树的题目虽然也是用递归,但是与前后序有很大区别,主要是因为搜索树是有序的,就可以根据条件决定某些递归就不必执行了,这也称为“剪枝”
二叉搜索树中搜索特定值
LeetCode700. 二叉搜索树中的搜索
https://leetcode.cn/problems/search-in-a-binary-search-tree/
思路分析
方法1:递归
- 如果 root==null 或者 root.val == val,返回根节点
- 如果 root.val < val,进入根节点的右子树中查找
- 如果 root.val > val,进入根节点的左子树中查找
方法2:迭代
同递归
代码实现
方法1:递归
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
# 方法1:递归
if not root or root.val == val:
return root
elif root.val > val:
return self.searchBST(root.left, val)
else:
return self.searchBST(root.right, val)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
# 方法1: 递归 代码精简
while root:
if root.val == val:
return root
root = self.searchBST(root.left if root.val > val else root.right, val)
return None
方法2:迭代
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
# 方法2: 迭代
while root and root.val != val:
if root.val > val:
root = root.left
else:
root = root.right
return root
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
# 方法2: 迭代 代码精简
while root:
if root.val == val:
return root
root = root.left if root.val > val else root.right
return None
验证二叉搜索树
LeetCode98. 验证二叉搜索树
https://leetcode.cn/problems/validate-binary-search-tree/
思路分析
方法1:中序遍历
根据题目给出的性质,我们可以进一步知道二叉搜索树 中序遍历 得到的值构成的序列一定是升序的,在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可
方法2:递归
设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r)(l,r)(l,r) 的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r)(l,r)(l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。
函数递归调用的入口为 helper(root, -inf, +inf), inf 表示一个无穷大的值
代码实现
方法1:中序遍历
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
pre = float('-inf')
def in_order_compare(root):
# 在局部函数中访问外部变量时,如果需要对外部变量进行修改,使用 nonlocal 关键字将变量声明为外部变量
nonlocal pre
if not root:
return True
# 比较左子树
if not in_order_compare(root.left):
return False
# 访问当前节点:当前节点不满足小于中序遍历的前一个节点,退出
if not root.val > pre:
return False
# 节点比较完成后,修改比较值
pre = root.val
# 比较右子树
return in_order_compare(root.right)
return in_order_compare(root)
方法2:递归
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
# 方法二:递归
def helper(node, lower, upper):
if not node:
return True
if node.val <= lower or node.val >= upper:
return False
if not helper(node.right, node.val, upper):
return False
if not helper(node.left, lower, node.val):
return False
return True
return helper(root, float('-inf'), float('inf'))
题目拓展
可以继续研究
LeetCode530
LeetCode501