數據結構與算法-排序算法總

       所謂排序就是將待排序文件中的記錄,按關鍵字非遞增或者非遞減次序排列起來。即將一組“無序”的記錄序列調整成爲“有序”的記錄序列。記錄是進行排序的基本單位,它由若干個數據項組成。其中有一項可用來唯一標識一條記錄,稱爲關鍵字項,該數據項的值稱爲關鍵字key,關鍵字的選取根據實際問題的需求而定。

        排序的穩定性:通俗的說就是排序結束後,相同關鍵字的相對位置和排序前是一樣的沒有發生更改,那麼就是穩定的排序算法,反之爲不穩定的。

        排序的分類,根據排序時數據所佔用存儲器的不同可分兩大類:內排序和外排序。平日中用的快速排序,堆排序,冒泡等等這都是內排序,整個排序過程全在內存裏面完成。而整個排序過程需要藉助外存輔助才能完成,數據量非常大的時候,內存一次性放不下這麼多數據,就需要內、外存交換,先從外存裏讀一些數據到內存裏排好序完了放進外存裏,這樣一部分一部分完成整個排序的過程,最後是一個合併的過程,稱爲外部排序。

        內排序是首要掌握的,內排序主要分四大類:插入排序、選擇排序、交換排序、歸併排序。至於分配類的排序,像計數排序、桶排序可以不做細講。

 

       

   排序算法的性能評價:執行時間和所需要的輔助空間,算法本身的複雜程度。若一個排序算法所需要的輔助空間並不依賴於問題的規模,也就是輔助空間爲O(1),則稱爲就地排序,非就地排序一般要求的輔助空間爲O(n)。大多數排序算法的時間消耗主要是,關鍵字之間的比較,記錄的移動,尤其是數組之間的移動極爲耗時。

1、插入排序

  • 直接插入排序

        整體的思想是逐個把未排序的序列插入到已經排好序的序列中合適的位置,主要過程包括,從已排序的序列中找合適的插入位置,找到了的話,就插入在該位置,數組的插入就涉及移動元素,沒找到就說明當前要插入的數據大於等於已排序的序列所有元素,不用插入,保持在原地不動。

        整個過程和打牌整理手上牌相似,摸上來第一張牌不用整理,此後每次摸上來的牌(無序區)插入到手中的牌(有序區)中正確的位置上,每次插入前尋找這個正確的位置,應將摸來的牌和手中的牌自左向右的逐一比大小,直到摸完牌爲止,就可以得到一個有序的牌。

插入排序
像是玩樸克一樣,我們將牌分作兩堆,每次從後面一堆的牌抽出最前端的牌,然後插入前面一
堆牌的適當位置,例如:
排序前:92 77 67 8 6 84 55 85 43 67
[77 92] 67 8 6 84 55 85 43 67 將77插入92前
[67 77 92] 8 6 84 55 85 43 67 將67插入*77前
[8 67 77 92] 6 84 55 85 43 67 將8插入67前
[6 8 67 77 92] 84 55 85 43 67 將6插入8前
[6 8 67 77 84 92] 55 85 43 67 將84插入92前
[6 8 55 67 77 84 92] 85 43 67 將55插入67前
[6 8 55 67 77 84 85 92] 43 67 ......
[6 8 43 55 67 77 84 85 92] 67 ......
[6 8 43 55 67 67 77 84 85 92] 
 */
//插入排序是穩定的排算法
void DirectInsertSorted(int *array, int array_length) {
	int i;
	int j;
	int t;
	int temp;

	for(i = 1; i < array_length; i++) {
		temp = array[i];
		for(j = 0; j < i && array[j] <= array[i]; j++);
		for(t = i; t > j; t--) {
			array[t] = array[t - 1];
		}
		array[t]  = temp;
	}
}

 

  •     希爾排序

 

        希爾排序是(ShellSort)又稱爲“縮小增量排序”,是1959年有D.L.Shell提出來的,基本思路是:將整個待排序元素序列劃分成若干個子序列,分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序時,再對全體元素進行一次直接插入排序。直接插入排序在元素基本有序的情況下,效率是很高的,因此希爾排序在事件效率上比直接插入排序有很大的提高。

        而這個增量的設置,一開始設置爲序列長度的一半,然後每次減一,直到這個增量等於0的時候結束。

        每一次產生了交換需要判斷一下是否影響到了前面的序列。

void shellSort(int *array, int array_length) {
	int i,j;
	int gap;

	for(gap = array_length / 2; gap > 0; gap /= 2) {
		for(i = gap; i < array_length; i++) {
			for(j = i - gap; j >= 0 && array[j] > array[j + gap]; j -= gap) {
				swapData_way1(&array[j], &array[j + gap]);
				showArray(array, array_length);
			}
		}
	}
}

 

2.選擇排序

 

 

  • 簡單選擇排序

 

        選擇類排序的基本思想是:每次在未排序的序列裏面找到一個最小的元素,挨個從數組前面開始放。主要就是如何從剩餘的元素裏面找出最小或者最大的元素項。

 

        簡單選擇排序是最容易想的一個,每次就從有序序列的後面一個最小的元素,放在排序序列裏面,通過交換這一步完成,直到把所有的元素放完位置。

選擇排序
將要排序的對象分作兩部份,一個是已排序的,一個是未排序的,從後端未排序部份選擇一個
最小值,並放入前端已排序部份的最後一個,例如:
排序前:70 80 31 37 10 1 48 60 33 80
[1] 80 31 37 10 70 48 60 33 80 選出最小值1
[1 10] 31 37 80 70 48 60 33 80 選出最小值10
[1 10 31] 37 80 70 48 60 33 80 選出最小值31
[1 10 31 33] 80 70 48 60 37 80 ......
[1 10 31 33 37] 70 48 60 80 80 ......
[1 10 31 33 37 48] 70 60 80 80 ......
[1 10 31 33 37 48 60] 70 80 80 ......
[1 10 31 33 37 48 60 70] 80 80 ......
[1 10 31 33 37 48 60 70 80] 80 ...... 
 選擇排序不是穩定的排序 */

void selectedSort(int *array, int array_length) {
	int i; 
	int j;
	int min;

	for(i = 0; i <  array_length; i++) {
		min = i;
		for(j = i; j < array_length; j++) {
			min = array[j] < array[min] ? j : min;
		}
		if(min != i) {
			swapData(&array[i], &array[min]);			
		}
		
	}
}

 

  • 堆排序

 

        堆排序將數組的序列看成一個完全二叉樹,根節點爲在數組中的下標爲ri,那麼其左右孩子對應下標分別爲2*ri + 1, 2 * ri + 2,哪個下標超出了數組下標範圍就說明這個節點沒有該孩子。

         完成堆排序總共有兩大步,要得到一個升序的序列的話。

            1.初始化大堆: 根據數組序列構造的完全二叉樹調整成一個大根堆。

            2.剪枝和維護堆:將二叉樹的根節點移到數組後面,也就是把根節點的數據和堆尾交換,並且堆中的數量減一(剪枝),然後維護當前堆使其仍保持是一個大堆。

void adjustHeap(int *array, int count, int root) {
	int leftChild;
	int rightChild;
	int whichChild;


	while(root <= count/2 - 1) {

		leftChild = 2 * root + 1;
		rightChild = ((leftChild + 1) >= count) ? -1 : (leftChild + 1);

		whichChild = (rightChild == -1) ? leftChild : (array[leftChild] > array[rightChild] ? leftChild : rightChild);
		whichChild = array[root] > array[whichChild] ? -1 : whichChild;

		if(-1 == whichChild) {
			return;
		}
		swapData(&array[root], &array[whichChild]);

		root = whichChild;
	}
}

//通過堆排序完成升序 先初始化大頂堆
//然後交換堆頂的和對重最後一個元素 繼續維護大頂堆 不斷去交換
//最後完成的堆是一個小頂堆
void heapSort(int *array, int array_length) {
	int root;

	for(root = array_length / 2 - 1; root >= 0; root--) {
		adjustHeap(array, array_length, root);
	}
	swapData(&array[0], &array[array_length - 1]);
	for(array_length--; array_length > 1; array_length--) {
		adjustHeap(array, array_length, 0);
		swapData(&array[0], &array[array_length - 1]);
	}
}

 

3.交換類排序

 

 

  • 冒泡排序

        冒泡排序可能是第一個接觸的排序算法,冒泡冒泡,輕的往上走,重的自行往下沉。用冒泡排序來完成一個數組的排序,兩兩比較,大的項向後走,小的項向前走,一趟的冒泡排序可以把一個元素放到最終的位置不再發生改變。

        

void BubbleSort(int *array, int array_length) {
	int i;
	int j;

	for(i = 0; i < array_length - 1; i++) {
		for(j = 0; j < array_length - i - 1; j++) {
			if(array[j] > array[j + 1]) {
				swapData(&array[j], &array[j + 1]);
			}
		}
	}
}

 

  • 改良後的冒泡

 

           從上面普通的冒泡排序可以看出在第四趟冒泡結束之後整個數組就已經有序了,後面的幾趟並沒有做什麼有用的功,後面幾趟的遍歷並沒有元素,也就是白白了循環了這麼多次。

            而改良後的冒泡排序加入了檢測一趟冒泡下來是否交換元素,如果有某一趟沒有產生交換元素的行爲,那麼就不必再繼續了,這個序列已經排好了,再接着循環也是白白浪費時間。對於某些元素序列可以提前結束外層循環,但最壞情況如果給的虛了是一個完全降序的序列的情況,循環的情況是和普通的冒泡相同的。

void ShakerSort(int *array, int array_length) {
	int i;
	int j;

	for(i = 0; i < array_length - 1; i++) {
		boolean hasSwapflag = 0;
		for(j = 0; j < array_length - i - 1; j++) {
			if(array[j] > array[j + 1]) {
				swap(&array[j], &array[j + 1]);
				hasSwapflag = 1;
			}
		}
		if(!hasSwapflag) {
			break;
		}
	}
}

 

  • 快速排序

 

        從冒泡排序可見,每次掃描只能對相鄰的兩個記錄進行比較,是從序列的一頭到另一頭單向的進行,因爲做一次交換隻能消除一個逆序,如果能通過一次交換可以消除多個逆序,那麼必將加快排序的速度。

        快速排序的每一趟給出一箇中間軸,然後一個指針從頭向後走遇到大於中間軸的關鍵字停下來,一個指針從尾向前走遇到小於中間軸關鍵字的停下來,然後交換頭尾指針位置的關鍵字,直到頭尾指針相遇或者頭指針超過了尾指針就結束這一趟快速排序,然後遞歸對此時頭指針左邊的序列和尾指針右邊的序列繼續進行同樣的快速排序。

每一趟快排後的結果以及頭尾指針停止時在數組中的下標

解法這邊所介紹的快速演算如下:將最左邊的數設定爲軸,並記錄其值爲 s
廻圈處理:
令索引 i 從數列左方往右方找,直到找到大於 s 的數
令索引 j 從數列左右方往左方找,直到找到小於 s 的數
如果 i >= j,則離開回圈d
如果 i < j,則交換索引i與j兩處的值
將左側的軸與 j 進行交換
對軸左邊進行遞迴
對軸右邊進行遞迴
說明在快速排序法(一)中,每次將最左邊的元素設爲軸,而之前曾經說過,快速排序法的
加速在於軸的選擇,在這個例子中,只將軸設定爲中間的元素,依這個元素作基準進行比較,
這可以增加快速排序法的效率。
解法在這個例子中,取中間的元素s作比較,同樣的先得右找比s大的索引 i,然後找比s小的
索引 j,只要兩邊的索引還沒有交會,就交換 i 與 j 的元素值,這次不用再進行軸的交換了,
因爲在尋找交換的過程中,軸位置的元素也會參與交換的動作,例如:
41 24 76 11 45 64 21 69 19 36
首先left爲0,right爲9,(left+right)/2 = 4(取整數的商),所以軸爲索引4的位置,比較的元素是
45,您往右找比45大的,往左找比45小的進行交換:
41 24 76* 11 [45] 64 21 69 19 *36
41 24 36 11 45* 64 21 69 19* 76
41 24 36 11 19 64* 21* 69 45 76
[41 24 36 11 19 21] [64 69 45 76]
完成以上之後,再初別對左邊括號與右邊括號的部份進行遞迴,如此就可以完成排序的目的。
 */
void quickSortOnce(int *array, int left, int right);

void quickSortOnce(int *array, int left, int right) {
	int i;
	int j;
	int s;
	static int count = 0;

	if(left < right) {
		s = array[(left + right) / 2];
		i = left - 1;
		j = right + 1;
		while(1) {
			while(array[++i] < s);
			while(array[--j] > s);
			if(i >= j){
				break;
			}
			swapData(&array[i], &array[j]);		
		}
		printf("for %dth time quickSort: mid = %d, low = %d, high = %d  ", ++count, s, i , j);
		showArray(array, 9);
		quickSortOnce(array, left, i - 1);// 對停止循環後i的左右進行遞歸
		quickSortOnce(array, j + 1, right);	//對j的右側進行遞歸  
	}
}

void quickSort(int *array, int array_length) {
	quickSortOnce(array, 0, array_length - 1);
}

 

 

4.歸併排序

 

 

 

  • 二路歸併排序

        歸併排序時首先將原始無序序列劃分劃分,直到劃分成每一個序列只包含一個元素,可以視爲包含一個元素序列的序列有序,然後再合併序列,整個過程是劃分序列和合並序列。

        而合併兩個有序的序列的過程,比較序列頭,取出較小的進入結果序列,接着繼續比較,直到其中一個序列爲空,結束比較的過程,如果還有一個序列沒放進來完,把該序列的所有元素再放進來。和兩個有序鏈表的合併思想完全相同。在合併兩個有序的數組的時候需要額外申請一塊空間來完成這個合併的過程。

//二路歸併排序
void Divided(int *array1, int first, int last, int *array2) {
	if(first < last) {
		int mid = (first + last) / 2;
		Divided(array1, first, mid, array2);
		Divided(array1, mid + 1, last, array2);
		mergeArray(array1, first, mid, last, array2);
	}
}

void mergeArray(int *array1, int first, int mid, int last, int *array2) {
	int i = first;
	int j = mid + 1;
	int m = mid;
	int n = last;
	int k = 0;

	while(i <= m && j <= n) {
		array2[k++] = array1[i] <= array1[j] ? array1[i++] : array1[j++];
	}

	while(i <= m) {
		array2[k++] = array1[i++];
	}
	while(j <= n) {
		array2[k++] = array1[j++];
	}

	for(i = 0; i < k; i++) {  //將array2的值返回給array1
		array1[first + i] = array2[i];
	}
}

void MergeSort(int *array, int array_length) {
	int *array2;

	array2 = (int *)calloc(sizeof(int), array_length);
	Divided(array, 0, array_length - 1, array2);

	free(array2);
}

 

5.計數排序和桶排序

 

        這一類的排序主要的思想是對元素分類、空間換時間的思想。

        計數排序把數組的值當做下標,去統計序列中各個關鍵字的出現次數,遍歷這個統計各個關鍵字出現次數的序列放進原數組,就自然而然的完成了一個排序。計數排序如果要排序的數值是分佈在很小範圍的整數,那麼計數排序會很好用。一旦涉及大範圍的數據或者高精度數字浮點數,這種排序就很難再直接去使用,浮點數得放大成整數才能正確的使用下標。

        在完成計數排序的時候需要申請一個足夠大的數組,這個數組的長度取決於要排序序列的極差。

//計數排序主要思想是將待排序序列的值當做下標,在另外一個數組中進行統計
//有類似統計一段字符串中各個字符出現的頻率,也是空間換時間的思想
//新申請的數組長度是待排序序列中max-min+1,min當做一個偏移量
//計數排序對於待排序序列數值分佈在小範圍有很好的效果,前提是整數,這很關鍵,浮點數無法去尋找下標,除非放大浮點數

//主要是找出數組array中的最大值最小值,傳遞countSort_Tool函數去申請空間
void countSort(int *array, int array_length);

void countSort_Tool(int *array, int array_length, int maxValue, int minValue);

void countSort_Tool(int *array, int array_length, int maxValue, int minValue) {
	int *countArray;
	int countArray_length = maxValue - minValue + 1;
	int i;
	int index = 0;

	countArray = (int *)calloc(sizeof(int), countArray_length);
	//進行計數 注意有一個偏移量minvalue
	for(i = 0; i < array_length; i++) {
		countArray[array[i] - minValue]++;
	}

	for(i = 0; i < countArray_length; i++) {
		while(countArray[i] > 0) {
			array[index++] = i + minValue;
			countArray[i]--;
		}
	}

	kwen_free(countArray);
}

void countSort(int *array, int array_length) {
	int max;
	int min;
	int i;

	for(i = 1, max = array[0], min = array[0]; i < array_length; i++) {
		max = max > array[i] ? max : array[i];
		min = min < array[i] ? min : array[i];
	}

	countSort_Tool(array, array_length, max, min);

}

 

        桶排序將序列中的數字按照大小範圍劃分到各個桶裏面去,單個桶裏的元素有序依次放入,最後把各個桶裏的元素再合併起來還給數組。具體實現可以通過數組加鏈表,數組來表示多個桶,鏈表表示單個桶裏面的元素。

 

// 桶由鏈表和數組構成  count負責記錄這個桶由幾個元素 也就是鏈表的長度
typedef struct BUCKET{
	int count;
	struct LinkList *list;
}BUCKET;

//記住bucket只是一個結構體數組
void destoryBucket(BUCKET *bucket, int BucketCount);
void initBucket(BUCKET *bucket, int BucketCount);
void insertDataToBucket(int *array, int array_length, BUCKET *bucket, int minValue);
void BucketDataToArray(int *array, int array_length, BUCKET *bucket, int BucketCount);
void BUCKETSORT(int *array, int array_length);

void BucketDataToArray(int *array, int array_length, BUCKET *bucket, int BucketCount) {
	int index = 0;
	int i;
	LinkList *p;

	for(i = 0; i < BucketCount && index < array_length; i++) {
		p = bucket[i].list->next;
		while(p != NULL) {
			array[index++] = p->data;
			p = p->next;
		}
	}
}

void insertDataToBucket(int *array, int array_length, BUCKET *bucket, int minValue) {
	int i;
	int index;
	LinkList *p;

	for(i = 0; i < array_length; i++) {
		LinkList *node;
		index = (array[i] - minValue) / array_length;
		p = bucket[index].list;    //每次讓p指向當前桶的頭結點
		node = (LinkList *)calloc(sizeof(LinkList), 1);
		node->data = array[i];
//將數組的數據升序的放進鏈表 考慮鏈表是否爲空
//爲空的話直接放在頭結點後面
//不爲空的話需要找要插入位置的前驅節點
		if(bucket[index].count == 0) {
			node->next = p->next;
			p->next = node;
		}else {	
			p = (array[i] > (p->next->data)) ? p->next : p;		 
			//如果此時一個桶是H->6,讓5進桶,此時p還是指向head,
			//如果5大於頭結點的下一個節點值,再讓p移動 不然的話p不懂還是指向頭
			//進入下面循環後發現5不大於6 p也沒有移動還是在頭結點 插入5到頭結點後面
			while(p->next && (array[i] > p->next->data)) {
				p = p->next;
			}	
			node->next = p->next;
			p->next = node;	
		}
		bucket[index].count++;
	}
}

void initBucket(BUCKET *bucket, int BucketCount) {
	int i;

	for(i = 0; i < BucketCount; i++) {
		bucket[i].count = 0;
		initLinkList(&(bucket[i].list));
	}
}

void destoryBucket(BUCKET *bucket, int BucketCount) {
	int i;

//銷燬每一個桶裏面的鏈表
	for(i = 0; i < BucketCount; i++) {
		destoryLinkList(&(bucket[i].list));
	}

	free(bucket);
	bucket = NULL;
}

void BUCKETSORT(int *array, int array_length) {
	int BucketCount;
	int maxValue;
	int minValue;
	BUCKET *bucket;

	maxValue = getArrayMaxValue(array, array_length);
	minValue = getArrayMinValue(array, array_length);

	BucketCount = (maxValue - minValue + array_length) / array_length;
	bucket = (BUCKET *)calloc(sizeof(BUCKET), BucketCount);

	initBucket(bucket, BucketCount);
	insertDataToBucket(array, array_length, bucket, minValue);
	BucketDataToArray(array, array_length, bucket, BucketCount);

	destoryBucket(bucket, BucketCount);
}

 

測試各個排序的時間

 

將排序的函數名裝在一個結構體,通過指向函數的指針去訪問

const SortFunction ALLSORT[] = {
	quickSort,
	MergeSort,
	heapSort,
	shellSort,
	countSort,
	// BUCKETSORT,
	selectedSort,
	DirectInsertSorted,
	BubbleSort_high,
	BubbleSort_low
};

const char* sortName[] = {
	"QuickSort",
	"MergeSort",
	"HeapSort",
	"ShellSort",
	"countSort",
	// "BUCKETSORT",
	"SelectedSort",
	"InsertSort",
	"BubbleSort_high",
	"BubbleSort_low"
};

const int ALLSORT_LENGTH = sizeof(ALLSORT) / sizeof(SortFunction);

 

準備數據函數

 

int *createArray(int count, int maxValue, int minValue)  {
	int *result = NULL;
	int index = 0;

	result = (int *)calloc(sizeof(int), count);

	srand(time(NULL));
	for(index = 0; index < count; index++) {
		result[index] = rand()%(maxValue - minValue + 1) + minValue;
	}

	return result;
}

測試函數

#include<stdio.h>

#include"KWENSORTTOOLS.h"


int main(void) {
	long before_time;
	long after_time;
	long total_Time;
	int i;
	const int randomDataCeil = 1000;
	const int randomDataLow = 1;
	const int array_length = 100000;
	int *array;

	for(i = 0; i < ALLSORT_LENGTH; i++) {
		array = createArray(array_length, randomDataCeil, randomDataLow);
		before_time = clock();
		allsort(array, array_length, i);
		after_time = clock();
		total_Time = after_time - before_time;
		printf("%d datas %s :  %ld.%03ld s\n",
		 array_length, sortName[i], total_Time / 1000, total_Time % 1000);
	}
	// showArray(array, array_length);

	kwen_free(array);

	return 0;
}

 

測試分佈在1-1000之間100000個數據各個排序時間消耗  簡單選擇冒泡實在太慢 先測試10萬個數據

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章