二分查找常用模板、例题

前言

二分法的思想:将一段区间(代码中往往指一个数组)一分为二,进而排除掉不符合条件的一半,选择符合条件的一半继续进行,直至寻找到正确答案(或无元素可供查找)

时间复杂度:因为每次都能排除一半的区间,因此时间复杂度为log(n)

适用条件:一般都要求数组有序,或间接有序(后面有例题)

二分查找法的思想在 1946 年就被提出,但是第 1 个没有 Bug 的二分查找法在 1962 年才出现。

《计算机程序艺术设计 · 第三卷》

如上,思想虽然简单,但是往往在边界条件的处理上让人头疼。在比赛或者面试过程中,没有太多时间

模板

步骤

二分法的处理步骤:

  1. 查看数组是否有序或间接有序,即每次将数组分成2部分时,是否能够确认目标值在哪一部分
  2. 构造判断条件
  3. 处理边界条件(由于基本都是(left + right) / 2,每次缩减一半,因此通常只需分析当数组长度为2时的特殊情况即可)

1. 常用模板

给定一个有序数组nums,再给定一个target,查找target在数组中的下标,没有则返回-1
nums = [1, 2, 3 ,5 ,6 ,7 ,8] target = 5
ans = 3

   // 常见二分法
   int binarySearch(int[] nums, int target) {
       int left = 0;
       int right = nums.length - 1;
       int mid = 0;
       while(left <= right) {
           mid = (left + right) / 2;
           if (target == nums[mid]) {
               return mid;
           } else if (nums[mid] < target) {
               left = mid + 1;
           } else {
               right = mid - 1;
           }
       }
       return -1;
   }
  1. 数组有序
  2. 构造判断条件,即当target == nums[mid]时,返回下标。当nums[mid] < target时,说明左半部分区间均< target,因此可以排除左半区间,left = mid + 1继续查找。反之同理
  3. 当待搜索的数组长度为2时,假设搜索[4, 5],此时left = 0 right = 1,则mid = 0,因此先搜索4,4 < 5排除左半区间,left = mid + 1 = 1,则下次搜索只剩[5],得出正确答案。如还未搜索到,此时left+1left > right跳出循环,返回-1

2. 左边界模板

给定一个有序数组nums,再给定一个target,查找target在数组中的下标,没有则返回-1
nums = [1, 2, 3 ,3 ,3 ,7 ,8] target = 3
ans = 2

// 左边界
    public int searchLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int mid = 0;

        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        return nums[left] == target ? left : -1;
    }
  1. 数组有序
  2. 判断条件,上一题中当nums[mid] == target时,直接return。此时由于寻找的是左边界,因此当nums[mid] == target,依旧需要向左继续查找
  3. 与上一题边界条件不同,此时如果依旧写while(left <= right),上一题中当nums[mid] == target时,直接return。而本题由于没有return,而是right = mid,因此将会陷入死循环。例如:nums = [5],target = 5,left = 0,right = 0,mid = 0,此时如果条件while(left <= right)将会陷入死循环。因此在边界条件的处理上,我们将==条件独立出来判断

为了与下方右边界模板统一,我们也可以将左边界模板的右指针初始话成right = nums.length,但是由此带来的问题就是。当target大于数组内所有数时,left会不断右移,直到left = right = nums.length,此时就会发生数组越界。因此需要特殊判断

    public int searchLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        if(left == nums.length) return -1;
        return nums[left] == target ? left : -1;
    }

3. 右边界模板

给定一个有序数组nums,再给定一个target,查找target在数组中的下标,没有则返回-1
nums = [1, 2, 3 ,3 ,3 ,7 ,8] target = 3
ans = 4

扫描二维码关注公众号,回复: 11259607 查看本文章
    // 右边界
    public int searchRight(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] <= target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        if(left == 0) return nums[left] == target ? left : -1;
        return nums[left - 1] == target ? left - 1 : -1;
    }
  1. 数组有序
  2. 判断条件,此时由于寻找的是右边界,因此当nums[mid] == target,依旧需要向右继续查找
  3. 值得注意的是,int right = nums.length,此时的右指针不再指向最右侧节点,而是最右侧+1节点。其实跟左边界最大的区别在于(0 + 1) / 2 = 0,由于向下取整的缘故,每次除都是向左靠齐的。而现在搜索的是右边界,使用了left = mid + 1;,其实left是偏向右一位去进行尝试的,实际可能出现正确答案其实在[left-1],同时还需要考虑特殊情况,即target在最左侧的情况if(left == 0) return nums[left] == target ? left : -1;,否则left - 1越界

力扣真题

力扣35(easy) 0ms

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
示例 1:
输入: [1,3,5,6], 5
输出: 2

示例 2:
输入: [1,3,5,6], 2
输出: 1

此题是典型的二分法应用题,通过二分法搜索插入位置,数组搜索范围只剩1个元素时,如果 该元素 <= target时,则返回当前位置。否则返回target下一个位置

class Solution {
    public int searchInsert(int[] nums, int target) {
        if(nums.length == 0) return 0;
        int left = 0;
        int right = nums.length - 1;
        int mid = 0;
        // 此时没有等号,因为当数组搜索只剩一个元素,即left == right时,我们停止,自行判断
        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] == target) {
                return mid;
            } else if(nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        if(nums[left] == target) {
            return left;
        }else if(nums[left] < target) {
            return left + 1;
        }else {
            return left;
        }
    }
}

该代码只是为了方便读者理解,其实最后一段代码是没有必要的,完全可以合并到循环中,因此最终代码为

class Solution {
    public int searchInsert(int[] nums, int target) {
        if(nums.length == 0) return 0;
        int left = 0;
        int right = nums.length;
        int mid = 0;

        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] == target) {
                return mid;
            } else if(nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        return left;
    }
}

力扣34(mid) 0ms

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。

示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]

这题很明显就是搜索左右边界问题,左边界我们有2套模板,右边界一套,直接代入即可

class Solution {
    public int[] searchRange(int[] nums, int target) {
        if(nums.length == 0) return new int[]{-1, -1};
        
        int[] ans = new int[2];
        ans[0] = searchLeft(nums, target);
        ans[1] = searchRight(nums, target);

        return ans;
    }

    public int searchLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        if(left == nums.length) return -1;
        return nums[left] == target ? left : -1;
    }

    public int searchRight(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        int mid = 0;

        while(left < right) {
            mid = (left + right) / 2;
            if(nums[mid] <= target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }

        if(left == 0) return nums[left] == target ? left : -1;
        return nums[left - 1] == target ? left - 1 : -1;
    }
}

力扣33(mid) 0ms

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。

示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4

解法1
这题呼应上面所说,数组应该有序或间接有序。如示例1,假设此时nums[mid] = 1target = 4,通常当nums[mid] < target时,我们需要往右半部分查找,但是此时因为数组倒置,也就是4 > nums[nums.length - 1],所以实际我们应该往左半部分查找。由此会有以下伪代码。这是一种做法,分析出所有可能,笔者认为稍微复杂了一些

if(nums[mid] < target && taget >= nums[nums.length - 1]) {
	// 查找左半区
}
if(nums[mid] < target && taget < nums[nums.length - 1]) {
	// 查找右半区
}
...

解法2
如果我们能知道数组在具体的哪个点处被倒置,那么我们剩下的工作就简单多了,只需要把targetnums[0]做一下比较,就可以知道应该在前半段还是在后半段进行搜索。而寻找倒置点的工作也恰好时一个二分法的过程。

class Solution {
    public int search(int[] nums, int target) {
        if (nums.length == 0) return -1;
        
        int left = 0;
        int right = nums.length - 1;
        // 寻找倒置点,如果数组没倒置则返回-1
        int mid = searchMid(nums);
        if (mid == -1) {
        	// 数组没倒置吗,直接搜索全数组
            return searchTarget(nums, target, left, right);
        } else if (target >= nums[left]) {
        	// 数组倒置,但是target大于nums[0],所以在左半部分找
            return searchTarget(nums, target, left, mid);
        } else {
        	// 在右半部分找
            return searchTarget(nums, target, mid + 1, right);
        }
    }

    public int searchMid(int[] nums) {
        if (nums.length <= 1) {
            return -1;
        }

        int left = 0;
        int right = nums.length - 1;
        int mid = 0;
        while (left <= right) {
            mid = (left + right) / 2;
            // 如果mid数大于mid+1,说明改点就是倒置点
            if (mid + 1 < nums.length && nums[mid] > nums[mid + 1]) {
                return mid;
            } else if (nums[mid] >= nums[left]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return -1;
    }
	// 常用的二分法搜索模板
    public int searchTarget(int[] nums, int target, int left, int right) {
        int mid = 0;
        while (left <= right) {
            mid = (left + right) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }
}

力扣4(hard) 2ms

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。

示例 1:
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0

这道题当真有点难,首先看到2个数组时有序的,大概可以想到二分法。而这道题的解题关键则在于需要找到二分法的目标,即最终目标是找到第K小的数

当nums1与nums2的数组长度和为奇数时,则求第 (m + n + 1) / 2小的数。如果为偶数,则需要求第(m + n + 1) / 2小与(m + n + 2) / 2小的数的平均值。又因为当长度和为奇数时(m + n + 1) / 2等于(m + n + 2) / 2,所以其实问题可以简化(该思想可大量用在奇偶数的例题中)

// findK为寻找第k小的数
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int m = nums2.length;

        int k1 = (m + n + 1) / 2;
        int k2 = (m + n + 2) / 2;
        return (findK(nums1, 0, nums2, 0, k1) + findK(nums1, 0, nums2, 0, k2)) / 2.0;
    }
// findk...

所有问题都落到findK的求解上。我们要求第K小的数,那么我们从nums1和nums2中分别取第k / 2下标的数比较大小,例如此时nums1较小,那么我们就可以排除nums1中k / 2左区间的所有数,同时,继续调用findK寻找第K - (k / 2)小数。

当然,由于我们数组是固定的,因此我们用i, j分别代表nums1和nums2正在搜索的下标

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int m = nums2.length;

        int k1 = (m + n + 1) / 2;
        int k2 = (m + n + 2) / 2;
        return (findK(nums1, 0, nums2, 0, k1) + findK(nums1, 0, nums2, 0, k2)) / 2.0;
    }

    public int findK(int[] nums1, int i, int[] nums2, int j, int k){
    	// 如果i大于nums1的长度,则直接返回nums[2]的第k小数
        if(nums1.length == i) return nums2[j + k - 1];
        if(nums2.length == j) return nums1[i + k - 1];

		// 如果k为1,则直接返回小的元素
        if(k == 1) {
            return nums1[i] > nums2[j] ? nums2[j] : nums1[i];
        }

		// mid1代表nums1往下k/2个数的下标
        int mid1 = i + k / 2 - 1;
        // 如果下标超过当前数组,那就以最后一个元素为比较点
        if (mid1 > nums1.length - 1) mid1 = nums1.length - 1;

        int mid2 = j + k / 2 - 1;
        if (mid2 > nums2.length - 1) mid2 = nums2.length - 1;

        if(nums1[mid1] >= nums2[mid2]) {
            // nums1[mid]大,nums2[mid2]小,则说明可以排除nums2[mid2]左半区间的数
        	// 因此nums2从mid2 +1开始搜索
        	// mid2 - j + 1是此次排除掉的元素个数,下一次寻找则应该是第k - (mid2 - j -1)小的数
            return findK(nums1, i, nums2, mid2 + 1, k - mid2 + j - 1);
        } else {
            return findK(nums1, mid1 + 1, nums2, j, k - mid1 + i - 1);
        }
    }
}
``

猜你喜欢

转载自blog.csdn.net/chaitoudaren/article/details/106106710
今日推荐