对有序表进行查找运算的时候,可以通过缩减问题的规模,大幅度提高查找效率。
首节点 5 的位置为0,尾结点 为 199 的地址为 11;
求和折半后( (11+1)/ 2 )计算出中间位置的地址为 5;
与 位置5 上的元素 43 比较,21 小于 43,因此 21 只能出现在左半段;
缩小查找范围,舍弃右半段;
重复折半查找的过程:
计算出中间位置为2 (此处2,指的是下标为2的位置),与 位置2 的 元素12 相比较,21>12。因此 21 只能出现在右半段,舍弃左半段。
现在查找段的首结点地址为3,尾结点地址为4,
求和折半后,计算出中间位置的地址为 3,与 位置3 上的 元素21 比较,恰好等于待查找元素21,查找成功!
核心思想:① 计算中值位置;② 缩小查找区间。
left 表示起点,right 表示终点,mid 表示中值点,数组 a 存放元素,x 为待查找元素
left | 表示起点 |
right | 表示终点 |
mid | 表示中值点 |
数组 a[] | 存放元素 |
x | 待查找元素 |
三条语句:
① 计算中值点:mid = (left + right) / 2
② 若 x = a[mid],查找成功,返回mid,结束;
2.1 若 x < a[mid],则往左缩小查找区间,重复上述过程;
2.2 若 x > a[mid],则往右缩小查找区间,重复上述过程。
代码:
int binary_search(int a[],int x,int left,int right){
int mid;
mid = ( left + right )/ 2;// 计算中值点
if( x == a[mid] )// 若 x = a[mid],查找成功,返回mid
return mid;
if(x < a[mid] )
return binary_search(a,x,left,mid - 1);// 问题的规模缩小了,但是问题的性质没变化,可以采用递归的方式,只是查找区间变为了 left ~ mid - 1 阶段。
else
return binary_search(a,x,mid + 1,right);
}
例题:用二分法查找元素 83
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
元素 | 5 | 8 | 12 | 21 | 29 | 43 | 52 | 64 | 70 | 81 | 87 | 199 |
left | mid | right |
① left = 0,right = 11;
② 计算中值点:mid = (0+11)/ 2;
③ 第一次比较结果:x > a[5] (83 > 43),因此去掉左半段,修改 left 的值 left = mid + 1 = 6,变成如下:
下标 | 6 | 7 | 8 | 9 | 10 | 11 |
元素 | 52 | 64 | 70 | 81 | 87 | 199 |
left = mid + 1 = 6 | right |
开始第二次比较:
① left = 6,right = 11;
② 计算中值点:mid =(6 + 11)/ 2 = 8;
③ 第二次比较结果:x > a[8](83 > 70),因此去掉左半段,修改 left 值,left = mid + 1 = 9,变成如下:
下标 | 9 | 10 | 11 |
元素 | 81 | 87 | 199 |
left = mid + 1 = 9 | right |
开始第三次比较:
① left = 9,right = 11;
② 计算中值点:mid =(9+11)/ 2 = 10;
③ 第三次比较结果:x < a[10](83 < 87),因此去掉右半段,修改 rigth 值,right = mid - 1 = 9,变成如下:
下标 | 9 | 10 | 11 |
元素 | 81 | 87 | 199 |
right = mid - 1 = 9 | mid | right |
下标 | 9 |
元素 | 81 |
right = mid - 1 = 9; left = 9; |
开始第四次比较:
① left = 9,right = 9;
② 计算中值点:mid =(9+9)/ 2 = 9;
③ 第四次比较结果:x > a[9](83 > 81),因此修改 left 值,left = mid + 1 = 10,当 left > right 时,查找段已经不复存在,换言之,查找失败。
二分查找算法----代码部分:
① 第一种写法
int binary_search(int a[],int x,int left,int right){
int mid;
if(left > right) return -1;// 查找失败,返回 -1
mid =(left + right)/ 2;
if( x == a[mid] )
return mid;// 查找到,返回
if( x < a[mid] )
return binary_search(a,x,left,mid - 1);// 左端查找
return binary_searc(a,x,mid + 1,right);// 右端查找
}
② 第二种写法
int binary_search(int a[],int n,int x){
int left,right,mid;
left = 0;right = n-1;// 确定查找段的 起点 和 终点
while(left <= right){
mid = (left + right)/ 2;
if(x == a[mid])return mid;// 查找到,返回
if(x < a[mid])
right = mid - 1;// 左端查找
else
left = mid + 1; // 右端查找
}
return -1;
}
关于上述两种算法:
第 ① 种:使用递归,代码更简洁清晰,可读性更好。但由于递归需要系统堆栈,所以空间消耗要比非递归
代码大很多。而且,如果递归深度太大,系统可能撑不住。
第 ② 种:速度快,结构简单,但可读性略逊一筹。
二分查找算法的核心思想是 “分而治之” :将一个难以直接解决的大问题,分割成一些规模较小的,性质相同的子问题,以便各个击破,分而治之。
二分查找算法有两个前提:① 顺序存储;② 有序表。
二分查找法性能分析:
此处借助 “判定树” 简要分析一下算法的时间复杂度:
① 以有序数组 a[6] 为例,执行二分算法时
② 首先比较的是下标为 2 的元素 24,mid = (5+0)/ 2 = 2 // 注意,此处的 2 ,是下标!
③ 如果查找元素为 24,查找成功!
④ 如果比 24 小,那么再计算出来的 mid 值为 0,mid =(第②步里的 mid - 1)/ 2 =(2-1)/ 2 = 0
关于此处mid:开始时 mid =(5+0)/ 2 = 2 ,由于是 lift 和 right 都是 int 型,因此计算结果如例所示:
9/2 = 4;7/2 = 3;11/2 = 5;
⑤ 如果查找元素比 14 大,那么再计算出的 mid 值就是 1。// 舍弃 mid 与 mid 左侧段落,然后mid + 1 = 1
成功的查找:查找路径终结于结点 i,查找长度 = 结点 i 的层数
判定树中,24 需要一次比较,14和53 需要两次比较,17和43和76 需要三次比较,因此查找这个六个结点的平均查找长度为 =(1 + 2 + 2 + 3 + 3 + 3)/ 6 = 14/6,括号里的数字,可以这样理解:
① 假如我们寻找的元素是 24 ,那么开始时,仅寻找一个,就能找到它,因此查找长度为1;
② 假如我们寻找的元素是 14 ,那么开始时,需要途径两个元素(包括被寻找元素本身),因此长度为2;
又因为第二层,有两个元素,也就是 “14” 和 “53”,所以查找长度就是 2 + 2
③ 第三层同理
不成功的查找:
上图中用方框表示的结点,就是外结点。查找长度 = 外结点 i 之父的层数;
下面 x 对应的是待查找的元素
图中 0 灰色方片,对应 x < a[0] // 假如 x < 14 ,这个不等式可以表达为 x < a[0]
图中 1 灰色方片,对应 a[0] < x < a[1] // 假如 14< x < 17,这个不等式可以 表达为 a[0] < x < a[1]
图中 2 灰色方片,对应 a[1] < x < a[2] // 同上
图中 5 灰色方片,对应 a[4] < x < a[5] // 同上
图中 6 灰色方片,对应 x > a[5] // 同上
查找不成功的平均查找长度 =(2+3+3+3+3+3+3)/ 7 = 20 / 7
2: 24 → 14 → 方片0,所以两次 // 这是 x < 14 的情况;
3: 24 → 14 → 17 → 方片1,所以三次 // 这是 14< x < 17 的情况;
3: 24 → 14 → 17 → 方片2,所以三次 // 这是 17< x < 24 的情况;
3: 24 → 53 → 43 → 方片3,所以三次 // 这是 24< x < 43 的情况;
3: 24 → 53 → 43 → 方片4,所以三次 // 这是 43< x < 53 的情况;
3: 24 → 53 → 76 → 方片5,所以三次 // 这是 53< x < 76 的情况;
3: 24 → 53 → 76 → 方片6,所以三次 // 这是 x > 76 的情况。
因为 查找了 7 次,所以 查找总数 / 7 = 查找不成功的平均查找长度