二分查找
参考链接:https://www.bilibili.com/video/BV1Ft41157zW
一,算法思想
假设我们要找的值在区间 [l, r] 中,我们根据区间的中间位置的值 a[m] 与目标值的关系,更新 l 和 r 的取值,从而对区间 [l, r] 进行二分来缩小区间。而当区间缩小到足够小时,即可确定目标值。
二,代码(以查询大于等于k且下标最小的数字的下标为例)
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #define N 100 int a[N], k; int bsearch(int l, int r) // 循环 { while (l + 1 < r) { int m = l + (r - l) / 2; if (a[m] < k) l = m; else r = m; } return r; } int find(int l, int r) // 递归 { if (l + 1 >= r) return r; int m = l + (r - l) / 2; if (a[m] < k) return find(m, r); else return find(l, m); } int main(void) { int n; while (scanf("%d%d", &n, &k) != EOF) { for (int i = 0; i < n; i++) scanf("%d", &a[i]); printf("%d\n", bsearch(-1, n)); printf("%d\n", find(-1, n)); } system("pause"); return 0; } /* 7 1 1 2 3 4 5 6 7 7 7 1 2 3 4 5 6 7 7 4 1 2 3 4 5 6 7 7 4 1 2 4 4 4 6 7 7 4 1 2 3 5 6 7 8 7 0 1 2 3 4 5 6 7 7 8 1 2 3 4 5 6 7 */
三,代码解析(以查询大于等于k且下标最小的数字的下标为例)
1, l 与 r 的指向
① 算法进行时
l:小于 k 的值的坐标
r:大于等于 k 的值的下标
② 算法结束时
l:小于 k 且下标最大的值的下标
r:大于等于 k 且下标最小的值的下标
2,为什么算法结束后, l 与 r 的指向会是这样?
因为
① 算法进行时,l 必须满足小于 k 的坐标;r 必须满足大于等于 k 的值的坐标。
② 算法终止的条件是 l + 1 >= r,即 l 与 r 位置刚好相邻。
所以,只有一种情况,算法才有可能终止:
l:小于 k 且下标最大的值的下标;r:大于等于 k 且下标最小的值的下标。此时,l 和 r 相邻
3,注意点
① 数组中未必有 k
② 注意边界
当数组为 [1, 3, 5, 6],目标值为 0 时,预期的答案是 0,但如果 l 的初值为 0 的话,最终的答案只能为 1。
因为 l 无法再小了,所以 r 最小也之能到 1。
当数组为 [1, 3, 5, 6],目标值为 7 时,预期的答案是 4,但如果 r 的初值为 3 的话,最终的答案只能为 1。
因为 r 无法再大了,所以 r 最大也之能到 3。
而且,但 l 和 r 的初始值为 -1 和 n 时,该下标对应的值 (虽然没有对应的值) 是不会被动用的
因为,但 l == -1 时,且要找的值下溢出,此时动的只有 r,且当 r 移动到 0 时,算法结束,没有动用过 -1 对应的值。
因为,但 r == n 时,且要找的值上溢出,此时动的只有 l,且当 l 移动到 n-1 时,算法结束,没有动用过 n 对应的值。
③ 区间 <==> 结束条件
如果区间决定了,则结束条件也相应的确定了。反之亦然。(见例题⑤)
4,递归说明
在搜索阶段,通过判断区间的中间位置的值 a[m] 与目标值的关系,更新 l 和 r 的取值,从而对区间 [l, r] 进行二分来缩小区间。
在回溯阶段,不断将搜索的结果 return 下去。
四,百花
根据三种结束条件,二分法可以有三种写法。(其中,l + 1 < r 最简单)
① l + 1 < r;② l < r;③ l <= r;
根据实现的方法不同,每种二分法可以有两种写法
① 循环;② 递归
根据 l 和 r 的判断条件,每种二分可以用来求解两种问题
① 求小于等于 k 且下标最大的数字的下标;
② 求大于等于 k 且下标最小的数字的下标;
下面给出求解大于等于 k 且下标最小的数字的下标的问题的代码,另外一个问题可以自己尝试着自己推导一下。
① l + 1 < r && 求大于等于 k 且下标最小的数字的下标 && 包含循环和递归的写法
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #define N 100 int a[N], k; int bsearch(int l, int r) // 循环 { while (l + 1 < r) { int m = l + (r - l) / 2; if (a[m] < k) l = m; else r = m; } return r; } int find(int l, int r) // 递归 { if (l + 1 >= r) return r; int m = l + (r - l) / 2; if (a[m] < k) return find(m, r); else return find(l, m); } int main(void) { int n; while (scanf("%d%d", &n, &k) != EOF) { for (int i = 0; i < n; i++) scanf("%d", &a[i]); printf("%d\n", bsearch(-1, n)); printf("%d\n", find(-1, n)); } system("pause"); return 0; } /* 7 1 1 2 3 4 5 6 7 7 7 1 2 3 4 5 6 7 7 4 1 2 3 4 5 6 7 7 4 1 2 4 4 4 6 7 7 4 1 2 3 5 6 7 8 7 0 1 2 3 4 5 6 7 7 8 1 2 3 4 5 6 7 */
② l < r && 求大于等于 k 且下标最小的数字的下标 && 包含循环和递归的写法
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 100 int a[N], k; int find(int l, int r) // 递归 { if (l >= r) return r; int m = l + (r - l) / 2; if (a[m] < k) return find(m + 1, r); else return find(l, m); } int bsearch(int l, int r) // 循环 { while (l < r) { int m = l + (r - l) / 2; if (a[m] < k) l = m + 1; else r = m; } return r; } int main(void) { int n; while (scanf("%d%d", &n, &k) != EOF) { for (int i = 0; i < n; i++) scanf("%d", &a[i]); printf("%d\n", bsearch(0, n)); printf("%d\n", find(0, n)); } system("pause"); return 0; } /* 7 1 1 2 3 4 5 6 7 7 7 1 2 3 4 5 6 7 7 4 1 2 3 4 5 6 7 7 4 1 2 4 4 4 6 7 7 4 1 2 3 5 6 7 8 7 0 1 2 3 4 5 6 7 7 8 1 2 3 4 5 6 7 */
③ l <= r && 求大于等于 k 且下标最小的数字的下标 && 包含循环和递归的写法
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 100 int a[N], k; int find(int l, int r) // 递归 { if (l > r) return l; int m = l + (r - l) / 2; if (a[m] < k) return find(m + 1, r); else return find(l, m - 1); } int bsearch(int l, int r) // 循环 { while (l <= r) { int m = l + (r - l) / 2; if (a[m] < k) l = m + 1; else r = m - 1; } return l; } int main(void) { int n; while (scanf("%d%d", &n, &k) != EOF) { for (int i = 0; i < n; i++) scanf("%d", &a[i]); printf("%d\n", bsearch(0, n - 1)); printf("%d\n", find(0, n - 1)); } system("pause"); return 0; } /* 7 1 1 2 3 4 5 6 7 7 7 1 2 3 4 5 6 7 7 4 1 2 3 4 5 6 7 7 4 1 2 4 4 4 6 7 7 4 1 2 3 5 6 7 8 7 0 1 2 3 4 5 6 7 7 8 1 2 3 4 5 6 7 */
五,例题
① 链接:https://leetcode-cn.com/problems/search-insert-position/
求解思路:
求插入的位置就是要求 > k 的值的下标;求目标值就是求 == k 的值的下标。综上就是求大于等于 k 且下标最小的数字的下标。
class Solution { public: int k; int find(vector<int>& a, int l, int r) // 递归 { if (l+1 >= r) return r; int m = l + (r - l) / 2; if (a[m] < k) return find(a, m, r); else return find(a, l, m); } int bsearch(vector<int>& a, int l, int r) // 循环 { while (l + 1 < r) { int m = l + (r - l) / 2; if (a[m] < k) l = m; else r = m; } return r; } int searchInsert(vector<int>& nums, int target) { k = target; return find(nums, -1, nums.size()); return bsearch(nums, -1, nums.size()); } };
② 链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
求解思路:
主要是没有找到的判断条件:
数组为空 return -1;
找到的值超出边界,return -1;(要是 u 或者 d == -1 最后就返回 -1 了,所以这里可以不用判断)
找到的值不等于边界值,return -1;
class Solution { public: int k; int findU(vector<int>& a, int l, int r) { if (l + 1 >= r) return r; int m = l + (r - l) / 2; if (a[m] < k) return findU(a, m, r); else return findU(a, l, m); } int findD(vector<int>& a, int l, int r) { if (l + 1 >= r) return l; int m = l + (r - l) / 2; if (a[m] <= k) return findD(a, m, r); else return findD(a, l, m); } vector<int> searchRange(vector<int>& nums, int target) { if(nums.empty()) return {-1, -1}; k = target; int u = findU(nums, -1, nums.size()); int d = findD(nums, -1, nums.size()); if(u == nums.size() || d == nums.size()) return {-1, -1}; if(nums[u] != target || nums[d] != target) return {-1, -1}; return {u, d}; } };
③ 链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/
求解思路:
以最小值为分界线,左边的值大于 nums.back(),右边的值小于 nums.back(),最小值在 >= nums.back() 且下标最小的数字的下标。
所以,可以设 k 为 nums.back(),令 l 指向大于 nums.back() 的值的下标,令 r 指向小于等于 nums.back() 的值的下标。
于是,算法结束后 r 就指向最小值的下标。
class Solution { public: int find(vector<int>& nums, int l, int r) { if(l + 1 >= r) return r; int m = l + (r - l) / 2; if(nums[m] > nums.back()) return find(nums, m, r); else return find(nums, l, m); } int findMin(vector<int>& nums) { return nums[find(nums, -1, nums.size())]; } };
④ 链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array/
求解思路:
在上一题的基础上,先求出最小值的下标,在进行目标值与 nums.back() 的大小比较。
若大于 nums.back(),则对区间 [0, m-1] 进行二分;
若小于 nums.back(),则对区间 [m, n-1] 进行二分。
class Solution { public: int findM(vector<int>& nums, int l, int r) { if(l + 1 >= r) return r; int m = l + (r - l) / 2; if(nums[m] > nums.back()) return findM(nums, m, r); else return findM(nums, l, m); } int k; int find(vector<int>& nums, int l, int r) { if(l + 1 >= r) return r; int m = l + (r - l) / 2; if(nums[m] < k) return find(nums, m, r); else return find(nums, l, m); } int search(vector<int>& nums, int target) { if(nums.empty()) return -1; k = target; int m = findM(nums, -1, nums.size()); int l, r; if(target <= nums.back()) l = m - 1, r = nums.size(); else l = -1, r = m; int x = find(nums, l, r); if(nums.size() == x || nums[x] != target) return -1; return x; } };
⑤ 链接:https://leetcode-cn.com/problems/find-peak-element/
求解思路:
① 若 a[m] < a[m+1],则说明 m+1 ~ n-1 的区间必存在峰值。
② 若 a[m] > a[m+1],则说明 0 ~ m 的区间必存在峰值。
证 ①:
若 m+2 ~ n-1 的值皆小于 a[m+1],则 a[m+1] 为峰值;
若 m+2 ~ n-1 的值皆大于 a[m+1],则分为两种情况:
若 m+2 ~ n-1 的值是单调递增的,则 a[n-1] 为峰值;
若 m+2 ~ n-1 的值是不具有单调性的,则其中某个值必为峰值;
综上,① 得证。
注意点:
这一题的结束条件不能用 l + 1 >= r,因为:
定性分析:因为当最后 l 和 r 指向最后剩下的两个时,并不一定是 l 或者是 r 的指向是峰值,它们的较大值才是峰值。所以 l 和 r 必须指向同一个值才能判定哪个值是峰值,即结束条件只能是 l >= r
定量分析:因为判断条件后执行的语句只能是 l = m + 1 或者 r = m,所以结束条件只能是 l >= r
所以说,区间决定结束条件。
class Solution { public: int find(vector<int>& nums, int l, int r) { if (l >= r) return r; int m = l + (r - l) / 2; if (nums[m] < nums[m + 1]) return find(nums, m + 1, r); else return find(nums, l, m); } int findPeakElement(vector<int>& nums) { return find(nums, 0, nums.size() - 1); } };
⑥ 链接:https://leetcode-cn.com/problems/h-index-ii/
求解思路:
问题概述:设 h 满足:在升序序列中 h 个数大于 h,求 h 的最大值。
因为需要在数组取 h 个数,所以 h ~ [0, n],
所以,可以用二分在 [0, n] 内查找:
设 m 为 h
如果 nums[ nums.size() - m ] >= m,则说明 nums.size() - m ~ nums.size() - 1 之间 m 个都是大于等于 m,
所以此时 h 一定在区间 [m, n ] 之间;
如果 nums[ nums.size() - m ] < m,则说明大于等于 m 的个数小于 m,
所以此时 h 一定在区间 [0, m-1 ] 之间;
注意点:
① 还是区间决定结束条件,所以结束条件还是只能用 l >= r
② 因为 h ~ [0, n],所以取中间值要用 l + r + 1 >> 1
class Solution { public: int find(vector<int>& nums, int l, int r) { if(l>=r) return r; int m = r + l + 1 >> 1; if(nums[nums.size()-m]>=m) return find(nums, m,r); else return find(nums, l,m-1); } int hIndex(vector<int>& citations) { return find(citations, 0, citations.size()); } };
========== ========== ======== ======= ====== ====== ==== === == =
问刘十九 白居易(唐)
绿蚁新醅酒,红泥小火炉。
晚来天欲雪,能饮一杯无?