這篇文章是我在學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)時(shí)作筆記的用途,這篇文章會(huì)紀(jì)錄下我學(xué)習(xí)的幾種排序算法,以及在學(xué)習(xí)的時(shí)候會(huì)加上配圖說(shuō)明!
相關(guān)的定義和基礎(chǔ)
如果某組信息在內(nèi)存空間中存儲(chǔ),則稱為表(list)。如果存儲(chǔ)于外存中則稱為文件(file)。把信息中的對(duì)象稱為記錄,每個(gè)記錄的信息劃分為更小的單元,稱為域。
??順序查找對(duì)關(guān)鍵字域沒(méi)有任何限制,而折半查找需要關(guān)鍵字有序的排列。其中折半查找可以使用二叉查找樹(shù)來(lái)描述。
??二叉查找樹(shù)的特征
其左子樹(shù)不大于根節(jié)點(diǎn)值,右子樹(shù)不小于根節(jié)點(diǎn)值。而且各個(gè)子樹(shù)還是一棵二叉查找樹(shù)。
內(nèi)部排序是指在表的規(guī)模足夠小,能夠全部放在內(nèi)存中排序的方法。外部排序指數(shù)據(jù)規(guī)模太大,不能全部放在內(nèi)存中時(shí)。這篇文章中我主要紀(jì)錄的是內(nèi)部排序算法,其中包含了:插入排序、快速排序、堆排序、歸并排序、基數(shù)排序。
插入排序
插入排序類似于玩紙牌時(shí),每次拿一張牌,將這張牌放在合適的位置,使手中所有紙牌按順序排列。
void insertion_sort(ele list[], int n){
/// 最壞情況時(shí)間復(fù)雜度:O(n*n)
int i,j;
ele next;
for (i = 1; i < n; i++) {
next = list[i];
for (j = i - 1; j >= 0 && next.key < list[j].key; j--) {/// 說(shuō)明不是按照升序排列
list[j+1] = list[j];
}
list[j+1] = next;
}
}
最壞情況的時(shí)間復(fù)雜度為:O(n*n),還可以通過(guò)檢查輸入表的相對(duì)無(wú)序性來(lái)估計(jì)插入排序的計(jì)算時(shí)間。為了計(jì)算相對(duì)無(wú)序性,需要測(cè)量每個(gè)記錄是左無(wú)序(LOO)的區(qū)域長(zhǎng)度。
如果為左無(wú)序記錄的個(gè)數(shù),則插入排序的計(jì)算時(shí)間為O((k+1)n)。
??插入排序適用范圍:
當(dāng)只有少數(shù)紀(jì)錄是LOO紀(jì)錄時(shí),插入排序的運(yùn)行效果很好。
快速排序
快速排序是平均時(shí)間性能非常好的排序方法。在學(xué)習(xí)快速排序時(shí)借鑒了阮一峰的解讀、坐在馬桶上學(xué)算法和排序算法動(dòng)畫(huà)演示。
1.找一個(gè)基準(zhǔn)點(diǎn)(一般是第一個(gè)元素),把小于該基準(zhǔn)點(diǎn)的放在左邊稱為S1,大于基準(zhǔn)點(diǎn)的放在右邊稱為S2。將S1中找一個(gè)基準(zhǔn)點(diǎn),然后做和上面類似的,左邊稱為S1-1,然后一直遞歸下去S1-1-...1。右邊稱為S1-2。同理遞歸操作集合中其他的元素。
2.在上一條中提到的基準(zhǔn)點(diǎn),我說(shuō)一般是第一個(gè)元素,這里說(shuō)一下時(shí)間復(fù)雜度為O(n*n)的情況,如果在排序之前該組數(shù)據(jù)已經(jīng)是有序排列(升序和降序)的情況。那么怎么選擇基準(zhǔn)點(diǎn)呢?一般是找最右邊和最左邊和中間數(shù)字找一個(gè)中位數(shù)。
圖解分析
測(cè)試一組數(shù)據(jù):26,5,37,1,61,11,59,14,48,19,55;下圖中提到的i,j在圖中可能看得不清楚,所以我說(shuō)明一下:i 用于紀(jì)錄在一次移動(dòng)過(guò)程中小于基準(zhǔn)點(diǎn)數(shù)據(jù)的下標(biāo)。j 則相反,它是紀(jì)錄大于基準(zhǔn)點(diǎn)數(shù)據(jù)的下標(biāo)。
- 1)、第一次以26為基準(zhǔn)點(diǎn)的元素移動(dòng)情況:

自此以第一個(gè)為基準(zhǔn)點(diǎn)的元素移動(dòng)完成,需要注意的是:先由i移動(dòng),當(dāng)i移動(dòng)停止時(shí),移動(dòng)j。在i和j的移動(dòng)完成之后,需要i<j才能進(jìn)行位置交換。
-
2)、上一步得到的最后序列中操作26左邊部分的的元素:
11、5、19、1、14。
3)、同樣來(lái)操作26左邊部分的的元素:
59、61、48、37、55。

- 4)、通過(guò)上訴三步得到如下結(jié)果,然后使用遞歸的方式分別對(duì)基準(zhǔn)點(diǎn)
11、59的左右元素進(jìn)行相同的排序操作。

最后結(jié)果如下:

編碼實(shí)現(xiàn)
C語(yǔ)言實(shí)現(xiàn)
void quick_sort(int list[], int left, int right);
void quicksort(int list[], int n){
quick_sort(list, 0, n - 1);
}
void swap(int *lhs,int *rhs){
int temp = *lhs;
*lhs = *rhs;
*rhs = temp;
}
#define AfterMove 1
void quick_sort(int list[], int left, int right){
if (left >= right) {
return;
}
int pivot = list[left];/// 這里我就簡(jiǎn)單的取最左邊為基準(zhǔn)點(diǎn)
#if AfterMove
/// 哨兵i,j
int i = left + 1;
int j = right;
while (i < j) {
/// 先移動(dòng)i
while (list[i] < pivot) {
i++;
}///找出當(dāng)前不滿足條件的元素所在的位置
/// 再移動(dòng)j
while (list[j] > pivot) {
j--;
}
if (i < j) {/// 進(jìn)行元素位置互換
swap((list + i), (list + j));
}
}
if (*(list + left) > *(list + j)) {
swap((list + left), (list + j));
}
#else
/// 哨兵i,j
int i = left;
int j = right + 1;
do {
/// 先移動(dòng)i
while (list[++i] < pivot) {}///找出當(dāng)前不滿足條件的元素所在的位置
/// 再移動(dòng)j
while (list[--j] > pivot) {}
if (i < j) {/// 進(jìn)行元素位置互換
swap((list + i), (list + j));
}
} while (i < j);
/// 交換基準(zhǔn)點(diǎn)和j位置元素
swap((list + left), (list + j));
#endif
/// 遞歸處理當(dāng)前基準(zhǔn)點(diǎn)左右兩邊元素
quick_sort(list, left, j-1);/// 左邊
quick_sort(list, j+1, right);/// 右邊
}
調(diào)用,以及最后的結(jié)果打?。?/p>
int list[11] = {26,5,37,1,61,11,59,14,48,19,55};
quicksort(list, 11);
for (int i = 0; i < 11; i++) {
printf("Quick Sort Result is :%d\n",list[i]);
}
Quick Sort Result is :1
Quick Sort Result is :5
Quick Sort Result is :11
Quick Sort Result is :14
Quick Sort Result is :19
Quick Sort Result is :26
Quick Sort Result is :37
Quick Sort Result is :48
Quick Sort Result is :55
Quick Sort Result is :59
Quick Sort Result is :61
Swift實(shí)現(xiàn)
swift -v
Apple Swift version 3.1 (swiftlang-802.0.53 clang-802.0.42)
var list:Array<Int> = [19,30,1,5,13,37,15,29,40,5]
func QuicSort(list:inout Array<Int>){
__quick_sort(list: &list, left: 0, right: list.count - 1)
}
func __swap(_ lhs:inout Int,_ rhs:inout Int){
let tmp = lhs
lhs = rhs
rhs = tmp
}
func __quick_sort(list:inout Array<Int> ,left:Int ,right:Int) -> Void{
if left >= right {
return
}
var pivot:Int = list[left]
/**
var i:Int = left + 1
var j:Int = right
while i < j {
while list[i] < pivot {
i = i + 1
}
while list[j] > pivot {
j = j - 1
}
if i < j {
__swap(&list[i], &list[j])
}
}
if list[left] > list[j] {
__swap(&list[left], &list[j])
}
*/
var i:Int = left
var j:Int = right+1
while i < j{
repeat{
i = i + 1
}while list[i] < pivot
repeat{
j = j - 1
}while list[j] > pivot
if i < j {
__swap(&list[i], &list[j])
}
}
__swap(&list[left], &list[j])
__quick_sort(list: &list, left: left, right:j - 1)/// 左邊
__quick_sort(list: &list, left: j + 1, right: right)/// 右邊
}
QuicSort(list: &list)
print(list)/// [1, 5, 5, 13, 15, 19, 29, 30, 37, 40]
歸并排序
歸并排序:也是遞歸(迭代)、合并排序。主要是經(jīng)過(guò)兩步,將一組元素分解成多個(gè)子序列,然后使用遞歸或者迭代的方式合并成一個(gè)整體序列,在合并的過(guò)程對(duì)每個(gè)子序列進(jìn)行排序。
將一組無(wú)序的序列分解成多個(gè)子序列
/// 將一個(gè)數(shù)組分割成length為1的n個(gè)子序列(n為原數(shù)組長(zhǎng)度),第一步分割子序列
void __merge_sort(int list[], int n){
int length = 1;
int tmp_list[n];
/// 做分割序列,并對(duì)每個(gè)子序列進(jìn)行合并
while (length < n) {
__merge_pass(list, tmp_list, length, n);
length *= 2;
__merge_pass(tmp_list, list, length, n);
length *= 2;
}
}
將子序列合并
void __merge_pass(int list[], int tmp[], int length, int n){
/// 將子序列兩兩進(jìn)行merge
int i;
for (i = 0; i < n - 2*length; i += 2*length) {
__merge(list, tmp, i, i + length, i + 2*length - 1);
}
if (i + length < n) {/// 說(shuō)明當(dāng)前這個(gè)子序列,后面還跟了一個(gè)子序列(<n)。所以需要merge最后兩個(gè)子序列
__merge(list, tmp, i, i + length, n - 1);
}else{/// 當(dāng)前只有一個(gè)子序列,直接放入即可
for (int j = i; j < n; j++) {
tmp[j] = list[j];
}
}
}
在合并子序列時(shí)候進(jìn)行排序
/// ls := left_start
/// rs := right_start
/// re := right_end
void __merge(int list[], int tmp[],int ls,int rs,int re){
int i,j;
i = ls;
j = rs;
int k = i;/// tmp的下標(biāo)標(biāo)量
while (i < rs && j <= re) {
tmp[k++] = list[i] <= list[j] ? list[i++] : list[j++];
}
while (i < rs) {
tmp[k++] = list[i++];
}
while (j <= re) {
tmp[k++] = list[j++];
}
}
調(diào)用
int merge_list[10] = {26,5,77,1,61,11,59,15,48,19};
__merge_sort(merge_list, 10);
for (int i = 0; i < 10; i++) {
printf("merge_list Sort Result is :%d\n",merge_list[i]);
}
歸并排序存在的最大問(wèn)題就是需要一個(gè)額外的空間來(lái)進(jìn)行排序!
堆排序
堆排序放在最大堆一節(jié)做說(shuō)明!
基數(shù)排序
對(duì)于基數(shù)排序,其中有兩個(gè)重要的概念,分別是:MSD(主位優(yōu)先排序)、LSD(次位優(yōu)先排序)。什么會(huì)有這兩種不同的排序方式呢?因?yàn)榛鶖?shù)排序主要是針對(duì)于待排序列紀(jì)錄中存在多個(gè)關(guān)鍵字。針對(duì)這不同的關(guān)鍵字,導(dǎo)致我們?cè)谂判驎r(shí)可以有主位和次位之分!
??在基數(shù)排序中,把排序關(guān)鍵字用基數(shù)r分解為多個(gè)數(shù)位。當(dāng)r=10時(shí),就得到十進(jìn)制的分解結(jié)果。當(dāng)r=2時(shí),就得到二進(jìn)制的分解結(jié)果。
??下面通過(guò)一系列十進(jìn)制數(shù)排序來(lái)說(shuō)明一下基數(shù)排序。在排序的一系列數(shù)字中最大為3位數(shù):
[179、208、306、93、859、984、55、9、271、33
- 1)、 第一遍排序個(gè)位數(shù)
將上訴的數(shù)字根據(jù)個(gè)位數(shù)的大小,分別填入對(duì)應(yīng)的鏈表中。比如當(dāng)個(gè)位數(shù)是9的數(shù)字有179,859,9三個(gè)數(shù)字,每掃描一邊數(shù)字將前一個(gè)數(shù)字的link字段賦值為當(dāng)前數(shù)字,在下面代碼中展示的并不是直接使用ptr->link = ptr,而是用front數(shù)組來(lái)保存第一個(gè)元素,并由rear數(shù)組來(lái)更新相應(yīng)的鏈表link字段(具體代碼可看下面??地代碼實(shí)現(xiàn))。
在放入各自鏈表完成之后,我們需要根據(jù)現(xiàn)有的順序,重新放入ptr指針中,以便進(jìn)行根據(jù)十位上的數(shù)字再進(jìn)行一次排序
271 -> 93 -> 33 -> 984 -> 55 -> 306 -> 208 -> 179 -> 859 -> 9
- 2)、 對(duì)十位上的數(shù)字進(jìn)行排序
經(jīng)過(guò)第一排序之后得到數(shù)字序列,并對(duì)該序列上的數(shù)字根據(jù)十位上的數(shù)字同第一步一樣再進(jìn)心排序,得到的結(jié)果是:
306 -> 208 -> 9 -> 33 -> 55 -> 859 -> 271 ->179 ->984 -> 93
- 3)、對(duì)百位上的數(shù)字進(jìn)行排序
同樣是根據(jù)上一步得到的順序,在該序列的基礎(chǔ)上對(duì)序列中數(shù)字百位上的數(shù)字排序,結(jié)果如下:
9 -> 33 -> 55 -> 93 -> 179 -> 208 -> 271 -> 306 -> 859 -> 984
可以看出在前面三步完成之后,能夠正確將原序列經(jīng)過(guò)從小到大的順序排序。
代碼實(shí)現(xiàn)
#undef MAX_DIGIT
#define MAX_DIGIT 3
#undef RADIX_SIZE
#define RADIX_SIZE 10
typedef struct __list_node* list_node_ptr;
typedef struct __list_node{
int key;
list_node_ptr link;
}list_node;
/// 排序數(shù)字中最多由三位數(shù)字組成
list_node_ptr digit_sort(list_node_ptr ptr){
if (!ptr) {
return NULL;
}
list_node_ptr front[RADIX_SIZE],rear[RADIX_SIZE];
///1.基數(shù)排序,使用lsd排序,需要有三個(gè)基數(shù):從個(gè)位數(shù),十位數(shù),百位數(shù)
int digit,k;
for (int i = 0; i < MAX_DIGIT; i++) {
for (k = 0; k < RADIX_SIZE; k++) {
front[k] = rear[k] = NULL;
}
///2.依次放入上一次順序排列的數(shù)列
for (; ptr; ptr = ptr -> link) {
digit = (ptr->key/(int)pow(10, i))%10;/// 拿出該數(shù)字中第個(gè)/十/百位上的數(shù)字
if (!front[digit]) {
front[digit] = ptr;/// 保證該數(shù)位上有數(shù)據(jù)時(shí),front數(shù)組對(duì)應(yīng)的鏈表元素指向第一個(gè)
}else{
rear[digit]->link = ptr;/// 這一步實(shí)際上是紀(jì)錄ptr->link的
}
rear[digit] = ptr;///rear始終只想該鏈表的最后一個(gè)數(shù)據(jù)
}
ptr = NULL;
///3.每一輪按照基數(shù)放在對(duì)應(yīng)的數(shù)組里面之后,需要將其取出,按照在front數(shù)組中的順序鏈接ptr
for (k = RADIX_SIZE - 1; k >= 0; i--) {
/// 這里解釋一下為什么要用倒敘的方式?為了讓代碼簡(jiǎn)單,我們從位數(shù)上數(shù)字最大的開(kāi)始,依次向前來(lái)鏈接鏈表
rear[k]->link = ptr;
ptr = front[k];
}
}
return ptr;
}
內(nèi)部排序總結(jié)
| 排序算法 | 平均時(shí)間復(fù)雜度 | 最壞時(shí)間復(fù)雜度 | 額外空間復(fù)雜度 | 穩(wěn)定性 |
|---|---|---|---|---|
| 插入排序 | O(N*N) | O(N*N) | O(1) | 穩(wěn)定 |
| 快速排序 | O(N*log(N)) | O(N*N) | O(log(N)) | 不穩(wěn)定 |
| 歸并排序 | O(N*log(N)) | O(N*log(N)) | O(N) | 穩(wěn)定 |
| 堆排序 | O(N*log(N)) | O(N*log(N)) | O(1) | 不穩(wěn)定 |
| 基數(shù)排序 | O(P(N+B)) | O(P(N+B)) | O(N+B) | 穩(wěn)定 |
說(shuō)明:
1、由于快速排序是遞歸進(jìn)行的,所以額外的空間復(fù)雜度是O(log(N));
2、歸并排序由于需要一個(gè)額外的temp_list,故其空間復(fù)雜度為O(N);
3、由于快速排序、歸并排序和堆排序的時(shí)間復(fù)雜度都是O(N*log(N)),但是由于快速排序的前面系數(shù)很小。平均時(shí)間性能,快速排序是最好的;
具體的使用場(chǎng)景:
- 1、當(dāng)輸入序列部分有序時(shí),選擇插入排序;
- 2、基數(shù)排序的時(shí)間復(fù)雜度取決于關(guān)鍵字的規(guī)模和基數(shù)的選??;
- 3、快速排序的基準(zhǔn)點(diǎn)選取也直接影響排序的時(shí)間性能;
因此在實(shí)際使用中,把插入排序、快速排序和歸并排序結(jié)合使用,即在歸并排序中使用快速排序來(lái)處理子序列,而在快速排序中使用插入排序處理在其遞歸過(guò)程中的子序列。
