算法通关 9 - 二分查找与二叉树的中序遍历
二分查找与二叉树的中序遍历—二分查找
通关进度
题目 | 说明 |
---|---|
二分查找 | 通关 |
递归和迭代的二分查找 | 通关 |
存在重复元素的二分查找 | 通关 |
参考资料
基本查找
1 | public int search(int[] arr, int key) { |
二分查找与分治
迭代方式
1 | public int binarySearch(int[] array, int low, int high, int target) { |
递归方式
1 | public int binarySearch(int[] array, int low, int high, int target) { |
存在重复元素的二分查找
如果存在重复元素,请找出重复元素中的左侧第一个元素
关键在于:找到目标结果后不是返回,,而是继续向左侧移动
1 | public int binarySearch(int[] nums, int target) { |
扩展
- 假如重复的数量特别多,怎么办?
此时可以对内层的 while 进一步二分,在找到目标元素后继续递归查找
【更多问题 1】寻找大于等于某数最左侧的位置
在一个有序数组中,找大于等于某个数最左侧的位置 [重要]
1 | public int nearestIndex(int[] arr, int value) { |
【更多问题 2】 局部最小值问题
二分查找与二叉树的中序遍历—二分查找与搜索树高频问题
通关进度
题目 | 说明 |
---|---|
山脉数组的峰顶索引 | 通关 |
寻找旋转排序数组中的最小值 | 通关 |
寻找缺失数字 | 通关 |
优化求平方根 | 通关 |
中序与搜索树原理 | 通关 |
二叉树中搜索特定值 | 通关 |
验证二叉搜索树 | 通关 |
二分查找拓展
山脉数组的峰顶索引
问题
【LeetCode 852】:符合下列属性的数组 arr
称为 山脉数组 :
arr.length >= 3
- 存在
i(0 < i < arr.length - 1)
使得:arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给你由整数组成的山脉数组 arr
,返回满足给你由整数组成的山脉数组 arr
,返回满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
的下标 i
枚举
- 当遍历到下标
i
时,如果有 $arr{i−1}< arr_i >arr{i+1}$,,那么i
就是需要找出的下标 - 即只需要让
i
满足 $arri > arr{i+1}$ - 在遍历的过程中,最先遍历到的满足 $arri > arr{i+1}$ 的下标
i
一定也满足 $arr{i-1}arr {i+1}$ 的位置
1 | public int peakIndexInMountainArray(int[] arr) { |
二分查找
- 假设题目要求的下标 $i$ 为 $i_{ans}$
- 当 $i<i{ans}$ 时,$arr_i < arr{i+1}$ (递增排列)恒成立
- 当 $i ≥ i{ans}$ 时,$arr_i > arr{i+1}$ (递减排列) 恒成立
- 因此 $i{ans}$ 即为「最小的满足 $arr_i > arr{i+1}$ 的下标」
1 | public int peakIndexInMountainArray(int[] arr) { |
寻找旋转排序数组中的最小值
问题
【LeetCode 153】:已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。
数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
二分查找
一个不包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图:
其中横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要查找的目标。
考虑数组中的最后一个元素 $x$:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 $x$;而在最小值左侧的元素,它们的值一定都严格大于 $x$。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。
在二分查找的每一步中,左边界为 low
,右边界为 high
,区间的中点为 pivot
,最小值就在该区间内。我们将中轴元素 nums[pivot]
与右边界元素 nums[high]
进行比较,可能会有以下的三种情况:
第一种情况是 nums[pivot]<nums[high]
。如下图所示,这说明 nums[pivot]
是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。
第二种情况是 nums[pivot]>nums[high]
。如下图所示,这说明 nums[pivot]
是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。
由于数组不包含重复元素,并且只要当前的区间长度不为 1
,pivot
就不会与 high
重合;而如果当前的区间长度为 1
,这说明我们已经可以结束二分查找了。因此不会存在 nums[pivot]=nums[high]
的情况。
当二分查找结束时,我们就得到了最小值所在的位置。
1 | public int findMin(int[] nums) { |
寻找缺失数字
问题
【剑指 Offer】:一个长度为 n-1
的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围 0~n-1
之内。在范围 0~n-1
内的 n
个数字中有且只有一个数字不在该数组中,请找出该数字
使用二分查找,在缺失数字前,必然有 nums[i] == i
,在缺失数字之后必然有 nums[i] != i
,因此需要二分查找出第一个 nums[i] != i
,此时的下标 i
即为所求,如果数组中没有找到下标,那么缺失的就是 n
1 | public int takeAttendance(int[] arr) { |
优化求平方根
问题
【剑指 Offer】:使用最快方式求平方根
由于 $x$ 平方根的整数部分 ans
是满足 $k^2≤x$ 的最大 k
值,因此可以对 k
进行二分查找,从而得到答案。
二分查找的下界为 0
,上界可以粗略地设定为 x
,在二分查找的每一步中,只需要比较中间元素 mid
的平方与 x
的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案 ans
后,也就不需要再去尝试 ans+1
了。
1 | public int sqrt(int num) { |
更多问题
- CC 150 面试题 53:在排序数组中查找数字
- LeetCode 34:在排序数组中查找元素的第一个和最后一个位置
- LeetCode 875:爱吃香蕉的珂珂
- LeetCode 29:两数相除
中序与搜索树原理
二叉搜索树中搜索特定值
问题
【LeetCode 700】:给定二叉搜索树(BST)的根节点 root
和一个整数值 val
。
需要在 BST 中找到节点值等于 val
的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null
。
递归
- 若
root
为空,则返回空节点 - 若
val = root.val
,则返回root
- 若
val < root.val
,则递归左子树 - 若
val > root.val
,则递归右子树
1 | public TreeNode searchBST(TreeNode root, int val) { |
迭代
- 若
root
为空则跳出循环,并返回空节点; - 若
val=root.val
,则返回root
; - 若
val<root.val
,将root
置为root.left
; - 若
val>root.val
,将root
置为root.right
。
1 | public TreeNode searchBST(TreeNode root, int val) { |
验证二叉搜索树
问题
【LeetCode 98】:给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
如果该二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左右子树也为二叉搜索树。
递归
- 函数表示考虑以
root
为根的子树,判断子树中所有节点的值是否都在 $(l,r)$ 的范围内(注意是开区间) - 如果
root
节点的值val
不在 $(l,r)$ 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。 - 递归调用左子树时,需要把上界
upper
改为root.val
- 递归调用右子树时,需要把下界
lower
改为root.val
1 | public boolean isValidBST(TreeNode root) { |
中序遍历
- 二叉搜索树「中序遍历」得到的值构成的序列一定是升序的
- 在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可
- 如果均大于说明这个序列是升序的,整棵树是二叉搜索树,否则不是
1 | public boolean isValidBST(TreeNode root) { |
拓展
二分查找与二叉树的中序遍历—进阶问题
通关进度
题目 | 说明 |
---|---|
有序数组转为二叉搜索树 | 通关 |
寻找两个正序数组的中位数 | 通关 |
有序数组转为搜索二叉树
问题
【LeetCode 108】:给你一个整数数组 nums ,其中元素已经按升序排列,请你将其转换为一棵平衡二叉搜索树。
给定二叉搜索树的中序遍历,不能唯一地确定二叉搜索树
如果没有要求二叉搜索树的高度平衡,则任何一个数字都可以作为二叉搜索树的根节点,因此可能的二叉搜索树有多个。
如果增加一个限制条件,即要求二叉搜索树的高度平衡,不能唯一地确定二叉搜索树
可以选择中间数字作为二叉搜索树的根节点,这样分给左右子树的数字个数相同或只相差 1,可以使得树保持平衡
- 如果数组长度是奇数,则根节点的选择是唯一的
- 如果数组长度是偶数,则可以选择中间位置左边的数字作为根节点或者选择中间位置右边的数字作为根节点,选择不同的数字作为根节点则创建的平衡二叉搜索树也是不同的
递归的基准情形是平衡二叉搜索树不包含任何数字,此时平衡二叉搜索树为空
- 在给定中序遍历序列数组的情况下,每一个子树中的数字在数组中一定是连续的,因此可以通过数组下标范围确定子树包含的数字,下标范围记为
[left,right]
- 对于整个中序遍历序列,下标范围从
left=0
到right=nums.length−1
。当left>right
时,平衡二叉搜索树为空。
中序遍历,总是选择中间位置左边的数字作为根节点
选择中间位置左边的数字作为根节点,则根节点的下标为 mid=(left+right)/2
1 | public TreeNode sortedArrayToBST(int[] nums) { |
中序遍历,总是选择中间位置右边的数字作为根节点
选择中间位置右边的数字作为根节点,则根节点的下标为 mid=(left+right+1)/2
1 | public TreeNode helper(int[] nums, int left, int right) { |
中序遍历,选择任意一个中间位置数字作为根节点
选择任意一个中间位置数字作为根节点,则根节点的下标为 mid=(left+right)/2
和 mid=(left+right+1)/2
两者中随机选择一个
1 | Random rand = new Random(); |
寻找两个正序数组的中位数
问题
【LeetCode 4】:给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的中位数
思路
- 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。
- 不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。
1 | public double findMedianSortedArrays(int[] nums1, int[] nums2) { |