1. 程式人生 > >單鏈表的冒泡,快排,選擇,插入,歸併等多圖詳解

單鏈表的冒泡,快排,選擇,插入,歸併等多圖詳解

上節介紹了連結串列的基本操作[史上最全單鏈表的增刪改查反轉等操作彙總以及5種排序演算法(C語言)]() 這節介紹連結串列的5種排序演算法。 @[TOC] ### 0.穩定排序和原地排序的定義 **穩定排序**:   假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的**相對次序保持不變**,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。像**氣泡排序**,**插入排序**,**基數排序**,**歸併排序**等都是穩定排序 **原地排序**:   基本上**不需要額外輔助的的空間,允許少量額外的輔助變數**進行的排序。就是在原來的排序陣列中比較和交換的排序。像**選擇排序**,**插入排序**,**希爾排序**,**快速排序**,**堆排序**等都會有一項比較且交換操作(swap(i,j))的邏輯在其中,因此他們都是屬於原地(原址、就地)排序,而合併排序,計數排序,基數排序等不是原地排序 ### 1.氣泡排序 **基本思想:**   把第一個元素與第二個元素比較,如果第一個比第二個大,則交換他們的位置。接著繼續比較第二個與第三個元素,如果第二個比第三個大,則交換他們的位置....   我們對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣一趟比較交換下來之後,排在最右的元素就會 是最大的數。除去最右的元素,我們對剩餘的元素做同樣的工作,如此重複下去,直到排序完成。 **具體步驟**:   1.比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。   2.對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。   3.針對所有的元素重複以上的步驟,除了最後一個。   4.持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。 **時間複雜度**:O(N2) **空間複雜度**:O(1) **穩定排序**:是 **原地排序**:是 ![在這裡插入圖片描述](https://img2020.cnblogs.com/other/1421380/202012/1421380-20201216145417704-148584414.gif) ```c Node *BubbleSort(Node *phead) { Node * p = phead; Node * q = phead->next; /*有幾個資料就-1;比如x 個idata>q->data) { /*頭結點和下一節點的交換,要特殊處理,更新新的頭head*/ if (p == phead) { p->next = q->next; q->next = p; head = q; phead = q; /*這裡切記要把p,q換回來,正常的話q應該在p的前面,進行的是p,q的比較 *但是經過指標域交換之後就變成q,p.再次進行下一次比較時, *就會變成q,p的資料域比較。假如原本p->data > q->data,則進行交換。變成q->data和p->data比較, *不會進行交換,所以排序就會錯誤。有興趣的可以除錯下。 */ Node*temp=p; p=q; q=temp; } /*處理中間過程,其他資料的交換情況,要尋找前驅節點if (p != phead)*/ else { /*p,q的那個在前,那個在後。指標域的連線畫圖好理解一點*/ if (p->next == q) { /*尋找p的前驅節點*/ Node *ppre = FindPreNode(p); /*將p的下一個指向q的下一個*/ p->next = q->next; /*此時q為頭結點,讓q的下一個指向p,連線起來*/ q->next = p; /*將原來p的前驅節點指向現在的q,現在的q為頭結點*/ ppre->next = q; Node*temp=p; p=q; q=temp; } else if (q->next == p) { Node *qpre = FindPreNode(q); q->next = p->next; p->next = q; qpre->next = p; Node*temp=p; p=q; q=temp; } } } /*地址移動*/ p = p->next; q = q->next; } /*進行完一輪比較後,從頭開始進行第二輪*/ p = phead; q = phead->next; } head = phead; return head; } ``` ### 2.快速排序 **基本思想**   我們從陣列中選擇一個元素,我們把這個元素稱之為中軸元素吧,然後把陣列中所有小於中軸元素的元素放在其左邊, 所有大於或等於中軸元素的元素放在其右邊,顯然,此時中軸元素所處的位置的是有序的。也就是說,我們無需再移動中軸 元素的位置。   從中軸元素那裡開始把大的陣列切割成兩個小的陣列(兩個陣列都不包含中軸元素),接著我們通過遞迴的方式,讓中軸元素 左邊的陣列和右邊的陣列也重複同樣的操作,直到陣列的大小為1,此時每個元素都處於有序的位置。 **具體步驟**:   1.從數列中挑出一個元素,稱為 "基準"(pivot);   2.重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作;   3.遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序; **時間複雜度**:O(NlogN) **空間複雜度**:O(logN) **穩定排序**:否 **原地排序**:是 ![在這裡插入圖片描述](https://img2020.cnblogs.com/other/1421380/202012/1421380-20201216145418425-1111565855.gif) ```c int *QuickSort(Node* pBegin, Node* pEnd) { if(pBegin == NULL || pEnd == NULL || pBegin == pEnd) return 0; //定義兩個指標 Node* p1 = pBegin; Node* p2 = pBegin->next; int pivot = pBegin->data; //每次只比較小的,把小的放在前面。經過一輪比較後,被分成左右兩部分。其中p1指向中值處,pbegin為pivot。 while(p2 != NULL)/* && p2 != pEnd->next */ { if(p2->data < pivot) { p1 = p1->next; if(p1 != p2) { SwapData(&p1->data, &p2->data); } } p2 = p2->next; } /*此時pivot並不在中間,我們要把他放到中間,以他為基準,把資料分為左右兩邊*/ SwapData(&p1->data, &pBegin->data); //此時p1是中值節點 //if(p1->data >pBegin->data) QuickSort(pBegin, p1); //if(p1->data < pEnd->data) QuickSort(p1->next, pEnd); } ``` ### 3.插入排序 **基本思想**:每一步將一個待排序的記錄,插入到前面已經排好序的有序序列中去,直到插完所有元素為止。 **具體步驟**:   1.將待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列;   2.取出下一個元素,在已經排序的元素序列中從後向前掃描;   3.如果該元素(已排序)大於新元素,將該元素移到下一位置;   4.重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;   5.將新元素插入到該位置後;   6.重複步驟2~5。 **時間複雜度**:O(N2) **空間複雜度**:O(1) **穩定排序**:是 **原地排序**:是 ![在這裡插入圖片描述](https://img2020.cnblogs.com/other/1421380/202012/1421380-20201216145419390-419083433.gif) ```c /*不好理解可以除錯下看下具體過程*/ Node *InsertSort(Node *phead) { /*為原連結串列剩下用於直接插入排序的節點頭指標*/ Node *unsort; /*臨時指標變數:插入節點*/ Node *t; /*臨時指標變數*/ Node *p; /*臨時指標變數*/ Node *sort; /*原連結串列剩下用於直接插入排序的節點連結串列:可根據圖12來理解。*/ unsort = phead->next; /*只含有一個節點的連結串列的有序連結串列:可根據圖11來理解。*/ head->next = NULL; /*遍歷剩下無序的連結串列*/ while (unsort != NULL) { /*注意:這裡for語句就是體現直接插入排序思想的地方*/ /*無序節點在有序連結串列中找插入的位置*/ /*跳出for迴圈的條件: *1.sort為空,此時,sort->data < t->data,p存下位置,應該放在有序連結串列的後面 *2.sort->data > t->data ,跳出迴圈時,t->data放在有序連結串列sort的前面 *3.sort為空 sort->data > t->data,也插入在sort前面的位置 */ /*q為有序連結串列*/ for (t = unsort, sort = phead; ((sort != NULL) && (sort->data < t->data)); p = sort, sort = sort->next); /*退出for迴圈,就是找到了插入的位置插入位置要麼在前面,要麼在後面*/ /*注意:按道理來說,這句話可以放到下面註釋了的那個位置也應該對的,但是就是不能。原因:你若理解了上面的第3條,就知道了。*/ /*無序連結串列中的第一個節點離開,以便它插入到有序連結串列中。*/ unsort = unsort->next; /*插在第一個節點之前*/ /*sort->data > t->data*/ /*sort為空 sort->data > t->data*/ if (sort == phead) { /*整個無序連結串列給phead*/ phead = t; } /*p是sort的前驅,這樣說不太確切,當sort到最後時,for裡面有個sort = sort->next, *就會把sort置空,所以要用p暫存上一次sort的值。而且執行判斷sort->data < t->data時,用的也是上一次的sort */ /*sort後面插入*/ /*sort遍歷到了最後,此時,sort->data < t->data,sort和p都為最後一個元素。*/ else { p->next = t; } /*if處理之後,t為無序連結串列,因為要在phead前插入。這裡先把t賦值給phead,再把t的next指向sort, *就完成了在sort之前插入小的元素,很巧妙的一種方法 *else處理完之後,sort存放的是sort的下一次,真正的sort存放在p中。不滿足條件跳出迴圈時,判斷的是下一次的sort, 但是我們要用的插入的位置為上一次的sort,所以要記錄下sort上一次的位置 */ /*完成插入動作*/ /*當sort遍歷完成為空時,t->next就是斷開後面的元素(sort為空)*/ /*當sort不為空時,sort->data > t->data,sort存放的元素比t要大,放在後面,t->next就是再連結起來sort*/ t->next = sort; /*unsort = unsort->next;*/ } head = phead; return phead; } ``` ### 4.選擇排序 **基本思想**:首先,找到陣列中最小的那個元素,其次,將它和陣列的第一個元素交換位置(如果第一個元素就是最小元素那麼它就和自己交換)。其次,在剩下的元素中找到最小的元素,將它與陣列的第二個元素交換位置。如此往復,直到將整個陣列排序。這種方法我們稱之為選擇排序。 **具體步驟**:   1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。   2.再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。   3.重複第二步,直到所有元素均排序完畢。 **時間複雜度**:O(N2) **空間複雜度**:O(1) **穩定排序**:否 **原地排序**:是 ![在這裡插入圖片描述](https://img2020.cnblogs.com/other/1421380/202012/1421380-20201216145420469-1757574217.gif) ```c Node* SelectSort(Node* phead) { Node *temp; Node *temphead = phead; /*將第一次的最大值設定為頭結點*/ int max = phead->data; /*交換變數*/ for(int i = 0;i
data; while (temphead->next !=NULL) { /*尋找最大值*/ if(max < temphead->next->data) { max = temphead->next->data; } /*移動指標位置*/ temphead = temphead->next; } /*找到最大值的位置*/ temp = FindList(max); /*判斷最大值是否和頭節點相同*/ if(phead != temp) { SwapNode(phead,temp); } /*更新下一次遍歷的頭結點*/ temphead = temp->next; phead = temphead; } } ```   SwapNode相關程式碼如下。當時考慮只需要理解排序思想就好了,就沒有把這個函式的程式碼放出來。這個程式碼寫的太長太複雜了,有時間我會重新精簡下。(說實話,我都快忘了怎麼寫的了) ```c /*交換相鄰節點*/ void Swanext(Node *p,Node *q) { /*中間相鄰節點*/ if ((p != head)&&(q != head)) { // /*p為前一個節點,q的前驅為p*/ // /*尋找p的前驅結點*/ // Node *ppre = FindPreNode(p); // Node *temp; // /*暫存p節點的後繼結點,指向q*/ // temp = p->next; // /*將q節點的後繼節點賦值給p的後繼結點,即將p節點放到了q位置(此時q的前驅節點的next指標還指向的是q)*/ // p->next = q->next; // /*將p節點給q的next,即將完成了q與p的重新連線*/ // q->next =p; // /*找到原來p的前驅節點,指向q,即完成了原來p的前驅結點和q節點的連線*/ // ppre->next =q; if (p->next == q) { Node *ppre = FindPreNode(p); p->next = q->next; q->next = p; ppre->next = q; // PrintList(head); } else if (q->next == p) { Node *qpre = FindPreNode(q); q->next = p->next; p->next = q; qpre->next = p; } } /*頭結點相鄰的交換*/ else { if(p == head) { p->next = q->next; q->next = p; head = q; } else { q->next = p->next; p->next = q; head = p; } } } /*交換頭結點和任意節點(除尾節點外)*/ void SwapHeadAnother(Node *tmphead,Node *p) { /*尋找p的前驅節點*/ Node *ppre = FindPreNode(p); Node *temp; if(p!=tmphead->next) { /*暫存p節點*/ temp = p->next; /*將tmphead節點的後繼節點賦值給p的後繼結點,即將tmphead節點放到了p位置(此時p的前驅節點的next指標還未斷開)*/ p->next = tmphead->next; /*將p的後繼結點賦值給tmphead的後繼結點,同時連線p的前驅和tmphead*/ tmphead->next = temp; ppre->next =tmphead; /*新的頭結點返回給全域性head*/ head = p; } else { /*頭結點和下一節點*/ tmphead->next = p->next; p->next = tmphead; head = p; } } /*交換尾結點和任意節點(除頭節點外)*/ void SwapEndAnother(Node *tmpend,Node *p) { /*尋找p的前驅節點*/ Node *ppre = FindPreNode(p); Node *endpre = FindPreNode(tmpend); Node *temp; if((tmpend==end)&&(p!=tmpend)) { /*暫存p節點*/ temp = p->next; /*將tmpend節點的後繼節點賦值給p的後繼結點,即將tmpend節點放到了p位置(此時p的前驅節點的next指標還未斷開)*/ p->next = tmpend->next; endpre->next = p; /*將p的後繼結點賦值給tmpend的後繼結點,同時連線p的前驅和tmpend(斷開了之前的連線)*/ tmpend->next = temp; ppre->next =tmpend; /*新的頭結點返回給全域性head*/ end = p; } else { p->next = tmpend->next; tmpend->next = p; end = p; } } /*交換頭結點和尾節點*/ void SwapHeadEnd(Node *tmphead,Node *tmpend) { /*尋找tmpend的前驅節點*/ Node *endpre = FindPreNode(tmpend); Node *temp; /*暫存tmpend節點*/ temp = tmpend->next; /*將tmphead節點的後繼節點賦值給tmpend的後繼結點,即將tmpend節點放到了tmphead位置(此時tmpend的前驅節點的next指標還未斷開)*/ tmpend->next = tmphead->next; /*將p的後繼結點賦值給tmpend的後繼結點,同時連線p的前驅和tmpend(斷開了之前的連線)*/ tmphead->next = temp; endpre->next =tmphead; /*新的頭結點返回給全域性head*/ head = tmpend; end = tmphead; // PrintList(tmpend); } void SwapRandom(Node *p,Node *q) { /*除了首尾節點,中間不相鄰的兩個節點*/ if((p->next != q)||(q->next != p)) { /*尋找前驅結點*/ Node *ppre = FindPreNode(p); Node *qpre = FindPreNode(q); /*藉助一箇中間節點傳遞資料域*/ Node *temp; temp = p->next; /*交換p和q*/ /*2、p的新後繼結點要變成q的原後繼結點*/ p->next = q->next; /*3、q的原前趨結點(qpre)的新後繼結點要變成p*/ qpre->next = p; /*4、q的新後繼結點要變成p的原後繼結點(p->next)*/ q->next = temp; /*1、p的原前趨結點(ppre)的新後繼結點要變成q*/ ppre->next = q; } /*中間相鄰節點的處理*/ else if (p->next == q) { Node *ppre = FindPreNode(p); p->next = q->next; q->next = p; ppre->next = q; } else if (q->next == p) { Node *qpre = FindPreNode(q); q->next = p->next; p->next = q; qpre->next = p; } } /*交換任意兩個節點*/ void SwapNode(Node*p, Node*q) { // if(LengthList(head)<2) // printf("Can not swap!The Length of list is:%d\r\n ",LengthList(head)); /*檢查是否是頭尾節點*/ /*對於頭尾節點有四種情況 *1.p頭節點和q為中間節點 *2.p尾節點和q為中間節點 *3.q頭節點和p為中間節點 *4.q尾節點和p為中間節點 *5.p頭結點和q尾節點 *6.q頭結點和p尾節點 *7.其他中間交換的情況 */ /*2.兩個節點是否相鄰 除去頭結點和下一節點相鄰的情況,放在headanother處理*/ if((p->
next == q)&&(p !=head)&&(q !=head)) Swanext(p,q); else if((q->next == p)&&(p !=head)&&(q !=head)) Swanext(q,p); /*1.p頭節點和q為中間節點*/ else if((p == head)&&(q != end)) SwapHeadAnother(p,q); /*2.p尾節點和q為中間節點*/ else if ((p == end)&&(q != head)) SwapEndAnother(p,q); /*3.q頭節點和p為中間節點*/ else if((q == head)&&(p != end)) SwapHeadAnother(q,p); /*4.q尾節點和p為中間節點*/ else if((q == end)&&(p != head)) SwapEndAnother(q,p); /*5.p頭結點和q尾節點*/ else if((p == head)&&(q == end)) SwapHeadEnd(p,q); /*6.q頭結點和p尾節點*/ else if((q == head)&&(p == end)) SwapHeadEnd(q,p); /*7.其他中間交換的情況*/ else SwapRandom(p,q); } ``` ### 5.歸併排序 **基本思想**:歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。 作為一種典型的分而治之思想的演算法應用,歸併排序的實現由兩種方法: 自上而下的遞迴(所有遞迴的方法都可以用迭代重寫,所以就有了第 2 種方法); 自下而上的迭代; **具體步驟**:   1.申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列;   2.設定兩個指標,最初位置分別為兩個已經排序序列的起始位置;   3.比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置;   4.重複步驟 3 直到某一指標達到序列尾;   5.將另一序列剩下的所有元素直接複製到合併序列尾。 **時間複雜度**:O(NlogN) **空間複雜度**:O(N) **穩定排序**:是 **原地排序**:否 ![在這裡插入圖片描述](https://img2020.cnblogs.com/other/1421380/202012/1421380-20201216145421211-935778889.gif) ```c /*獲取連結串列中間元素*/ Node *getMiddleNode(Node *pList) { if (pList == NULL) { return NULL; } Node *pfast = pList->
next; Node *pslow = pList; while (pfast != NULL) { pfast = pfast->next; if (pfast != NULL) { pfast = pfast->next; pslow = pslow->next; } } return pslow; } /*合併有序連結串列,合併之後升序排列*/ Node *MergeList(Node *p1, Node *p2) { if (NULL == p1) { return p2; } if (NULL == p2) { return p1; } Node *pLinkA = p1; Node *pLinkB = p2; Node *pTemp = NULL; /*較小的為頭結點,pTemp存下頭結點*/ if (pLinkA->data <= pLinkB->data) { pTemp = pLinkA; pLinkA = pLinkA->next; } else { pTemp = pLinkB; pLinkB = pLinkB->next; } /*初始化頭結點,即頭結點指向不為空的結點*/ Node *pHead = pTemp; while (pLinkA && pLinkB) { /*合併先放小的元素*/ if (pLinkA->data <= pLinkB->data) { pTemp->next = pLinkA; /*儲存下上一次節點。如果下一次為NULL,儲存的上一次的節點就是連結串列最後一個元素*/ pTemp = pLinkA; pLinkA = pLinkA->next; } else { pTemp->next = pLinkB; pTemp = pLinkB; pLinkB = pLinkB->next; } } /*跳出迴圈時,有一個為空。把不為空的剩下的部分插入連結串列中*/ pTemp->next = pLinkA ? pLinkA:pLinkB; head = pHead; return pHead; } Node *MergeSort(Node *pList) { if (pList == NULL || pList->next == NULL) { return pList; } /*獲取中間結點*/ Node *pMiddle = getMiddleNode(pList); /*連結串列前半部分,包括中間結點*/ Node *pBegin = pList; /*連結串列後半部分*/ Node *pEnd = pMiddle->next; /*必須賦值為空 相當於斷開操作。pBegin--pMiddle pEnd---最後 */ pMiddle->next = NULL; /*排序前半部分資料,只有一個元素的時候停止,即有序*/ pBegin = MergeSort(pBegin); /*排序後半部分資料 遞迴理解可以參考PrintListRecursion;*/ pEnd = MergeSort(pEnd); /*合併有序連結串列*/ return MergeList(pBegin, pEnd); } ``` **  大家的鼓勵是我繼續創作的動力,如果覺得寫的不錯,歡迎關注,點贊,收藏,轉發,謝謝!** **以上程式碼均為測試後的程式碼。如有錯誤和不妥的地方,歡迎指出。** **圖片來自網路,侵權請聯絡我刪除** **如遇到排版錯亂的問題,可以通過以下連結訪問我的CSDN。** **CSDN:[CSDN搜尋“嵌入式與Linux那些事”](https://blog.csdn.net/qq_16933601?spm=1000.2115.3001.5113)** **歡迎歡迎關注我的公眾號:嵌入式與Linux那些事,領取秋招筆試面試大禮包(華為小米等大廠面經,嵌入式知識點總結,筆試題目,簡歷模版等)和2000G學習資料。** ![](https://img2020.cnblogs.com/other/1421380/202012/1421380-20201216145422238-674121