赞
踩
二分查找是一种非常简单易懂的快速查找算法,生活中到处可见。
例子一
我们现在来做一个猜字游戏。我随机写一个 0 到 99 之间的数字,然后你来猜我写的是什么。猜的过程中,你每猜一次,我就会告诉你猜的大了还是小了,直到猜中为止。你来想想,如何快速猜中我写的数字呢?
假设我写的数字是 23,猜数过程如下。(如果猜测范围的数字有偶数个,中间数有两个,就选择较小的那个。)
例子2
假设有1000条订单数据,已经按照订单金额从小到大排序,每个订单金额不同,并且最小单位是元。我们现在想知道支付存在金额等于19元的订单。如果存在,则返回订单数据,如果不存在,则返回null。
最简单的方法是从第一各订单开始,一个一个遍历这100个订单,直到找到金额等于19元的订单为止。但这样查找就比较慢,最快情况下,可能要遍历完这1000条记录才能找到。那用二分查找能不能更快速地解决呢?
我们假设只有 10 个订单,订单金额分别是:8,11,19,23,27,33,45,55,67,98。,每次都与区间的中间数据比对大小,缩小查找区间的范围。如下图,low 和 high 表示待查找区间的下标,mid 表示待查找区间的中间元素下标。
二分查找针对的是一个有序的数据集合,查找思想有点类型分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到知道要查找的元素,或者区间被缩小为0
二分查找是一种非常高效的查找算法,其时间复杂度达到了惊人的O(logn)。分析如下:
O(logn) 这种对数时间复杂度。这是一种极其高效的时间复杂度,有的时候甚至比时间复杂度是常量级 O(1) 的算法还要高效。为什么这么说呢?
实际上,简单的二分查找并不难写,二分查找的变体才是真正烧脑的。
最简单的情况就是有序数组中不存在重复元素,写法如下:
bool bsearch(std::vector<int> &vec, int value){ if(vec.empty()){ return false; } int low = 0, high = vec.size() - 1; //当前查找的区间 while (low <= high ){ int mid = low + ((high - low ) >> 1); // 相当于 mid=(low+high)/2 ; if(vec[mid] == value){ //找到了 return true; }else if(vec[mid] < value){ low = mid + 1;// 更新要查找的区间 }else{ high = mid - 1;// 更新要查找的区间 } } return false; }
注意,比较容易出错的三个地方如下:
(1)循环退出条件
(2)mid的取值
(3)low 和 high 的更新
实际上,二分查找除了用循环来实现,还可以用递归来实现
bool process(std::vector<int> &vec, int low, int high, int value){ if(low > high){ return false; } int mid = low + ((high - low) >> 1); if(vec[mid] == value){ return true; }else if(vec[mid] < value){ return process(vec, mid + 1, high, value); }else{ return process(vec, low, mid - 1, value); } } bool bsearchInternally(std::vector<int> &vec, int value){ return process(vec, 0, vec.size() - 1, value); }
对数器
#include <algorithm> bool test(std::vector<int> vec, int num){ return std::find(vec.begin(), vec.end(), num) != vec.end(); } std::default_random_engine e; std::vector<int> generateRandom(int maxLen, int minValue, int maxValue){ std::uniform_int_distribution<int> distS(1, maxLen); std::uniform_int_distribution<int> distV(minValue, maxValue); std::vector<int> arr; int size = distS(e); arr.resize(size); for (int i = 0; i < size; ++i) { arr[i] = distV(e); } return arr; } int generateNum(int minValue, int maxValue){ std::uniform_int_distribution<int> distV(minValue, maxValue); return distV(e); } template<typename T> void display(std::vector<T>& vec) { std::cout << "vec: " ; for (auto it: vec) { std::cout << it << ","; } std::cout << "\n"; } int main(){ int testTime = 500000; int maxSize = 10; int minValue = -100; int maxValue = 100; bool succeed = true; bool right, ans1, ans2; for (int i = 0; i < testTime; i++) { auto arr = generateRandom(maxSize,minValue, maxValue); std::sort(arr.begin(), arr.end()); int value = generateNum(minValue, maxValue); right = test(arr, value); ans1 = bsearch(arr, value); ans2 = bsearchInternally(arr, value); if (right !=ans1 || ans1 != ans2) { succeed = false; display(arr); printf("target : %d , right : %d, answer: %d, %d\n", value, right, ans1, ans2); break; } } printf("%s", succeed ? "Nice!" : "Fucking fucked!"); }
除了返回是否存在,还可以返回索引
int process(std::vector<int> &vec, int low, int high, int value){
if(low > high){
return -1;
}
int mid = low + ((high - low) >> 1);
if(vec[mid] == value){
return mid;
}else if(vec[mid] < value){
return process(vec, mid + 1, high, value);
}else{
return process(vec, low, mid - 1, value);
}
}
现在有一个从小到大排序的有序数据集合,这个有序数据集合中存在重复的数据,我们希望能够找到第一个等于给定值的数据。
思路:
arr[mid] = arr[mid - 1] == arr[mid - i]== value
// 查找最左的==value的元素 int bsearch(std::vector<int> &arr, int value){ int low = 0, high = arr.size() - 1; //当前查找的区间 while(low <= high){ int mid = low + ((high - low) >> 1); //易错点 ()一定要用起来 if(value < arr[mid]){ high = mid - 1; }else if(arr[mid] < value){ low = mid + 1; }else{ if((mid == 0) || arr[mid - 1] != value){ return mid; }else{ high = mid - 1; } } } return -1; }
对数器
#include <algorithm> int test(std::vector<int> &vec, int num){ for (int i = 0; i <= (int)vec.size() - 1; ++i) { if(vec[i] == num){ return i; } } return -1; } std::default_random_engine e; std::vector<int> generateRandom(int maxLen, int minValue, int maxValue){ std::uniform_int_distribution<int> distS(1, maxLen); std::uniform_int_distribution<int> distV(minValue, maxValue); std::vector<int> arr; int size = distS(e); arr.resize(size); for (int i = 0; i < size; ++i) { arr[i] = distV(e); } return arr; } int generateNum(int minValue, int maxValue){ std::uniform_int_distribution<int> distV(minValue, maxValue); return distV(e); } template<typename T> void display(std::vector<T>& vec) { std::cout << "vec: " ; for (auto it: vec) { std::cout << it << ","; } std::cout << "\n"; } int main(){ int testTime = 100000; int maxSize = 100; int minValue = -50; int maxValue = 10; bool succeed = true; bool right, ans1; for (int i = 0; i < testTime; i++) { auto arr = generateRandom(maxSize,minValue, maxValue); std::sort(arr.begin(), arr.end()); int value = generateNum(minValue, maxValue); right = test(arr, value); ans1 = bsearch(arr, value); if (right !=ans1 ) { succeed = false; display(arr); printf("target : %d , right : %d, answer: %d\n", value, right, ans1); break; } } printf("%s", succeed ? "Nice!" : "Fucking fucked!"); }
// 查找最右的==value的元素 int bsearch(std::vector<int> &arr, int value){ int N = arr.size(); int low = 0, high = N - 1; //当前查找的区间 while(low <= high){ int mid = low + ((high - low) >> 1); if(value < arr[mid]){ high = mid - 1; }else if(arr[mid] < value){ low = mid + 1; }else{ if((mid == N - 1) || arr[mid + 1] != value){ return mid; }else{ low = mid + 1; } } } return -1; }
对数器
#include <algorithm> int test(std::vector<int> &vec, int num){ for (int i = (int)vec.size() - 1; i >= 0; --i) { if(vec[i] == num){ return i; } } return -1; } std::default_random_engine e; std::vector<int> generateRandom(int maxLen, int minValue, int maxValue){ std::uniform_int_distribution<int> distS(1, maxLen); std::uniform_int_distribution<int> distV(minValue, maxValue); std::vector<int> arr; int size = distS(e); arr.resize(size); for (int i = 0; i < size; ++i) { arr[i] = distV(e); } return arr; } int generateNum(int minValue, int maxValue){ std::uniform_int_distribution<int> distV(minValue, maxValue); return distV(e); } template<typename T> void display(std::vector<T>& vec) { std::cout << "vec: " ; for (auto it: vec) { std::cout << it << ","; } std::cout << "\n"; } int main(){ int testTime = 10000; int maxSize = 100; int minValue = -50; int maxValue = 10; bool succeed = true; bool right, ans1; for (int i = 0; i < testTime; i++) { auto arr = generateRandom(maxSize,minValue, maxValue); std::sort(arr.begin(), arr.end()); int value = generateNum(minValue, maxValue); right = test(arr, value); ans1 = bsearch(arr, value); if (right !=ans1 ) { succeed = false; display(arr); printf("target : %d , right : %d, answer: %d\n", value, right, ans1); break; } } printf("%s", succeed ? "Nice!" : "Fucking fucked!"); }
在有序数组中,查找第一个大于等于给定值的元素。比如,数组中存储的这样一个序列:3,4,6,7,10。如果查找第一个大于等于 5 的元素,那就是 6。
实现一
思路:
// 查找最右的==value的元素 int bsearch(std::vector<int> arr, int value) { int N = arr.size(); int low = 0, high = arr.size() - 1; while (low <= high) { int mid = low + ((high - low) >> 1); if(arr[mid] >= value){ if((mid == 0) || (arr[mid - 1] < value)){ return mid; }else{ high = mid - 1; } }else{ low = mid + 1; } } return -1; }
实现二
int bsearch(std::vector<int> &arr, int value){ int N = arr.size(); int low = 0, high = N - 1; //当前查找的区间 int idx = -1; while(low <= high){// 记录最左的对号 int mid = low + ((high - low) >> 1); if(arr[mid] >= value){ idx = mid; high = mid - 1; }else{ low = mid + 1; } } return idx; }
对数器
#include <algorithm> int test(std::vector<int> &arr, int value){ for (int i = 0; i < arr.size(); ++i) { if(value <= arr[i]){ return i; } } return -1; } std::default_random_engine e; std::vector<int> generateRandom(int maxLen, int minValue, int maxValue){ std::uniform_int_distribution<int> distS(1, maxLen); std::uniform_int_distribution<int> distV(minValue, maxValue); std::vector<int> arr; int size = distS(e); arr.resize(size); for (int i = 0; i < size; ++i) { arr[i] = distV(e); } return arr; } int generateNum(int minValue, int maxValue){ std::uniform_int_distribution<int> distV(minValue, maxValue); return distV(e); } template<typename T> void display(std::vector<T>& vec) { std::cout << "vec: " ; for (auto it: vec) { std::cout << it << ","; } std::cout << "\n"; } int main(){ int testTime = 10000; int maxSize = 100; int minValue = -50; int maxValue = 10; bool succeed = true; bool right, ans1; for (int i = 0; i < testTime; i++) { auto arr = generateRandom(maxSize,minValue, maxValue); std::sort(arr.begin(), arr.end()); int value = generateNum(minValue, maxValue); right = test(arr, value); ans1 = bsearch(arr, value); if (right !=ans1 ) { succeed = false; display(arr); printf("target : %d , right : %d, answer: %d\n", value, right, ans1); break; } } printf("%s", succeed ? "Nice!" : "Fucking fucked!"); }
查找最后一个小于等于给定值的元素。比如,数组中存储了这样一组数据:3,5,6,8,9,10。最后一个小于等于 7 的元素就是6
int bsearch(std::vector<int> &arr, int value){
int low = 0, high = arr.size() - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (arr[mid] > value) {
high = mid - 1;
} else {
if ((mid == arr.size() - 1) || (arr[mid + 1] > value)) return mid;
else low = mid + 1;
}
}
return -1;
}
实现二:
int bsearch(std::vector<int> &arr, int value){
int N = arr.size();
int low = 0, high = N - 1; //当前查找的区间
int index = -1; // 记录最右的对号
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (arr[mid] <= value) {
index = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
return index;
}
思路
int getLessIndex(std::vector<int> arr, int value) { int N = arr.size(); if(N == 0){ return -1; } if(N == 1 || arr[0] < arr[1] ){ return 0; } if(arr[N - 2] > arr[N - 1]){ return N - 1; } int left = 1, right = N - 2, mid; while (left < right) { mid = (left + right) / 2; if (arr[mid] > arr[mid - 1]) { right = mid - 1; } else if (arr[mid] > arr[mid + 1]) { left = mid + 1; } else { return mid; } } return left; }
为什么能二分
二分不一定必须要有序的,不是只有有序才能二分,
一种策略,如果有一边肯定有或者有一边肯定没有或者有一边可能有但另一边一定没有,则可以去进行二分操作
对数器
二分查找的时间复杂度是 O(logn),查找数据的效率非常高。不过,并不是什么情况下都可以用二分查找,它的应用场景是有很大局限性的。那什么情况下适合用二分查找,什么情况下不适合呢?
(1)首先,二分查找依赖的是顺序表结构,简单的来说是数组
(2)其次,二分查找针对的是有序数据
(3)再次,数据量太小不适合二分查找
(4)最后,数据量太大也不适合二分查找
假设我们有 1000 万个整数数据,每个数据占 8 个字节,如何设计数据结构和算法,快速判断某个整数是否出现在这 1000 万数据中? 我们希望这个功能不要占用太多的内存空间,最多不要超过 100MB。
我们的内存限制是100MB,每个数据大小是8字节,最简单的方法就是将数据存储在数组中,内存占用差不多是80MB,符合内存的限制。我们可以先对这1000万数据从小到大排序,然后在利用二分查找算法,就可以快速查找相要的数据了。
问题是,散列表、二分树这些动态数据结构也能支持快速查找,能不能用散列表和二叉树解决这个问题呢?
答案是不行的。虽然大部分情况下,用二分查找可以解决的问题,用散列表、二叉树都可以解决。但是,不管是散列表还是二叉树,都会需要比较大的额外的内存空间,如果用散列表或者二叉树来存储这 1000 万的数据,用 100MB 的内存肯定是存不下的。而二分查找底层依赖的是数组,除了数据本身之外,不需要额外存储其他信息,是最省内存空间的存储方式。所以刚好能在限定的内存大小下解决这个问题。
问题模式
通过 IP 地址来查找 IP 归属地的功能,如下:
这个功能并不复杂,它是通过维护一个很大的IP地址库来实现的。地址库中包括IP地址范围和归属地的方式。
当我们想要查询 202.102.133.13 这个 IP 地址的归属地时,我们就在地址库中搜索,发现这个 IP 地址落在 [202.102.133.0, 202.102.133.255] 这个地址范围内,那我们就可以将这个 IP 地址范围对应的归属地“山东东营市”显示给用户了。
[202.102.133.0, 202.102.133.255] 山东东营市
[202.102.135.0, 202.102.136.255] 山东烟台
[202.102.156.34, 202.102.157.255] 山东青岛
[202.102.48.0, 202.102.48.255] 江苏宿迁
[202.102.49.15, 202.102.51.251] 江苏泰州
[202.102.56.0, 202.102.56.255] 江苏连云港
现在我的问题是,在庞大的地址库中逐一比对 IP 地址所在的区间,是非常耗时的。假设我们有 12 万条这样的 IP 区间与归属地的对应关系,如何快速定位出一个 IP 地址的归属地呢?
分析
如果IP区间和归属地的对应关系不经常更新,我们可以先预先处理这万条数据,让其按照起始IP从小到达排序。如何来排序呢?我们知道,IP地址可以转换为32位的整数。所以,我们可以将起始地址,按照对应的整形值的大小关系,从小到大进行排序。
然后,这个问题就转换为“在有序数据中,查找最后一个小于等于某个给定值的元素了”
当我们要查询某个IP归属地时,可以可以先通过二分查找,找到最后一个起始IP小于等于这个IP的IP区间,然后,检测这个IP是否在这个IP区间内,如果在,我们就取出对应的归属地显示,如果不在,就返回未找到
二分查找的核心思想理解起来非常简单,有点类似分治思想。即每次都通过跟区间中的中间元素对比,将待查找的区间缩小为一半,直到找到要查找的元素,或者区间被缩小为 0。
二分查找虽然性能比较优秀,但应用场景也比较有限。底层必须依赖数组,并且还要求数据是有序的。对于较小规模的数据查找,我们直接使用顺序遍历就可以了,二分查找的优势并不明显。二分查找更适合处理静态数据,也就是没有频繁的数据插入、删除操作。
凡是能用二分查找解决的,绝大部分我们更倾向于用散列表或者二叉查找树。即便是二分查找在内存使用上更节省,但是毕竟内存紧缺的情况并不多。那二分查找真的没什么用处了吗?
实际上,求“值等于给定值”的二分查找确实不怎么会被用到,二分查找更适合用在“近似”查找问题,在这类问题上,二分查找的优势更加明显。比如上面的这几种变体问题,用其他数据结构,比如散列表、二叉树,就比较难实现了。
变体的二分查找算法写起来非常烧脑,需要重点关注的是:终止条件、区间上下界更新方法、返回值选择
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。