對有序表進行查找運算的時候,可以通過縮減問題的規模,大幅度提高查找效率。
首節點 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 = 查找不成功的平均查找長度