二分法是一個非常高效的算法,它常常用於計算機的查找過程中。
先玩一個小游戲。預先給定一個小於100的正整數x,讓你猜,猜測過程中給予大小判斷的提示,問你怎樣快速地猜出來?
這樣猜測最快,先猜50,如果猜對了,結束;如果猜大了,往小的方向猜,再猜25;如果猜小了,往大的方向猜,再猜75;…,每猜測1次就去掉一半的數,就這樣可以逐步逼近預先給定的數字。這種思想就是二分法。
在用二分法進行查找時,查找對象的數組必須是有序的,即各數組元素的次序是按其值的大小順序存儲的。其基本思想是先確定待查數據的范圍(可用 [left,right] 區間表示),然后逐步縮小范圍直到找到或找不到該記錄為止。具體做法是:先取數組中間位置(mid=(left+right)/2)的數據元素與給定值比較。若相等,則查找成功;否則,若給定值比該數據元素的值小(或大),則給定值必在數組的前半部分[left,mid-1](或后半部分[mid+1,right]),然后在新的查找范圍內進行同樣的查找。如此反復進行,直到找到數組元素值與給定值相等的元素或確定數組中沒有待查找的數據為止。因此,二分查找每查找一次,或成功,或使查找數組中元素的個數減少一半,當查找數組中不再有數據元素時,查找失敗。
二分法查找是一種非常高效的搜索方法,主要原理是每次搜索可以拋棄一半的值來縮小范圍。其時間復雜度是O(log2n),一般用於對普通搜索方法的優化。
二分法的適用情況一般滿足以下幾點:(1)該數組數據量巨大,需要對處理的時間復雜度進行優化;(2)該數組已經排序;(3)一般要求找到的是某一個值或一個位置。
【例1】二分查找。
有若干個數按由小到大的順序存放在一個一維數組中,輸入一個數x,用二分查找法找出x是數組中第幾個數組元素的值。如果x不在數組中,則輸出“無此數!”。
(1)編程思路。
設有一數組a[n],數組中的元素按值從小到大排列有序。用變量low、high和mid分別指示待查元素所在區間的下界、上界和中間位置。初始時,low=0,high=n-1。
1)令 mid = (low+ high) /2 。
2)比較給定值x與a[mid]值的大小
若a[mid] == x ,則查找成功,結束查找;
若a[mid]> x ,則表明給定值x只可能在區間low ~ mid-1內,修改檢索范圍。令high=mid-1,low值保持不變;
若a[mid]< x ,則表明給定值x只可能在區間mid+1~high內,修改檢索范圍。令low=mid+1,high值保持不變。
3)比較當前變量low和high的值,若low≤high,重復執行第1)、2)兩步,若low>high,表明數組中不存在待查找的元素,查找失敗。
例如,設一有序的數組中有11個數據元素,它們的值依次為{3,8,15,21,35,54,63,79,82,92,97},用二分查找在該數組中查找值為82和87的元素的過程如圖1所示。
圖1 二分查找的查找過程
圖1(a)所示為查找成功的情況,僅需比較2次。若用順序查找,則需比較9次。圖2(b)所示為查找不成功的情況,此時因為low>high,說明數組中沒有元素值等於87的元素。得到查找失敗信息,也只需比較4次。若用順序查找,則必須比較12次。
二分查找過程通常可用一個二叉判定樹表示。對於上例給定長度的數組,二分查找過程可用圖2所示的二叉判定樹來描述,樹中結點的值為相應元素在數組中的位置。查找成功時恰好走了一條從根結點到該元素相應結點的路徑,所用的比較次數是該路徑長度加1或結點在二叉判定樹上的層次數。所以,折半查找在查找成功時所用的比較次數最多不超過相應的二叉判定樹的深度[log2n]+ 1。同理,查找不成功時,恰好走了一條從根結點到某一終端結點的路徑。因此,所用的比較次數最多也不超過[log2n] + 1。
圖2 描述折半查找過程的二叉判定樹
(2)源程序。
#include <iostream>
using namespace std;
int main()
{
const int n=20;
int a[n]={1,6,9,14,15,17,18,23,24,28,34,39,48,56,67,72,89,92,98,100};
int x,low,high,mid;
cout<<"Please input a number x:";
cin>>x;
low =0; high =n-1; // 置區間初值
while (low<=high)
{
mid = (low+high)/2 ;
if (x == a[mid]) break; // 找到待查記錄
else if (x<a[mid]) high=mid-1; // 繼續在前半區間進行檢索
else low=mid+1; // 繼續在后半區間進行檢索
}
if (low<=high) // 找到待查記錄
cout<<x<<" is a["<<mid<<"]"<<endl;
else
cout<<"No found!"<<endl;
return 0;
}
【例2】求平方根 。
編寫一個程序計算x的平方根,x保證是一個非負整數。
(1)編程思路。
已求5的平方根為例,說明應用二分法求平方根的思路。
設 f(x)=x2 ,在 x∈[1,5]的范圍內, f(x) 隨着 x的增大而增大的(單調遞增),這就給二分法創造了條件。
首先,令浮點型 left 和 right 的初值分別為1和5,然后通過比較 left 和 right 的中點 mid 處 f(x) 的數值與5的大小來選擇子區間進行逼近。有以下兩種情況:
1)如果 f(mid)>5,說明當前mid比5的平方根大,應當在 [left,mid]的范圍內繼續逼近,故令 right=mid;
2)如果 f(mid)<5,說明當前 mid比5的平方根小,應當在 [mid, right]的范圍內繼續逼近,故令 left=mid。
當 right−left<10−5時結束,此時已經滿足精度要求,即為所求的近似值。
(2)源程序。
#include <stdio.h>
double f(double x)
{
return x * x;
}
int main()
{
int x;
double left,right,mid;
scanf("%d",&x);
while (x!=0)
{
left=1.0, right=1.0*x;
while ((right-left)>1e-5)
{
mid=(left+right)/2;
if (f(mid)<x) left=mid;
else right=mid;
}
printf("%.4f\n",mid);
scanf("%d",&x);
}
return 0;
}
【例3】木材加工 。
木材廠有一些原木,現在想把這些木頭切割成一些長度相同的小段木頭(木頭有可能有剩余),需要得到的小段的數目是事先給定的,切割時希望得到的小段越長越好。
編寫程序,輸入原木的數目 N 和需要得到的小段的數目 K以及各段原木的長度,計算能夠得到的小段木頭的最大長度。
木頭長度的單位是 cm。原木的長度都是正整數,要求切割得到的小段木頭的長度也是正整數。
例如,輸入原木的數目 N 和需要得到的小段的數目 K 分別為3和8,輸入的3段原木的長度分別為124、224和319,則能夠切割得到的小段的最大長度為 74。
(1)編程思路。
這個問題可以采用二分法進行解決。
設left是切割的小段木頭的最短長度,right是最大長度,初始時,left為0,right為最長的原木長度加1。
每次取left和right的中間值mid(mid = (left + right) / 2)進行嘗試,測試采用當前長度mid進行加工,能否切割出需要的段數K,測試算法描述為:
num = 0;
for (i = 0; i < n; i++)
{
if (num >= k) break;
num = num + len[i] / mid ;
}
如果當前mid值可以加工出所需段數(即num >= k),說明當前mid值偏小,可能有余量,就增大mid值繼續試(通過讓left = mid的方法來增大mid);不符合要求,當前mid值加工不出所需段數,顯然mid偏大了,就減小mid值繼續試(通過讓right = mid的方法來減小mid)。直到left +1>= right結束嘗試,所得的left值就是可以加工出的小段木頭的最大長度。
(2)源程序。
#include <iostream>
using namespace std;
int main()
{
int n, k, len[10000], i, left, right, mid,num;
cout<<"請輸入原木的數目 N 和需要得到的小段的數目 K :"<<endl;
cin>>n>>k;
right = 0;
cout<<"請輸入各段原木的長度:"<<endl;
for (i = 0; i < n; i++)
{
cin>>len[i];
if (right < len[i]) right = len[i];
}
right++;
left = 0 ;
while ( left + 1 < right)
{
mid = (left + right) / 2;
num = 0;
for (i = 0; i < n; i++)
{
if (num >= k) break;
num = num + len[i] / mid ;
}
if ( num >= k )
left = mid;
else
right = mid;
}
cout<<"能夠切割得到的小段的最大長度為 "<<left<<endl;
return 0;
}