编程导航算法通关村第九关 | 二分查找和搜索树高频问题

70 阅读6分钟

基于二分查找的拓展问题

山脉数组的顶峰索引

. - 力扣(LeetCode)

最简单的方式是对数组进行一次遍历。

当我们遍历到下标i时,如果有arr[i-1]<arr[i]>arr[i+1],那么i就是我们需要找出的下标。

其实还可以更简单一些,因为是从左开始找的,开始的时候必然是arr[i-1]<a[i],所以只要找到第一个arr[i]>arr[i+1]的位置即可。

代码就是:

public int peakIndexInMountainArray1(int[] arr) {
    int n = arr.length;
    int ans = -1;
    for (int i = 1; i < n - 1; ++i) {
        if (arr[i] > arr[i + 1]) {
            ans = i;
            break;
        }
    }
    return ans;
}

使用二分查找优化:

对于二分的某一个位置 mid,mid 可能的位置有3种情况:

  • mid在上升阶段的时候,满足arr[mid]>a[mid-1] && arr[mid]<arr[mid+1]
  • mid在顶峰的时候,满足arr[i]>a[i-1] && arr[i]>arr[i+1]
  • mid在下降阶段,满足arr[mid]<a[mid-1] && arr[mid]>arr[mid+1]

因此我们根据 mid 当前所在的位置,调整二分的左右指针,就能找到顶峰。

public int peakIndexInMountainArray2(int[] arr) {
    if (arr.length == 3) {
        return 1;
    }
    int left = 1, right = arr.length - 2;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (arr[mid] > arr[mid-1] && arr[mid] > arr[mid + 1]) {
            return mid;
        }
        if (arr[mid] < arr[mid+1] && arr[mid] > arr[mid-1]) {
            left = mid + 1;
        }
        if (arr[mid] > arr[mid+1] && arr[mid] < arr[mid-1]) {
            right = mid - 1;
        }
    }
    return left;
}

旋转数字的最小数字

. - 力扣(LeetCode)

一个不包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图:

其中横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要查找的目标。 我们考虑数组中的最后一个元素 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] 的情况。 当二分查找结束时,我们就得到了最小值所在的位置。

public int findMin(int[] nums) {
    int low = 0;
    int high = nums.length - 1;
    while (low < high) {
        int pivot = low + ((high - low) >> 1);
        if (nums[pivot] < nums[high]) {
            high = pivot;
        }else {
            low = pivot + 1;
        }
    }
    return nums[low];
}

这里你是否注意到high = pivot;而不是我们习惯的high = pivot-1呢?

这是为了防止遗漏元素,例如[3,1,2],执行的时候nums[pivot]=1,小于nums[high]=2,此时如果high=pivot-1,则直接变成了0。所以对于这种边界情况,很难解释清楚,最好的策略就是多写几种场景测试一下看看。

这也是二分查找比较烦的情况,一般来说解释比较困难,也不容易理解清楚,所以写几个典型的例子试一下,面试的时候大部分case能过就能通过。

找缺失数字

. - 力扣(LeetCode)

对于有序的也可以用二分查找,这里的关键点是在缺失的数字之前,必然有nums[i]==i,在缺失的数字之后,必然有nums[i]!=i。

因此,只需要二分找出第一个nums[i]!=i,此时下标i就是答案。若数组元素中没有找到此下标,那么缺失的就是n。

代码如下:

public int missingNumber (int[] a) {
    int left = 0;
    int right = a.length-1;
    while(left <= right){
        int mid = (left+right)/2;
        if(a[mid]==mid){
            left = mid+1;
        }else{
            right = mid-1;
        }
    }
    return left;
}

优化求平方根

实现函数 int sqrt(int x).计算并返回x的平方根这个题的思路是用最快的方式找到n*n=x的n。

如果整数没有平方根,一般采用向下取整的方式得到结果。采用折半进行比较的实现过程是:

public int sqrt(int x) {
    int left = 1, right = x;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (x / mid > mid) {
            left = mid + 1;
        }else if (x / mid < mid) {
            right = mid - 1;
        }else if (x / mid == mid) {
            return mid;
        }
    }
    return right;
}

中序和搜索树原理

二叉搜索树是一个很简单的概念,但是想说清楚却不太容易。简单来说就是如果一棵二叉树是搜索树,则按照中序遍历其序列正好是一个递增序列。比较规范的定义是:

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 它的左、右子树也分别为二叉排序树。下面这两棵树一个中序序列是{3,6,9,10,14,16,19},一个是{3,6,9,10},因此都是搜索树:

二叉搜索树中搜索特定值

. - 力扣(LeetCode)

使用递归:

  • 如果根节点为空 root == null 或者根节点的值等于搜索值 val == root.val,返回根节点。
  • 如果 val < root.val,进入根节点的左子树查找 searchBST(root.left, val)。
  • 如果 val > root.val,进入根节点的右子树查找 searchBST(root.right, val)。
public TreeNode searchBST(TreeNode root, int val) {
    if (root == null || val == root.val) return root;
    return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}

如果采用迭代方式,也不复杂:

  • 如果根节点不空 root != null 且根节点不是目的节点 val != root.val:
    • 如果 val < root.val,进入根节点的左子树查找 root = root.left。
    • 如果 val > root.val,进入根节点的右子树查找 root = root.right。
public TreeNode searchBST(TreeNode root, int val) {
    while (root != null && val != root.val)
    root = val < root.val ? root.left : root.right;
    return root;
}

验证二叉搜索树

. - 力扣(LeetCode)

根据题目给出的性质,我们可以进一步知道二叉搜索树「中序遍历」得到的值构成的序列一定是升序的,在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。

long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
    if (root == null) {
        return true;
    }
    // 如果左子树下某个元素不满足要求,则退出
    if (!isValidBST(root.left)){
        return false;
    }
    // 访问当前节点:如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历
    if (root.val <= pre) {
        return false;
    }
    pre = root.val;
    // 访问右子树
    return isValidBST(root.right);
}