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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

算法入门篇:排序算法(一)

發(fā)布時(shí)間:2023/12/16 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 算法入门篇:排序算法(一) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

引子

筆者剛剛學(xué)習(xí)自己的的一門編程語言(C語言)的時(shí)候,正在runoob上面刷經(jīng)典一百道題。

第一次見到排序問題,我內(nèi)心是不屑的,

“這?不是張口就來?”

然后我就貢獻(xiàn)了一整個(gè)下午的時(shí)間在一個(gè)簡單的排序上面。

初學(xué)者不知到排序的時(shí)候可以有交換兩個(gè)值這樣的操作,所以基本的選擇排序都沒想出來,深陷于“找出最小值,放入新數(shù)組,再擦除這個(gè)值…”的死胡同里面。不僅貢獻(xiàn)了一下午的時(shí)光,還順帶自尊心極度受挫。

其實(shí)當(dāng)時(shí)我的這種思路非常接近一種叫選擇排序的算法,只需要一點(diǎn)點(diǎn)指點(diǎn)就可以馬上取得撥云見日的效果。

如果你也是這樣子的,那最好看完本文。本文系我讀《算法導(dǎo)論》的一份筆記,希望可以給還不懂排序的你帶來幫助。

約定:

本文代碼都是在Windows10下,使用64位GCC編譯器編譯,編譯標(biāo)準(zhǔn)為C++17

我本來想讓代碼兼容C11標(biāo)準(zhǔn),讓只會(huì)使用C語言的同學(xué)們也可以流暢閱讀。但無奈人太懶,半途而廢了……

為還不會(huì)C++或者C++不夠熟練的同學(xué)們附上參照表:

本文的表達(dá)C解釋
_Bool, bool_Bool布爾類型
using compare = bool (*)(const Type &a, const Type &b);typedef _Bool (*compare)(Type a, Type b);給函數(shù)指針取別名
auto無可替換自動(dòng)推導(dǎo)類型
auto i{123}int i=123初始化

Now, Let’s GO!

選擇排序

大多數(shù)地方都把冒泡排序作為第一種教授的排序算法。其實(shí)我認(rèn)為選擇排序才是新人最容易理解的排序算法。還記得我當(dāng)時(shí)的思路嗎?從還沒排序的數(shù)組內(nèi)找出最小值,然后放到第一個(gè),如次往復(fù)。選擇排序就是這樣的。我們只需要設(shè)置循環(huán)來實(shí)現(xiàn)就好了。

typedef bool (*compareFunction)(int, int); typedef unsigned int size_t;// 待會(huì)會(huì)以函數(shù)指針的方式將其傳入排序函數(shù)內(nèi) _Bool df_compare(int a, int b){ return a<b; }// 使用兩個(gè)指針來標(biāo)記數(shù)組內(nèi)要排序的部分 void Selection_sort(int *begin, int *end, compareFunction compare) {size_t size{end - begin};for (size_t i{0}; i < size - 1; ++i) {// i是已排序部分和未排序部分的分界for (size_t j{i + 1}; j < size; ++j) {// j不斷找出未排序部分的最小值if (compare(begin[j], begin[i])) {// 交換二值int tmp{begin[j]};begin[j] = begin[i];begin[i] = tmp;}}}}

上面這一個(gè)函數(shù)就演示了指針、數(shù)組的使用,還演示了使用函數(shù)指針一定程度上實(shí)現(xiàn)多態(tài)。基本語法還不熟悉的同學(xué)們得加油啦。

其實(shí)這個(gè)排序方法也可以在對(duì)鏈表使用,而且還不會(huì)有過多麻煩。實(shí)在覺得難,起碼也要把這一種算法記住。

插入排序

插入排序很像打撲克牌的時(shí)候,你抓了一手凌亂的撲克牌然后排序的時(shí)候的樣子。手里是已經(jīng)按照從大到小排號(hào)的撲克,然后你每次抓一張新的牌就把它插入到合適位置,然后你手里的牌就永遠(yuǎn)是有序的。

當(dāng)然,有經(jīng)驗(yàn)的對(duì)手可能會(huì)據(jù)此猜測你抽到了什么牌。。

有了撲克牌這個(gè)例子應(yīng)該好理解了,不過應(yīng)用到算法上面,就又可以難倒一大票萌新。為什么?因?yàn)閿?shù)組不是你手里的撲克牌,要往靜態(tài)數(shù)組內(nèi)插入一個(gè)值或者移除一個(gè)值,這樣的操作是有一點(diǎn)難度的!使用Python或者C++或者Java的同學(xué)也還別得意,就算別人幫你封裝好了這樣的操作,你還是需要自己處理一大票問題。看下面的代碼:

int arr[]={1, 2, 4, 3, 2, 6, 8}; int i=3, j=4; printf("%d\t%d", arr[i], arr[j]);

目前是會(huì)輸出3 2字樣,但你如果向前面插入一個(gè)什么數(shù),后面的內(nèi)容就都向后推了一格,所以你還要讓i,j同步變化。這樣不說做不到排序,起碼寫出來的代碼不會(huì)很優(yōu)雅了。

如果你使用鏈表,那確實(shí)沒那樣的問題了。不過我猜,來看這篇文章的人,還真就不一定都能寫得出鏈表。。。

還不會(huì)鏈表的同學(xué),可以來看看我寫的鏈表教程。

如果仍然像之前一樣,把數(shù)組分為已排序和未排序兩部分,我們可以這樣做:

void Insertion_sort(int *begin, int *end, compareFunction compare) {for (auto i{1}; i < end - begin; ++i) {// i把數(shù)組分割為了兩個(gè)部分int tmp{begin[i]};// 把begin[i]的內(nèi)容保存下來,之后插入到合適的地方去int j{i-1};while (j >= 0 && compare(tmp, begin[j])) {// 從后往前查找已排序部分里合適的位置來插入begin[j + 1] = begin[j];// 順便把后面的內(nèi)容向后移,空出來位置--j;}begin[j + 1] = tmp;// 注意j代表的意義}}

窮鬼沒錢做高端大氣的動(dòng)畫來演示原理,就…勉強(qiáng)看看吧。

當(dāng)然,如果你閑的蛋疼,也可以弄個(gè)遞歸版本出來(除了可以讓你熟悉一下遞歸沒有任何好處):

void Insertion_sort_Recurition(int *begin, int *end,compareFunction compare = df_compare) {if (--end - begin > 1) {Insertion_sort_Recurition(begin, end);}int *p = end - 1;int tmp = *end;while (p >= begin && compare(tmp, *p)) {*(p + 1) = *p;p--;}*(p + 1) = tmp;}

感覺這個(gè)遞歸和傻逼一樣。。。

冒泡排序

冒泡排序原理很相似,不過不同于選擇排序,它是讓待排序的值像泡泡一樣浮到它該去的地方。不多講。

void Bubble_sort(int *begin, int *end, compareFunction compare) {size_t size{end - begin};for (size_t i{0}; i < size - 1; ++i) {for (size_t j{size - 1}, k{j - 1}; j > i; --j, --k) {// The Bubbleif (compare(begin[j], begin[k])) { // Swap 2 values.int tmp{begin[j]};begin[j] = begin[k];begin[k] = tmp;}}}}

歸并排序

上面的排序方法雖然好理解,但如果數(shù)據(jù)一多起來,那就會(huì)很慢。你看看它們基本上都用上了兩層嵌套循環(huán),數(shù)據(jù)一增加,消耗時(shí)間就是平方級(jí)別增長。我們想要的,是那種力速雙A的強(qiáng)大算法。

歸并排序采用了分治法這種極為玄學(xué)的思想。它的思路倒是非常樸素:數(shù)組里面元素越少,排序起來不就越容易?

如果等待排序的數(shù)組有10個(gè)元素,它的想法是這樣的:

  • 把它對(duì)半分,不就只需要排序5個(gè)元素兩次了?

  • 再對(duì)半分,就只需要對(duì)2~3個(gè)元素排序四次。

  • 。。。。

  • 一直分到10份,只剩下一個(gè)元素,不就不用排序了?

    雖然現(xiàn)在這個(gè)想法看起來跟傻狍子一樣,但其實(shí)它說的沒錯(cuò),問題在于,如何把兩個(gè)已經(jīng)排序的數(shù)組再組合為一個(gè)?

    這就是歸并排序的核心,合并兩個(gè)已排序的數(shù)組

    //這里假定這兩個(gè)數(shù)組相鄰,mid左右都是已經(jīng)排序好的數(shù)組。 void merge(int *begin, int *mid, int *end,compareFunction compare = df_compare) {const auto n1{mid - begin}, n2{end - mid}; // 2 new arrays' length.// 把兩個(gè)數(shù)組內(nèi)容復(fù)制一次int l1[n1], l2[n2];for (int i{0}; i < n1; ++i) {l1[i] = begin[i];}for (int i{0}; i < n2; ++i) {l2[i] = mid[i];}// 歸并int i{0}, j{0}, k{0}; // i在l1內(nèi)運(yùn)動(dòng),j在l2內(nèi),k在l3內(nèi)// 兩個(gè)數(shù)組不一定等長,所以還不能一步到位for (; i < n1 && j < n2; ++k) {if (compare(l1[i], l2[j])) {begin[k] = l1[i];++i;} else {begin[k] = l2[j];++j;}}// 合并剩下的部分if (i == n1) {for (; j < n2; ++j, ++k) {begin[k] = l2[j];}}if (j == n2) {for (; i < n1; ++i, ++k) {begin[k] = l1[i];}}}

代碼有點(diǎn)多,但確實(shí)做到了。接下來我們只需要使用遞歸,來把數(shù)組無限分割為個(gè)體就好了:

void Merge_sort(int *begin, int *end, compareFunction compare = df_compare) {if (begin < end - 1) { // 當(dāng)begin和end已經(jīng)相鄰就停下來auto mid{begin + (end - begin) / 2};// 因?yàn)橹羔槻恢С窒嗉?#xff0c;所以出此下策Merge_sort(begin, mid, compare);Merge_sort(mid, end, compare);// 歸并!merge(begin, mid, end, compare);}}

**歸并排序是沒有嵌套循環(huán)的!**歸并兩個(gè)總共有n個(gè)元素的數(shù)組,只需要先復(fù)制n個(gè)元素一次,然后遍歷一次。隨著n增加,消耗時(shí)間t還只是an+b的樣子。不過因?yàn)橐葟淖钌⒌臓顟B(tài)下開始?xì)w并,如果還是10個(gè)元素的數(shù)組:

也就是先把1個(gè)元素的歸并5次,變成4個(gè)有2~3個(gè)元素的有序序列;

2~3個(gè)元素的歸并2次,變成兩個(gè)有5個(gè)元素的有序序列;

5個(gè)元素的歸并一次,變成一個(gè)有10個(gè)元素的有序序列;

完成。

數(shù)學(xué)好的同學(xué)就會(huì)發(fā)現(xiàn),這是個(gè)指數(shù)-對(duì)數(shù)模型,如果有n個(gè)元素,這樣的歸并需要執(zhí)行大概log2nlog_2nlog2?n次。演算如下:
假設(shè)數(shù)組內(nèi)含n個(gè)數(shù)據(jù),需要?dú)w并t次n=2t所以t=log2n消耗總時(shí)間f(n)=Anlog2n+B假設(shè)數(shù)組內(nèi)含n個(gè)數(shù)據(jù),需要?dú)w并t次\\ n=2^t\\ 所以t=log_2n\\ 消耗總時(shí)間f(n)=Anlog_2n+B 設(shè)數(shù)內(nèi)n個(gè)數(shù)據(jù)tn=2tt=log2?n時(shí)f(n)=Anlog2?n+B

當(dāng)然,這個(gè)公式只能描述個(gè)大概走勢,并非準(zhǔn)確(怎么可能執(zhí)行log210log_210log2?10次歸并呢?)。不過這已經(jīng)可以說明歸并排序比前幾種方式有明顯優(yōu)勢。如果數(shù)據(jù)輸入量足夠大,這幾種算法消耗時(shí)間的差距會(huì)非常恐怖。

快速排序

快速排序,顧名思義,就是一個(gè)字快。它和歸并排序一樣采用了分治法原理,消耗總時(shí)間也是nlog2nnlog_2nnlog2?n級(jí)別,但事實(shí)上它比歸并排序快一些,算法競賽卡時(shí)間選手的最愛。

找到一個(gè)某數(shù),盡量把大于某數(shù)的都扔去一邊,小于的扔去另一邊。然后就形成了大于這個(gè)數(shù)的都在右邊,小于這個(gè)數(shù)的都在左邊。再對(duì)兩左右部分再重復(fù)相同操作,一直到不能再分為兩部分為止。

這個(gè)某數(shù)是什么?答案是隨便。我一般會(huì)選擇數(shù)組正中間的那個(gè)數(shù)。這樣最終結(jié)果就剛好會(huì)以這個(gè)數(shù)為分界點(diǎn)。

但在我看來,快排比歸并還要難理解一些,使用的時(shí)候是依靠記憶多于依靠理解的。。

void Quik_sort(int *begin, int *end, compareFunction compare = df_compare) {if (begin < end-1) {// 遞歸終點(diǎn),到兩指針相鄰就說明只還剩一個(gè)元素,就不必繼續(xù)排序了。auto mid{begin + (end - begin) / 2};auto left{begin}, right{end - 1};while (left < right) {//尋找左邊的、大于某數(shù)的值while (compare(*left, *mid) && left < right) {left++;}while (!compare(*right, *mid) && left < right) {right--;}// 交換,同時(shí)再把left向右推一格,right向左推一格。沒有這一步就會(huì)陷入死循環(huán)。auto tmp{*left};*left++ = *right;*right-- = tmp;}// 遞歸排序Quik_sort(begin, mid);Quik_sort(mid, end);}} };

快速排序比歸并排序代碼量少不少,適合速用。但快速排序是不穩(wěn)定的算法。什么叫不穩(wěn)定?比如說我排序下面的結(jié)構(gòu)體數(shù)組,再用不同的排序打印結(jié)果:

struct Obj {int i;string label;};Obj objarr[] = {{2, "B"}, {2, "A"}, {4, "D"}, {3, "C"}, {3, "F"}};

但目前我們的函數(shù)只接受int類型數(shù)組,為了讓我們的算法可以適應(yīng)這樣的數(shù)據(jù)類型,我們需要先做一下泛型

template <class Type> using compare = bool (*)(const Type &a, const Type &b);template<class Type = int>void Quik_sort(Type *begin, Type *end, compare<Type> compare ) {if (begin < end-1) {auto mid{begin + (end - begin) / 2};auto left{begin}, right{end - 1};while (left < right) {while (compare(*left, *mid) && left < right) {left++;}while (!compare(*right, *mid) && left < right) {right--;}auto tmp{*left};*left++ = *right;*right-- = tmp;}Quik_sort<Type>(begin, mid, compare);Quik_sort<Type>(mid, end, compare);}}

排序打印出結(jié)果:

Quik_sort<Obj>(objarr, objarr + 5,[](const Obj &a, const Obj &b) -> bool { return a.i < b.i; });// 這是個(gè)lambda表達(dá)式for (auto &i : objarr) {cout<<i.label<<" ";}

輸出:B A F C D。你看,都具有一樣的索引的情況下,C,F的順序被顛倒了,但B,A并沒有。所以一旦對(duì)這樣的數(shù)組使用快速排序,結(jié)果將會(huì)是無法預(yù)料的!!

欲戴王冠,必承其重。正因如此,才更有必要根據(jù)實(shí)際需求選擇不同的排序算法。如果要求高穩(wěn)定性,可以使用歸并排序代替。

畢竟,算法也是有極限的嘛~~

后記

排序算法非常多,而且各有各的特色。這里只介紹了簡單一些的排序算法。許多算法利用了二叉樹這樣的數(shù)據(jù)結(jié)構(gòu),還有的則是幾種算法的復(fù)合或者改進(jìn)來達(dá)到特殊目的(如改進(jìn)插入排序的希爾排序)。這些算法我會(huì)在排序篇(二)里面詳細(xì)介紹的!所以你若覺得自己有時(shí)間等我鴿的,不妨點(diǎn)個(gè)關(guān)注,沒準(zhǔn)下星期我就更了呢……

總結(jié)

以上是生活随笔為你收集整理的算法入门篇:排序算法(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。