內部排序是指待排序列完全存放在內存中所進行的排序過程,適合不太大的元素序列。排序是計算機程序設計中的一種重要操作,其功能是對一個數據元素集合或序列重新排列成一個按數據元素某個相知有序的序列。排序分為兩類:內排序和外排序。其中快速排序的是目前排序方法中被認為是最好的方法。內部排序方法:1.插入排序(直接插入排序);2.快速排序;3.選擇排序(簡單選擇排序);4.歸並排序;5.冒泡排序;6.希爾排序(希爾排序是對直接插入排序方法的改進);7.堆排序;——摘自百度百科
#ifndef SORT_H_ #define SORT_H_ #define ARRAY_LEN 1000 // 數組長度 #define MIN 1 // 數組的最小值 #define MAX 1000 // 數組的最大值 int Comparisons_num; // 比較次數 int Mobile_num; // 移動次數 void Create_data(int *a, int n, int min, int max); // 建立偽隨機 void Copy_array(int *tar, int *arr, int len); // 復制數組 void Swap_element(int *a, int *b); // 交換元素 void Insert_sort(int *arr, int len); // #1 直接插入排序 void Shell_sort(int *arr, int len); // #2 希爾排序 void Bubble_sort(int *arr, int len); // #3 冒泡排序 int Division(int *a, int left, int right); // 分隔過程(快速排序) void Quick_sort(int *arr, int left, int right, int count); // #4 快速排序(left和count初始值為0,right初始值為數組長度 - 1) void Select_sort(int *arr, int len); // #5 選擇排序 void Heap_adjust(int arr[], int i, int len); // 構成堆過程 (堆排序) void Heap_sort(int arr[], int len); // #6 堆排序 void Merge(int arr[], int target[], int start, int mid, int end); // 歸並過程(歸並排序) void Merge_sort(int arr[], int target[], int start, int end, int count); // #7 歸並排序(start和count初始值為0,end初始值為數組長度 - 1) void Print_sort_positive(int *arr, int len); // 正序輸出 void Print_sort_negative(int *arr, int len); // 逆序輸出 void Print_mob_com(); // 顯示移動次數和比較次數 #endif
在實現排序前,先定義函數的功能模塊,即sort.h。
頭文件中定義了三個常量為ARRAY_LEN,MIN和MAX,代表數組的長度為1000,最大值為1000,最小值為1;建立偽隨機函數Create_data()、復制數組函數Copy_array()和交換元素函數Swap_element(),這三個函數功能為初始化數組的元素;七種內部排序的函數與過程的定義,最后定義了數組的正序和逆序輸出,以及打印排序過程中元素的移動次數和比較次數。
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <math.h> #include <conio.h> #include <string.h> //#pragma warning (disable:4996) extern int Comparisons_num; // 比較次數 extern int Mobile_num; // 移動次數 // 建立偽隨機數組 void Create_data(int *a, int n, int min, int max) { int flag; // 避免取重復取值 srand(time(NULL)); if (n > max - min + 1) return 0; for (int i = 0, j = 0; i<n; i++) { do { a[i] = (max - min + 1) * rand() / (RAND_MAX + 1) + 1; flag = 0; for (j = 0; j < i; j++) { if (a[i] == a[j]) flag = 1; } } while (flag); } } // 復制數組 void Copy_array(int *tar, int *arr, int len) { int i; for (i = 0; i < len; i++) tar[i] = arr[i]; } // 交換元素 void Swap_element(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; Mobile_num += 3; // 一次關鍵字交換計為3次移動 }
以上為文件sort.c中實現數組的初始化程序,函數Create_data()創建沒有重復取值的數組,函數Copy_array()將數組arr全部內容復制到數組tar中,函數Swap_element()負責交換元素內容,每次調用移動次數Mobile_num將會增加三次。
// 直接插入排序 void Insert_sort(int *arr, int len) { int i, j; int tmp; // 待排序的元素 for (i = 0; i < len; i++) { tmp = arr[i]; for (j = i - 1; j >= 0 && tmp < arr[j]; j--) { Swap_element(arr + j, arr + j + 1); // tmp < arr[j],因此arr[j]向后移動 Comparisons_num++; } arr[j + 1] = tmp; } }
// 希爾排序 void Shell_sort(int *arr, int len) { int i, j; int d = len / 2; int lookouts; // 監視哨 while (d >= 1) { for (i = d; i < len; i++) { lookouts = arr[i]; for (j = i - d; j >= 0 && lookouts < arr[j]; j = j - d) { Swap_element(arr + j + d, arr + j); Comparisons_num++; } if (arr[j + d] != lookouts) { Swap_element(arr + j + d, &lookouts); Comparisons_num++; } } d /= 2; } }
-
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率。
-
但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位。
先取一個len/2的整數d1作為第一個增量,把文件的全部記錄分組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然后,取第二個增量d2=d1/2,重復上述的分組和排序,直至所取的增量 =1(
<
…<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
// 冒泡排序 void Bubble_sort(int *arr, int len) { int i, j; int flag = 1; // 標記循環過程是否進行過交換,如果為1則進行了交換 for (i = 0; i < len && flag; i++) { flag = 0; for (j = 1; j < len - i; j++) { if (arr[j - 1] > arr[j]) { Swap_element(arr + j, arr + j - 1); flag = 1; } Comparisons_num++; } } }
-
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
-
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。在這一點,最后的元素應該會是最大的數。(就好比大的氣泡浮出了水面)
-
針對所有的元素重復以上的步驟,除了最后一個。
-
持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
// 分隔(快速排序) int Division(int *a, int left, int right) { int base = a[left]; while (left < right) { while (left < right && base < a[right]) { right--; Comparisons_num++; } a[left] = a[right]; Mobile_num++; while (left < right && a[left] < base) { left++; Comparisons_num++; }
a[right] = a[left]; Mobile_num++; }
a[left] = base; return left; } // 快速排序 // left 和 count初始值為0 right 初始值為數組長度 - 1 void Quick_sort(int *arr, int left, int right, int count) { int i; int count_temp = count + 1; if (left < right) { i = Division(arr, left, right); Quick_sort(arr, left, i - 1, count_temp); Quick_sort(arr, i + 1, right, count_temp); } }
- 設置兩個變量left和right,排序開始的時候:left = 0, right = len - 1;
- 每次分割過程中,以第一個數組元素作為關鍵數據,賦值給base,即base = a[left];
- 從right開始向前搜索,即由后開始向前搜索(right--),找到第一個小於base的值a[right],將A[left]和A[right]互換;
- 接着從left開始向后搜索,即由前開始向后搜索(left++),找到第一個大於base的a[left],將A[left]和A[right]互換;
- 重復第3、4步,直到left >= right; (3,4步中,沒找到符合條件的值,即3中a[right]不小於base,4中a[left]不大於base的時候改變left和right的值,使得right--,left++,直至找到為止。找到符合條件的值,進行交換的時候left和right指針位置不變。另外,left >= right這一過程一定正好是left++或right--完成的時候,此時令循環結束)。
// 選擇排序 void Select_sort(int *arr, int len) { int i, j; int tmp; // 記錄待排序元素的下標 for (i = 0; i < len - 1; i++) { tmp = i; for (j = i + 1; j < len; j++) { if (arr[tmp] > arr[j]) tmp = j; Comparisons_num++; } if (tmp != i) Swap_element(arr + tmp, arr + i); } }
直接選擇排序的算法:
程序采用雙層嵌套循環,外循環按順序每次選擇一個待排序的元素,內循環每次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。
// 構成堆 (堆排序) void Heap_adjust(int arr[], int parent, int len) { int child; int temp; for (temp = arr[parent]; 2 * parent + 1 < len; parent = child) { child = 2 * parent + 1; if (child < len - 1 && arr[child + 1] > arr[child]) { child++; Comparisons_num++; } Comparisons_num++; if (temp < arr[child]) { Swap_element (arr + child, arr + parent); } else break; } } // 堆排序 void Heap_sort(int arr[], int len) { int i; for (i = (len - 1) / 2; i >= 0; i--) Heap_adjust(arr, i, len); for (i = len - 1; i > 0; i--) { Swap_element(arr, arr + i); // 每次將最大的數排在最后 Heap_adjust(arr, 0, i); // 重新構成堆,將最大的數放在第一位 } }
堆分為大根堆和小根堆,是完全二叉樹。
堆排序的算法:
- 建堆,建堆是不斷調整堆的過程,從len/2處開始調整,一直到第一個節點,此處len是堆中元素的個數。建堆的過程是線性的過程,從len/2到0處一直調用調整堆的過程,相當於o(h1)+o(h2)…+o(hlen/2) 其中h表示節點的深度,len/2表示節點的個數,這是一個求和的過程,結果是線性的O(n)。
-
調整堆:調整堆在構建堆的過程中會用到,而且在堆排序過程中也會用到。利用的思想是比較節點i和它的孩子節點left(i),right(i),選出三者最大(或者最小)者,如果最大(小)值不是節點i而是它的一個孩子節點,那邊交互節點i和該節點,然后再調用調整堆過程,這是一個遞歸的過程。調整堆的過程時間復雜度與堆的深度有關系,是lgn的操作,因為是沿着深度方向進行調整的。
- 堆排序:堆排序是利用上面的1、2兩個過程來進行的。首先是根據元素構建堆。然后將堆的根節點取出(一般是與最后一個節點進行交換),將前面len-1個節點繼續進行堆調整的過程,然后再將根節點取出,這樣一直到所有節點都取出。堆排序過程的時間復雜度是O(nlgn)。因為建堆的時間復雜度是O(n)(調用一次);調整堆的時間復雜度是lgn,調用了n-1次,所以堆排序的時間復雜度是O(nlgn)
// 歸並 (歸並排序) void Merge(int arr[], int target[], int start, int mid, int end) { int i, j, k; for (i = mid + 1, j = start; start <= mid && i <= end; j++) { if (arr[start] < arr[i]) target[j] = arr[start++]; else target[j] = arr[i++]; Mobile_num++; Comparisons_num++; } if (start <= mid) { for (k = 0; k <= mid - start; k++) { target[j + k] = arr[start + k]; Mobile_num++; } } if (i <= end) { for (k = 0; k <= end - i; k++) { target[j + k] = arr[i + k]; Mobile_num++; } } } // 歸並排序 // start 和 count初始值為0 end 初始值為數組長度 - 1 void Merge_sort(int arr[], int target[], int start, int end, int count) { int mid; int count_temp = count + 1; int * temp_arr = (int *)calloc(end + 1, sizeof(int)); if (start == end) { target[start] = arr[start]; Mobile_num++; } else { mid = (start + end) / 2; Merge_sort(arr, temp_arr, start, mid, count_temp); Merge_sort(arr, temp_arr, mid + 1, end, count_temp); Merge(temp_arr, target, start, mid, end, count_temp); } if (count == 0) { free(temp_arr); } }
歸並排序采用了分治法,將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。
歸並排序的算法通常用遞歸實現,先把待排序區間[s, e]以中點二分,接着把左邊子區間排序,再把右邊子區間排序,最后把左區間和右區間用一次歸並操作合並成有序的區間[s, e]。
其算法如下:
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並后的序列
- 設定兩個指針,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置
- 重復步驟3直到某一指針超出序列尾
- 將另一序列剩下的所有元素直接復制到合並序列尾
// 顯示打印 // 正序輸出 void Print_sort_positive(int *arr, int len) { int i; for (i = 0; i < len; i++) { if (i % 10 == 0 && i != 0) putchar('\n'); printf("%3d ", arr[i]); } putchar('\n'); } // 逆序輸出 void Print_sort_negative(int *arr, int len) { int i; for (i = 0; i < len; i++) { if (i % 10 == 0 && i != 0) putchar('\n'); printf("%3d ", arr[len - i - 1]); } putchar('\n'); } // 顯示移動次數和比較次數 void Print_mob_com() { printf("移動次數:%d\n", Mobile_num); printf("比較次數:%d\n\n", Comparisons_num); // 初始化 Mobile_num = Comparisons_num = 0; }
最后是實現打印信息的函數:正序輸出函數Print_sort_positive()、逆序輸出函數Print_sort_negative()和顯示移動次數和比較次數函數Print_mob_com()。
#include "Sort.h" #include <stdio.h> int main(void) { int arr[ARRAY_LEN]; int temp_arr[ARRAY_LEN]; int target_arr[ARRAY_LEN]; Create_data(arr, ARRAY_LEN, MIN, MAX); Copy_array(target_arr, arr, ARRAY_LEN); printf("排序前: \n"); Print_sort_positive(target_arr, ARRAY_LEN); Bubble_sort(target_arr, ARRAY_LEN); printf("\n完全正序: \n"); Print_sort_positive(target_arr, ARRAY_LEN); printf("\n完全逆序: \n"); Print_sort_negative(target_arr, ARRAY_LEN); // 開始進行七種排序比較 Copy_array(target_arr, arr, ARRAY_LEN); Bubble_sort(target_arr, ARRAY_LEN); printf ("冒泡排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Quick_sort(target_arr, 0, ARRAY_LEN - 1, 0); printf ("快速排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Copy_array(temp_arr, arr, ARRAY_LEN); Merge_sort(temp_arr, target_arr, 0, ARRAY_LEN - 1, 0); printf ("歸並排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Heap_sort(target_arr, ARRAY_LEN); printf ("堆排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Insert_sort(target_arr, ARRAY_LEN); printf ("直接插入排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Select_sort(target_arr, ARRAY_LEN); rintf ("選擇排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Shell_sort(target_arr, ARRAY_LEN); printf ("希爾排序:\n"); Print_mob_com(); return 0; }
編寫測試程序的文件use_sort.c,並比較七種排序的結果,如下圖:
其中移動和比較次數最多的排序方法為冒泡排序,而移動次數最少的則是快速排序,比較次數最少為希爾排序。。