1. 程式人生 > >演算法期末複習-----遞迴與分治

演算法期末複習-----遞迴與分治

第二章遞迴與分治

直接或間接地呼叫自身的演算法稱為遞迴演算法。用函式自身給出定義的函式稱為遞迴函式。

 

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(清華大學出版社,嚴蔚敏)*/