二分法常见套路

1,108 阅读3分钟

众所周知,二分法能非常高效地从有序数组中找出特定的元素,时间复杂度为 O(logN)。

一般而言,遇到要求 O(logN) 时间复杂度的搜索问题,十有八九会用到二分查找。

注意:求二分法的中点时注意防止溢出,两个 32 位正整数相加可能会溢出。

下面整理下二分法常见的套路:

套路一:搜索数组中等于某个元素的位置

这种问题基本就是套二分法的模版,二分法模版整理如下:

public int binarySearch(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        int mid;
        while (left <= right) {
            mid = ((right - left) >> 1) + left; // 这里不用 (left + right) / 2 是为了防止溢出
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1; // 没找到
}

这类型的题举例如下:

LeetCode 74 题:搜索二维矩阵

题目: 编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: 每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。

解析: 由题目可以看出,每一行的第一个数大于上一行最后一个数,如果将二维数组看成一个大的一维数组,那么整个数组是严格升序的,可以直接套用模板,唯一的问题是要将一维数组的索引,转化为二维数组的索引,这个转化是很简单的:假设一维数组的索引是 i,二维数组的大小是 m x n,则 i 在二维数组中的位置是:i / n, i % n。

答案:

    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int left = 0, right = m * n - 1;
        int mid;
        while (left <= right) {
            mid = ((right - left) >> 1) + left;
            if (matrix[mid/n][mid%n] == target) {
                return true;
            }
            if (matrix[mid/n][mid%n] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return false;
    }

套路二:搜索数组中大于等于某个元素的最小位置

这也是一个常见套路,元素存在时便返回其下标,不存在时则返回其应该被插入在数组中的下标,整理模版如下:

    public int binarySearchInsert(int[] nums, int target) {
        int left = 0, right = nums.length - 1, ans = nums.length;
        int mid;
        while (left <= right) {
            mid = ((right - left) >> 1) + left; // 这里不用 (left + right) / 2 是为了防止溢出
            if (nums[mid] >= target) { // 当中点值大于等于要查找的值时,记录下位置,然后在左边搜索
                ans = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }

这种类型的题目举例如下:

LeetCode 34 题:在排序数组中查找元素的第一个和最后一个位置

题目: 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target,返回 [-1, -1]。

思路: 起始位置就是大于等于这个元素的第一个位置,结束位置就是大于这个元素的位置的上一个位置,两者都可以套用上面的模版。

答案:

	private int binarySearch(int[] nums, int target, boolean lower) {
        int left = 0, right = nums.length - 1, ans = nums.length;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
             // 这里根据是否包含元素有不同的条件
            if (nums[mid] > target || (lower && nums[mid] == target)) {
                right = mid - 1;
                ans = mid;
            } else {
                left = mid + 1;
            }
        }
        return ans;
    }

    public int[] searchRange(int[] nums, int target) {
        int leftIndex = binarySearch(nums, target, true);
        int rightIndex = binarySearch(nums, target, false) - 1;
        if (leftIndex < nums.length && nums[leftIndex] == target && rightIndex < nums.length){
            return new int[]{leftIndex, rightIndex};
        }
        return new int[]{-1, -1};
    }

套路三:部分有序数组中搜索某个元素

二分法的精髓在于在有序数组中快速的减少搜索范围。但是很多时候给的条件并非是在一个完全有序的数组中进行搜索,这个时候就需要充分利用有序的部分减少搜索范围,灵活运用上面两个模版,快速写出高性能的代码。

LeetCode 33 题:搜索旋转排序数组

题目: 整数数组 nums 按升序排列,数组中的值互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]。

给你 旋转后 的数组 nums 和一个整数 target,如果 nums 中存在这个目标值 target,则返回它的下标,否则返回 -1。

思路: 旋转后的数组中元素大小呈锯齿形,从下标 0 到某个位置,是升序的,然后从下一个位置到结尾也是升序的,第一段的数都大于第二段的数。

假设我们用二分法的套路来搜索,取中间元素,若中间元素落在第一段,则起始点到中间元素这一段是有序的;若中间元素落在第二段,则中间元素到结尾这一段是有序的。

也就是说无论如何,总有一段元素是有序的,那么对这一段有序的元素可以判断出目标元素到底在哪一部份。

答案:

	public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[left] <= nums[mid]) {  // 这个条件说明前半段是有序的
                if (target >= nums[left] && target < nums[mid]) { // target 介于前半段元素中,则搜索范围在前半段
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else { // 到这里说明后半段有序
                if(target <= nums[right] && target > nums[mid]){ // target 介于后半段元素中,则搜索范围在后半段
                    left = mid + 1;
                } else {
                    right = mid- 1;
                }
            }
        }
        return -1;
    }

LeetCode 81 题:搜索旋转排序数组 II

题目: 已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]。

给你 旋转后 的数组 nums 和一个整数 target,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target,则返回 true,否则返回 false。

思路: 这题跟上一题基本一样,主要的区别点在于数组中的元素可能重复。思路也跟上一题一样,但是要注意有个特别的点:这里由于元素相同,旋转之后可能出现最左边的元素等于中间的元素等于最右边元素,这时我们无法判断到底哪一块是有序的。这时就只能把左右边界分别往中间移动一位。

答案:

	public boolean search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
            if (nums[mid] == target) {
                return true;
            }
            if (nums[mid] == nums[left] && nums[mid] == nums[right]) {
                left++;
                right--;
                continue;
            }
            if (nums[left] <= nums[mid]) {
                if (target < nums[mid] && target >= nums[left]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                if (target <= nums[right] && target > nums[mid]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return false;
    }

LeetCode 4 题:寻找两个正序数组中位数

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

思路: 对于总共奇数个 ( 2 * k + 1) 元素的两个数组, 寻找的就是第 k + 1 个元素, 对于总共偶数个元素 ( 2 * k ) 的数组,寻找的就是第 k 个和第 k + 1 个元素。 所以问题变成了如何在两个有序数组中找到第 k 个元素。 这里我们的思路是从第一个数组中找出第 k / 2 个元素, 第二个数组中也找出第 k / 2 个元素, 第 k 个元素肯定不可能在较小的 k / 2 元素中。这样就将搜索的范围减小了一半。 另外这里需要注意很多边界问题,不能下标越界。

答案:

     public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n = nums1.length + nums2.length;
        if (n % 2 == 1) {
            return findKth(nums1, nums2, n / 2 + 1) * 1.0;
        } else {
            return (findKth(nums1, nums2, n / 2) + findKth(nums1, nums2, n / 2 + 1)) / 2.0;
        }
    }

    private int findKth(int[] nums1, int[] nums2, int k) {
        int index1 = 0, index2 = 0;
        int newIndex1, newIndex2;
        while (true) {
            if (index1 == nums1.length) {
                return nums2[index2 + k - 1];
            }
            if (index2 == nums2.length) {
                return nums1[index1 + k - 1];
            }
            if (k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }
            newIndex1 = Math.min(index1 + k / 2, nums1.length) - 1; // 防止越界
            newIndex2 = Math.min(index2 + k / 2, nums2.length) - 1;
            if (nums1[newIndex1] < nums2[newIndex2]) {
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;
            } else {
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }

点此与我进一步交流