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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

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

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

引子

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

第一次見到排序問題,我內心是不屑的,

“這?不是張口就來?”

然后我就貢獻了一整個下午的時間在一個簡單的排序上面。

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

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

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

約定:

本文代碼都是在Windows10下,使用64位GCC編譯器編譯,編譯標準為C++17

我本來想讓代碼兼容C11標準,讓只會使用C語言的同學們也可以流暢閱讀。但無奈人太懶,半途而廢了……

為還不會C++或者C++不夠熟練的同學們附上參照表:

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

Now, Let’s GO!

選擇排序

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

typedef bool (*compareFunction)(int, int); typedef unsigned int size_t;// 待會會以函數(shù)指針的方式將其傳入排序函數(shù)內 _Bool df_compare(int a, int b){ return a<b; }// 使用兩個指針來標記數(shù)組內要排序的部分 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;}}}}

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

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

插入排序

插入排序很像打撲克牌的時候,你抓了一手凌亂的撲克牌然后排序的時候的樣子。手里是已經按照從大到小排號的撲克,然后你每次抓一張新的牌就把它插入到合適位置,然后你手里的牌就永遠是有序的。

當然,有經驗的對手可能會據(jù)此猜測你抽到了什么牌。。

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

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

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

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

還不會鏈表的同學,可以來看看我寫的鏈表教程。

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

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

窮鬼沒錢做高端大氣的動畫來演示原理,就…勉強看看吧。

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

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;}

感覺這個遞歸和傻逼一樣。。。

冒泡排序

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

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án),數(shù)據(jù)一增加,消耗時間就是平方級別增長。我們想要的,是那種力速雙A的強大算法。

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

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

  • 把它對半分,不就只需要排序5個元素兩次了?

  • 再對半分,就只需要對2~3個元素排序四次。

  • 。。。。

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

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

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

    //這里假定這兩個數(shù)組相鄰,mid左右都是已經排序好的數(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.// 把兩個數(shù)組內容復制一次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內運動,j在l2內,k在l3內// 兩個數(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];}}}

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

void Merge_sort(int *begin, int *end, compareFunction compare = df_compare) {if (begin < end - 1) { // 當begin和end已經相鄰就停下來auto mid{begin + (end - begin) / 2};// 因為指針不支持相加,所以出此下策Merge_sort(begin, mid, compare);Merge_sort(mid, end, compare);// 歸并!merge(begin, mid, end, compare);}}

**歸并排序是沒有嵌套循環(huán)的!**歸并兩個總共有n個元素的數(shù)組,只需要先復制n個元素一次,然后遍歷一次。隨著n增加,消耗時間t還只是an+b的樣子。不過因為要先從最散的狀態(tài)下開始歸并,如果還是10個元素的數(shù)組:

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

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

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

完成。

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

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

快速排序

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

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

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

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

void Quik_sort(int *begin, int *end, compareFunction compare = df_compare) {if (begin < end-1) {// 遞歸終點,到兩指針相鄰就說明只還剩一個元素,就不必繼續(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--;}// 交換,同時再把left向右推一格,right向左推一格。沒有這一步就會陷入死循環(huán)。auto tmp{*left};*left++ = *right;*right-- = tmp;}// 遞歸排序Quik_sort(begin, mid);Quik_sort(mid, end);}} };

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

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

但目前我們的函數(shù)只接受int類型數(shù)組,為了讓我們的算法可以適應這樣的數(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);}}

排序打印出結果:

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

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

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

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

后記

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

總結

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

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