《演算法導論》讀書筆記(01)——ch02 演算法基礎【插入排序、歸併排序】
《演算法導論》第二章主要討論了兩個演算法問題:插入排序和歸併排序,在介紹兩個演算法的同時,對兩個演算法從執行效率上做了分析。最後對分治演算法進行了做了簡要介紹。下面對這兩種演算法從頭開始分析,並用C語言和JAVA語言進行實現。
目錄
1.插入排序
1.1 演算法思路
假設要對元素進行非遞減排序。插入排序的執行方式為:對於一組待排序的數列,可認為數列的第一個元素是有序的,然後訪問數列的第二個元素,將其與第一個元素比較,如果第二個元素比第一個元素大,則認為數列的前兩個元素已經有序,繼續訪問第三個元素,如果第二個元素比第一個元素小,則交換前兩個元素,使得前兩個元素有序,接著訪問第三個元素。訪問第三個元素時,將其與它的前一個元素(第二個元素)比較,如果第三個元素大於第二個元素,則此時前三個元素便是有序的,如果第三個元素小於第二個元素,再將第三個元素與第一個元素比較,如果第三個元素大於第一個元素,則說明第三個元素的大小位於第一個元素與第三個元素之間,如果第三個元素小於第一個元素,則說明第三個元素是前三個元素中最小的,將之前第一個和第二個位置上的元素後移,將之前第三個位置上的元素放到第一個位置上即可。以此類推,向後掃描,假設數列共有n個元素,第i次迴圈後,前i個元素已經有序,第i+1次迴圈,將原數列i+1位置上的元素放到了有序的位置。
《演算法導論》裡面對於插入排序有一個很形象的例子:玩撲克牌
想一下我們玩撲克牌的時候,摸牌(摸到的牌都是已經打亂了的)並按照大小將牌整理好的過程,其實就是一個很形象的插入排序的例子。第一次先從桌子上摸一張牌,假設摸到了6,然後再摸一張牌,假設摸到了9,這比手中已有的牌的點數大,我們把摸到的9放到之前的6的右邊。接著進行第三次摸牌,假設摸到了7,和手中已有的牌(6和9)進行比較,應該把7放到已有兩張牌之間。接著第四次,假設摸到了3,和手中已有的牌比較,3應該放到6的前面。接著第五次假設又摸到了6(出現重複值),這時就把新摸到的6放到之前的6的相鄰的左邊或右邊就好。就這樣一直下去,每摸一張牌,就和手中已有的牌比較點數大小,並將新摸到的牌插入到合適的位置,直到摸完所有的牌,這時手中的牌都是已排序的。
特點:
(1)手中的所有牌都是已排序了的,桌子上還沒有摸到的牌是待排序的;
(2)每次摸牌相當於一次迴圈,每次迴圈都將一個未排序的牌插入到了已排序的牌中,並且沒有破壞其有序性;
(3)第一次摸牌後,手中只有一張牌,可將手中的一張牌看作是有序的;
說明:
每次摸到牌之後,需要和手中已有的牌比較,我們實際玩牌的時候就是隨便瞅一眼就知道這張牌應該插入到什麼地方,但是如果從計算機實現的角度看,這個過程應該是:拿著新摸到的牌,從手中最大的牌開始,一張一張地比較,如果位置不合適,將比較過的牌後移(給待插入的牌騰出一個位置),然後繼續向前比較,不合適再後移騰位置,直到找到了一個合適的位置,將新摸到的牌插入即可。
再舉個栗子:
下面的例子可能需要在紙上自己畫一畫寫一寫才能更清楚一些這個過程……
待排序的數列為:【 3 2 5 8 4 7 6 9 】,要求遞增排列。
注:箭頭所指的位置的前面的所有元素已經排序好。
【 3 2 5 8 4 7 6 9 】
↑
首先假定第一個元素為有序的,然後掃描第二個元素,第二個元素2,需要和第一個元素交換,交換後數列變成了:
【 2 3 5 8 4 7 6 9 】
↑
此時前兩個元素有序了。第二個迴圈結束
然後掃描第三個元素5,比它的前一個元素3小,沒毛病,繼續下去是8,也沒毛病,不需要交換,這個過程跑了兩次迴圈,此時數列是:
【 2 3 5 8 4 7 6 9 】
↑
繼續掃描,掃描到了4,小於8,需要把4和8交換,交換完成後如下:
【 2 3 5 4 8 7 6 9 】
然後比較4和5,這裡還是得交換,交換後如下:
【 2 3 4 5 8 7 6 9 】
↑
最後比較4和3,4大於3,不需要交換,此次迴圈結束。
然後掃描到7,7大於8,需要交換,交換後7比5小,不需要交換,此次迴圈結束。
【 2 3 4 5 7 8 6 9 】
↑
然後掃描到6,原理同上,一直和前面相鄰的元素比較並交換,直到前面相鄰的元素小於6時停止。就會得到下面的序列:
【 2 3 4 5 6 7 8 9 】
↑
最後一個迴圈,處理9,該元素的位置沒問題,不需要調整。迴圈結束,排序完成。
1.2 演算法實現
1.2.1 虛擬碼描述
/*
虛擬碼描述插入排序演算法,見《演算法導論》 P10。
A :待排序陣列
*/
for j = 2 to A.length
key=A[j]
i = j - 1;
while i>0 and A[i]>key
A[i+1] = A[i]
i = i-1
A[i+1] = key
end
1.2.2 C語言實現
下面用《資料結構與演算法分析——C語言描述》上面的例程進行實現。
#include <stdio.h>
#define N 8
/*
插入排序演算法
data :待排序陣列
length :陣列元素個數
*/
void Insertion_Sort(int data[],int length)
{
int i,j;
int key;
for(i = 1 ; i < length ; i++){
key = data[i];
for(j=i;j>0 && data[j-1]>key;j--){
data[j] = data[j-1];
}
data[j] = key;
}
}
//主函式測試程式碼
int main(void)
{
int data[N] = {3,2,5,8,4,7,6,9};
int i;
Insertion_Sort(data,N);
for(i=0;i<N;i++){
printf("%d\t",data[i]);
}
printf("\n");
return 0;
}
1.2.3 JAVA實現
public class Sort{
/**
* 插入排序例程
* @param data 待排序陣列
*/
public static void InsertionSort(int[] data) {
int j, key;
for (int i = 1; i < data.length; i++) {
key = data[i];
for (j = i; j > 0 && data[j - 1] > key; j--) {
data[j] = data[j - 1];
}
data[j] = key;
}
}
/**
* 輸出一個數組中的值
* @param data
*/
public static void PrintData(int[] data) {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
}
/**
* 主函式,測試函式
* @param args
*/
public static void main(String[] args) {
int[] data = { 3, 2, 5, 8, 4, 7, 6, 9 };
InsertionSort(data);
PrintData(data);
}
}
1.3 演算法分析與評價
對於少量元素,插入排序是一個有效的演算法。插入排序時間複雜度為O(n^2),空間複雜度為O(1),是一種穩定的排序。插入排序是原址排序輸入的數的,只需要一個交換變數的臨時空間。
插入排序是用來增量放大:在排序子陣列data[0...i]之後,考察元素data[i+1],將其插入到data[0...i]的適當位置,構成有序陣列data[0...(i+1)]。
暫時簡單總結這些,後序遇到插入排序的特點了再來補充......
2.歸併排序
2.1 演算法思路
歸併排序是基於分治法的思想,遞迴地對陣列進行排序的。(所以要想真正理解歸併排序的思路並進行實現,一定要理解遞迴操作)
歸併排序演算法的的操作可分為兩個步驟,第一步是分解,第二步是合併。簡單得說,分解操作是將待排序陣列分解為一個個有序的陣列,合併操作是將一個個有序數組合併為一個大的有序陣列。我們常說的歸併排序是二路歸併排序,即每次分解過程是將陣列分解為兩個有序的陣列,合併過程是將兩個有序的數組合併為一個更大的有序陣列。
依然可以用上面整理撲克牌的例子來說明一下歸併排序的操作:
下面的描述純手打,看起來可能比較繁瑣,最好在紙上畫一下這個過程,但相信認真讀的話會理解歸併排序的基礎操作的,
假定我們手裡有8張打亂的撲克牌,我們現在要對其進行排序。首先進行分解,一直分解到每堆牌都是有序的為止,第一次,將8張牌分成兩堆,每堆4張,這時發現每堆牌依然不是有序的,所以分別拿起已經分解了一次的兩堆牌繼續分解,4張牌分成兩堆,每堆2張,一共4堆,假設這時每堆牌依然不是有序的,那就繼續分,分成了每堆1張,這時候一定是有序的了,分解操作結束,此時8張牌被分成了8堆,每堆1張。下面是合併操作,首先拿起兩堆(共2張)牌,按照大小進行排序,然後放下。再拿起還沒排序的兩堆牌,排序,放下,這樣操作四次,原先的8堆牌變成了四堆牌,每堆2張,並且每堆的2張都是排序好了的。下面在這4堆中再拿起兩堆,這兩堆牌中各有2張,並且均已排序,現在要做的就是把這4張牌進行排序,這裡實際是將兩個有序序列合併為一個有序序列的基本操作。合併結束後,現在的局面是,有三堆牌,都是已排序了的,其中一堆4張,另外二堆各2張,下面拿起每堆2張的那兩堆,依然是個有序序列合併的操作,完成之後,剩下兩堆(各4張)有序的牌了,繼續將兩堆有序的牌合併成一堆有序的牌即可。這時候就只剩下一堆牌(8張)了,並且這堆牌是有序的,排序完成。
下面用一個圖展示一下上面描述的過程(圖片來自網路,侵刪)。
2.2 演算法實現
如上所述,歸併排序分為兩個步驟,演算法的思想是基於遞迴的,因此實現也是通過遞迴的方法實現的。其中遞迴中呼叫的過程就是分解的過程,呼叫結束返回的過程就是合併兩個有序序列的過程。下面首先實現合併兩個有序序列的過程,即兩個有序序列合併為一個有序序列的操作,然後利用該操作的思想,實現歸併排序演算法。
2.2.1 C語言實現
Part 01:兩個有序序列合併為一個有序序列
不多說了,C語言的基礎操作,直接上程式碼了。
#include <stdio.h>
#include <stdlib.h>
int* Merge(int dataA[],int lengthA,int dataB[],int lengthB)
{
int i = 0,j = 0,k = 0;
int* temp = malloc(sizeof(int)*(lengthA+lengthB));
while(i<lengthA && j<lengthB){//處理陣列的公共部分
if(dataA[i] < dataB[j]){
temp[k++] = dataA[i++];
}
else{
temp[k++] = dataB[j++];
}
}
while(i<lengthA){//處理陣列A可能的剩餘部分
temp[k++] = dataA[i++];
}
while(j<lengthB){//處理陣列B可能的剩餘部分
temp[k++] = dataB[j++];
}
return temp;
}
int main(void)
{
int dataA[5] = {1,3,5,7,9};
int dataB[6] = {-1,2,4,6,8,11};
int lengthA = 5;
int lengthB = 6;
int *temp = NULL;
int i;
temp = Merge(dataA,lengthA,dataB,lengthB);
for(i = 0;i < lengthA+lengthB ; i++){
printf("%d\t",temp[i]);
}
free(temp);
return 0;
}
Part 02:無序序列的分解操作
分解操作的過程上一節的例子中已經有較詳細的說明,這裡基於分治思想,遞迴的方式進行實現。不多說了。
完整的歸併排序C語言實現程式碼如下:
說明:下面的程式碼參考了《演算法導論》(機工)以及《資料結構與演算法分析——C語言實現》(機工)這兩本書上的內容。C語言實現起來還是有點小麻煩的,像陣列長度之類的值,還有那個快取陣列,呼叫函式的時候得到處傳遞……可能有更好的實現吧,以後遇到了貼上來。
#include <stdio.h>
#include <stdlib.h>
void Merge(int data[],int left,int mid,int right,int *temp);//合併兩個升序列的操作
void MergeSort(int data[],int *temp,int left,int right);//歸併排序
void PrintData(int data[],int length);//資料輸出
int main(void)
{
int data[8] = {3,2,5,8,4,7,6,9};
int length = 8;
int i;
int *temp = NULL;
temp = malloc(sizeof(int)*(length));
MergeSort(data,temp,0,length-1);//歸併排序操作
PrintData(data,length);
free(temp);
return 0;
}
//合併兩個升序列的操作
void Merge(int data[],int left,int mid,int right,int *temp)
{
int i = left,j = mid+1,k = left;
while(i <= mid && j <= right){//處理公共部分
if(data[i] < data[j]){
temp[k++] = data[i++];
}
else{
temp[k++] = data[j++];
}
}
while(i <= mid){//處理陣列A可能的剩餘部分
temp[k++] = data[i++];
}
while(j <= right){//處理陣列B可能的剩餘部分
temp[k++] = data[j++];
}
//將排序完的序列寫入原始陣列中
for(i = left;i <= right;i++){
data[i] = temp[i];
}
}
//歸併排序
void MergeSort(int data[],int *temp,int left,int right)
{
int mid;
if(left < right){
mid = (right+left)/2;
MergeSort(data,temp,left,mid);
MergeSort(data,temp,mid+1,right);
Merge(data,left,mid,right,temp);
}
}
//資料輸出
void PrintData(int data[],int length)
{
int i;
for(i = 0 ; i < length ;i++){
printf("%d\t",data[i]);
}
printf("\n");
}
2.2.2 JAVA語言實現
public class Sort{
public static void main(String[] args) {
int[] data = { 3, 2, 5, 8, 4, 7, 6, 9 };
int[] temp = new int[data.length];
int length = 8;
MergeSort(data, temp, 0, length - 1);// 歸併排序操作
PrintData(data, length);
}
// 合併兩個升序列的操作
public static void Merge(int[] data, int left, int mid, int right, int[] temp) {
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {// 處理公共部分
if (data[i] < data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
while (i <= mid) {// 處理陣列A可能的剩餘部分
temp[k++] = data[i++];
}
while (j <= right) {// 處理陣列B可能的剩餘部分
temp[k++] = data[j++];
}
// 將排序完的序列寫入原始陣列中
for (i = left; i <= right; i++) {
data[i] = temp[i];
}
}
// 歸併排序
public static void MergeSort(int[] data, int[] temp, int left, int right) {
int mid;
if (left < right) {
mid = (right + left) / 2;
MergeSort(data, temp, left, mid);
MergeSort(data, temp, mid + 1, right);
Merge(data, left, mid, right, temp);
}
}
// 資料輸出
public static void PrintData(int[] data, int length) {
int i;
for (i = 0; i < length; i++) {
System.out.print(data[i]+" ");
}
System.out.println();
}
}
2.3 演算法評價
歸併排序的時間複雜度是O(NlogN),它是遞迴演算法一個很好的例項。雖然歸併排序執行時間很快,所使用的比較次數幾乎也是最優的,但是它很難用於主存排序,主要問題實在合併兩個順序序列的操作中需要線性的附加記憶體,同時拷貝過程需要線性附加的時間。不過由於歸併排序運用了分治思想,在大資料的排序中還是有很不錯的應用的,