數據結構之-----排序算法理解與應用


算法的穩定性:如果待排序的兩個元素Ri,Rj,其對應的關鍵字keyi=keyj,且在排序前Ri在Rj的前面,如果排序后Ri還在Rj的前面,則稱這種排序算法是穩定的,否則稱排序算法是不穩定的。

內部排序和外部排序:內部排序是指在排序期間,元素全部存放在內存中的排序。外部排序是指排序期間元素無法全部同時存放在內存中,必須在排序過程中根據要求不斷地在內外存之間移動的排序。

1.插入排序

1)插入排序:每次將一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子序列中,直到全部記錄插入完成。

void InsertSort(ElemType A[], int n) { int i, j; for (i = 2; i <= n; i++) //依此將A[2]~A[n]插入到前面已排序序列 if (A[i].key < A[i - 1].key) //若A[i]的關鍵碼小於其前驅,須將A[i]插入有序表 { A[0] = A[i]; //復制為哨兵 for (j = i - 1; A[0].key < A[j].key; --j) //從后往前查找待插入位置 A[j+1]=A[j]; //向后拖動 A[j+1]=A[0]; //復制到插入位置 } }

時間復雜度:O(n2),空間復雜度:O(1).穩定性:穩定的排序方法。

2)希爾排序:將待排序表分割成若個形如L[i,i+d,i+2d,i+3d,.....i+kd]的特殊子表,分別進行直接插入排序。當整個表呈基本有序時,在對全體記錄進行一次直接插入排序。

過程:先去一個小於n的步長d1,把表中全部記錄分成d1個組,所有距離為d1的倍數的記錄放在同一組中,在各組中進行直接插入排序。然后取第二個步長d2<d1.重復上述過程,直到di=1,即所有記錄在同一組中,再進行直接插入排序。

增量求法:目前不統一,一般采用d1=n/2,,​最后一個增量為1.

void ShellSort(ElemType A[], int n) { //對順序表作希爾插入排序,和插入排序算法相比,做了以下修改:前后記錄位置增量是dk,不是1.A[0]只是暫存單元,不是哨兵,j<0時,插入位置已達 for(dk=len/2;dk>=1;dk=dk/2) //步長變化 for(i=dk+1;i<=n;i++) if (A[i].key < A[i - dk].key) //需將A[i]插入到有序增量子表 { A[0]=A[i]; //暫存在A[0] for (j = i - dk; j > 0 && A[0].key < A[j].key; j -= dk) A[j+dk]=A[j]; A[j + dk] = A[0]; } }

時間復雜度:當n在某個特定范圍時為,最壞情況下為:,空間復雜度O(1)

不穩定排序

2.交換排序

冒泡排序:將設待排序表長為n,從后往前兩兩比較相鄰元素的值,若為逆序,則交換他們,直到序列比較完。此為一趟冒泡。結果為將最小的元素交換到待排序的第一個位置。下一趟冒泡時,前一趟確定的最小元素不再參與比較,待排序列減少一個元素,每趟排序吧最小元素放到最終位置,這樣最多做n-1趟冒泡就把所有元素排好。

void BobbleSort(ElemType A[], int n) { //用冒泡排序法將序列A中的元素按從小到達排列 for (i = 0; i < n - 1; i++) { flag = false; //表示本趟冒泡是否發生交換的標志 for (j = n - 1; j > i; j--) //一趟冒泡過程 if (A[j - 1].key > A[j].key)//若為逆序 { swap(A[j-1],[j]); //交換 flag = true; } if (flag == false) return; } }
  1. 快速排序:快速排序是對冒泡排序的一種改進。其基本思想是基於分治法的:在待排序L[1....n]中任意取一個元素pivot作為基准,通過一趟排序將待排序表划分為獨立的兩部分L[1...k-1],L[k+1....n]使得L[1....k-1]中所有元素小於等於pivot,L[k+1...n]中所有元素大於pivot,則pivot則放置在最終位置上L(k),這個過程稱為一趟快速排序。而后分別遞歸的對兩個子表重復上述過程,直到每一部分內只有一個元素或空為止(所有元素放置在最終位置上)

過程:首先假定划分算法已知,記為partition(),返回上述中的k,L(k)已經在最終位置上,所以可以先對表進行划分,而后對表調用同樣的排序操作。遞歸的調用快速排序算法進行排序。程序結構如下:

void QuickSort(ElemType A[], int low, int high) { if (low < high) //邊界條件,即遞歸跳出的條件 {//partition() 就是划分操作,將表A[low...high]划分為滿足上述條件的兩個子表 int pivotpos = partition(A,low,high); //划分 QuickSort(A,low,pivotpos-1); //依此對兩個子表進行遞歸排序 QuickSort(A,pivotpos+1,high); } } //不難看出,快速排序的關鍵在於划分操作,性能取決於划分操作的好壞 //快速排序分治partition有兩種方法

1)兩個下標分別從首,尾向中間掃描的方法

假設每次都是以當前表中第一個元素作為樞紐值對表進行划分,則必須將表中比樞紐值大的元素向右移動,比樞紐值小的元素向左移動,使得一趟partition()操作后,表的元素被樞紐值一分為二。

int partition(elemtype A[], int low, int high) { elemtype pivot = A[low]; //將當前表中第一個元素設為樞紐值,對表進行划分 wihle(low < high) //循環跳出條件 { while (low < high&&A[high] >= pivot) --high; A[low] = A[high]; //將比樞紐值小的元素移動到左端 while (low < high&&A[low <= pivot) low++; A[high]=A[low]; //將比樞紐值大的元素移動到右端 } A[low] = pivot; //樞紐元素存放到最終位置 return low; //返回存放樞紐的最終位置 }

若初始序列3,8,7,1,2,5,6,4排序過程如下:

2 8 7 1 2 5 6 4

2 8 7 1 8 5 6 4

2 1 7 1 8 5 6 4

2 1 7 7 8 5 6 4

2 1 3 7 8 5 6 4    //A[high]A[low]

2)兩個指針索引一前一后逐步向后掃描

int partition(elemtype A[], int p, int r) { elemtype x = A[r]; //以最后一個元素,A[r]為主元 int i = p - 1; for (int j = p; j <= r - 1; ++j) { if (A[j] <= x) { ++i; exchange(A[i],A[j]); } } exchange(A[i+1],A[r]); return i + 1; }//若初始化序列3 8 7 1 2 5 6 4則排序的大致如下: 3 8 7 1 2 5 6 4 //3與3交換,不移動元素,比較一次 3 1 7 8 2 5 6 4 //8與1交換,交換依此,比較三次 3 1 2 8 7 5 6 4 //7與2交換,交換一次,比較一次 3 1 2 4 7 5 6 8 //8與4交換,交換一次,比較兩次

快速排序是所有內部排序算法中平均性能最優的排序算法。在快速排序算法中,並不產生有序子序列,但每一趟排序后將一個元素(基准元素)放在其最終位置上。當初始排序表基本有序或基本逆序是,就得到最壞情況下的時間復雜度O(n2).

A快排一次排序的應用

A)區分數組中大小寫字母(編寫函數,讓小寫字母在所有大寫字母之前)

bool isUpper(char a)
{
	if (a >= 'A'&&a <= 'Z')
		return true;
	return false;
}

bool isLower(char a)
{
	if (a >= 'a'&&a <= 'z')
		return true;
	return false;
}

void partition(char A[], int low, int high)  //開排一次排序第一種策略的另外一種實現
{
	while (low < high)
	{
		while(low < high&&isUpper(A[high]))high--;
		while (low < high&&islower(A[low]))low++;
		char temp = A[high];
		A[high]=A[low];
		A[low] = temp;
	}

}
void main()
{
	char a[7] = {'a','A','Z','d','B','s','b'};
	partition(a,0,6);
}

 

b)給定含n個元素的整型數組a,包含0和非0,對數組進行排序,使排序后滿足1.排序后的所有0元素在前,非零元素在后,且非零元素排序前后相對位置不變,不能使用額外的存儲空間。

void partition(int A[], int p, int r) { int i = r + 1; for (int j = r; j >= p; j--) //從后往前遍歷,也可從前往后遍歷 { if (A[j] != 0) { --i; int temp = A[i]; A[i]=A[j]; A[j] = temp; } } } void main() { int a[7] = {0,3,0,2,1,0,0}; partition(a,0,6); }

c)荷蘭國旗問題

while (current <= end) { if (array[current] == 0) { swap(array[current],array[begin]); current++; begin++; } else if (array[current == 1]) { current++; } else { //array[current]==2 swap(array[current], array[end]); end--; } }

 

D)輸入n個整數,輸出其中最小的k個。

思路1:將輸入的n個數排序,這樣排在最前面的k個數就是最小的k個數。

思路2:假設最小的k個數中最大的為A。在快排中,先在數組中隨機選擇一個數字,然后調整數組中數字的順序,使得比選中數字小的數字排在他的左邊,比選中數字大的排在他的右邊(快排一次)

若選中的數字下表剛好是k-1(從0開始),那么這個數字(A)加上左側的k-1個數就是最小的k個數。如果他的小標大於k-1,則A位於他的左側,我們可以在他的左邊部分的數組中查找。若小標小於k-1,那么A應該位於他的右邊,我們可以接着在他的右邊部分中尋找。(發現這是一個遞歸問題,但是我們找到的k個數不一定是有序的)

//input是輸入數組,元素個數為n,output用來保存最小的k個數的數組。 void getLeastKNum(int* input, int n, int* output, int k) { if (input == NULL || output == NULL || k > n || n <= 0 || k <= 0) return; int start = 0; int end = n - 1; int index = partition(intput, start, end);//一次划分函數見前 while (index != k - 1) { if (index > k - 1) { end = index - 1; index = partition(input, start, end); } else { start = index + 1; index = partition(input, start, end); } } for (int i = 0; i < k; i++) output[i] = input[i]; } //該算法的平均時間復雜度為O(n)

3.選擇排序

思想:每一趟在后面n-i+1(i=1,2..n-1)個待排序元素中選取關鍵字最小的元素,作為有序子序列的第i個元素,直到n-1趟做完,待排序元素只剩下1就不用再選了。

1)簡單選擇排序

 

void SelectSort(elemtype A[], int n) {//對表A作簡單選擇排序,A[]從0開始存放元素 for(i=0;i<n-1;i++) //總共進行n-1趟排序 { min=i; //記錄最小元素位置 for(j=i+1;j<n;j++) if (A[j]<A[i]) //在A[i..n-1]中選擇最小元素 min=j; //更新最小元素位置 if(min!=i)swap(A[i],A[min]); //與第i個位置交換 } }

 

空間復雜度:O(1)。時間復雜度:元素移動較少不超過3(n-1)(一次swap三次元素移動)。最好移動0次(此時表已經有序)。但是元素間比較的次數與序列的初始狀態無關,始終為n(n-1)/2次。時間復雜度為O(n2).

2)堆排序

堆排序是一種樹形選擇排序方法,在排序過程中將L[1..n]視為一棵完全二叉樹的順序村粗結構。利用完全二叉樹中雙親結點和孩子結點之間的內在關系,在當前無序區中選擇關鍵字最大(或最小)的元素。

堆排序的實質是構建初始堆,對初始序列建堆,就是一個反復篩選的過程。

A)根據初始關鍵字序列(20,18,22,16,30,19)構建初始大根堆。

void BuildMaxHeap(elemtype A[], int len) { for (int i = len / 2; i > 0; i--) AdjustDown(A,i,len); } void AdjustDown(elemtype A[], int k, int len) {//adjustDown將元素k向下進行調整,堆主要的兩個函數之一,另一個adustun A[0]=A[k]; //A[0] 暫存 for (i = 2 * k; i <= len; i = i * 2) //沿k較大的子節點向下篩選 { if(i < len&&A[i] < A[i + 1]) i++; //取key較大的子節點的下標 if (A[0] >= A[i]) break; //篩選結束 else { A[k]=A[i]; //將A【i】調到雙親結點 k = i; //修改k值,繼續向下篩選 } } A[k] = A[0]; //被篩選結點的值放入最終位置 }

在元素個數為n的序列上建堆,其時間復雜度為O(n),這說明可以在線性時間內,將一個無序數組建成一個大頂堆。

B)堆排序的思想

由於堆本身的特點(以大頂堆為例),堆頂元素就是最大值。輸出堆頂元素后,通常將堆底元素放入堆頂,此時根節點已不滿足堆的性質,將堆頂元素向下調整繼續保持大頂堆性質,輸出堆頂元素,重復,直到僅剩一個元素為止。

void HeapSort(elemtype A[], int len) { BuildMaxHeap(A,len); //初始建堆 for (i = len; i > 1; i--) //n-1趟交換和建堆過程 { swap(A[i],A[1]); //輸出堆頂元素(和堆底元素交換) adjustDown(A,1,i-1); //整理,把剩余的i-1個元素整理成堆 } }

C)堆的插入和刪除

刪除堆頂元素時,先將堆的最后一個元素與堆頂元素交換,有序性質破壞,需要堆根結點進行向下調整。

對堆進行插入操作時,先將新結點放在堆的末端,再對這個新結點執行向上調整操作,大頂堆插入操作如下圖所示:

向上調整算法如下所示:

D)堆排序的應用(最小k個數)

輸入n個整數,輸出其中最小的k個.(用堆排序來解決,適合處理海量數據)

思路:首先讀入k個數創建一個大小為k的大頂堆,然后依此讀入剩余數據,如果當前數據比大頂堆的堆頂小,則用這個數代替當前堆頂元素,並調整時期保持大頂堆性質,如果當前數據比堆頂大,則此數不可能為最小的k個整數之一,故拋棄此數。(時間復雜度:O(nlogk))

 

int a[n]; //數組a中存放輸入的n個數 int b[k + 1];//從a中依此讀入k個數a[0].....a[k-1]第一個數存在b[1]中 BuildMaxHeap(b,k);//調整b為大頂堆 for (int i = k; i < n; i++) { if (a[i] > a[1]) continue; else { b[1] = a[i]; adjustdown(b,1,k); } } //當需要求最大的k個數時,只需將大頂堆換位小頂堆。

 

4.歸並排序

  1. 二路歸並排序(內部排序,基於分治算法的,使用輔助空間)

含義:將兩個或兩個以上的有序表組合成一個新的有序表。假定待排序表含有n個記錄,則可視為n個有序子表,每個子表長度為1,兩兩歸並,得到​長度為2的有序表,再兩兩歸並...如此重復,直到合成一個長度為n的有序表為止。

過程:分解:將n個元素的待排序表分成各含n/2個元素的子表,采用二路歸並算法對兩個子表遞歸的進行排序。

合並:合並兩個已排序的子表得到排序結果。

void MergeSort(elemtype A[], int low, int high) { if (low < high) { int mid = (low + high) / 2; //對中間划分兩個子序列 mergeSort(A,low,mid); //對左側子序列進行遞歸排序 mergeSort(A,mid+1,high); //對右側子序列進行遞歸排序 merge(A, low, mid, high); } }

Merge()的功能時將前后相鄰的兩個有序表歸並為一個有序表的算法。設兩段有序表A【low...mid】A[mid+1...high]存放在同一順序表中相鄰的位置上,先將他們復制到輔助數組B中,每次從對應B中的兩個段取出一個記錄進行關鍵字比較,將較小者放入A中,當輸入B中有一段超出其表長,則將另一段剩余部分直接復制到A中。

elemtype *B = (elemtype *)malloc(n + 1) * sizeof(elemtype)); //輔助數組B void Merge(elemtype A[], int low, int mid, int high) { //表A的兩段A[low...mid]和A[mid+1...high]各自有序,將他們合並成一個有序表 for (int k = low; k <= high; k++) B[k]=A[k]; //加A中所有元素復制到B中 for (int i=low, j = mid + 1, k = i; i <= mid&&j <= high; k++) { if (B[i] < B[j]) //比較B的左右兩段元素 A[k] = B[i++]; //將較小的值復制到A中 else A[k]=B[j++]; } while (i <= mid) A[k++]=B[i++]; //若第一個表未檢測完,復制 while (j <= high) A[k++] = B[j++]; //若第二表未檢測完,復制 }//最后兩個while循環中只有一個會執行

 A)合並兩個排好序的鏈表(連個遞增排序鏈表,合並他們使新鏈表結點仍然是按照遞增排序的)

struct LIstNode { int value; ListNode *pNext; }; //原理:二路歸並排序的merge函數,遞歸代碼如下: ListNode* mergeList(ListNode* list1, ListNode* list2) { if (list1 == NULL) return list2; else if (list2 == NULL) return list1; ListNode* pHead = NULL; if (list1->value < list2->value) { pHead = list1; phead->pNext = mergeList(list1->pNext, list2); } else { pHead = list2; phead->pHead = mergeLIst(list1,list2->pNext); } return pHead; }

b)給定有序數組a,b.已知數組a末尾有足夠空間容納b,請實現將b合並到a中。函數頭如下:

Void merge(int a[],int b[],int n,int m)//n為數組a的元素個數,m為數組b的元素個數

思路:先計算總元素個數,從數組末尾(最大元素)開始歸並。

void merge(int a[], int b[], int n, int m) { int k = m + n - 1; int i = n - 1; int j = m - 1; while (i> = 0 && j >= 0) { if (a[i] > b[j]) { a[k--] = a[i--]; } else { a[k--] = a[j--]; } } while (j >= 0) { a[k--]=b[j--]; } }

C)原地歸並排序(二叉歸並排序 內部排序,不適用輔助空間)

原地歸並排序不需要輔助數組即可歸並。關鍵在merge這個函數。假設有兩段遞增的子數組arr[begin....mid-1]和arr[mid...end],但整個數組不是遞增的。其中i=begin,j=mid,k=end.

然后把i到mid-1的部分和mid到j-1的部分對調(可通過三次逆序實現)較小部分就調到前面去了,此時數組變為0 1 2 3 4 5 6 9 7 8(前面有序了,后面又是兩個遞增子數組,繼續迭代即可)

 

void reverse(int *arr, int n) { //將長度為n的數組逆序 int i = 0, j = n - 1; while (i < j) { swap(arr[i],arr[j]);//將兩個實參圖解交換 i++; j--; } } void exchange(int *arr, int n, int i) //將含有n個元素的數組向左循環移位i個位置 { reverse(arr,i); reverse(arr+i,n-i); reverse(arr, n); } //數組兩個有序部分合並,本節圖解的實現 void merge(int *arr,int begin,int mid,int end) { int i = begin, j = mid, k = end; while (i < j&&j <= k) { int step = 0; while (i < j&&arr[i] < arr[j]) i++; while (j < k&&arr[j] <= arr[i]) j++; step++; } //arr+i為子數組首地址,j-i為子數組元素個數,j-i-step為左循環移位個數 exchange(arr+i,j-i,j-i-step); i = i + step; } void MergeSort(int *arr, int l, int r) { if (l < r) { int mid = (l + r) / 2; MergeSort(arr,l,mid); MergeSort(arr,mid+1,r); merge(arr,l,mid+1,r); } } void main() { int arr[] = {6,4,3,1,7,8,2,9,5,0}; int len = sizeof(arr) / sizeof(arr[0]); MergeSort(arr,0,len-1); }

D)多路歸並排序(外部排序)

外部排序是指大文件的排序,即待排序的記錄存儲在外部存儲器上,待排序的文件無法一次裝入內存,需要在內存和外部存儲器之間進行多次數據交換,以達到排序整個文件的目的。

思路:外部排序最常用的算法是多路歸並排序,即將源文件分解成多個能夠一次性裝入內存的部分,分別把每一部分調入內存完成排序,然后對已排序的子文件進行歸並排序。

從二路到多路,增大k可以減少外存信息讀寫時間,但k個歸並段中選擇最小的記錄需要比較k-1次,為了降低選出每個記錄需要的比較次數k,引入敗者數

敗者樹可視為一棵完全二叉樹,每個葉結點存放各歸並段在歸並過程中當前參加比較的記錄,內部結點用來記憶左右子樹中的失敗者,讓勝者網上繼續進行比較,一直到根節點。如果比較兩個數,大的為失敗者,小的為勝利者,則根節點指向的數為最小數。

圖中第一個葉子結點為b0.k路歸並的敗者樹深度為​,因此k個記錄中選擇最小關鍵字,最多需要​次比較,比依此比較的k-1次小得多。

案例:有20個有序數組,每個數組有500個unsigned int元素,降序排序。要求從這10000個元素中選出最大的500ge.

思路:依此從20個有序數組中選擇一個當前元素,兩兩比較,然后找出最大的數,循環500次,即可選擇出500個最大的數。但是這里每選擇一個最大元素,需要比較19次,效率低。

改進方法1:利用堆,從20個數組中各取一個數,並記錄每個數的來源數組,建立一個含有20個元素的大頂堆。此時堆頂就是最大元素,去除堆頂元素,並從堆頂元素的來源數組中取下一個元素加入堆,調整堆后再取最大值,一直這樣進行500次即可。時間復雜度,其中n為要選出的元素個數,k為有序數組個數。

改進方法2:利用敗者樹。從20個數組中各取一個數,並記錄每個數的來源數組,建立一個20路歸並的敗者樹。此時敗者樹輸出的就是最大的數,然后從最大數的來源數組繼續取下一個數加入敗者樹,繼續比較,直到輸出500個數為止。時間復雜度為其中n為要選出的元素個數,k為有序數組個數。

const int branchesLength = 20;//共有20路數組 //20個一維數組,每個數組有500個元素,為葉子結點提供數據。本題只給出測試用的40個元素,輸出最大的前10個元素。 int branches[branchesLength][500] = { {1000,900} ,{999,888} ,{1001,990} ,{887,877} ,{987,978}, {1001,901} ,{992,883}, {1005,992}, {887,877}, {987,978}, {1002,902} ,{993,884}, {1007,991}, {887,877}, {987,978}, {1003,903} ,{994,882} ,{989,900} ,{887,877} ,{987,978} }; //敗者樹的非葉子結點,記錄數據源的索引位置,根據結點的值可以定位到所指向的數據源。 int tree[branchesLength]; //敗者樹的葉子結點,葉子結點和數據源是一一對應的,即第一個葉子結點記錄第一個數據源的當前數據,第一個葉子結點為b0 int nodes[branchesLength]; int nodes_iterator[branchesLength] = {0};//nodes_iterator[i]記錄第i路數組當前已取得第幾個元素 void put(int index)//設置第index葉結點的下一個數據 { nodes[index] = branches[index][nodes_iterator[index]++]; } int get(int index)//獲取第index個葉子結點的當前數據 { return nodes[index]; } //調整第index個葉子結點,具體調整為:葉子結點和父節點比較,敗者留在父結點位置,勝者繼續和父節點的父節點,兄弟節點比較,直到整個樹的根節點。 void adjust(int index) //此函數為主要函數 { int size = branchesLength; int t = (size + index) / 2; //計算父節點 while (t > 0) { if (get(tree[t]) > get(index)) {//敗者留在父節點位置 int temp = tree[t]; tree[t] = index; index = temp; } t /= 2; } tree[0] = index; } vector<int>merge() //依此讀取數據源的數據進行歸並排序,返回排序后的數據列表 { vector<int> list1; //記錄排好序的數據 int top; int i = 0; while (i < 10) {//僅輸出10個數據供測試 top = tree[0]; list1.push_back(top); i++; put(tree[0]); adjust(tree[0]); } return list1; } void init() //初始化構建敗者樹 { int size = branchesLength; for (int i = 0; i < size; i++) //為葉子節點賦值 put(i); int winner = 0; for (int i = 1; i < size; i++) { if (get(i) < get(winner)) { winner = i; } } for (int i = 0; i < branchesLength; i++) //非葉子節點初始化為冠軍節點 tree[i] = winner; for (int i = size - 1; i >= 0; i--) //從后向前依此調整非葉子節點 adjust(i); } void main() { init(); merge(); }

 不同排序算法的比較

 

總結:

1.比較次數和初始排列無關的是選擇排序。

2.在初始序列基本有序的情況下,最優的是插入排序,此時插入排序時間復雜度為O(n),其次是冒泡排序,時間復雜度也為O(n).快速排序此時性能最差,時間復雜度為,同時快速排序在廚師序列逆序的時候,性能也最差,時間復雜度也為

3.堆排序對初始數據集的排列順序不敏感,在最好,最壞和平均情況下,堆排序的時間復雜度為

4.節儉排序,一對數字不進行兩次或兩次以上的比較。包括(插入排序,歸並排序)

5.基於比較的排序算法時間復雜度的下界(最好的時間復雜度)為:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM