日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

选择排序、插入排序、冒泡排序、希尔排序算法的总结 - 复杂度、实现和稳定性

發(fā)布時間:2024/7/5 编程问答 56 豆豆
生活随笔 收集整理的這篇文章主要介紹了 选择排序、插入排序、冒泡排序、希尔排序算法的总结 - 复杂度、实现和稳定性 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

原文地址:https://www.jianshu.com/p/916b15eae350

常見排序算法的總結 - 復雜度、實現(xiàn)和穩(wěn)定性

2018.08.29 16:20*

最基礎的算法問題,溫故知新。排序算法的幾個主要指標是,時間復雜度(最好,最差和平均),空間復雜度(額外空間)和穩(wěn)定性。本文主要描述八種常見算法:簡單選擇排序、冒泡排序、簡單插入排序、希爾排序、歸并排序、快速排序、堆排序和基數排序,關于它們的指標統(tǒng)計可以直接看最后。本文均為基本C++實現(xiàn),不使用STL庫。

值得一提的是排序算法的穩(wěn)定性,之前關注較少。穩(wěn)定性的意思是對于序列中鍵值(Key value)相同的元素,它們在排序前后的相對關系保持不變。對于int這樣的基本數據類型,穩(wěn)定性基本上是沒有意義的,因為它的鍵值就是元素本身,兩個元素的鍵值相同他們就可以被認為是相同的。但對于復雜的數據類型,數據的鍵值相同,數據不一定相同,比如一個Student類,包括Name和Score兩個屬性,以Score為鍵值排序,這時候鍵值相同元素間的相對關系就有意義了。

簡單選擇排序

應該是最自然的思路。選擇排序的思想是,從全部序列中選取最小的,與第0個元素交換,然后從第1個元素往后找出最小的,與第一個元素交換,再從第2個元素往后選取最小的,與第2個元素交換,直到選取最后一個元素。

void selectionSort(int a[], int n) {for (int i = 0; i < n - 1; ++i) {int minIdx = i;for (int j = i + 1; j < n; ++j) {if (a[j] < a[minIdx]) {minIdx = j;}}int tmp = a[i];a[i] = a[minIdx];a[minIdx] = tmp;} }

無論如何都要完整地執(zhí)行內外兩重循環(huán),故最好、最差和平均時間復雜度都是O(n2),不需要額外空間。選擇排序是不穩(wěn)定的。

冒泡排序

冒泡排序的思想是,從第0個元素到第n-1個元素遍歷,若前面一個元素大于后面一個元素,則交換兩個元素,這樣可將整個序列中最大的元素冒泡到最后,然后再從第0個到第n-2遍歷,如此往復,直到只剩一個元素。

void bubbleSort(int a[], int n) {for (int i = 0; i < n - 1; ++i) {for (int j = 0; j < n - i - 1; ++j) {if (a[j] > a[j + 1]) {int tmp = a[j];a[j] = a[j + 1];a[j + 1] = tmp;}}} }

冒泡排序與簡單選擇排序類似,無論如何都要執(zhí)行完兩重循環(huán),故最好、最壞和平均時間復雜度均為O(n2),不需要額外空間。冒泡排序是穩(wěn)定的。
冒泡排序的一個改進是,在內層循環(huán)之前設置一個標記變量,用于標記循環(huán)是否進行了交換,在內層循環(huán)結束時,若判斷沒有進行交換,則說明剩下的序列中,每個元素都小于等于后面一個元素,即已經有序,可終止循環(huán)。這樣,冒泡排序的最好時間復雜度可以提升到O(n)。

簡單插入排序(Insertion Sort)

思路是類似撲克牌的排序,每次從未排序序列的第一個元素,插入到已排序序列中的合適位置。假設初始的有序序列為第0個元素(本文描述的序號都從0開始),只有一個元素的序列肯定是有序的,然后從原先序列的第1個元素開始到第n-1個元素遍歷,每次將當前元素插入到它之前序列中的合適位置。

void insertionSortBSearch(int a[], n) {for (int i = 1; i < n; ++i) { int j, val = a[i]; for (j = i - 1; j >= 0 && a[j] > val; --j) {a[j + 1] = a[j];}a[j + 1] = val;} }

兩重循環(huán),最差和平均時間復雜度為O(n2),最好情況是原序列已有序,則忽略內層循環(huán),時間復雜度O(n)。插入排序是穩(wěn)定的。
這里,內層循環(huán)我們用的是從后向前遍歷,來找到合適的插入位置,而內層循環(huán)所遍歷的,是已排序的數組,所以我們可以使用二分查找來尋找插入位置,從而使時間復雜度提高到O(n*log n)。代碼如下。

// 二分查找改進的插入排序 void insertionSortBSearch(int a[], n) {for (int i = 1; i < n; ++i) { int j, val = a[i]; int begin = 0, end = i - 1;while (begin < end) {int mid = begin + (end - begin) / 2;if (a[mid] > val) {end = mid - 1;}else {begin = mid;}}for (j = i - 1; j >= begin; --j) {a[j + 1] = a[j];}a[begin] = val;} }

希爾排序

希爾排序可以被認為是簡單插入排序的一種改進。插入排序一個比較耗時的地方在于需要將元素反復后移,因為它是以1為增量進行比較的元素的后移可能會進行多次。一個長度為n的序列,以1為增量就是一個序列,以2為增量就形成兩個序列,以i為增量就形成i個序列。希爾排序的思想是,先以一個較大的增量,將序列分成幾個子序列,將這幾個子序列分別排序后,合并,在縮小增量進行同樣的操作,知道增量為1時,序列已經基本有序,這是進行簡單插入排序的效率就會較高。希爾排序的維基詞條上有一個比較好的解釋例子如下:

// 原始序列 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 // 以5為增量劃分,5列,每列即為一個子序列 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 // 對每一個子序列進行插入排序得到以下結果 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 // 恢復一行顯示為 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 // 再以3為增量劃分,3列,每列即為一個子序列 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 // 對每一個子序列進行插入排序得到如下結果 10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94 // 恢復一行為 10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94 // 然后再以1為增量進行插入排序,即簡單插入排序 // 此時序列已經基本有序,分布均勻,需要反復后移的情況較少,效率較高

上面的例子中,我們依次選取了5、3和1為增量,實際中增量的選取并沒有統(tǒng)一的規(guī)則,唯一的要求就是最后一次迭代的增量需為1。最初的增量選取規(guī)則為,從n/2折半遞減直到1。還有一些關于希爾排序增量選取的研究,針對不同數據有不同的表現(xiàn),在此不做展開。下面是增量從n/2折半遞減到1的代碼示例。

void shellSort(int a[], int n) {for (int step = n / 2; step > 0; step /= 2) {for (int i = step; i < n; ++i) {int j, val = a[i];for (j = n - step; j >= 0 && a[j] > val; j -= step) {a[j + step] = a[j];}a[j + 1] = val;}} }

希爾排序在簡單插入排序的基礎上做了些改進,它的最好及最差時間復雜度和簡單插入排序一樣,分別是是O(n)和O(n2),平均時間復雜度試增量選取規(guī)則而定,一般認為介于O(n)和O(n2)之間。它不需要額外空間。它是不穩(wěn)定的。

歸并排序

歸并排序的思想是,利用二分的特性,將序列分成兩個子序列進行排序,將排序后的兩個子序列歸并(合并),當序列的長度為2時,它的兩個子序列長度為1,即視為有序,可直接合并,即達到歸并排序的最小子狀態(tài)。基于遞歸的實現(xiàn)如下:

void mergeSortRecursive(int a[], int b[], int start, int end) {if (start >= end) {return;}int mid = start + (end - start) / 2,start1 = start, end1 = mid,start2 = mid + 1, end2 = end;mergeSortRecursive(a, b, start1, end1);mergeSortRecursive(a, b, start2, end2);int i = 0;while (start1 <= end1 && start2 <= end2) {b[i++] = a[start1] < a[start2] ? a[start1++] : a[start2++];}while (start1 <= end1) {b[i++] = a[start1++];}while (start2 <= end2) {b[i++] = a[start2++];}for (i = start; i < end; ++i) {a[i] = b[i];} }

void mergeSort(int a[], int n) {
int *b = new int[n];
mergeSortRecursive(a, b, 0, n - 1);
delete[] b;
}

歸并排序的最好,最壞和平均時間復雜度都是O(n*logn)。但需要O(n)的輔助空間。歸并排序是穩(wěn)定的。

快速排序

快速排序可能是最常被提到的排序算法了,快排的思想是,選取第一個數為基準,通過一次遍歷將小于它的元素放到它的左側,將大于它的元素放到它的右側,然后對它的左右兩個子序列分別遞歸地執(zhí)行同樣的操作。

void quickSortRecursive(int a[], int start, int end) {if (start >= end)return;int mid = a[start];int left = start + 1, right = end;while (left < right) {while (a[left] <= mid && left < right)++left;while (a[right] > mid && left < right)--right;swap(a[left], a[right]);}if (a[left] <= a[start])swap(a[left], a[start]);else--left;if (left)quickSortRecursive(a, start, left - 1);quickSortRecursive(a, left + 1, end); }

void quickSort(int a[], int n) {
quickSortRecursive(a, 0, n - 1);
}

快速排序利用分而治之的思想,它的最好和平均實際復雜度為O(nlogn),但是,如果選取基準的規(guī)則正好與實際數值分布相反,例如我們選取第一個數為基準,而原始序列是倒序的,那么每一輪循環(huán),快排都只能把基準放到最右側,故快排的最差時間復雜度為O(n2)。快排算法本身沒有用到額外的空間,可以說需要的空間為O(1);對于遞歸實現(xiàn),也可以說需要的空間是O(n),因為在遞歸調用時有棧的開銷,當然最壞情況是O(n),平均情況是O(logn)。快速排序是不穩(wěn)定的。

堆排序

堆排序利用的是二叉樹的思想,所謂堆就是一個完全二叉樹,完全二叉樹的意思就是,除了葉子節(jié)點,其它所有節(jié)點都有兩個子節(jié)點,這樣子的話,完全二叉樹就可以用一個一塊連續(xù)的內存空間(數組)來存儲,而不需要指針操作了。堆排序分兩個流程,首先是構建大頂堆,然后是從大頂堆中獲取按逆序提取元素。
首先是大頂堆,大頂堆即一個完全二叉樹,的每一個節(jié)點都大于它的所有子節(jié)點。大頂堆可以按照從上到下從左到右的順序,用數組來存儲,第i個節(jié)點的父節(jié)點序號為(i-1)/2,左子節(jié)點序號為2i+1,右子節(jié)點序號為2(i+1)。構建大頂堆的過程即從后向前遍歷所有非葉子節(jié)點,若它小于左右子節(jié)點,則與左右子節(jié)點中最大的交換,然后遞歸地對原最大節(jié)點做同樣的操作。下面是一個較好的示意圖來自bubkoo:

構建大頂堆示意圖
構建完大頂堆后,我們需要按逆序提取元素,從而獲得一個遞增的序列。首先將根節(jié)點和最后一個節(jié)點交換,這樣最大的元素就放到最后了,然后我們更新大頂堆,再次將新的大頂堆根節(jié)點和倒數第二個節(jié)點交換,如此循環(huán)直到只剩一個節(jié)點,此時整個序列有序。下面是一個較好的示意圖來自 bubkoo:
從大頂堆逆序提取元素使其有序示意圖

void updateHeap(int a[], int i, int n) {int iMax = i,iLeft = 2 * i + 1,iRight = 2 * (i + 1);if (iLeft < n && a[iMax] < a[iLeft]) {iMax = iLeft;}if (iRight < n && a[iMax] < a[iRight]) {iMax = iRight;}if (iMax != i) {int tmp = a[iMax];a[iMax] = a[i];a[i] = tmp;updateHeap(a, iMax, n);} }

void heapSort(int a[], int n) {
for (int i = (n - 1) / 2; i >= 0; i–) {
updateHeap(a, i, n);
}
for (int i = n - 1; i > 0; --i) {
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
updateHeap(a, i, n);
}
}

堆排序的整個過程中充分利用的二分思想,它的最好、最壞和平均時間復雜度都是O(nlogn)。堆排序不需要額外的空間。堆排序的交換過程不連續(xù),顯然是不穩(wěn)定的。

基數排序

基數排序是一種典型的空間換時間的排序方法。以正整數為例,將所有待比較數值統(tǒng)一為同樣的數位長度,數位較短的數前面補零。然后,從最低位開始,依次進行一次排序。這樣從最低位(個位)排序一直到最高位排序完成以后,數列就變成一個有序序列。
對正整數我們常以10為基數,每一位可以為0到9,對于其它數據類型如字符串,我們可以進一步拓展基數,基數越大越占空間,但時間更快,如果有一段足夠長的內存空間,也就是說基數為無窮大,那就足夠表示所有出現(xiàn)的數值,我們就可以通過一次遍歷就實現(xiàn)排序,當然實現(xiàn)上這是不可能的(對已知輸入范圍的數據是可能的,而且非常有用的,可以用這種思想來模擬一個簡單的hash函數)。

int maxBit(int a[], int n) {int maxData = a[0]; for (int i = 1; i < n; ++i){if (maxData < a[i]) {maxData = a[i];} }int d = 1;int p = 10;while (maxData >= p){maxData /= 10;++d;}return d; } void radixsort(int a[], int n) {int d = maxBit(a, n);int *tmp = new int[n];int *count = new int[10];int i, j, k;int radix = 1;for (i = 1; i <= d; i++, radix *= 10){for (j = 0; j < 10; j++) {count[j] = 0;} for (j = 0; j < n; j++){k = (a[j] / radix) % 10;count[k]++;}for (j = 1; j < 10; j++) {count[j] = count[j - 1] + count[j];} for (j = n - 1; j >= 0; j--){k = (a[j] / radix) % 10;tmp[count[k] - 1] = a[j];count[k]--;}for (j = 0; j < n; j++) {a[j] = tmp[j];}}delete[]tmp;delete[]count; }

基數排序的最好,最好、最壞和平均時間復雜度都是O(n*k),其中n是數據大小,k是所選基數。它需要O(n+k)的額外空間。它是穩(wěn)定的。

八種排序算法總結

上面介紹了最常提到的八種排序算法,最基礎的是選擇和插入,基于選擇和插入分別改進出了冒泡和希爾。基于二分思想又提出了歸并、快排和堆排序。最后基于數據的分布特征,提出了基數排序。這些排序算法的主要指標總結如下。

算法最好時間最壞時間平均時間額外空間穩(wěn)定性
選擇n2 n2 n2 1不穩(wěn)定
冒泡nn2 n2 1穩(wěn)定
插入nn2 n2 1穩(wěn)定
希爾nn2 n1.3(不確定) 1不穩(wěn)定
歸并nlog2nnlog2nnlog2nn穩(wěn)定
快排nlog2nn2 nlog2nlog2n至n不穩(wěn)定
nlog2nnlog2nnlog2n1不穩(wěn)定
基數n*kn*kn*kn+k穩(wěn)定

參考

排序算法時間復雜度:https://www.geeksforgeeks.org/time-complexities-of-all-sorting-algorithms/
排序算法穩(wěn)定性:https://www.geeksforgeeks.org/stability-in-sorting-algorithms/

</div></div> </div>

總結

以上是生活随笔為你收集整理的选择排序、插入排序、冒泡排序、希尔排序算法的总结 - 复杂度、实现和稳定性的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。