我们学习二分查找也好,或者其它的算法或者排序也好。最重要的一点是搞清楚算法的思想,只有我们搞明白算法的思想以后,我们才能真正的脑子中记住它,知道它的使用条件或者在实现过程中有哪些坑,这是最重要的。不仅仅是本篇的二分查找,其它的算法也是如此。实现并不重要,理解,清楚才是重中之重。
二分查找简介:
二分查找是一种快速从大量有序数据中查找目标数据的一种算法,当然二分查找也是一种高效的查找方法。
要使用二分查找必须满足的两个前提:
- 数据必须是顺序存储(比如顺序表、数组这样的顺序存储结构)
- 待排序列必须有序
算法思想:
- 通过两个变量left和right同时标记数组的第一个位置的,和数组的最后一个位置。
- 每一次取到数组最中间数的下标mid对应的那个数nums[mid],与需要查找的数x进行比较
- 如果 nums[mid] < x , 那么下标【0,mid】对应的数肯定都小于x,所以目标数字不会在【0,mid】,将区间范围缩小到【mid+1,right】
- 如果 nums[mid] > x, 那么下标【mid, right】对应的数肯定都大于x,所以目标数字不会在【mid,right】,将区间范围缩小到【0,mid-1】
- 如果 nums[mid] == x, 那么直接返回目标数字x对应的下标mid
栗子:
我们要从以下有序数组中找到目标数字8
算法的分析:
1. 时间复杂度的分析
相较于遍历数组查找,找出数组中是否含有特定的值x,也就是需要从头到尾遍历一个数组。时间复杂度为O(N).。
而二分查找,查找一次若一次直接找到特定值x,直接返回这个数的下标;若第一次没有找到特定值x,则数组中间的值与x进行比较,选择符合条件的一半区间,淘汰掉一半区间。 即数组长度为n,则2^M = n , 那么M= log以2为底的n, 所以二分查找的时间复杂度记为O(lgN)(lgN 其实就是log以2为底N的对数)
为了进一步理解二分查找的优势: 我们其实可以想象,如果在30亿个数据的有序数组中,找到一个数, 普通的遍历查找需要查找30亿次(最坏情况下),而二分查找只需要32次(最坏情况下)。 我们便可以知道二分查找算法的性能多么高效。
2. 空间复杂度的分析
二分查找是在原数组上进行查找,并没有开额外的空间,所以二分查找的空间复杂度为O(1).
下面我们看一下代码是如何实现的?
算法代码实现:
实现的方法一: 闭区间【0,n-1】的情况
int BinaryFind(int *nums, int n, int x)
{
int left = 0;
int right = n - 1;
while(left <= right)
{
//不推荐这一种写法,容易发生溢出,超过int的范围
//int mid = (left + right)/2;
//这样的写法不会产生溢出,但是还需要改进,位运算符的效率比加减乘除的效率要高
//还可以优化
//int mid = left + (right-left)/2;
int mid = left + ((right - left) >> 1);
if(nums[mid] < x)
{
left = mid + 1;
}
else if(nums[mid] > x)
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
实现方法二:左闭右开的情况 【0,n)
int BinaryFind(int *nums, int n, int x)
{
int left = 0;
int right = n;
while(left < right) //注意 左闭右开的时候不能取 = 号 !!!
{
//不推荐这一种写法,容易发生溢出,超过int的范围
//int mid = (left + right)/2;
//这样的写法不会产生溢出,但是还需要改进,位运算符的效率比加减乘除的效率要高
//还可以优化
//int mid = left + (right-left)/2;
int mid = left + ((right - left) >> 1);
if(nums[mid] < x)
{
left = mid + 1; //保持它是一个闭区间
}
else if(nums[mid] > x)
{
right = mid ; //保持它是一个开区间
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int nums[] = { 1, 3, 4, 5, 6, 8, 9, 10};
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 1));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 2)); //不存在 输出-1
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 3));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 4));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 5));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 6));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 8));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 9));
printf("%d\n", BinaryFind(nums, sizeof(nums) / sizeof(int), 10));
system("pause");
}
结果: 运行正确。
运行失败的栗子:
如果while循环中取到’=’号while(left<=right),程序到找目标数字2的时候则崩溃。
崩溃原因:
程序走到最后一步left = 1, right = 1, mid也等于1, 但是nums[mid] = 3 > x(2) , right = mid = 1. 就这样一直死循环,所以程序崩溃。
两种实现方法需要注意的地方:
- while循环中必须是left <= right 因为如果循环条件中是while(left < right)的时候,假如【3,3】的时候,我们还需要在比较一次mid =(3+3)/2, 而不是直接进不去while循环,直接返回-1.
- 当缩小区间的时候必须是left = mid+1,或者是right = mid-1。left或right不能等于mid
当某些情况发生时, left和right取【7,8】时, x的值要比mid对应的值要大,而如果left = mid = (7+8)/2 = 7。 那么left的值一直会是7,这样就会发生死循环。
总结:当每次比较的时候,都要保证它的比较的区间是闭区间,所以mid每次都比较过了,所以要left=mid+1或者right = mid-1。
- while循环中必须是left<right,要不然可能引起程序一直死循环 如代码实现方法二中的运行崩溃的原因。
- 一直要保持下一个查找区间是左闭右开,所以要left = mid+1,营造左边是闭区间的能取到mid+1; 或者right = mid,营造右边开区间不能取到mid