二分查找与二叉树中序遍历——面试算法

目录

二分查找与分治

循环方式

递归方式

元素中有重复的二分查找

基于二分查找的拓展问题

山脉数组的顶峰索引——局部有序

旋转数字中的最小数字

找缺失数字

优化平方根

中序与搜索树

二叉搜索树中搜索特定值

验证二叉搜索树

有序数组转化为二叉搜索树

寻找两个有序数组中的中位数


凡是在排好序的地方查找的都就可以考虑用二分来优化查找效率。

不一定全局都排好才行,只要某个部分是排好的,就可以针对该部分进行二分查找,这是很多题目优化查找的重要途径。

为了更有通用性,插值查找使用的公式是:

mid=low+(key- a[low])/(a[high]-a[low])*(high-low)

二分查找与分治

二分查找就是将中间结果与目标进行比较,一次去掉一半,因此二分查找可以说是最简单、最典型的分治了。

循环方式

public static int binarySearch(int[] array, int low, int high, int target) {
    while (low <= high) {
        int mid = low + ((high - low) >> 1);
        if (array[mid] == target) {
            return mid;
        } else if (array[mid] > target) {
            // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
            high = mid - 1;
        } else {
            // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
            low = mid + 1;
        }
    }
    return -1;
}

递归方式

public  int binarySearch1(int[] array, int low, int high, int target) {
    //递归终止条件
    if(low <= high){
        int mid = low + ((high - low) >> 1);
        if(array[mid] == target){
            return mid  ;  // 返回目标值的位置,从1开始
        }else if(array[mid] > target){
            // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
            return binarySearch(array, low, mid-1, target);
        }else{
            // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
            return binarySearch(array, mid+1, high, target);
        }
    }
    return -1;   //表示没有搜索到
}

元素中有重复的二分查找

假如在上面的基础上,元素存在重复,如果重复则找左侧第一个。

这里的关键是找到目标结果之后不是返回而是继续向左侧移动。第一种,也是最简单的方式,找到相等位置向左使用线性查找,直到找到相应的位置。

public static int search(int[] nums, int target) {
    if (nums == null || nums.length == 0)
        return -1;
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            //找到之后,往左边找
            while (mid != 0 && nums[mid] == target)
            mid--;
            if (mid == 0 && nums[mid] == target) {
                return mid;
            }    
            return mid + 1;
        }
    }
    return -1;
}

基于二分查找的拓展问题

山脉数组的顶峰索引——局部有序

在数组中的某位位置i开始,从0到i是递增的,从i+1 到数组最后是递减的,让你找到这个最高点。

符合下列属性的数组 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[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就是我们需要找出的下标。

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

public int peakIndexInMountainArray(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 peakIndexInMountainArray(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;
}

旋转数字中的最小数字

已知一个长度为 n 的数组,预先按照升序排列,经由1到n次旋转后,得到输入数组。给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

示例1:

输入:nums = [4,5,1,2,3]

输出:1

解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例2:

输入:nums = [4,5,6,7,0,1,2]

输出:0

解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

我们考虑数组中的最后一个元素 x:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

在二分查找的每一步中,左边界为 low,右边界为 high,区间的中点为 pivot,最小值就在该区间内。我们将中轴元素 nums[pivot] 与右边界元素 nums[high] 进行比较,可能会有以下的三种情况:

第一种情况是nums[pivot]<nums[high]。如下图所示,这说明nums[pivot] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。

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];
}

找缺失数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

对于有序的也可以用二分查找,这里的关键点是在缺失的数字之前,必然有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 l=1,r=x;
    while(l <= r){
        int mid = l + ((r - l)>>1);
        if(x/mid > mid){
            l = mid + 1;
        } else if(x / mid < mid){
            r = mid - 1;
        } else  if(x/mid == mid){
            return mid;
        }
    }
    return r;
}

中序与搜索树

如果一棵二叉树是搜索树,则按照中序遍历其序列正好是一个递增序列。

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

  • 它的左、右子树也分别为二叉排序树。

搜索树的题目虽然也是用递归,但是与前后序有很大区别,主要是因为搜索树是有序的,就可以根据条件决定某些递归就不必执行了,这也称为“剪枝”。

二叉搜索树中搜索特定值

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

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);
}

public TreeNode searchBST(TreeNode root, int val) {
    while (root != null && val != root.val)
    root = val < root.val ? root.left : root.right;
    return root;
}

验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。

  • 节点的右子树只包含 大于 当前节点的数。

  • 所有左子树和右子树自身必须也是二叉搜索树。

结合二叉搜索树的性质,中序遍历构成的序列一定是升序的。在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。

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);
}

有序数组转化为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

理论上如果要构造二叉搜索树,可以以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树。 本题要求高度平衡,因此我们需要选择升序序列的中间元素作为根节点,这本质上就是二分查找的过程。

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return helper(nums, 0, nums.length - 1);
    }

    public TreeNode helper(int[] nums, int left, int right) {
        if (left > right) {
            return null;
        }

        // 总是选择中间位置左边的数字作为根节点
        int mid = (left + right) / 2;

        TreeNode root = new TreeNode(nums[mid]);
        root.left = helper(nums, left, mid - 1);
        root.right = helper(nums, mid + 1, right);
        return root;
    }
}

寻找两个有序数组中的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数 。

首先,中位数到底是什么?

  • 如果合并后的数组长度是奇数,中位数就是中间那个数(比如总长度7,中位数是第4个数)。

  • 如果是偶数,中位数是中间两个数的平均值(比如总长度8,中位数是第4和第5个数的平均)。

所以问题可以转化为:如何找到两个有序数组中第k小的数,其中k可能是(m+n)/2或(m+n)/2+1。

二分思想延伸思路:每次排除不可能的部分 假设我们要找第k小的数,可以比较两个数组中第k/2位置的元素:

  • 比如k=5,我们比较nums1的第2个元素(k/2=2,索引从0开始是1)和nums2的第2个元素。

  • 如果nums1的这个元素更小,说明nums1的前k/2个元素都不可能是第k小的数,可以排除掉这部分,问题规模就缩小了一半!

举个栗子🌰: nums1 = [1,3,5], nums2 = [2,4,6], 找第4小的数(k=4)。

  1. 比较nums1的第2个元素(k/2=2,即nums1[1]=3)和nums2的第2个元素(nums2[1]=4)。

  2. 3 < 4 → 排除nums1的前2个元素(1和3),现在nums1剩下[5]。

  3. 问题变成从[5]和[2,4,6]中找第4-2=2小的数。

  4. 现在k=2,比较剩下的数组的第1个元素(k/2=1):

    1. nums1[0]=5 vs nums2[0]=2 → 2更小,排除nums2的前1个元素(2)。

  5. 现在问题变成从[5]和[4,6]中找第2-1=1小的数,即最小的数:4。

  6. 所以第4小的数是4。

边界情况怎么办?

  • 如果某个数组长度不够k/2: 比如nums1只剩2个元素,但k/2=3,这时候只能取nums1剩下的所有元素,排除掉这部分后k减去实际排除的数量。

  • 当k=1时: 直接比较两个数组当前剩下的第一个元素,取较小的那个。

  • 一个数组被完全排除: 直接在另一个数组中找第k小的数。

为什么能保证时间复杂度是O(log(m+n))? 每次排除掉k/2个元素,相当于每次将问题规模减半,类似二分查找,所以时间复杂度是对数级别的。

再举个栗子🌰(处理边界): nums1 = [1,2], nums2 = [3,4],找第3小的数(总长度4,中位数是第2和3的平均)。

  1. 找第3小的数:k=3。

  2. 比较nums1的第1个元素(k/2=1,nums1[0]=1)和nums2的第1个元素(nums2[0]=3)。

  3. 1 < 3 → 排除nums1的前1个元素(1),k=3-1=2。

  4. 现在nums1剩下[2],nums2是[3,4]。

  5. 找第2小的数:比较nums1[0]=2和nums2[0]=3 → 2更小,排除nums1的1个元素,k=2-1=1。

  6. 现在k=1,取剩下数组的第一个元素:nums2[0]=3。

  7. 所以第3小的是3,中位数是(2+3)/2=2.5。

总结步骤:

  1. 始终保持nums1是较短的数组(减少边界处理)。

  2. 递归或循环比较两个数组的k/2位置:

    1. 排除较小元素的前半部分。

    2. 更新k值(减去排除的数量)。

  3. 处理边界(数组长度不足、k=1等)。

通过不断排除不可能的部分,最终就能高效找到第k小的数啦!虽然细节有点多,但多举例子就能理解啦~ (๑•̀ㅂ•́)و✧

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        if (totalLength % 2 == 1) {
            int midIndex = totalLength / 2;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    public int getKthElement(int[] nums1, int[] nums2, int k) {
        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;

        while (true) {
            // 边界情况
            if (index1 == length1) {
                return nums2[index2 + k - 1];
            }
            if (index2 == length2) {
                return nums1[index1 + k - 1];
            }
            if (k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }

            // 正常情况
            int half = k / 2;
            int newIndex1 = Math.min(index1 + half, length1) - 1;
            int newIndex2 = Math.min(index2 + half, length2) - 1;
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            if (pivot1 <= pivot2) {
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;
            } else {
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值