1. 引入
我们从一个小案例来探讨
在一个指定的有序数组中,查找具体的一个数
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
在arr数组中,找7。如果找到了,就打印下标,找不到,就打印找不到。
在⼀个升序的数组中查找指定的数字k,很容易想到的⽅法就是遍历数组。
这里用代码实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
// 0 1 2 3 4 5 6 7 8 9
int k = 0;
scanf("%d", &k);//7
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int find = 0;//假设找不到
for (i = 0; i < sz; i++)
{
if (k == arr[i])
{
printf("找到了,下标是%d\n", i);
find = 1;
break;
}
}
if (find == 0)
{
printf("找不到\n");
}
return 0;
}
但是这种⽅法效率⽐较低。 ⽐如我买了⼀双鞋,你好奇问我多少钱,我说不超过300元。你还是好奇,你想知道到底多少,我就让你猜,你会怎么猜?你会1,2,3,4…这样猜吗?显然很慢;⼀般你都会猜中间数字,⽐如:150,然后看⼤了还是⼩了,这就是⼆分查找,也叫折半查找。
2. 算法思路
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
3. 算法步骤
(1) 如图假设要查找数字7,首先确定最左边和最右边的下标元素,分别命名为left,right
(2) 再确定查找区间的中间下标元素 mid = (left + right ) / 2
(3) 用待查找值与中间下标元素所确认的值进行比较,若相等,就打印下标。
(3) 若大于,则在前子表中继续进行二分查找
(4) 若小于,则在后子表中继续进行二分查找
(5) 查找成功后,返回数值的数组下标
/*假设查找数组*/int arr[] = {1,2,3,4,5,6,7,8,9,10}//中的数值7
分析:一共进行了四次查找,到left = right时查找结束。第一次mid = 4,下标对应值5小于7,则left = mid + 1 = 5,继续在后子表折半查找,第二次mid = 7,下标对应值8大于7,则right = mid - 1 = 6,继续在前子表折半查找,第三次mid = 5,下标对应值6小于7,则left = mid + 1 = 6,继续在后子表折半查找。第四次mid = 6,下标对应值等于7,查找成功 此时left = right,找到了7,对应下标为6
4. 算法实现
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
scanf("%d", &k);
//查找-二分查找
int left = 0;
int right = sz - 1;
int find = 0;//假设找不到
while (left<=right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
printf("找到了,下标是%d\n", mid);
find = 1;
break;
}
}
if (find == 0)
{
printf("找不到了\n");
}
return 0;
}
5. 问题
求中间元素的下标,使⽤ mid = (left + right) / 2 ,如果left和right⽐较⼤的时候可能存在问题
如下代码:
#include <limits.h>
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
//INT_MAX;
int left = 2147483646;
int right = 2147483646;
int mid = (left + right)/2;
printf("%d\n", mid);
return 0;
}
显然运行结果是错的!
为什么错了?
原因很简单因为left 和 right 太大了,两者相加已经超出了整形所能表示的最大值了,这时再除于2就有一些溢出了。
这时在求两数平均值时,我们用另一种巧妙的方法。
假设 有两个这样的台阶 怎么让他们一样高?
很容易想到 用两者之间的差值除2 再分给L 这样就能一样高了。
所以平均值就可以用 mid = left + (right - left) / 2
这样就不会有问题了 因为R不会越界的话,L只是加了一小部分绝对不可能越界,只要给一个正确的R,算法就不会有问题。
原代码就可以改成:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
scanf("%d", &k);
//查找-二分查找
int left = 0;
int right = sz - 1;
int find = 0;//假设找不到
while (left<=right)
{
int mid = left + (right-left)/2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
printf("找到了,下标是%d\n", mid);
find = 1;
break;
}
}
if (find == 0)
{
printf("找不到了\n");
}
return 0;
}