演算法期末複習-----遞迴與分治
第二章遞迴與分治
直接或間接地呼叫自身的演算法稱為遞迴演算法。用函式自身給出定義的函式稱為遞迴函式。
1.全排列
演算法思想:當n=1時,Perm(R)=(r),當n>1時,perm (R)=(r1)perm(R1),Ri=R-{ri),而perm(R1)=(r2)perm(R2),perm (R2)=(r3)perm(R3).....................perm(list,k,m)遞迴的地產生所有字首是list[0:k-1],且字尾是list[k:m]的全排列的所有排列。呼叫演算法perm(list,0,m-1)則產生list[0:n-1]的全排列。
例如:abc
一、交換a a
以a為字首,求bc的全排列----以b為字首,求c的全排列----得到abc
交換b c
----以c為字首,求b的全排列----得到acb
二、交換a b
以b為字首,求ac的全排列----以a為字首,求c的全排列----得到bac
交換a c
----以c為字首,求a的全排列----得到bca
三、交換a c
以c為字首,求ba的全排列----以b為字首,求a的全排列----得到cba
交換b a
----以a為字首,求b的全排列----得到cab
#include<stdio.h>
void swap(char* a,char* b){
char c;
c=*a;
*a=*b;
*b=c;
}
void perm(char* list,int k,int m){
if(k==m){
for(int i=0;i<=m;i++){
printf("%c",list[i]);
}
printf("\n");
}else{
for(int i=k;i<=m;i++){
swap(&list[k],&list[i]);
perm(list,k+1,m);
swap(&list[k],&list[i]);
}
}
}
int main(){
char list[4]={'a','b','c'};
perm(list,0,2);
return 0;
}
2.整數劃分
對正整數的不同劃分中,將最大加數n1不大於m的劃分個數記為q(n,m)。n代表整數n,m代表最大加數。
由此可建立遞迴關係
6; q(6,6)=q(6,5)+1
5+1; =q(6,4)+q(1,5)+1
4+2,4+1+1; =q(6,3)+q(2,4)+q(1,5)+1
3+3,3+2+1,3+1+1+1; =q(6,2)+q(3,3)+q(2,4)+q(1,5)+1
2+2+2,2+2+1+1,2+1+1+1+1; =q(6,1)+q(4,2)+q(3,3)+q(2,4)+q(1,5)+1
1+1+1+1+1+1。
q(6,6)=11 即代表整數6,最大加數小於等於6的劃分
q(6,5)=10 即代表整數6,最大加數小於等於5的劃分
q(6,4)=9 即代表整數6,最大加數小於等於4的劃分,即標藍部分。
遞迴方程:
#include<stdio.h>
int q(int n,int m){
if(n==1||m==1)
return 1;
else if(m>n)
return q(n,n);
else if(m==n)
return q(n,n-1)+1;
else if(n>m)
return q(n-m,m)+q(n,m-1);
}
int main(){
int n;
while(~scanf("%d",&n)){
int res=q(n,n);
printf("%d\n",res);
}
return 0;
}
3.漢諾塔
三個柱子要求從1號移動到2號,輔助柱子3號,規則:每次只能移動一個盤子,任何時刻都不允許將較大的圓盤壓到較小的圓盤上。
演算法思想:
n個圓盤的移動問題可分為兩次n-1個圓盤的移動問題,即將n-1個圓盤藉助2號從1號移到3號,再將第n個圓盤從1號移動到2號,再將n-1個圓盤藉助1號從3號移到2號。
#include<stdio.h>
//n為盤子總數,a為起始盤子,b為終點盤子,c為輔助盤子
int count=0;
void move(int n,int a,int b){
count++;
printf("%d號盤子從%d柱子--->%d柱子\n",n,a,b);
}
void hanoi(int n,int a,int b,int c){
if(n==1)
move(1,a,b);
if(n>1){
hanoi(n-1,a,c,b);
move(n,a,b);//若不跟蹤每個盤子的移動,則move(a,b)
hanoi(n-1,c,b,a);
}
}
int main(){
hanoi(3,1,2,3);//把盤子從上到下編號1-n,把n個盤子從1號柱子移到2號柱子
printf("移動次數:%d\n",count);
return 0;
}
4.要求二叉樹上任意兩個節點的最近公共子節點
解題思路
這個題目要求樹上任意兩個節點的最近公共子節點。分析這棵樹的結構不難看出,不論奇數偶數,每個數對2做整數除法,就走到它的上層結點。
我們可以每次讓較大的一個數(也就是在樹上位於較低層次的節點)向上走一個結點,直到兩個結點相遇。如果兩個節點位於同一層,並且它們不相等,可以讓其中任何一個先往上走,然後另一個再往上走,直到它們相遇,
設common(x, y) 表示整數x和y的最近公共子節點,那麼,根據比較x 和y 的值,我們得到三種情況:
x = y,則common(x, y)=x =y
x > y,則common(x, y)=common(x/2, y)
x < y,則common(x, y)=common(x, y/2)
#include<stdio.h>
int common(int x,int y){
if(x==y)
return x;
if(x>y)
return common(x/2,y);
if(x<y)
return common(x,y/2);
}
int main(){
int x,y;
while(~scanf("%d%d",&x,&y))
printf("%d\n",common(x,y));
return 0;
}
分治
分治法所能解決的問題一般具有以下幾個特徵:
1.該問題的規模縮小到一定的程度就可以容易地解決;(因為問題的計算複雜性一般是隨著問題規模的增加而增加,因此大部分問題滿足這個特)
2.該問題可以分解為若干個規模較小的相同問題,即該問題具有最優子結構性質(這條特徵是應用分治法的前提,它也是大多數問題可以滿足的,此特徵反映了遞迴思想的應用)
3.利用該問題分解出的子問題的解可以合併為該問題的解;(能否利用分治法完全取決於問題是否具有這條特徵,如果具備了前兩條特徵,而不具備第三條特徵,則可以考慮貪心演算法或動態規劃。)
4.該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。(這條特徵涉及到分治法的效率,如果各子問題是不獨立的,則分治法要做許多不必要的工作,重複地解公共的子問題,此時雖然也可用分治法,但一般用動態規劃較好。)
二分搜尋技術
先排序
基本思想:將n個元素分成個數大致相同的兩半,取a[n/2]與x進行比較,如果x=a[n/2],則找到x,演算法終止。如果x<a[n/2],則在陣列的左半部分繼續搜尋x。如果x>a[n/2],則只要在陣列a的右半部分繼續搜尋x。
//查詢下標
#include<stdio.h>
#include<algorithm>
using namespace std;
int binarySearch(int* a,int x,int n){
int left=0,right=n-1;
while(left<=right){
int middle=(left+right)/2;
if(x==a[middle])
return middle;
if(x<=a[middle])
right=middle-1;
if(x>=a[middle])
left=middle+1;
}
}
int main(){
int a[10]={1,5,3,2,9,22,8,23,36,90};
sort(a,a+10);
printf("%d\n",binarySearch(a,36,10));
return 0;
}
演算法複雜度分析:
每執行一次演算法的while迴圈, 待搜尋陣列的大小減少一半。因此,在最壞情況下,while迴圈被執行了O(logn) 次。迴圈體內運算需要O(1) 時間,因此整個演算法在最壞情況下的計算時間複雜性為O(logn) 。
合併排序
基本思想:將待排序元素分成大小大致相同的2個子集合,分別對2個子集合進行排序,最終將排好序的子集合合併成為所要求的排好序的集合。
分治策略基本思想:將原問題劃分成n個規模較小而結構與原問題相似的小問題;遞迴地解決這些子問題,然後再合併其結果,就得到原問題的解。
分治模式在每一層遞迴上都有三個步驟;
分解(Divide):將原問題分解成這一系列子問題。
解決(Conquer):遞迴地解各子問題。若子問題足夠小,則直接求解。
合併(Combine):將子問題的結果合併成原問題的解。
合併排序演算法完全依照了上述模式,直觀地操作如下:
分解:將n個元素分成各含n/2個元素的子序列;
解決:用合併排序法對兩個子序列遞迴地排序;
合併:合併兩個已排序的子序列以得到排序結果;
public static void mergeSort(Comparable a[], int left, int right)
{
if (left<right) {//至少有2個元素
int i=(left+right)/2; //取中點
mergeSort(a, left, i);
mergeSort(a, i+1, right);
merge(a, b, left, i, right); //合併到陣列b
copy(a, b, left, right); //複製回陣列a
}
}
快速排序
快速排序基本思想
快速排序採用了一種分治的策略,通常稱其為分治法,其基本思想是:將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞迴地解這些子問題,然後將這些子問題的解組合為原問題的解。
1、先從數列中取出一個數作為基準數。
2、分割槽過程,將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊。
3、再對左右區間重複第二步,直到各區間只有一個數。
快速排序步驟
1、設定兩個變數I、J,排序開始的時候:I=0,J=N-1;
2、以第一個陣列元素作為關鍵資料,賦值給key,即 key=A[0];
3、從J開始向前搜尋,即由後開始向前搜尋(J=J-1),找到第一個小於key的值A[J],並與A[I]交換;
4、從I開始向後搜尋,即由前開始向後搜尋(I=I+1),找到第一個大於key的A[I],與A[J]交換;
5、重複第3、4、5步,直到 I=J; (3,4步是在程式中沒找到時候j=j-1,i=i+1,直至找到為止。找到並交換的時候i, j指標位置不變。另外當i=j這過程一定正好是i+或j-完成的最後另迴圈結束。)
6、採用同樣的方法,對左邊的組和右邊的組進行排序,直到所有記錄都排到相應的位置為止。
初始關鍵資料:X=49,注意關鍵X永遠不變,永遠是和X進行比較,無論在什麼位子,最後的目的就是把X放在中間,小的放前面大的放後面。
最壞時間複雜度:O(n2)
平均時間複雜度:O(nlogn)
輔助空間:O(n)或O(logn)
穩定性:不穩定
#include <iostream>
using namespace std;
void Qsort(int a[], int low, int high)
{
if(low >= high)
{
return;
}
int first = low;
int last = high;
int key = a[first];/*用字表的第一個記錄作為樞軸*/
while(first < last)
{
while(first < last && a[last] >= key)
{
--last;
}
a[first] = a[last];/*將比第一個小的移到低端*/
while(first < last && a[first] <= key)
{
++first;
}
a[last] = a[first];
/*將比第一個大的移到高階*/
}
a[first] = key;/*樞軸記錄到位*/
Qsort(a, low, first-1);
Qsort(a, first+1, high);
}
int main()
{
int a[] = {49,38,65,97,76,13,27};
Qsort(a, 0, sizeof(a) / sizeof(a[0]) - 1);/*這裡原文第三個引數要減1否則記憶體越界*/
for(int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
cout << a[i] << " ";
}
return 0;
}/*參考資料結構p274(清華大學出版社,嚴蔚敏)*/