資料結構排序演算法總結
Sort.h
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int DataType; typedef int (*Pcompare)(DataType left, DataType right); typedef struct Heap { DataType *arr; int size; int capacity; Pcompare compare; }Heap; int Less(DataType left, DataType right); int Greater(DataType left, DataType right); void HeapSort(Heap *hp); void CreatHeap(Heap *hp, int size); void DownAdjust(Heap *hp, int i, int size); void InitHeap(Heap *p, int size, int *array,Pcompare compare); void DestroyHeap(Heap *hp); void InsertSort(int *array, int size); void ShellSort(int *array, int size); void SelectSort(int *array, int size); void QuickSortRecursion(int *array, int left, int right); void QuickSort(int *array, int left, int right); void MergerSort(int *array, int left, int right, int *tmp);
1.插入排序:
基本思想:每一步將一個代排的元素,按其排序碼的大小,插入到前面已經排好序的一組元素的合適位置上去,直到元素全部插完為止。
當插入第i個元素時,前面的i個已經是有序的了,然後我們將第i個元素與前面的元素比較,如果前面的元素大於要插入的元素的話,將前面的元素後移,直到你要插入的元素找到合適的插入位置時插入。如下圖所示:
後面的元素和第二個元素的插入一樣
程式碼:
//普通的插入排序 void Swap(int *p, int *q) { int tmp = *p; *p = *q; *q = tmp; } void InsertSort(int *array, int size) { int i = 1;//用來標記要插入的數剛開始的位置 int end = 0;//用來表示插入陣列中最後一個數 int key = 0;//用來表示你要插入的數 assert(array); while( i < size) { end = i-1; key = array[i];//key儲存你要插入的數 while(end >= 0 && key < array[end])//找到你要插入數的位置(從最後一位數開始比較,如果小於的話,將最後一位數向後移動,直到找到你要插入的位置) { array[end+1] = array[end]; end--; } array[end+1] = key;//將你要插入的數插入對應的位置 i++; } }
插入排序適合於元素接近有序,這樣直接插入排序的效率比較高
最優的情況:時間效率為O(n)
最差的情況:時間效率為O(n^2)
空間複雜度為:O(1);
直接插入排序是一種穩定的排序演算法。
2.希爾排序
希爾排序是插入排序的優化:希爾排序與插入排序很相似,希爾排序不是一個接一個的排,比如第一個插入後與它後面的第i+grap(間隔個數)個元素比較。如下圖所示:
//優化插入排序 //希爾排序(和插入排序差不多,只是希爾排序不是與它前一個比較,而是與它的下標相隔garp的數比較,直到間隔數為0時,停止) void ShellSort(int *array, int size) { int i = 0;//用來標記要插入的數剛開始的位置 int end = 0;//用來表示插入陣列中最後一個數 int key = 0;//用來表示你要插入的數 int garp = 3;//代表每隔幾個數來比較 assert(array); while(garp > 0) { i = 0; while( i < size) { end = i+garp;//用來標記下一個要插入的資料的下標 key = array[end];//key儲存你要插入的數 while(end > 0 && key < array[i])//找到你要插入數的位置(從最後一位數開始比較,如果小於的話,將最後一位數向後移動,直到找到你要插入的位置) { array[end-garp] = array[end]; end-=garp; } array[end+1] = key;//將你要插入的數插入對應的位置 i++; } garp--; } }
3.選擇排序
基本思想:第一次遍歷選出最大值和最小值,然後將最大值和最小值放到首和尾。r然後將首和尾的標記向前和向後移一步。
迴圈上述操作,直到最後首標記大於等於尾標記時退出。
直接選擇排序的時間複雜度為O(n^2);它是一種不穩定的排序演算法
程式碼為:
//選擇排序(每次選出一個最大值和最小值)
void SelectSort(int *array, int size)
{
int beagin = 0;//用來記錄每次最小值所放的位置
int end = size-1;//用來記錄每次最大值所放的位置
while(beagin < end)//當最小值的位置大於最大值的位置時,停止
{
int minPos = beagin;
int maxPos = beagin;
int i = beagin;
while(i <= end)//遍歷一遍,記錄這次最大值和最小值所在的下標
{
if(array[minPos] > array[i])//如果
{
minPos = i;
}
if(array[maxPos] < array[i])
{
maxPos = i;
}
i++;
}
if(maxPos != end)//如果最大值在它應該在的位置的話,不用交換,如果不在的話,交換
{
Swap(&array[maxPos], &array[end]);
}
if(minPos == end)//如果最大值應該放置的位置正好時最小值標記的位置的話,記錄,交換後最小值的位置
{
minPos = maxPos;
}
if(minPos != beagin)////如果最小值在它應該在的位置的話,不用交換,如果不在的話,交換
{
Swap(&array[minPos], &array[beagin]);
}
beagin++;//每次將最大值最小值放在首和末時,將首尾向中間靠攏,繼續找他們中間的資料的最大值和最小值
end--;
}
}
4. 堆排序:
建立堆:升序---》大堆, 降序----》小堆
假設要升序的話,首先將這個堆調整為大堆。
1.然後將堆頂的元素與堆的最後一個元素交換
2.堆的元素個數-1
3.因為改變了堆頂所以此時的堆可能不滿足大堆的特性,所以重新調整堆
重複上述的操作,直到堆內的元素為1時停止。
程式碼為:
//堆排序
//判斷元素大小的函式(大堆的話,比較大小時用第二個函式,反之用第一個函式)
int Less(DataType left, DataType right)
{
return (left) < (right);
}
int Greater(DataType left, DataType right)
{
return (left) > (right);
}
//初始化堆
void InitHeap(Heap *p, int size, int *array,Pcompare compare)
{
int i = 0;
assert(p);
(p)->arr = (DataType *)malloc(sizeof(DataType)*size);
if(NULL == (p)->arr)
exit(0);
for(i = 0; i < size; i++)//初始化堆
{
(p)->arr[i] = array[i];
}
(p)->capacity = size;
(p)->size = size;
(p)->compare = compare;
}
//向下調整
void DownAdjust(Heap *hp, int i, int size)
{
int child = (i<<1) + 1;//左孩子
int parent = i;
assert(hp);
while(child < size)
{
//判斷左右孩子,選出最大值
if(child + 1 < size && (hp)->compare((hp)->arr[child+1] , (hp)->arr[child]))
child++;
//左右孩子的最大值與雙親節點比較
if((hp)->compare((hp)->arr[child], (hp)->arr[parent]))
{
Swap(&(hp)->arr[child], &(hp)->arr[parent]);
}
else
{
return;
}
parent = child;
child = (parent<<1)+1;
}
}
//建立堆
void CreatHeap(Heap *hp, int size)
{
int i = size/2-1;
for(i; i >= 0; i--)
{
DownAdjust(hp, i, size);
}
}
//堆排序
void HeapSort(Heap *hp)
{
int i = 0;
assert(hp);
i = (hp)->size-1;
while(i > 0)
{
//交換堆頂和堆尾的值,最大的放到最後
Swap(&(hp)->arr[i], &(hp)->arr[0]);
i--;
DownAdjust(hp, 0, i);
}
}
//釋放
void DestroyHeap(Heap *hp)
{
assert(hp);
if(hp->arr != NULL)
free(hp->arr);
hp->arr = NULL;
hp->capacity = 0;
hp->size = 0;
}
5.快速排序:
首先在元素序列中隨機選取一個元素作為基準值,然後遍歷序列,大於基準值的放到基準值的左邊,反之放到基準值的右邊,然後重複上述的操作,直到所有元素都在相應的位置上為止。
按照基準值分割序列有三種方式:
1.hoare版本:
首先選取最右邊的值為基準值,然後選取兩個下標,一個指向首,一個指向尾,兩個下標分別向中間遍歷,左邊的下標遇到大於基準值的元素時停止,右邊的元素遇到小於基準值的元素停止,然後交換兩個下標所指的元素,重複尋找,直到左邊的下標大於或等於右邊的下標時,退出迴圈,然後將左邊下標所指的元素與序列的最後一個元素交換。
//三數取中法
//防止快速排序時,取到極大值或極小值
int Mid(int *array, int left, int right)
{
int mid = left+((right-left)>>1);
if(array[left] < array[right])
{
if(array[left] > array[mid])
return left;
else if(array[mid] > array[right])
return right;
else
return mid;
}
else
{
if(array[right] > array[mid])
return right;
else if(array[left] < array[mid])
return left;
else
return mid;
}
}
//快速排序中的一次分割(取一箇中點,小於它的放左邊,大於它的放在右邊)
//首先取最後一個元素為切割的中點,然後取兩個下標,一個下標begin指向陣列的首,一個下標end指向陣列的尾,然後開始判斷,
//如果begin小於end,且以begin為下標的值小於等於key的話,begin向後移動
//如果end大於begin,且以end為下標的值大於key的話,end向前移動
//然後交換兩個下標所指向的兩個值
//直到begin大於等於end時退出,然後再交換begin和最後一個元素的值,這樣大於key的數在key的右邊,小於key的數在左邊
int FastSort(int *array, int left, int right)
{
int begin = left;
int end ;
int key;
int index = right-1;
if(right-1 != Mid(array,left, right-1))//如果三數取中法的基準值是最後一個元素時,不需要改變基準值的下標
index = Mid(array,left, right-1);//記錄基準值的下標
key = array[index];
//將基準值和陣列的最後一個元素交換(因為我下面的排序都是取陣列的最後一個元素作為基準值的)
Swap(&array[index], &array[right-1]);
end = right-1;
while(begin < end)
{
//如果begin小於end,且以begin為下標的值小於等於key的話,begin向後移動
while(begin < end && array[begin] <= key)//這裡是小於等於,因為當陣列中的元素全部相等時,程式會死迴圈
begin++;
//如果end大於begin,且以end為下標的值大於key的話,end向前移動
while(end > begin && array[end] >= key)
end--;
if(begin < end)
Swap(&array[begin], &array[end]);
}
Swap(&array[begin], &array[right-1]);
return begin;
}
2.挖坑法
挖坑法與第一種方法有點相似,挖坑法是先將兩個下標分別指向陣列的首(p)和尾(q),然後p開始向前查詢,遇到大於基準值的資料停止,然後將p指向的值賦給q指向的位置。q向前查詢,遇到小於基準值的資料時停止,然後將q指向的值賦給p所指向的位置,直到指向p >= q時退出,然後將基準值放在首下標所指向的位置
這樣的話,一次分割結束。
//挖坑法(快速排序中的切割方法中的一種)
//首先選擇陣列的最後一個元素為中點,然後設定兩個下標,一個指向陣列的首,一個指向陣列的尾
//然後開始比較,如果begin指向的數比key小的話,繼續向後尋找,如果大於key的話,停止,將現在指向的值賦給end指向的值
//如果end指向的資料大於key時向前走,如果小於時停止,將現在指向的值賦給begin指向的位置
//直到begin大於等於end時退出,
//此時,將key賦給begin指向的位置
int ExcavateSort(int *array, int left, int right)
{
int begin = left;
int end ;
int key;
int index= right-1;
if(right-1 != Mid(array,left, right-1))//判斷如果基準值是最後一個元素時,不需要再改變基準值的下標
index = Mid(array,left, right-1);//記錄基準值的下標
key = array[index];
//將基準值和陣列的最後一個元素交換(因為我下面的排序都是取陣列的最後一個元素作為基準值的)
Swap(&array[index], &array[right-1]);
end = right-1;
while(begin < end)
{
//當begin指向的元素小於key且begin小於end時,向前走
while(begin < end && array[begin] <= key)
{
begin++;
}
//將此時begin指向的值賦給end位置
if(begin < end)
{
array[end] = array[begin];
end--;
}
while(end > begin && array[end] >= key)
{
end--;
}
if(end > begin)
{
array[begin] = array[end];
begin++;
}
}
array[begin] = key;
return begin;
}
3.前後指標
首先建立兩個下標,一個指向陣列的首cur,一個指向cur後面的下標prve,然後cur開始向前查詢,也就是,首先找第一個大於基準值的資料,然後用prve指向它的前一個位置,而cur繼續向前查詢,然後遇到大於基準值的資料,cur向前移動,而prve不動,如果遇到小於基準值的資料就與prve的後一個位置的值交換,直到cur到陣列尾時,結束。
//前後下標法(快速排序分割的第三種方法)
//首先用三數取中法找到一個分割點,避免極值為分割點,
//用其中一個下標cur指向陣列的第一個元素,用另一個下標Prve指向cur-1,如果cur指向的值大於key時,cur向後移動,Prve不動,如果cur指向的資料小於key且Prve+1 < cur時,交換cur和Prve指向的資料
//重複上述操作,直到cur找到最後一個數據時退出
//最後將最後一個元素與FirstGreterKey指向的值交換。
int FrontAndBack(int *array, int left, int right)
{
int cur = left;
int index = right-1;
int key;
int Prve;
//如果三數取中法取得資料就是最後一個元素的話,不需要交換
if(right-1 != Mid(array,left, right-1))
index = Mid(array,left, right-1);
key = array[index];
//將基準值和陣列的最後一個元素交換(因為我下面的排序都是取陣列的最後一個元素作為基準值的)
Swap(&array[index], &array[right-1]);
Prve =cur-1;//用來記錄大於key的前一個值
while(cur < right)
{
if(array[cur] < key && ++Prve < cur)//如果cur指向的資料小於key且Prve+1 < cur時,交換cur和Prve指向的資料
{
Swap(&array[cur], &array[Prve]);
}
cur++;
}
//將最後一個元素放到它對應的位置上
if(++Prve < right)//判斷Prve是否越界
Swap(&array[Prve], &array[right-1]);
return Prve;
}
遞迴快速排序函式
//遞迴的快速排序
void QuickSortRecursion(int *array, int left, int right)
{
//如果left等於right時,只剩下一個元素,所以就不用再排序
if(left < right)
{
int MidPoint = FastSort(array,left,right);
QuickSortRecursion(array, left, MidPoint);
QuickSortRecursion(array, MidPoint+1,right);
}
}
非遞迴快速排序
//非遞迴的快速排序
//用棧來實現快排的非遞迴
//二叉樹的前序遍歷一樣,先排序它左區間的左區間,直到它左區間只有一個元素時停止,然後再排它右區間,直到只有一個元素為止
//然後向棧中取出左右區間,利用這左右區間進行一次排序,
//一次排序後先將基準值右邊的左右邊界入棧,然後將基準值左邊區間的左右邊界入棧,因為每次出棧都會取棧頂的元素,所以下次渠道的還是左區間
//重複上兩步的操作,直到棧為空時,退出
void QuickSort(int *array, int left, int right)
{
Stack q = {0};
int begin = left;
int end = right;
StackInit(&q, right);
//首先將陣列的左右邊界入棧
StackPush(&q, end);
StackPush(&q, begin);
while(q.top != 0)
{
int mid = 0;
begin = StackTopData(&q);
StackPop(&q);
end = StackTopData(&q);
StackPop(&q);
//將棧頂取到的區間之間的元素進行一次排序
mid = FrontAndBack(array, begin, end);
//如果它的右區間有不止一個元素,將它的右區間入棧
if(end > mid+1)
{
StackPush(&q, end);
StackPush(&q, mid+1);
}
//如果左區間有不只一個元素時,將它的左區間入棧
if(begin < mid)
{
StackPush(&q, mid);
StackPush(&q, begin);
}
}
}
快排的最壞的情況:序列接近有序
快排的最優的情況:每次基準值都是再區間的中間
快排的優化:拿陣列的第一個元素、最中間的元素、末尾的元素,三個元素排序,取中間的值
使用場景:資料隨機,資料量大
歸併的遞迴排序
基本思想:將代排序的元素序列分成兩個長度相等的子序列,對每一個子序列排序,然後將他們合併成一個序列。合併兩個子序列的過程稱為二路歸併。
和二叉樹的後續遍歷相同首先將陣列分為每個元素自己為一組的情況,然後兩兩合併,
//歸併兩個區間的資料
void MergerData(int *array, int left, int mid, int right, int *tmp)
{
int begin1 = left;
int i = left;
int end1 = mid;
int begin2 = mid+1;
int end2 = right;
int index = begin1;
//將兩個區間裡的資料從頭開始比較,哪邊比較小就往tmp裡插入,直到其中的一個區間結束
while(begin1 <= end1 && begin2 <= end2)
{
if(array[begin1] <= array[begin2])//如果前面的區間的begin1指向的資料小與後面區間begin2指向的資料時,將begin1指向的資料插入tmp中
{
tmp[index] = array[begin1];
begin1++;
index++;
}
else
{
tmp[index] = array[begin2];
begin2++;
index++;
}
}
//檢視哪個區間還有剩餘的元素,將剩餘的元素之間插入到tmp中
while(begin1 <= end1)
{
tmp[index] = array[begin1];
begin1++;
index++;
}
while(begin2 <= end2)
{
tmp[index] = array[begin2];
begin2++;
index++;
}
while(i <= right)
{
array[i] = tmp[i];
i++;
}
}
//歸併排序
void MergerSort(int *array, int left, int right, int *tmp)
{
//與二叉樹的後續遞迴排序特性相同
if(left < right)
{
int mid = left + ((right-left)>>1);
MergerSort(array, left, mid, tmp);
MergerSort(array, mid+1, right, tmp);
MergerData(array, left, mid, right, tmp);
}
}
歸併的非遞迴排序:
首先我們把陣列的元素看作一個元素是一個組,然後將他們兩兩歸併
//歸併的非遞迴實現
void MergerSortNor(int *array, int left, int right, int *tmp)
{
int size = 1;
int i = left;
while(size <= right)
{
for(i = 0; i < right; i+=2*size)
{
int mid = i+size-1;
int begin = i;
int end = i+ 2*size;
//如果越界
if(mid > right)
{
mid = i;
}
if(end > right)
{
end = right;
}
MergerData(array, begin, mid, end, tmp);
}
size*=2;
}
}
//歸併兩個區間的元素
void MergerData(int *array, int left, int mid, int right, int *tmp)
{
int begin1 = left;
int i = left;
int end1 = mid;
int begin2 = mid+1;
int end2 = right;
int index = begin1;
//將兩個區間裡的資料從頭開始比較,哪邊比較小就往tmp裡插入,直到其中的一個區間結束
while(begin1 <= end1 && begin2 <= end2)
{
if(array[begin1] <= array[begin2])//如果前面的區間的begin1指向的資料小與後面區間begin2指向的資料時,將begin1指向的資料插入tmp中
{
tmp[index] = array[begin1];
begin1++;
index++;
}
else
{
tmp[index] = array[begin2];
begin2++;
index++;
}
}
//檢視哪個區間還有剩餘的元素,將剩餘的元素之間插入到tmp中
while(begin1 <= end1)
{
tmp[index] = array[begin1];
begin1++;
index++;
}
while(begin2 <= end2)
{
tmp[index] = array[begin2];
begin2++;
index++;
}
while(i <= right)
{
array[i] = tmp[i];
i++;
}
}
main.c
#include"Sort.h"
void InsertText()
{
int array[10] = {4, 2, 5, 1, 9, 7 ,8, 0, 3, 6};
//int array[9] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
InsertSort(array, sizeof(array)/sizeof(array[0]));
ShellSort(array, sizeof(array)/sizeof(array[0]));
SelectSort(array, 9);
QuickSortRecursion(array, 0, sizeof(array)/sizeof(array[0]));
QuickSort(array, 0, sizeof(array)/sizeof(array[0]));
}
void HeapText()
{
int array[10] = {4, 2, 5, 1, 9, 7 ,8, 0, 3, 6};
Heap hp = {0};
InitHeap(&hp, 10, array,Greater);
CreatHeap(&hp, 10);
HeapSort(&hp);
DestroyHeap(&hp);
}
void MergerText()
{
int array[10] = {4, 2, 5, 1, 9, 7 ,8, 0, 3, 6};
int i = 0;
int *tmp = (int *)malloc(sizeof(int)*sizeof(array)/sizeof(array[0]));
if(tmp == NULL)
{
exit(0);
}
MergerSortNor(array, 0, sizeof(array)/sizeof(array[0])-1, tmp);
free(tmp);
tmp = NULL;
}
int main()
{
InsertText();
HeapText();
MergerText();
return 0;
}
各排序演算法的比較
類別 | 排序方法 | 時間複雜度 | 空間複雜度 | 穩定性 | ||
平均情況 | 最好情況 | 最壞情況 | ||||
插入排序 | 插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 穩定 |
希爾排序 | O(N^1.3) | O(N) | O(N^2) | O(1) | 不穩定 | |
選擇排序 | 選擇排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不穩定 |
堆排序 | O(N*lgN) | O(N*lgN) | O(N*lgN) | O(1) | 不穩定 | |
交換排序 | 氣泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 穩定 |
快速排序 | O(N*lgN) | O(N*lgN) | O(N^2) | O(lgN) | 不穩定 | |
歸併排序 | 歸併排序 | O(N*lgN) | O(N*lgN) | O(N*lgN) | O(N) | 穩定 |