摘要:本篇博客我要來(lái)和大家一起聊一聊數(shù)據(jù)結(jié)構(gòu)初階中的最后一篇博客八大經(jīng)典排序算法的總結(jié),其中會(huì)介紹他們的原來(lái),還有復(fù)雜度的分析以及各種優(yōu)化。快速排序遞歸版本快速排序是于年提出的一種二叉樹(shù)結(jié)構(gòu)的交換排序方法。
??本篇博客我要來(lái)和大家一起聊一聊數(shù)據(jù)結(jié)構(gòu)初階中的最后一篇博客——八大經(jīng)典排序算法的總結(jié),其中會(huì)介紹他們的原來(lái),還有復(fù)雜度的分析以及各種優(yōu)化。
??博客代碼已上傳至gitee:https://gitee.com/byte-binxin/data-structure/tree/master/Sort2.0
? 我們可以先了解一下兩個(gè)概念:
? 排序:所謂排序,就是使一串記錄,按照其中的某個(gè)或某些關(guān)鍵字的大小,遞增或遞減的排列起來(lái)的操作。
? 排序的穩(wěn)定性:假定在待排序的記錄序列中,存在多個(gè)具有相同的關(guān)鍵字的記錄,若經(jīng)過(guò)排序,這些記錄的相對(duì)次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩(wěn)定的;否則稱為不穩(wěn)定的。
?排序的在生活中應(yīng)用十分廣泛,比如在我們刷抖音短視頻的時(shí)候,大數(shù)據(jù)根據(jù)我們的喜好,會(huì)把我們喜歡的推送給我們,還有我們購(gòu)物可以根據(jù)價(jià)格升降序之類的來(lái)選擇商品等等。
?所以說(shuō)排序真的是十分的重要。
?基本思想:把待排序的數(shù)逐個(gè)插入到一個(gè)已經(jīng)排好序的有序序列中,直到所有的記錄插入完為止,得到一個(gè)新的有序序列。
?一般地,我們把第一個(gè)看作是有序的,所以我們可以從第二個(gè)數(shù)開(kāi)始往前插入,使得前兩個(gè)數(shù)是有序的,然后將第三個(gè)數(shù)插入直到最后一個(gè)數(shù)插入。
我們可以先看一個(gè)動(dòng)圖演示來(lái)理解一下:
為了讓大家更好地理解代碼是怎么實(shí)現(xiàn)的,我們可以實(shí)現(xiàn)單趟的排序,代碼如下:
int end = n-1;// 先定義一個(gè)變量將要插入的數(shù)保存起來(lái)int x = a[end + 1];while (end >= 0){ // 直到后面的數(shù)比前一個(gè)數(shù)大時(shí)就不往前移動(dòng),就直接把這個(gè)數(shù)放在end的后面 if (a[end] > x) { a[end + 1] = a[end]; end--; } else { break; }}a[end + 1] = x;
?前面我們也說(shuō)了,是從第二個(gè)是開(kāi)始往前插入,所以說(shuō)第一趟的end應(yīng)該為0,最后一趟的end應(yīng)該是end = n - 2,根據(jù)end+1
可以推出。
所以直接插入排序的整個(gè)過(guò)程的代碼實(shí)現(xiàn)如下:
void InsertSort(int* a, int n){ int i = 0; for (i = 0; i < n - 1; i++) { int end = i; // 先定義一個(gè)變量將要插入的數(shù)保存起來(lái) int x = a[end + 1]; // 直到后面的數(shù)比前一個(gè)數(shù)大時(shí)就不往前移動(dòng),就直接把這個(gè)數(shù)放在end的后面 while (end >= 0) { if (a[end] > x) { a[end + 1] = a[end]; end--; } else { break; } } a[end + 1] = x; }}
?時(shí)間復(fù)雜度和空間復(fù)雜度的分析
時(shí)間復(fù)雜度: 第一趟end最多往前移動(dòng)1次,第二趟是2次……第n-1趟是n-1次,所以總次數(shù)是1+2+3+……+n-1=n*(n-1)/2,所以說(shuō)時(shí)間復(fù)雜度是O(n^2)
最好的情況: 順序
最壞的情況: 逆序
:給大家看一下直接插入排序排100w個(gè)數(shù)據(jù)要跑多久
空間復(fù)雜度:由于沒(méi)有額外開(kāi)辟空間,所以空間復(fù)雜度為O(1)
?直接插入排序穩(wěn)定性的分析
直接插入排序在遇到相同的數(shù)時(shí),可以就放在這個(gè)數(shù)的后面,就可以保持穩(wěn)定性了,所以說(shuō)這個(gè)排序是穩(wěn)定的。
?基本思想:希爾排序是建立在直接插入排序之上的一種排序,希爾排序的思想上是把較大的數(shù)盡快的移動(dòng)到后面,把較小的數(shù)盡快的移動(dòng)到后面。先選定一個(gè)整數(shù),把待排序文件中所有記錄分成個(gè)組,所有距離為的記錄分在同一組內(nèi),并對(duì)每一組內(nèi)的記錄進(jìn)行排序。(直接插入排序的步長(zhǎng)為1),這里的步長(zhǎng)不為1,而是大于1,我們把步長(zhǎng)這個(gè)量稱為gap,當(dāng)gap>1時(shí),都是在進(jìn)行預(yù)排序,當(dāng)gap==1時(shí),進(jìn)行的是直接插入排序。
?可以先給大家看一個(gè)圖解:
看一下下面動(dòng)圖演示的過(guò)程:
我們可以先寫一個(gè)單趟的排序:
int end = 0;int x = a[end + gap];while (end >= 0){ if (a[end] > x) { a[end + gap] = a[end]; end -= gap; } else { break; }}a[end + gap] = x;
這里的單趟排序的實(shí)現(xiàn)和直接插入排序差不多,只不過(guò)是原來(lái)是gap = 1,現(xiàn)在是gap了。
由于我們要對(duì)每一組都進(jìn)行排序,所以我們可以一組一組地排,像這樣:
// gap組for (int j = 0; j < gap; j++){ int i = 0; for (i = 0; i < n-gap; i+=gap) { int end = i; int x = a[end + gap]; while (end >= 0) { if (a[end] > x) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = x; }}
也可以對(duì)代碼進(jìn)行一些優(yōu)化,直接一起排序,不要一組一組地,代碼如下:
int i = 0;for (i = 0; i < n - gap; i++)// 一起預(yù)排序{ int end = i; int x = a[end + gap]; while (end >= 0) { if (a[end] > x) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = x;}
?當(dāng)gap>1時(shí),都是在進(jìn)行預(yù)排序,當(dāng)gap==1時(shí),進(jìn)行的是直接插入排序。
?gap越大預(yù)排越快,預(yù)排后越不接近有序
?gap越小預(yù)排越慢,預(yù)排后越接近有序
?gap==1時(shí),進(jìn)行的是直接插入排序。
?所以接下來(lái)我們要控制gap,我們可以讓最初gap為n,然后一直除以2直到gap變成1,也可以這樣:gap = gap/3+1。只要最后一次gap為1就可以了。
所以最后的代碼實(shí)現(xiàn)如下:
void ShellSort(int* a, int n){ int gap = n; while (gap > 1)// 不要寫等于,會(huì)導(dǎo)致死循環(huán) { // gap > 1 預(yù)排序 // gap == 1 插入排序 gap /= 2; int i = 0; for (i = 0; i < n - gap; i++)// 一起預(yù)排序 { int end = i; int x = a[end + gap]; while (end >= 0) { if (a[end] > x) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = x; } }}
?時(shí)間復(fù)雜度和空間復(fù)雜度的分析
時(shí)間復(fù)雜度: 外層循環(huán)的次數(shù)前幾篇博客我們算過(guò)很多次類似的,也就是O(logN),
里面是這樣算的
:給大家看一下直接插入排序排100w個(gè)數(shù)據(jù)要跑多久
看這時(shí)間,比起直接插入排序真的是快了太多。
空間復(fù)雜度:由于沒(méi)有額外開(kāi)辟空間,所以空間復(fù)雜度為O(1)
?希爾排序穩(wěn)定性的分析
我們可以這樣想,相同的數(shù)被分到了不同的組,就不能保證原有的順序了,所以說(shuō)這個(gè)排序是不穩(wěn)定的。
?基本思想每一次從待排序的數(shù)據(jù)元素中選出最小(或最大)的一個(gè)元素,存放在序列的起始位置,直到全部待排序的數(shù)據(jù)元素排完 。
?我們先看一下直接選擇排序的動(dòng)圖演示:
像上面一樣,我們先來(lái)實(shí)現(xiàn)單趟排序:
int begin = 0;int mini = begin;int maxi = begin;int i = 0;for (i = begin; i <= end; i++){ if (a[i] > a[maxi]) { maxi = i; } if (a[i] < a[mini]) { mini = i; }}// 如果maxi和begin相等的話,要對(duì)maxi進(jìn)行修正if (maxi == begin) maxi = mini;Swap(&a[begin], &a[mini]);Swap(&a[end], &a[maxi]);
這里我要說(shuō)明一下,其中加了一段修正maxi的代碼,就是為了防止begin和maxi相等時(shí),mini與begin交換會(huì)導(dǎo)致maxi的位置發(fā)生變化,最后排序邏輯就會(huì)亂了,所以加上一段修正maxi的值得代碼。
if (maxi == begin) maxi = mini;
整體排序就是begin往前走,end往后走,相遇就停下,所以整體代碼實(shí)現(xiàn)如下:
void SelectSort(int* a, int n){ int begin = 0; int end = n - 1; while (begin < end) { int mini = begin; int maxi = begin; int i = 0; for (i = begin; i <= end; i++) { if (a[i] > a[maxi]) { maxi = i; } if (a[i] < a[mini]) { mini = i; } } // 如果maxi和begin相等的話,要對(duì)maxi進(jìn)行修正 if (maxi == begin) maxi = mini; Swap(&a[begin], &a[mini]); Swap(&a[end], &a[maxi]); begin++; end--; }}
?時(shí)間復(fù)雜度和空間復(fù)雜度的分析
時(shí)間復(fù)雜度: 第一趟遍歷n-1個(gè)數(shù),選出兩個(gè)數(shù),第二趟遍歷n-3個(gè)數(shù),選出兩個(gè)數(shù)……最后一次遍歷1個(gè)數(shù)(n為偶數(shù))或2個(gè)數(shù)(n為奇數(shù)),所以總次數(shù)是n-1+n-3+……+2,所以說(shuō)時(shí)間復(fù)雜度是O(n^2)
最好的情況: O(n^2)(順序)
最壞的情況: O(n^2)(逆序)
直接選擇排序任何情況下的時(shí)間復(fù)雜度都是 O(n^2),因?yàn)椴还苡行蜻€是無(wú)序都要去選數(shù)。
?給大家看一下直接選擇排序排100w個(gè)數(shù)據(jù)要跑多久
空間復(fù)雜度:由于沒(méi)有額外開(kāi)辟空間,所以空間復(fù)雜度為O(1)
?直接選擇排序穩(wěn)定性的分析
我們可以這樣想
所以說(shuō)直接選擇排序是不穩(wěn)定的。
?堆排序我在上上一篇博客已經(jīng)詳細(xì)介紹了,大家可以點(diǎn)擊這里去看堆排序
?基本思想:所謂交換,就是根據(jù)序列中兩個(gè)記錄鍵值的比較結(jié)果來(lái)對(duì)換這兩個(gè)記錄在序列中的位置,交換排序的特點(diǎn)是:將鍵值較大的記錄向序列的尾部移動(dòng),鍵值較小的記錄向序列的前部移動(dòng)。
?基本思想:它重復(fù)地走訪過(guò)要排序的元素列,依次比較兩個(gè)相鄰的元素,如果順序(如從大到小、首字母從Z到A)錯(cuò)誤就把他們交換過(guò)來(lái)。走訪元素的工作是重復(fù)地進(jìn)行直到?jīng)]有相鄰元素需要交換,也就是說(shuō)該元素列已經(jīng)排序完成。
?圖解如下:
?再看一個(gè)冒泡排序的動(dòng)圖:
先實(shí)現(xiàn)單趟冒泡排序:
int j = 0;for (j = 0; j < n - 1; j++){ // 比后面的數(shù)大就交換 if (a[j] > a[j + 1]) { exchange = 1; Swap(&a[j], &a[j + 1]); }}
再實(shí)現(xiàn)整體的排序:
void BubbleSort(int* a, int n){ int i = 0; for (i = 0; i < n - 1; i++) { int exchange = 0; int j = 0; for (j = 0; j < n - i - 1; j++) { if (a[j] > a[j + 1]) { exchange = 1; Swap(&a[j], &a[j + 1]); } } }}
?我們?cè)倏紤]這樣一個(gè)問(wèn)題,假如當(dāng)前的序列已經(jīng)有序了,我們有什么辦法讓這個(gè)排序盡快結(jié)束嗎?
這當(dāng)然是有的,我們可以定義一個(gè)exchange的變量,如果這趟排序發(fā)生交換就把這個(gè)變量置為1,否則就不變,不發(fā)生交換的意思就是該序列已經(jīng)有序了,利用這樣一個(gè)變量我們就可以直接結(jié)束循環(huán)了。
優(yōu)化后的代碼如下:
void BubbleSort(int* a, int n){ int i = 0; for (i = 0; i < n - 1; i++) { int exchange = 0; int j = 0; for (j = 0; j < n - i - 1; j++) { if (a[j] > a[j + 1]) { exchange = 1; Swap(&a[j], &a[j + 1]); } } // 不發(fā)生交換 if (exchange == 0) break; }}
?時(shí)間復(fù)雜度和空間復(fù)雜度的分析
時(shí)間復(fù)雜度: 第一趟最多比較n-1次,第二趟最多比較n-2次……最后一次最多比較1次,所以總次數(shù)是n-1+n-2+……+1,所以說(shuō)時(shí)間復(fù)雜度是O(n^2)
最好的情況: O(n)(順序)
最壞的情況: O(n^2)(逆序)
所以說(shuō)冒泡排序在最好的情況下比直接選擇排序更優(yōu)。
?給大家看一下冒泡排序排10w個(gè)數(shù)據(jù)要跑多久,因?yàn)樘耍赃@里只排10w
可以看出的是,10w個(gè)數(shù)冒泡排序都排的很久。
空間復(fù)雜度:由于沒(méi)有額外開(kāi)辟空間,所以空間復(fù)雜度為O(1)
?直接選擇排序穩(wěn)定性的分析
冒泡排序在比較遇到相同的數(shù)時(shí),可以不進(jìn)行交換,這樣就保證了穩(wěn)定性,所以說(shuō)冒泡排序數(shù)穩(wěn)定的。
?快速排序是Hoare于1962年提出的一種二叉樹(shù)結(jié)構(gòu)的交換排序方法。
?基本思想:任取待排序元素序列中的某元素作為基準(zhǔn)值,按照該排序碼將待排序集合分割成兩子序列,左子序列中所有元素均小于基準(zhǔn)值,右子序列中所有元素均大于基準(zhǔn)值,然后最左右子序列重復(fù)該過(guò)程,直到所有元素都排列在相應(yīng)位置上為止。
?我們先看一個(gè)分割一次的動(dòng)圖:
?我們要遵循一個(gè)原則:關(guān)鍵詞取左,右邊先找小再左邊找大;關(guān)鍵詞取右,左邊找先大再右邊找小。
?一次過(guò)后,2也就來(lái)到了排序后的位置,接下來(lái)我們就是利用遞歸來(lái)把key左邊區(qū)間和右邊的區(qū)間遞歸排好就可以了,如下:
遞歸左區(qū)間:[left, key-1] key 遞歸右區(qū)間:[key+1, right]
hoare版本找key值代碼實(shí)現(xiàn)如下:
int PartSort1(int* a, int left, int right){ int keyi = left; while (left < right) { // 右邊找小 while (left < right && a[right] >= a[keyi]) { right--; } // 左邊找大 while (left < right && a[left] <= a[keyi]) { left++; } Swap(&a[left], &a[right]); } Swap(&a[keyi], &a[left]); return left;}
快排代碼實(shí)現(xiàn)如下:
void QuickSort(int* a, int left, int right){ if (left > right) return; int div = PartSort1(a, left, right); // 兩個(gè)區(qū)間 [left, div-1] div [div+1, right] QuickSort(a, left, div - 1); QuickSort(a, div + 1, right);}
?我們考慮這樣一種情況,當(dāng)?shù)谝粋€(gè)數(shù)是最小的時(shí)候,順序的時(shí)候會(huì)很糟糕,因?yàn)槊看芜f歸right都要走到頭,看下圖:
此時(shí)會(huì)建立很多函數(shù)棧幀,遞歸的深度會(huì)很深,會(huì)導(dǎo)致棧溢出(stackover),看下圖:
為了優(yōu)化這里寫了一個(gè)三數(shù)取中的代碼,三數(shù)取中就是在序列的首、中和尾三個(gè)位置選擇第二大的數(shù),然后放在第一個(gè)位置,這樣就防止了首位不是最小的,這樣也就避免了有序情況下,情況也不會(huì)太糟糕。
下面是三數(shù)取中代碼:
int GetMidIndex(int* a, int left, int right){ int mid = left + (right - left) / 2; if (a[mid] > a[left]) { if (a[right] > a[mid]) { return mid; } // a[right] <= a[mid] else if (a[left] > a[right]) { return left; } else { return right; } } // a[mid] <= a[left] else { if (a[mid] > a[right]) { return mid; } // a[mid] <= a[right] else if (a[left] > a[right]) { return right; } else { return left; } }}
所以加上三數(shù)取中優(yōu)化后的代碼如下:
int PartSort1(int* a, int left, int right){ int index = GetMidIndex(a, left, right); Swap(&a[index], &a[left]); int keyi = left; while (left < right) { // 右邊找小 while (left < right && a[right] >= a[keyi]) { right
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/125071.html
摘要:今天,一條就帶大家徹底跨過(guò)排序算法這道坎,保姆級(jí)教程建議收藏。利用遞歸算法,對(duì)分治后的子數(shù)組進(jìn)行排序。基本思想堆排序是利用堆這種數(shù)據(jù)結(jié)構(gòu)而設(shè)計(jì)的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時(shí)間復(fù)雜度均為,它也是不穩(wěn)定排序。 ...
閱讀 1873·2021-11-25 09:43
閱讀 2151·2021-11-19 09:40
閱讀 3432·2021-11-18 13:12
閱讀 1744·2021-09-29 09:35
閱讀 665·2021-08-24 10:00
閱讀 2512·2019-08-30 15:55
閱讀 1717·2019-08-30 12:56
閱讀 1819·2019-08-28 17:59