- 分治法(Divide and Conquer)
作為五大算法之一的分治法,可算是最早接觸的一種算法。分治法,與其說是一種算法,不如將其稱為策略來的更貼切一些。算法的思想就是將大問題分成小問題,並解決小問題之后合並起來生成大問題的解。
- 二分法(Bisection)
二分法針對於有序集合的處理來說顯得比按序遍歷來得更快一些。記得有次我讀到一篇處理問題的博客:有一個城市到另一個城市之間的電桿不通電了,該如何排查?作為常用思維大概就是逐個去查了,如果能確定兩地間的電桿是串聯的節點,那么二分法顯然是比較有效率的,因為它跳過了很多不必要排查的電桿。
我們一般使用二分法去尋找一個流中的特定的數。比如查找有序數列中是否存在一個數,或者使用二分法求一個數的根號。
1.找數字
假設我們需要在1-10000里面找一個數200,使用逐個搜索的方法,我們會消耗200步。如果計入小數的畫,恐怕就大大超過200這個消耗了。
假如使用二分法:
第一步我們找到1-10000中間的那個數:5000。它大於200,所以200應該在1-4999這個區間內,這樣我們就丟掉了后5000個數。
第二步我們找到2500,也比200要大,200在1-2500這個區間內。
第三步找到1250這個數,也比200大。
第四步找到750。
第五步找到375。
第六步找到167,它比200要小了,說明200在167-375之間。
第七步找到271,它在167-271之間。
第八步找到219,它在167-219之間。
第九步找到193,它在193-219之間。
第十步找到206,它在193-206之間。
第十一步找到199,它在199-206之間。
第十二步找到202,它在199-202之間。
第十三步找到200。
在n=10000的這個問題來講,從200步到13步是一個很大的改進。如果我在這里寫200步,那我肯定是傻了。
使用二分法找數組中的一個值的下標,如果沒有找到則返回-1
int index=-1;
public void BisectionFind(int a[],int l,int r,int target){
if(l<r){
int mid=(l+r)/2;
if(a[mid]<target)
BisectionFind(a,mid+1,r,target);
else if(a[mid]>target)
BisectionFind(a, l, mid-1, target);
else
{
index=mid;
return;
}
}
}
public static void main(String[] args)
{
SqrtDemo sq=new SqrtDemo();
int a[]={5,13,19,21,37,56,64,75,80,88,92};
sq.BisectionFind(a, 0, a.length-1, 21);
System.out.println(sq.index);
}
2.求根號
對於我來說,最早接觸的有序數列大概就是數軸了,要在數軸上找到一個根號的具體值,就是從數軸上找一個數乘以自己看是否在所求數字的周圍。如果精確度可以接受的話,那么就采用這個值為這個數的根號的近似值。
這回我們舍棄的是半個數軸,半個數軸按照精確度的不同,它會產生不同的復雜度。所以二分法的效率,遠遠高於按序查找。
public double sqrt1(double number,double precision){
double up=(number>1?number:1);
double down=0;
double n;
int time=0;
while(true){
n=(down+up)/2;
if(n*n-number<precision && n*n-number>=0)
break;
else if(n*n-number>precision)
up=n;
else if(n*n-number<0)
down=n;
time++;
System.out.println(n);
}
System.out.println("time="+time);
return n;
}
SqrtDemo sq=new SqrtDemo(); System.out.println(sq.sqrt1(10, 0.001));

上面兩種問題,它們能確定這個問題的解就在序列的內部,所以它們在執行的時候都轉換成了尋找子問題的解。在不斷分割問題的過程中,問題的復雜度急劇下降,效率大大地提高了。

- 快速排序(QuickSort)
經典的分治法案例,在亂序數組中做到了O(nlogn)的效率,對冒泡法(O(n*n))的一個很大的改進。
快速排序的步驟:1.尋找一個基准元素
2.從右向左尋找大於(小於)基准元素的值
3.從左向右尋找小於(大於)基准元素的值
4.使得基准元素左邊都是小於(大於)它的元素,右邊都是大於(小於)它的元素
5.遞歸的處理基准元素左邊與右邊的模塊

C++版
int Partition1(int a[],int i,int j){
int start=i;
int end=j;
int x=a[i];
while(start<end){
while(a[end]>=x && end>start)
end--;
swap(a[start],a[end]);
while(a[start]<=x && end>start)
start++;
swap(a[start],a[end]);
}
cout<<"中心位置:"<<start<<endl;
return start;
}
void quickSort1(int a[],int p,int r){
if(p<r)
{
int x=Partition1(a, p, r);
quickSort1(a, p, x-1);
quickSort1(a, x+1, r);
}
}
Java版
public int Partition(int a[],int p,int r){
int start=p;
int end=r;
int x=a[p];
while(start<end){
while(start<end && a[end]>=x)
end--;
if(start<end)
a[start++]=a[end];
while(start<end && a[start]<=x)
start++;
if(start<end)
a[end--]=a[start];
}
a[start]=x;
return start;
}
public void quickSort(int a[],int i,int j){
if(i<j){
int p=Partition(a, i, j);
quickSort(a, i, p-1);
quickSort(a, p+1, j);
}
}
Python版
def Partition(a,p,r):
x=a[p]
i=p
j=r
while(1):
while(1):
if(a[i]<=x and i<len(a)-1):
i=i+1
else:
break
while(1):
if(a[j]>=x and j>0):
j=j-1
else:
break
if(i>=j):
break
else:
a [j], a [i] = a [i], a [j]
a[p]=a[j]
a[j]=x
return j
def quickSort(a,i,j):
if(i<j):
p=Partition(a,i,j)
quickSort(a,i,p-1)
quickSort(a,p+1,j)
def PartitionDemo(a,p,r):
x=a[p]
start=p
end=r
while start<end :
while start<end and a[end]>=x :
end-=1
while start<end and a[start]<x :
a[start]=a[end]
start+=1
a[end]=a[start]
a[start]=x
return start
def quickSortDemo(a,i,j):
if(i<j):
q=PartitionDemo(a,i,j)
quickSortDemo(a,i,q-1)
quickSortDemo(a,q+1,j)
a=[9,5,2,4,7,3,6,8,15,18,11,13]
quickSortDemo(a,0,len(a)-1)
print a
- 歸並排序(MergeSort)
作為經典排序算法,使用分治策略,歸並排序無疑是最能體現分治思想並且易於理解的一種算法。它在排序之前先將序列分割成最短為1的小數組。當長度為1的時候,數組無疑是有序的。合並的時候就如樹形結構逆着生成根節點一樣,子問題排序、合並,最終生成一個有序的數組。

C++版
void merge(int a[],int b[],int p,int mid,int r){
int i=p;
int j=mid+1;
int t=p;
while(i<=mid && j<=r){
if(a[i]<a[j])
b[t++]=a[i++];
else if(a[j]<=a[i])
b[t++]=a[j++];
}
if(i!=mid)
while(t<=r)
b[t++]=a[j++];
else
while(t<=r)
b[t++]=a[i++];
for(i=p;i<=r;i++)
a[i]=b[i];
}
void MergeSort(int a[],int b[],int start,int end){
if(start<end){
int mid=(start+end)/2;
MergeSort(a,b,start,mid);
MergeSort(a,b,mid+1,end);
merge(a,b,start,mid,end);
}
}
Java版
public void merge(int a[],int b[],int start,int mid,int end){
int i=start;
int j=mid+1;
int k=start;
while(i<=mid && j<=end){
if(a[i]<=a[j])
b[k++]=a[i++];
else
b[k++]=a[j++];
}
while(i<=mid)
b[k++]=a[i++];
while(j<=end)
b[k++]=a[j++];
for(i=start;i<=end;i++)
a[i]=b[i];
}
public void MergeSort(int a[],int b[],int start,int end){
if(start<end)
{
int mid=(start+end)/2;
MergeSort(a,b,start,mid);
MergeSort(a,b,mid+1,end);
merge(a,b,start,mid,end);
}
}
- 總結
分治法作為一個比較重要的算法,思想的理解來說還是比較簡單的。但是它寫起代碼來確實有些許抽象。從C++到java再到python,雖然我一直按照着它的思想來寫,但是不同語言的實現確實有些小小的區別。至今我還描述不出來,但是這給我提了個醒,學算法一定要做題!不僅地把經典算法的實現老老實實寫出來,還要認真地體會它的細節,不停在腦子里畫出程序的執行結構圖,使之形成習慣。
