單鏈表的冒泡,快排,選擇,插入,歸併5種排序算法詳解(多圖+代碼實現)

工科生一枚,熱衷於底層技術開發,有強烈的好奇心,感興趣內容包括單片機,嵌入式Linux,Uboot等,歡迎學習交流!
愛好跑步,打籃球,睡覺。
歡迎加我QQ1500836631(備註CSDN),一起學習交流問題,分享各種學習資料,電子書籍,學習視頻等。

上節介紹了鏈表的基本操作史上最全單鏈表的增刪改查反轉等操作彙總以及5種排序算法(C語言)
這節介紹鏈表的5種排序算法。

0.穩定排序和原地排序的定義

穩定排序
  假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。像冒泡排序插入排序基數排序歸併排序等都是穩定排序
原地排序
  基本上不需要額外輔助的的空間,允許少量額外的輔助變量進行的排序。就是在原來的排序數組中比較和交換的排序。像選擇排序插入排序希爾排序快速排序堆排序等都會有一項比較且交換操作(swap(i,j))的邏輯在其中,因此他們都是屬於原地(原址、就地)排序,而合併排序,計數排序,基數排序等不是原地排序

1.冒泡排序

基本思想:
  把第一個元素與第二個元素比較,如果第一個比第二個大,則交換他們的位置。接着繼續比較第二個與第三個元素,如果第二個比第三個大,則交換他們的位置…
  我們對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣一趟比較交換下來之後,排在最右的元素就會
是最大的數。除去最右的元素,我們對剩餘的元素做同樣的工作,如此重複下去,直到排序完成。
具體步驟
  1.比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2.對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3.針對所有的元素重複以上的步驟,除了最後一個。
  4.持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
時間複雜度:O(N2)
空間複雜度:O(1)
穩定排序:是
原地排序:是
在這裏插入圖片描述


Node *BubbleSort(Node *phead)
{

	Node * p = phead;
	Node * q = phead->next;
	/*有幾個數據就-1;比如x 個i<x-1*/
	for(int i=0;i<5;i++)
	{ 
		while((q!=NULL)&&(p!=NULL))
		{ 
			if(p->data>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)
穩定排序:否
原地排序:是

在這裏插入圖片描述


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)
穩定排序:是
原地排序:是

在這裏插入圖片描述

/*不好理解可以調試下看下具體過程*/
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)
穩定排序:否
原地排序:是
在這裏插入圖片描述

Node* SelectSort(Node* phead)                                                
{                                                                                                             
	Node *temp;
	Node *temphead = phead;
	/*將第一次的最大值設置爲頭結點*/
	int max = phead->data;
	/*交換變量*/
	 for(int i = 0;i<LengthList(phead);i++)
	 {
		 /*每次遍歷開始前都要把最大值設置爲頭結點*/
		  max = phead->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;
	 }

} 

5.歸併排序

基本思想:歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
作爲一種典型的分而治之思想的算法應用,歸併排序的實現由兩種方法:
自上而下的遞歸(所有遞歸的方法都可以用迭代重寫,所以就有了第 2 種方法);
自下而上的迭代;
具體步驟
  1.申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;
  2.設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;
  3.比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;
  4.重複步驟 3 直到某一指針達到序列尾;
  5.將另一序列剩下的所有元素直接複製到合併序列尾。
時間複雜度:O(NlogN)
空間複雜度:O(N)
穩定排序:是
原地排序:否
在這裏插入圖片描述

/*獲取鏈表中間元素*/
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); 
}

以上代碼均爲測試後的代碼。如有錯誤和不妥的地方,歡迎指出。
圖片來自網絡,侵權請聯繫我刪除

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章