【數據結構】快速排序非遞歸算法及其改進

在學數據結構中排序這一章節的時候,有一道有關快速排序的作業題描述如下:

按下述要求編寫快速排序的非遞歸算法:
定義一個棧(或隊列),把整個序列的上、下界入棧(或隊列)。當棧(或隊列)非空時進行如下操作:
(1)取棧頂(或隊頭)元素作為序列的上、下界,在區間的頭部、中間、尾部取關鍵字居中的元素作為中樞元素,進行一趟快速排序;
(2)在一趟排序過程中,如果子表已有序(沒有發生元素交換),則該子序列排序結束,否則先對劃分出的長度較短的子表進行排序,且將另一子表的上、下界入棧(或隊列)保存;
(3)若待排序區間中數據元素數小于等于3,則不再進行分割,而是直接進行比較完成排序。
完成算法實現后,用大數據量進行測試,同原有快速排序算法在時間上進行對比分析。

這道題本質上是在優化快速排序。

1.用棧代替遞歸

函數的遞歸本身就利用了堆棧,用棧改寫遞歸算法為非遞歸是常用思路。我們可以將序列的上下界入棧,排序時再取棧頂元素并出棧,本題中要求先對劃分出的長度較短的子表進行排序,那么我們就先入棧較長的子表,將較短的子表后入棧以便下次循環中優先出棧。(部分)代碼如下:

SeqStack<int> s;
while( !s.isEmpty())  //棧非空且未排好序{//得到某分區的左右邊界int r;s.getTop(r);s.pop();int l;s.getTop(l);s.pop();boundary = Part(a, l, r, Flag);  //boundary為樞軸元素所在位置if( boundary - l < r - boundary)    ///如果左分區比右分區短{if( boundary + 1 < r ) //判斷右分區是否存在{s.push(boundary + 1);s.push(r);}if( boundary - 1 > l ) //判斷左分區是否存在{//將左分區端點后入棧以便優先排序s.push(l);s.push(boundary - 1);}}else                                       //如果右分區比左分區短{if( boundary - 1 > l ) //判斷左分區是否存在{s.push(l);s.push(boundary - 1);}if( boundary + 1 < r ) //判斷右分區是否存在{///將右分區端點后入棧以便優先排序s.push(boundary + 1);s.push(r);}}}

2.中樞元素的取法

一般書本上的快速排序是取第一個待排序的元素作為中樞元素,而為了優化快速排序算法,如果能夠盡量將中樞元素取在整個待排序數組的中位數附近,那么兩個子表的長度就會接近,這樣一來就減少了時間復雜度,優化了快速排序算法,本題中采取的方法是:在區間的頭部、中間、尾部取關鍵字居中的元素作為中樞元素。其實我們也可以平均取五個點、七個點來找居中的元素。(頭中尾取中值)代碼如下:

int Mid(int first, int mid, int last)
//求頭,中,尾中關鍵字居中的元素
{if ((first>=mid&&first<=last)||(first<=mid&&first>=last))return first;else if ((mid>=first&&mid<=last)||(mid<=first&&mid>=last))return mid;elsereturn last;
}

注意,要在Partition函數中交換首元素和中樞元素的位置:

int pivot = Mid(low, (low+high)/2, high);//選擇區間的頭部、中間、尾部取關鍵字居中的元素作為中樞元素Swap(elem[pivot], elem[low]);  //保持代碼的統一性

3.在一趟排序中,如果子表已有序,則該子序列排序結束

這一點便于理解,代碼實現上可以定義一個bool類型的標記,來判斷Partition中是否發生過元素交換,如果沒有交換bool賦true。

4.待排區間中元素數<=3,則不再進行分割,則直接比較排序

快速排序是適用于大數據量的排序方式,在數據量比較小的時候,使用插入排序或者選擇排序較好,所以很多實用的排序算法使用快排+插排的方式排序。本題中要求:若待排序區間中數據元素數小于等于3,則不再進行分割,而是直接進行比較完成排序,代碼如下:

void JustSort(int a[], int left, int right)
//數組元素小于等于3時的直接比較排序
{if(right-left==1)         //序列只有兩個元素時{if(a[left] > a[right]){Swap(a[left], a[right]);}}else                      //三個元素時{if(a[left] > a[left+1])Swap(a[left], a[left+1]);if(a[left+1] > a[right])Swap(a[left+1], a[right]);if(a[left] > a[left+1])Swap(a[left], a[left+1]);}
}

應用以上優化方法后,用50000個隨機元素的數組進行排序,與書本上的遞歸快排進行時間比較,如下:
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
可見有一定的優化效果。

全部代碼如下:

#ifndef __ExQUICKSORT_H__
#define __ExQUICKSORT_H__
#include "SeqStack.h"int Mid(int first, int mid, int last)
///求頭,中,尾中關鍵字居中的元素
{if ((first>=mid&&first<=last)||(first<=mid&&first>=last))return first;else if ((mid>=first&&mid<=last)||(mid<=first&&mid>=last))return mid;elsereturn last;
}int Part(int elem[], int low, int high, int &flag)  ///參數flag用以判斷是否已經有序,以便結束排序
//原快速排序算法中的劃分部分,寫成函數方便循環調用
{int pivot = Mid(low, (low+high)/2, high);//選擇區間的頭部、中間、尾部取關鍵字居中的元素作為中樞元素Swap(elem[pivot], elem[low]);   /// 交換樞軸元素和首元素的位置,保持代碼的統一性int e = elem[low];				// 取樞軸元素int i = low, j = high;while (i < j){while (i < j && elem[j] >= e)	// 使j右邊的元素不小于樞軸元素j--;if (i < j){elem[i++] = elem[j];flag = 1;}while (i < j && elem[i] <= e)	// 使i左邊的元素不大于樞軸元素i++;if (i < j){elem[j--] = elem[i];flag = 1;}}elem[i] = e;return i;                           //返回樞軸元素位置
}void JustSort(int a[], int left, int right)
///數組元素小于等于3時的直接比較排序
{if(right-left==1)         //序列只有兩個元素時{if(a[left] > a[right]){Swap(a[left], a[right]);}}else                      //三個元素時{if(a[left] > a[left+1])Swap(a[left], a[left+1]);if(a[left+1] > a[right])Swap(a[left+1], a[right]);if(a[left] > a[left+1])Swap(a[left], a[left+1]);}
}template <class ElemType>
void ExQuickSort(ElemType a[], int left, int right)
// 操作結果:對數組elem[low .. high]中的元素進行快速排序
{int Flag = 0;             ///標記,用來判斷是否已經排好序(未發生過交換)SeqStack<int> s;if( left<right ){if(right-left < 3)   //如果序列小于等于3個元素{JustSort(a, left, right);return ;}int boundary = Part(a, left, right, Flag);   //劃分后的中樞所在位置if(Flag == 0)return ;if( boundary - left < right - boundary)    ///如果左分區比右分區短{if( boundary + 1 < right ) //判斷右分區是否存在{s.push(boundary + 1);s.push(right);}if( boundary - 1 > left ) //判斷左分區是否存在{///將左分區端點后入棧以便優先排序s.push(left);s.push(boundary - 1);}}else                                       ///如果右分區比左分區短{if( boundary - 1 > left ) //判斷左分區是否存在{s.push(left);s.push(boundary - 1);}if( boundary + 1 < right ) //判斷右分區是否存在{///將右分區端點后入棧以便優先排序s.push(boundary + 1);s.push(right);}}while( !s.isEmpty())  //棧非空且未排好序{//得到某分區的左右邊界int r;s.getTop(r);s.pop();int l;s.getTop(l);s.pop();boundary = Part(a, l, r, Flag);  //boundary為樞軸元素所在位置if(Flag == 0)        ///如果已經有序,結束排序。return ;if(right-left < 3)   //如果序列小于等于3個元素{JustSort(a, left, right);return ;}if( boundary - l < r - boundary)    ///如果左分區比右分區短{if( boundary + 1 < r ) //判斷右分區是否存在{s.push(boundary + 1);s.push(r);}if( boundary - 1 > l ) //判斷左分區是否存在{///將左分區端點后入棧以便優先排序s.push(l);s.push(boundary - 1);}}else                                       ///如果右分區比左分區短{if( boundary - 1 > l ) //判斷左分區是否存在{s.push(l);s.push(boundary - 1);}if( boundary + 1 < r ) //判斷右分區是否存在{///將右分區端點后入棧以便優先排序s.push(boundary + 1);s.push(r);}}}}
}#endif // __ExQUICKSORT_H__

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/384578.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/384578.shtml
英文地址,請注明出處:http://en.pswp.cn/news/384578.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【數據結構】對快速排序原理的理解(圖解,通俗易懂)

學習數據結構時&#xff0c;書本上直接給出了快速排序的過程以及代碼&#xff0c;對其原理解釋的不夠詳細&#xff0c;琢磨代碼后&#xff0c;發現其原理其實十分簡單&#xff0c;簡述如下&#xff1a; &#xff08;1&#xff09;在待排序列中找一個“中樞元素”&#xff08;書…

【離散數學】圖論基礎知識

文章目錄1 圖的基本概念2 圖的連通性3 圖的矩陣表示4 幾種特殊的圖4.1 二部圖4.2 歐拉圖4.3 哈密頓圖4.4 平面圖5 無向樹6 生成樹1 圖的基本概念 無向圖&#xff1a; 簡而言之&#xff0c;邊不帶方向的圖就是無向圖。 有向圖&#xff1a; 簡而言之&#xff0c;邊帶方向的圖就…

【操作系統】信號量解決經典同步問題

文章目錄1. 基本結構2. P,V操作3. 信號量的應用3.1 信號量實現進程互斥3.2 信號量實現前驅關系4. 用信號量解經典同步問題4.1 生產者消費者問題4.2 讀者寫者問題4.3 狒狒過橋問題4.4 理發師理發問題4.5 哲學家進餐問題信號量機制是Dijkstra提出的一種卓有成效的進程同步工具。信…

【運籌與優化】單純形法解線性規劃問題(matlab實現)

文章目錄單純形法步驟&#xff1a;1.將線性規劃問題化為標準形式2.列出單純形表3.進行最優性檢驗4.從一個基可行解轉換到另一個目標值更大的基可行解&#xff0c;列出新的單純形表5.重復3、4直到計算結束為止舉例單純形法matlab實現單純形法是一種解線性規劃問題的算法&#xf…

【Linux系統編程學習】 GCC編譯器

此為牛客網Linux C課程1.2&1.3的課程筆記。 0. 簡介 1. gcc和g的安裝 sudo apt install gcc g2. gcc常用參數選項 3. gcc工作流程 首先是預處理器對源代碼進行預處理&#xff08;后綴名.i&#xff09;&#xff0c;主要做以下事情&#xff1a; 把頭文件加入到源代碼當中刪…

Spring5底層原理之BeanFactory與ApplicationContext

目錄 BeanFactory與ApplicationContext BeanFactory ApplicationContext 容器實現 BeanFactory實現 ApplicationContext實現 ClassPathXmlApplicationContext的實現 AnnotationConfigApplicationContext的實現 AnnotationConfigServletWebServerApplicationContext的實…

【Linux系統編程學習】 靜態庫的制作與使用

此為牛客網Linux C課程 1.4&1.5 的課程筆記。 0. 關于靜態庫與動態庫 庫就是封裝好的、可服用的代碼&#xff0c;而靜態和動態是指鏈接。 這節課講的是靜態庫&#xff0c;是指在鏈接階段&#xff0c;會將匯編生成的目標文件.o與引用到的庫一起鏈接打包到可執行文件中&…

【Linux系統編程學習】 動態庫的制作與使用

此為牛客網Linux C課程1.6&1.7 的課程筆記。 1. 動態庫命名規則 2. 動態庫的制作 第一步&#xff0c;用gcc編譯生成.o目標文件&#xff0c;注意要用-fpic參數生成與位置無關的代碼&#xff1b; 第二步&#xff0c;用gcc的-shared參數生成動態庫。 涉及到的兩個參數之前學過…

【Linux系統編程學習】 靜態庫與動態庫的對比與總結

此為牛客網Linux C課程 1.9 的課程筆記。 1. 前幾節課知識總結 程序編譯成為可執行文件的過程&#xff1a; 靜態庫制作過程&#xff1a; 動態庫制作過程&#xff1a; 2. 靜態庫的優缺點&#xff1a; 3. 動態庫的優缺點&#xff1a; 更多可參考&#xff1a;吳秦&#xff1…

【Linux系統編程學習】 Makefile簡單入門

此為牛客網Linux C課程1.10&1.11&1.12 的課程筆記。 0. Makefile介紹 1. Makefile文件命名與規則 示例&#xff1a; 使用vim編寫如下名為Makefile的文件&#xff1a; app:sub.o add.o mult.o div.o main.ogcc sub.o add.o mult.o div.o main.o -o appsub.o:sub.cgcc …

【Linux系統編程學習】 GDB調試器的簡單使用

此為牛客網Linux C課程 1.13&1.14&1.15&1.16 的課程筆記。 0. GDB簡介 1. 準備工作 想要使用gdb調試&#xff0c;首先需要用gcc的-g參數生成可執行文件&#xff0c;這樣才能在可執行文件中加入源代碼信息以便調試&#xff0c;但是注意這并不是將源文件嵌入到可執行…

【Linux系統編程學習】C庫IO函數與系統IO函數的關系

此為黑馬Linux課程筆記。 1. C標準IO函數工作流程 如圖&#xff0c;以C庫函數的fopen為例&#xff0c;其返回類型是FILE類型的指針&#xff0c;FILE類型包含很多內容&#xff0c;主要包含三個內容&#xff1a;文件描述符、文件讀寫指針的位置和I/O緩沖區的地址。 文件描述符&…

【Linux系統編程學習】 文件描述符

此為牛客網Linux C課程1.19課程筆記。 1. 文件描述符表 如圖&#xff0c;我們知道每個進程都有其虛擬地址空間&#xff08;0~4G&#xff09;&#xff0c;其中3 ~ 4G部分為內核區。進程的進程控制塊保存就在內核區&#xff0c;而PCB中維護一個打開文件描述符表&#xff0c;每個…

【Linux系統編程學習】Linux系統IO函數(open、read、write、lseek)

此為牛客網Linux C課程1.20課程筆記。 1.open函數 open函數有兩種&#xff0c;分別是打開一個已經存在的文件和創建并打開一個不存在的文件。 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>// 打開一個已經存在的文件 int open(const…

【Linux系統編程學習】Linux進程控制原語(fork、exec函數族、wait)

此為牛客Linux C和黑馬Linux系統編程課程筆記。 1. fork函數 1.1 fork創建單個子進程 #include<unistd.h> pid_t fork(void);作用&#xff1a;創建一個子進程。 pid_t類型表示進程ID&#xff0c;但為了表示-1&#xff0c;它是有符號整型。(0不是有效進程ID&#xff0…

【Linux系統編程學習】匿名管道pipe與有名管道fifo

此為牛客Linux C和黑馬Linux系統編程課程筆記。 0. 關于進程通信 Linux環境下&#xff0c;進程地址空間相互獨立&#xff0c;每個進程各自有不同的用戶地址空間。任何一個進程的全局變量在另一個進程中都看不到&#xff0c;所以進程和進程之間不能相互訪問&#xff0c;要交換…

【Linux系統編程學習】信號、信號集以其相關函數

此為牛客Linux C和黑馬Linux系統編程課程筆記。 文章目錄0. 信號的概念1. Linux信號一覽表2. 信號相關函數3. kill函數4. raise函數5. abort函數6. alarm函數7. setitimer函數8. signal函數9. 信號集10. 自定義信號集相關函數11. sigprocmask函數12. sigpending函數13. sigacti…

【Linux系統編程學習】父進程捕獲SIGCHLD信號以處理僵尸進程

配合之前說過的sigaction函數和waitpid函數&#xff0c;我們可以解決子進程變成僵尸進程的問題。 先看如下示例程序&#xff1a; #include <sys/time.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> …

【Linux系統編程學習】Linux線程控制原語

此為牛客Linux C課程筆記。 0. 關于線程 注意&#xff1a;LWP號和線程id不同&#xff0c; LWP號是CPU分配時間片的依據&#xff0c;線程id是用于在進程內部區分線程的。 1. 線程與進程的區別 對于進程來說&#xff0c;相同的地址(同一個虛擬地址)在不同的進程中&#xff0c;反…

【Linux網絡編程學習】預備知識(網絡字節序、IP地址轉換函數、sockaddr數據結構)

此為牛客Linux C課程和黑馬Linux系統編程筆記。 1. 網絡字節序 我們已經知道&#xff0c;內存中的多字節數據相對于內存地址有大端和小端之分。 磁盤文件中的多字節數據相對于文件中的偏移地址也有大端小端之分。網絡數據流同樣有大端小端之分&#xff0c;那么如何定義網絡數…