1. 程式人生 > >基礎演算法之排序雜湊遞迴

基礎演算法之排序雜湊遞迴

基礎演算法學習筆記(一)

一. 選擇排序

1.選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
選擇排序的主要優點與資料移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對 n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。
2. 下面是過程演示:藍色為有序,[]內為無序,j,k指示元素下標。
在這裡插入圖片描述


3. 實現

/*選擇排序*/
void selectSort(int *a,int n){
	for(int i=0;i<n;i++){ //n趟操作
		int k=i;
		for(int j=i+1;j<n;j++){
			if(a[j]<a[k]){
				k=j;//選出[i,n]中最小的元素,下標為k
			}		
		}
		int temp=a[i];
		a[i]=a[k];
		a[k]=temp;//交換a[k]和a[i]
	
	}
	output(a,n);//輸出結果
}

二.插入排序

1.基本思想
插入排序(英語:Insertion Sort)是一種簡單直觀的排序演算法。類似於打撲克摸牌,考慮新摸到的牌該插入的位置。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到 O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

2.過程演示:
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

3.實現:

/*插入排序*/
void insertSort(int *a,int n){
	for(int i=1;i<n;i++){
		int temp=a[i];//temp暫時存放a[i]
		for(j=i-1;j>=0 && temp<a[j];j--)
			a[j]=a[j-1];//a[j-1]後移一位到a[j]
		a[j]=temp;//插入位置為j	
	}
	output(a,n);	
}

三. sort函式使用

1.介紹:
sort是C ++標準庫中用於進行比較排序的通用函式。該函式起源於標準模板庫(STL)。它根據具體情形使用不同的排序方法,效率較高。一般來說不推薦使用C語言中qsort函式,因為qsort函式用起來比較繁瑣,涉及到很多指標的操作。而且sort在實現中規避了經典快速排序中可能出現的會導致實際複雜度退化到O(n²)的情況。

2.如何使用sort排序
sort函式使用必須加上標頭檔案“#include”和“using namespce std;”,使用方法如下:
sort(首元素地址(必填),尾元素地址,比較函式(非必填));若不寫比較函式,則預設對前面給出的區間進行遞增排序。

3.實現:

/*定義結構體*/
struct Student {
   char id[10];//學號
   int score;//分數
}stu[3];

/*定義基本資料型別比較函式*/
bool cmp1(const int & a,const int &b){
   return a>b;//按照降序排序
}
/*定義結構體型別比較函式*/
bool cmp(Student a, Student b){
   if(a.score!=b.score)return a.score>b.score;//分數不同則按照分數降序排序
   else return strcmp(a.id,b.id)<0;//分數相同則按照學號升序排序
}

int p[6]={5,2,4,6,3,9};
int q[6]={5,2,4,6,3,9};
sort(p,p+6,cmp1);//呼叫基本資料型別排序函式
sort(q,q+6);//預設排序

struct Student stu[3]={"12345",98,"12346",77,"12347",98};
sort(stu,stu+3,cmp);//呼叫結構體型別排序函式

在這裡插入圖片描述

這裡應該注意
比較函式寫法1:

    bool cmp(int a,int b){
    return a>b;
    }

和比較函式寫法2:

    bool cmp1(const int & a,const int &b){
    	return a>b;//按照降序排序
    }

作為函式引數:int這種寫法是值傳遞,const int&則是引用傳遞
“值傳遞”——由於函式將自動產生臨時變數用於複製該引數,效率較低。
“引用傳遞”僅借用一下引數的別名而已,不需要產生臨時物件。效率較高。
“引用傳遞”有可能改變引數,const修飾可以解決這個問題。

四.雜湊(Hash)

1.Hash基本思想
Hash是把關鍵字Key經過Hash函式對映為整數,這個整數作為Hash表的某個陣列下標,儘量唯一的代表這個關鍵字。
Hash表是一個根據關鍵字Key而直接進行訪問的資料結構,結合了陣列和連結串列兩者的優點,即能夠在常數級別的時間複雜度內進行按址查詢,同時也很容易插入和刪除資料。Hash表是對隨機儲存的優化,先通過Hash函式對所有資料分類,按照類別查詢,極大加快查詢速度。若Hash函式不存在衝突,能直接找到待查詢元素;若Hash函式存在衝突,且通過拉鍊法解決衝突,能大幅度縮小查詢範圍。

2.Hash函式
Hash函式建立了關鍵字Key到Hash表陣列下標的對映關係,即:
Address=H(Key)

一個好的Hash函式應該滿足2個條件:
(1) Hash函式儘量要簡單,保證計算速度;
(2) 儘可能的保證不同的關鍵字對映為不同的索引地址,也就是避免衝突;

常見的Hash函式有:
(1) 直接定址法:取關鍵字或者是關鍵字的線性變換作為Hash表的陣列下標;
(2)平方取中法(很少用):取Key的平方的中間若干位作為Hash表的陣列下標;
(3)除留餘數法:取關鍵字除以一個數p得到的餘數作為Hash表的陣列下(Key=Key %p,可以將很大的數轉換為不超過p的整數。當p是一個素數時, H(Key)能夠覆蓋[0,p)內的每一個整數。一般p取不超過Hash表陣列長度的最大素數。

衝突是不可避免的。因此,兩個相同關鍵字的Hash值必定相同,但是Hash值相同的兩個關鍵字卻未必相同。

衝突的解決方法:
(1) 開放定址法:當一個關鍵字和另一個關鍵字發生衝突時,使用某種探測策略在Hash表中形成一個探測序列,然後沿著這個探測序列依次查詢下去,當碰到一個空的單元時,則插入其中。根據探測策略的不同,開放定址法又可分為:線性探查法和平方探查法。
(2) 拉鍊法:和開放定址法不同的是,拉鍊法並不會對衝突的關鍵字重新計算Hash值,而是將所有Hash值相同的關鍵字連線成一條單鏈表。

3.字串Hash初步
將字串對映為整數,可以用於字串的快速匹配。初步的字串Hash只討論將字串轉化為唯一的整數,且整數能夠在可表示的範圍內。實際上,當字串較長時,產生的整數會非常大,即使是無符號長整型(unsigned long long)也不能儲存。關於對長字串的Hash進階在《演算法筆記》的12.1節有進一步介紹。

字串Hash關鍵是選擇合適的進位制,一般大於或等於待處理字串中不同字元的數目,儘量也不要太大,因為進位制太大必然會增加Hash表的空間開銷。分以下幾種情況:
(1) 對於只有大寫字母或小寫字母的字串而言,不妨把A-Z(或a到z)對映為0-25,這樣26個字母對應到26進位制,然後將得到的26進位制數轉換為10進位制數,實現將字串轉換為唯一整數的需求。
(2) 對於既含有大寫字母,又含有小寫字母,同時又含有數字的字串而言,可以A-Z作為0-25,a-z作為26-51,0-9作為52-61,此時進位制數為62.
4.例題:字元統計
題目描述:
試編寫程式,找出一段給定文字中出現最頻繁的那個字母。
輸入格式:
在一行中給出一個長度不超過1000的字串。字串由ASCII表中任意可見字元及空格組成,至少包含一個英文字母,回車鍵結束。
輸出格式:
在一行中輸出出現頻率最高的那個英文字母和出現次數,其間以空格分開。如果有並列,則輸出字母序最小的那個字母。統計時不區分大小寫,輸出小寫字母。
輸入樣例:
This is a simple TEST. THERE are numbers and other symbols 1&2&3…
輸出樣例:
e 7

思路:
由於只需要針對英文字母(’A’-‘Z’和’a’-‘z’)輸出其中頻率最高的次數,所以根本不需要考慮除了英文字母之外的其他字元。並且不區分大小寫字母,可以開一個長度為26的Hash表來記錄每個字母出現的次數,比如Hashtable[0]=5表示字串中’a’和’A’共出現了5次。先遍歷給定的字串str,判斷各個字元是不是字母以及是大寫字母還是小寫字母,完成對字串中字母的統計,最後遍歷Hash表獲得最大元素即可。

#include <stdio.h>
#include <string.h>
int main(){
	char str[1010];   //字串
	gets(str);        //讀入字串
	int hashtable[26]={0},   //Hash表,儲存字串中各個字母的個數
		len=strlen(str);
	for(int i=0;i<len;i++)
	{
		if(str[i]>='a'&&str[i]<='z')    //若str[i]是小寫字母,出現次數加1
			hashtable[str[i]-'a']++;
		if(str[i]>='A'&&str[i]<='A')    //若str[i]是大寫字母,對應的小寫字母出現次數加1
			hashtable[str[i]-'A']++;
	}
	int Max=0;      //記錄Hash表中最大元素下標
	for(int i=1;i<len;i++)
		if(hashtable[i]>hashtable[Max])
			Max=i;
	printf("%c %d\n",'a'+Max,hashtable[Max]);   //輸出字元和出現次數
	return 0;

五.遞迴

1.分治

分治法可以通俗的解釋為:把一片領土分解,分解為若干塊小部分,然後一塊塊地佔領征服,被分解的可以是不同的政治派別或是其他什麼,然後讓他們彼此異化。
分治法的精髓:
分–將問題分解為規模更小的子問題;
治–將這些規模更小的子問題逐個擊破;
合–將已解決的子問題合併,最終得出“母”問題的解;
這個技巧是很多高效演算法的基礎,如排序演算法(快速排序,歸併排序),傅立葉變換(快速傅立葉變換)……
2.遞迴

程式呼叫自身的程式設計技巧稱為遞迴( recursion)。遞迴做為一種演算法在程式設計語言中廣泛應用。 一個過程或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的程式就可描述出解題過程所需要的多次重複計算,大大地減少了程式的程式碼量。遞迴的能力在於用有限的語句來定義物件的無限集合。一般來說,遞迴需要有邊界條件、遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回。
遞迴的缺點:
遞迴演算法解題相對常用的演算法如普通迴圈等,執行效率較低。因此,應該儘量避免使用遞迴,除非沒有更好的演算法或者某種特定情況,遞迴更為適合的時候。在遞迴呼叫的過程當中系統為每一層的返回點、區域性量等開闢了棧來儲存。遞迴次數過多容易造成棧溢位等。

經典例題:斐波那契數列
斐波那契數列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144……依次類推下去,你會發現,它後一個數等於前面兩個數的和。在這個數列中的數字,就被稱為斐波那契數。
遞迴思想:一個數等於前兩個數的和。(這並不是廢話,這是執行思路)
首先分析數列的遞迴表示式:
在這裡插入圖片描述
在這裡插入圖片描述

/**
 * 斐波那契數列的遞迴寫法
 *      核心:一個小的解決終點,然後大的問題可以迴圈在小問題上解決
 * @param n
 * @return
 */
long F(int n){
    if (n<=1) return n;
    return F(n-1)+F(n-2);
}
 
/**
 * 斐波那契數列的遞推寫法
 * @param n
 * @return
 */
long F1(int n){
    if (n<=1) return n;
    long fn = 0;
    long fn_1 = 1;
    long fn_2 = 0;
    for (int i = 2; i <= n; i++) {
        fn = fn_1 + fn_2;
        fn_2 = fn_1;
        fn_1 = fn;
    }
    return fn;
}

經典例題:N皇后問題:
在N×N格的國際象棋上擺放N個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上。

#include<stdio.h>
#define Debug
int count=0;
void Queen(int n,int l);
bool CanPut(int i,int j,int (*a)[8],int n);
void Print(int (*a)[8],int n);
 
int main()
{
	printf("請輸入棋盤的規模\n");
	int n;
	scanf("%d",&n);
 
	Queen(n,0);
	if(count)
		printf("解的總數是:%d\n",count);
	else
		printf("無解\n");
 
	return 1;
}
 
int a[8][8]={0};
 
void Queen(int n,int l)
{
	if(l==n-1)//最後一行,遞迴出口
	{
		for(int j=0;j<n;j++)
		{
			if(CanPut(l,j,a,n))//是否能放置皇后
			{
				count++;
				a[l][j]=1;//標記位置
				Print(a,n);
				a[l][j]=0;//取消這個位置
			}
		}
		printf("\n");
	}
	else//不是最後一行
	{--將問題分解為規模更小的子問題;
 
		for(int j=0;j<n;j++)
		{
			a[l][j]=1;//假設這個位置可以放
			if(CanPut(l,j,a,n))//如果可以,遞迴進入下一行
				Queen(n,l+1);
			a[l][j]=0;//回溯回來後或者不可以放置皇后歸0
		}
	}
	return;//如果是if,則遞迴出口,如果是else,則回溯到上一行
}
 
void Print(int (*a)[8],int n)
{//本演算法打印出N皇后的解
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			printf("%d ",a[i][j]);
		}
		printf("\n");
	}
}
 
bool CanPut(int i,int j,int (*a)[8],int n)
{//本演算法是判斷該位置能否放皇后
	int s=0;
	for(int k=i-1;k>=0;k--)
	{
		s++;
		if(a[k][j]==1 || j+s<n&&a[k][j+s]==1 || j-s>=0&&a[k][j-s]==1)
			return false;
	}
	return true;
}