1. 程式人生 > >五類八個排序比較+實現+詳細解釋

五類八個排序比較+實現+詳細解釋

前言

剛學完資料結構,寫此文目的如下

  • 加深對五大類共八個排序演算法思想的理解
  • 比較各自的演算法效率(主要是時間複雜度)
  • 尋找演算法思想間的聯絡與區別
  • 用順序結構、鏈式結構實現演算法
  • 用遞迴、非遞迴(迭代)實現演算法
  • 記錄下來以便複習鞏固

五類排序演算法思維導圖
**五類排序演算法思維導圖**

穩定性: 舉個例子,假如我們要對序列A進行排序,排序前A中存在兩個元素a1,a2,這兩個元素滿足同時條件1,a1=a2;條件2,a1在a2之前。若是在排好序的A’中a1依舊在a2之前,那麼,我們可以稱其滿足穩定性。

一句話,穩定性就是遵循先來後到的原則

總結並補充上圖:
1,常用排序有五大類,每一類中都是先出現簡單,符合人思維的演算法,為了提高效率,大家在此前基礎上改進演算法,進而出現了高效的演算法。可見演算法的發展是循序漸進,迭代而成,因此,若是按照其發展脈絡學習演算法,更容易理解演算法間的關係
2,簡單插入、交換、基數排序滿足穩定性,其餘皆不穩定
3,希爾排序效率與增量選取有關
4,堆排序有大、小頂堆之分。升序建大頂堆;降序建小頂堆

本文均以升序為例

資料結構

採用順序結構,為了程式通用性,使用動態陣列

typedef struct 
{
	int *elem;//陣列首地址
	int length;//陣列長度
}SqList;

//初始化動態陣列 
void InitSqList(SqList &L)//記得加引用,賦初值才有效 
{
	L.elem = NULL;
	L.length = 0;
}

自定義佇列及其基本操作

//佇列資料結構
typedef struct QNode
{
	int data;
	struct QNode* next;
}QNode,LNode,*LinkList;

typedef
struct { QNode* front; QNode* rear; }LinkQueue; //初始化 void InitQueue(LinkQueue &Q) { Q.front = Q.rear = (QNode*)malloc(sizeof(QNode));//頭尾均指向頭指標 Q.front->next = NULL;//尾部賦空 } //入隊:尾部插入 void Enqueue(LinkQueue &Q,int data) { QNode* p = (QNode*)malloc(sizeof(QNode)); p->data = data; p->
next = NULL; Q.rear->next = p; Q.rear = p; } //判空:空->true;非空->false bool IsEmpty(LinkQueue Q) { if(Q.front->next) return false; else return true; } //出隊:頭部刪除;注意刪除的節點為尾節點時,重新將尾節點指向頭結點 void Dequeue(LinkQueue &Q) { if(!IsEmpty(Q)) { Q.front->next = Q.front->next->next; if(IsEmpty(Q)) Q.rear = Q.front;//刪除尾節點,重新將尾指標指向頭 } } //獲取隊頭元素 int GetTop(LinkQueue &Q) { if(!IsEmpty(Q)) { return Q.front->next->data; } else return -111111;//表示獲取失敗,即隊空 }

前期準備函式

  • 由於使用檔案交換資料,所以需設計檔案儲存、讀取、重讀取函式;隨機產生不超過五位的非負整數
  • 由於兩個元素間交換頻繁,因此設計元素交換函式

//建立動態陣列 
void CreateSqList(SqList &L)//記得加引用 
{	
	L.length = N;//N是巨集定義,表示元素個數
	L.elem = (int*)malloc(N*sizeof(int));//動態陣列必須分配記憶體!!!!! 
	fstream inFile("Sort.dat",ios::binary | ios::in);//以只讀二進位制方式儲存檔案
	inFile.read((char*)L.elem,N*sizeof(int));
	inFile.close();//記得關閉  
}
//重新載入動態陣列,以相同的資料測試新的排序演算法
void ReloadSqList(SqList &L)
{
	if(L.elem != NULL)
	{
		L.length = N;
//	L.elem = (int*)malloc(N*sizeof(int));//動態陣列必須分配記憶體!!!!! 
		fstream inFile("Sort.dat",ios::binary | ios::in);//以只讀二進位制方式儲存檔案
		inFile.read((char*)L.elem,N*sizeof(int));//二進位制讀取,直接覆蓋L之前的元素
		inFile.close();//記得關閉
	}
}
//把隨機數以二進位制形式存入檔案。每次直接寫會覆蓋之前的內容 
void CreateRandFile()//檔案流無法作為引數???或許是因為檔案可以在任何處被開啟,可看做全域性陣列 
{
	SqList L;//中間儲存 
	InitSqList(L);
	L.length = N;
	L.elem = (int*)malloc(N*sizeof(int));
	srand((int)time(0));//隨機數種子 
	for(int i = 0; i < N; i++)
	{	
	 	L.elem[i] = rand()%100000;//保證產生5位數以內,為桶排序準備 
	}
	fstream outFile("Sort.dat",ios::binary | ios::out);//以只寫二進位制方式儲存檔案
	outFile.write((char*)L.elem,N*sizeof(int));
	outFile.close();//記得關閉讀,下次再開啟讀指標從頭開始 
	free(L.elem);//記得釋放 
} 
//交換 
void swap(int &a, int &b)//記得加引用,否則傳入的引數在此函式結束後不會改變
{
	int t = a;
	a = b;
	b = t;
}
//僅僅為測試從檔案讀取資料是否成功,排序是否成功
void TraverseSqList(SqList L)
{
	for(int i = 0; i < L.length; i++)
	{
		cout<<L.elem[i]<<" ";
	}
}

插入排序

簡單插入排序

演算法思想

一個元素插入一個有序序列L,新序列L’依舊有序

演算法描述

1,初始狀態:第一個元素預設已有序,加入有序序列L
2,將距離序列L最近的元素插入L,得到新的有序序列L
3,重複步驟2,直到所有元素均在L中

演算法實現(C++)

//=========================簡單插入排序============================ 

//簡單插入排序,從第二個開始 
void InsertSort(SqList L)
{
	for(int i = 1; i < L.length; i++)//從第二個開始 
	{
		for(int j = i; j > 0; j--)
		{
			if(L.elem[j] < L.elem[j-1])
			{
				swap(L.elem[j],L.elem[j-1]);
			}
			else break;//提高效率 
		}
	} 
}

希爾排序

演算法思想

  • 1, 確定一個起點a1,選取與其間距為id的元素構成一個子序列,L1={a1,a1+d,…a1+id},對其進行簡單插入排序,L1為有序
    在確定新的起點a2,選取與其間距為id的元素構成一個子序列,L2={a2,a2+d,…a2+id},對其進行簡單插入排序,L2為有序
    以次類推,最終得到d個子序列,且每個子序列均有序
    因此,在L中間隔為d的元素已排好序
    (在同一個子序列中的元素下標必構成公差為d的等差數列)
  • 2, 減小d,重複步驟1,直至d=1

例項講解

第一行為原始序列,d為增量
在這裡插入圖片描述

程式碼實現(C++)

tips:

  • 1,每個序列的第一個預設是排好序的
  • 2,一個序列分成dk個子序列時,
    • 思路一:傳統是將i%dk相同的ai抽出來成為新序列,進而對該新序列使用簡單插入排序,再處理下一個序列
    • 思路二:i%dk = {0,1,…dk-1} 在不同的子序列間來回處理,處理完餘數為0,立刻處理餘數為1,2,…dk-1。此思路為該程式的寫法
  • 3,判斷條件中一發現當前數更大,立刻break,否則時間複雜度可能比簡單插入還高
  • 4,這是測試快速排序時發現的問題:陣列下標越界,j>=dk,否則j-dk會溢位。很嚴重的問題是它溢位了照樣輸出,不終止,執行到快速排序就卡著了,誤導我以為是快排出現問題。
  • 5,通過列印輸出找問題時又發現了一個嚴重問題:即使不通過應用L,陣列L.elem依舊被改變,原因在於雖然L作為形參傳入,但L.elem是指標,代表著地址,在函式中對其操作,相當於直接在其對應地址改動
  • 6,要有自信,迅速定位問題位置,明白當前問題可能是由之前問題導致,連鎖反應,可見封裝的優越性。儘量不用遞迴,一是難調,二是耗空間
//=========================希爾排序============================ 

//希爾插入,按照一定的增量dk,進行插入排序 

void ShellInsert(SqList L, int dk)
{
	//=============思路一實現=========================== 
	for(int k = 0; k < dk; k++)
	{
		for(int i = k + dk; i < L.length; i += dk)//i%dk = {0,1,...dk} 間來回轉換 
		{
		//	for(int j = i; j > 0; j -= dk)錯誤示例,會溢位 
			for(int j = i; j >= dk; j -= dk)
			{
				if(L.elem[j] < L.elem[j-dk])
				{
					swap(L.elem[j], L.elem[j-dk]);
				}
				else break;//不跳出時間複雜度比簡單插入還高 
			}
		}
	} 
	
/* 
	//==================思路二實現================= 
	for(int i = 0 + dk; i < L.length; i ++)//i%dk = {0,1,...dk} 間來回轉換 
	{
		for(int j = i; j >= dk; j -= dk)//方案一:控制j的範圍,j>=dk,否則會溢位 
		{
		//	if(j - dk >= 0)//方案二:注意判斷是否存在,否則j<dk時必溢位!!!!!!!!! 
		//	{
				if(L.elem[j] < L.elem[j-dk])//兩種解決方案 
				{
					swap(L.elem[j], L.elem[j-dk]);
				}
		//	}
			else break;//不跳出時間複雜度比簡單插入還高 
		}
	}*/
} 

void ShellSort(SqList L)
{
	int d[3] = {5,3,1};//增量數列 
	for(int i = 0; i < 3; i++)
	{
		ShellInsert(L,d[i]);
	}
//	TraverseSqList(L);
}

交換排序

簡單交換(冒泡)排序

演算法思想

兩兩比較,較大者往後走/較小者往前走

實現程式碼(C++)

//通過兩兩交換,求得最小值,從前往後填入 
void BubbleSort(SqList L)
{
	for(int i = 0; i < L.length - 1; i++)
	{
		for(int j = i+1; j < L.length; j++)
		{
			if(L.elem[i] > L.elem[j])
			{
				swap(L.elem[i],L.elem[j]);
			}
		}
	} 
//	TraverseSqList(L); 
} 

快速排序

演算法思想

分割(核心):選取一個分割元素p,令p左邊元素均小於等於p,p右邊元素均大於等於p,因此,p的位置唯一確定。
因此,只需進行n次分割就可將長度為n的序列L排成有序序列

程式碼實現(C++)

遞迴版

//選擇一個值pivot作為軸對數列進行分割,左小右大,分割完成,pivot位置確定
//快速排序是對不斷對序列進行分割,每分一次,確定一個值的位置 
int Partition(SqList L,int low,int high)
{
	int i,j;//好習慣:儘量不改變引數 
	i = low;//頭 
	j = high;//尾 
	
	int t = L.elem[low];//儲存第一個,騰出空位 
	while(i < j)
	{
		while(i < j && t <= L.elem[j])	j--;//從j向前找到第一個比t小的值 
		L.elem[i] = L.elem[j];//填入空中 ,此時i的位置騰出 
		while(i < j && t >= L.elem[i])	i++;//從i向後找到第一個比t大的值
		L.elem[j] = L.elem[i];//填入空中 ,此時j的位置騰出
	}//跳出迴圈,i=j,且還必有一個空 
	L.elem[i] = t;//將開始的軸值填入最後一個空,該值位置確定,其左邊均比它小,右邊均比他大。 
	return i;//返回中軸的位置 
} 
//遞迴實現分割 
void QSort(SqList L,int low,int high)
{
	if(low < high)
	{
		int pivot  = Partition(L,low,high);
		QSort(L,low,pivot-1);
		QSort(L,pivot+1,high);
	}
}
//為了形式統一,介面一致,才寫了這個函式 
void QuickSort(SqList L)
{
	QSort(L,0,L.length - 1);	
}

選擇排序

演算法思想

每趟選取一個最小值,往前放/每天選一個最大值,往後放

實現程式碼(C++)

//每趟選出一個最小值,從前往後放入第一個非有序位置 
void SelectSort(SqList L)
{
	for(int i = 0; i < L.length; i++)
	{
		int k = i;
		for(int j = i + 1; j < L.length; j++)
		{
			if(L.elem[k] > L.elem[j])	k = j;//記錄最小值下標 
		}
		if(k != i)//如果ai不是最小值 ,交換ak,ai,使ai成為最小值 
		{
			swap(L.elem[k],L.elem[i]);
		}
	}
} 

堆排序(大頂堆)

大頂堆是完全二叉樹,且任一子樹根均大於左右孩子

演算法思想

1,篩選(核心):堆排序關鍵是如何篩選出最大元素,可分為兩種情況

  • 1,完全二叉樹除了根,左右子樹均已為大頂堆
  • 2,除了情況1的情況

A,第一種情況(好辦)

  • 1,令根a與其左右孩子中較大者a’比較,若a>=a’,無需再比;若a<a’,交換這兩個節點
  • 2,重複1,直至a為葉子

B,第二種情況(轉化分解)
可以將情況二轉化為多個情況一,從而解決問題。
倒過來反覆使用解決方案A

  • 1,完全二叉樹的葉子左右子樹均為大頂堆,所以可用A篩選
  • 2,從最後向前一步步篩選,所以每個即將要篩選的節點的左右子樹均已為大頂堆,所以均可使用A篩選

2,建大頂堆
使用反覆篩選建立大頂堆,來一個元素,篩選一次。
3,排序
建好大頂堆後,將根元素與最後一個元素交換,同時完全二叉樹去除最後一個元素,再利用篩選,得到元素少一個的新大頂堆,重複直至僅有一個元素即可。

實現程式碼(C++)

//大頂堆--》升序;小頂堆-》降序 
//篩選函式,使用前提:根的左右子樹都是堆,僅根破壞了大頂堆的定義 
void HeapAdjust(SqList L,int low,int high) 
{
	int i,j;
	i = low; 
	while(2*i <= high)//堆是完全二叉樹:左子堆存在 
	{
		j = 2*i;//初值 ,每次需更新 
		if(2*i+1 <= high && L.elem[2*i] < L.elem[2*i+1])	j = 2*i+1;//若右子堆也存在,選擇左右大者 
		if(L.elem[i] < L.elem[j])	swap(L.elem[i],L.elem[j]);//若ai<aj,交換 
		else break;//否則立刻跳出,提升效率 
		i = j;//更新i 
	}
//	TraverseSqList(L);
} 
//亂序數列通過從最後一個開始向前反覆篩選可變為堆 
//0號單元不用 
void HSort(SqList L,int low,int high)
{
	for(int i = high; i >= 1; i--)//整理成大頂堆 
	{
		HeapAdjust(L,i,high);
	} 
//	cout<<"12:";TraverseSqList(L);
	int i,j;
	i = low;
	j = high;
	//for(int k = 1; k < L.length; k++)//次數必須為n-1次,多一次少一次都不行 ???
	while(true)
	{//因為0號單元有元素,但是堆不用,0*x=0。所以排序次數多了會把0號元素捲入排序,導致錯誤
	//方案一:嚴格控制次數:n-1次
	//方案二:利用j=0跳出迴圈 
		if(j == 0)break;
		swap(L.elem[i],L.elem[j]);
		j--;
		HeapAdjust(L,i,j); 
	}
} 
//與快排相同,該函式僅是為了介面統一 
void HeapSort(SqList L)
{
	HSort(L,1,L.length-1);
}

歸併排序

兩個有序序列合併成一個有序序列

程式碼實現(C++)

  • 遞迴與非遞迴合併演算法一致
  • 遞迴簡潔,但效率一般不如非遞迴高,且不易除錯

合併兩條有序序列

//=========================歸併============================ 
void Merge(SqList &L1,int low,int m,int high)
{	
	int i,j,k=low;//錯誤示範:k=0。!!每次呼叫歸併起點並不都是0 
	i = low;
	j = m+1;
	
	SqList L2;
	L2.length = L1.length;
	L2.elem = (int*)malloc(N*sizeof(int));
	while(i <= m && j <= high)//兩條序列均未走完
	{
		if(L1.elem[i] < L1.elem[j])	L2.elem[k++] = L1.elem[i++];
		else L2.elem[k++] = L1.elem[j++];
	}
	//若是有一序列還未走完,直接將剩餘部分全部賦給新序列
	while(i <= m)	L2.elem[k++] = L1.elem[i++];
	while(j <= high)  L2.elem[k++] = L1.elem[j++];
	for(int t = low; t <= high; t++)//low->high 才需賦值 
	{
		L1.elem[t] = L2.elem[t];
	}
	free(L2.elem); 
}

遞迴形式

//遞迴形式 
void MSort(SqList L,int low,int high)
{
	if(low >= high )	return;
	int m = (low + high)/2;
	
	MSort(L,low,m);
	MSort(