C語言實現八大排序演算法詳解及其效能之間的
阿新 • • 發佈:2018-11-20
排序是資料結構中的重要一節,也是演算法的重要組成部分。主要分為內部排序以及外部排序,今天我們講內部排序,也就是八大排序。
插入排序
直接插入排序
演算法思想
名字已經暴露了他的演算法,就是往裡面插入資料,就拿我們生活中的例子來說,打撲克牌。我們往手裡碼牌的時候,是一張一張的碼,先碼一張,抓手心,不需要修改位置,因為只有一張牌,一定是有序的。再接一張,和手裡的牌對比大小,調整位置,選擇放在它的左邊或者右邊。然後接著碼,又接到一張牌,拿到先和右邊的牌比,比右邊還大就放到最右邊,如果比右邊這張小呢,在和左邊這張比。同樣,我們這裡也是這樣的,首先我們預設第一個元素,一定是有序,OK吧。然後第二個,元素比較,大,放到左邊,小放到右邊。然後第三個元素,直到第N個,比它前一個大,繼續往前找位置,直到找到對應位置了,就是有序數列了。(當然每次找位置都是在一個有序的序列中找,所以完全可以用二分查詢找位置,資料大的話,二分明顯快於我們一張一張比)
演算法圖解
演算法分析
時間複雜度:O(n^2)
空間複雜度:O(1)
穩定性:穩定
演算法實現
void InsertionSort(int arr[], int size)
{
int key;//對比牌(要插入的牌)
int i = 0;
int j = 0;
for (i = 1; i < size; i++)//需要排的牌的張數,預設第一張有序,所以陣列從1開始
{
key = arr[i];
for (j = i - 1; j >= 0; j--)
{
if (key < arr[j])
{
arr[j + 1] = arr[j];
}
else
{
break;//比它大了,那就是找到正確的位置了,跳出,插牌進去
}
}
arr[j + 1] = key;
}
}
希爾排序
演算法思想
名字好聽,其實就是直接插入的升級版,主要是插牌的效能差距太大,如果有序,那麼插排的事件負責度就是O(N)但是如果排一個逆序他就是O(N^2)。那麼引進希爾排序,它就是做一次(多次)預排序讓你的排序,儘可能的有序,然後再插排。這樣複雜度就明顯減小。首先我們將無序陣列先進行分組,三個一分,五個一分,十個一分,都行。把小組內成員,進行插排。然後排好的序列其實已經很接近有序了(分的越少越接近,但是分的太少,複雜度也就越大,分組越多,排序次數也就越少。)所以希爾就提出了一個動態的去選擇分組的方法。gap=size/3+1(組),例如20個元素,第一次分7組,第二次7/3+1=3組,然後2組,然後1組。
演算法圖解
演算法分析
時間複雜度:O(n^1.3)
空間複雜度:O(1)
穩定性:不穩定
演算法實現
void MultipleInsertionSort(int arr[],int size,int gap)
{
int g = 0;
for (g = 0; g < gap; g++)
{
int key;
int i = 0;
int j = 0;
for (i = g + gap; i < size; i+=gap)
{
key = arr[i];
for (j = i - gap; j >= 0; j-=gap)
{
if (key < arr[j])
{
arr[j + gap] = arr[j];
}
else
{
break;
}
}
arr[j + gap] = key;
}
}
}
void ShellSort(int arr[], int size)
{
int gap = size;
while (gap)
{
gap = gap / 3 + 1;
MultipleInsertionSort(arr, size, gap);
if (gap == 1)
{
break;
}
}
}
選擇排序
簡單選擇排序
演算法思想
對數列進行遍歷,每一次遍歷都找出(選擇)其中最大的數,然後把他放到最後面,然後對其他的數繼續遍歷。進行到最後一次,也就是隻有一個元素了,沒有遍歷了,一個元素直接有序。整個陣列就有序了。這是一個基本排序,直接上程式碼了。(我是按一頭找,找最大,也可以雙頭找,找最大最小,速度快一倍)
演算法圖解
演算法分析
時間複雜度:O(n^2)
空間複雜度:O(1)
穩定性:不穩定
演算法實現
void SelectionSort(int arr[], int size)
{
int i = 0;
int j = 0;
for (i = size; i > 1; i--)
{
int MAX = 0;
for (j = 0; j < i; j++)
{
if (arr[j]>arr[MAX])
{
MAX = j;
}
}
int tmp = arr[i - 1];
arr[i - 1] = arr[MAX];
arr[MAX] = tmp;
}
}
堆排序
演算法思想
這裡需要對堆有一定的瞭解,堆就是一個比較特殊的完全二叉樹,在最大堆裡,每個節點的值都大於其左右兩個孩子節點的值。這就是最大堆。反之就是最小堆。拿最大堆舉例子,每次堆頂的元素值,不就是當前數列的最大嗎?這不就成選擇排序裡的簡單排序了嗎?找完之後,將他和完全二叉樹裡最後一個結點的值進行交換,然後做一個自頂向下的自我調整,將他再次調整成一個完全二叉堆。第二次取最大的樹,這時我們需要將上一次找到的結點遮蔽掉,不然會陷入一個死迴圈。無數次找完之後,再按層序的思想將二叉樹裡的資料遍歷到一個數組當中,這時的陣列為一個有序的陣列。
這裡有一個需要自己理解的就是在選擇建大堆和建小堆的選擇,我們對比說明為什麼建大堆,比建小堆要好。每次拿走元素和末尾元素交換,調整時大堆交換完,下面也是滿足最大堆的要求,下面接著調整,可以調整成為一個完全二叉大堆。而選擇最小堆的話,無法自頂向下的調整,而需要自低向上的去調整,就等於重現建堆。如果我們學過了最大堆和最小堆都知道,最大堆的調整難度完全小於新建一個最小堆。所以優選建大堆。
演算法圖解
演算法分析
時間複雜度:O(nlog2n)
空間複雜度:O(1)
穩定性:不穩定
演算法實現
void AdjustDown(int arr[], int size, int root)
{
int left = 2 * root + 1;
int right = 2 * root + 2;
if (left >= size)
{
return;
}
int max = left;
if (right < size && arr[right] > array[left])
{
max = right;
}
if (arr[root] >= arr[max])
{
return;
}
Swap(arr + root, arr + max);
AdjustDown(arr, size, max);
}
void CreateHeap(int arr[], int size)
{
for (int i = size / 2 - 1; i >= 0; i--)
{
AdjustDown(arr, size, i);
}
}
void HeapSort(int arr[], int size)
{
CreateHeap(arr, size);
for (int i = 0; i < size; i++)
{
Swap(&arr[0], &arr[size - 1 - i]);
AdjustDown(arr, size - i - 1, 0);
}
}
交換排序
氣泡排序
演算法思想
這個應該是最基礎的排序,我大學接觸的第一個排序,冒牌排序,拿陣列第一個元素和第二個元素比,然後拿第二個在和第三個比,然後第三個和第四個比。第一遍遍歷結束,最大的數來到最末端,然後下次對剩下的比較。這裡我們叫一個FLAG,也是就是說如果冒泡一遍,完全沒有發生交換,那就是剩下的數字已經有序了,可以跳出,不需要再執行剩下的比較,完全是浪費。
演算法圖解
求你了,冒泡實在不想畫了,畫圖真的費時間,冒泡的圖能把人畫瘋。。。冒泡看不懂聊我QQ292217869
演算法分析
時間複雜度:O(n^2)
空間複雜度:O(1)
穩定性:穩定
演算法實現
void BubbleSort(int arr[], int size)
{
int i = 0;
int j = 0;
for (i = 0; i < size-1; i++)
{
for (j = 0; j < size-1-i; j++)
{
int count = 0;
if (arr[j] > arr[j + 1])
{
int tmp = 0;
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
count++;
}
if (count == 0)
{
return;
}
}
}
}
快速排序
演算法思想
我們老師給我們花了100個星星的重要,那就是非常重要,快速排序。名字就很囂張。。。言歸正傳,快排採用了分治演算法。把大問題,分解成小問題。首先我們先找一個基準值,基準值的尋找法,有很多,這裡我先用一個取邊上值得方法,找到基準值以後呢拿著這個基準值和所有陣列比較,使這個陣列中比基準值小的都放左邊,比基準值大的都放到右邊,然後就把原來陣列分成三塊,中間基準值,左邊都是比它小的,右邊都是比它大的。然後這兩個陣列,繼續分,一直分。直到他的終止條件,也就是小陣列有序了就停止,那麼什麼時候有序停止呢?小區間長度為1或者長度為0的時候,就是有序了。所有小陣列都有序了,那麼就是整個陣列有序了。只是原理,那麼問題,又來了,怎麼放左放右呢?我目前會三種。
Hover法,什麼意思呢?假設最邊上是基準值,剩下的元素最右是一個Begin指標,最左是一個End指標,之前說放右邊的,放Begin的右邊,指標向左移動,之前放左邊的放End左邊,所以End指標向右移動。慢慢的慢慢的,Begin和End指標指向了同一塊空間,就代表著結束了,那麼我們的基準值的位置,就是這個相同地址的位置。
挖坑法,挖坑法應該叫邊挖邊填坑法比較好。主要就是先把基準值(我們這裡找最右邊的)的位置,挖下來,賦值給pivot,那麼最右邊的位置是坑,然後左邊的指標尋找比pivot大的,找到了,放進坑裡,這個位置形成了一個新的坑,然後從右再往左找。遇見了,再挖再填,直到兩個指標指向相同的空間。把最新挖出的數,填進去,一遍就好了。那麼整個陣列就變成有序的陣列了。
左右指標法,怎麼說呢,HOVER法的圖形畫出來,不就是,小的,不知道的,大的,基準值。而左右指標法,就是小的,大的,不知道的,基準值。就是開始定義兩個指標(其實也可以不用),他們都指向最左邊,一個叫cur,一個叫div,我們保證,div的左邊都是比基準值小的。然後開始遍歷cur,當cur比基準值小的時候,cur和div換一下資料,然後,div往前走一格,保證左邊比它小。cur繼續遍歷,直到cur走到基準值身邊,代表遍歷完了,交換cur和div的值。就好了。
演算法圖解
演算法分析
時間複雜度:O(nlog2n)
空間複雜度:O(nlog2n)
穩定性:不穩定
演算法實現
int Partition3(int arr[], int left, int right)//左右指標法
{
int cur = left;
int div = left;
while (cur < right)
{
if (arr[cur] < arr[right])
{
int tmp = arr[cur];
arr[cur] = arr[div];
arr[div] = tmp;
div++;
}
cur++;
}
int tmp = arr[cur];
arr[cur] = arr[div];
arr[div] = tmp;
return div;
}
int Partition2(int arr[], int left, int right)//挖坑法
{
int begin = left;
int end = right;
int pivot = arr[end];
while (begin < end)
{
while (begin < end&&arr[begin] <= pivot)
{
begin++;
}
arr[end] = arr[begin];
while (begin < end&&arr[end] >= pivot)
{
end--;
}
arr[begin] = arr[end];
}
arr[begin] = pivot;
return begin;
}
int Partition1(int arr[], int left, int right)//hover法
{
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end&&arr[begin] <= arr[right])
{
begin++;
}
while (begin < end&&arr[end] >= arr[right])
{
end--;
}
int tmp = arr[begin];
arr[begin] = arr[end];
arr[end] = tmp;
}
int tmp = arr[begin];
arr[begin] = arr[right];
arr[right] = tmp;
return begin;
}
void __QuickSort(int arr[], int left, int right)
{
if (left == right)
{
return;
}
if (left > right)
{
return;
}
int div = Partition1(arr, left, right);//這裡選擇三種方法
__QuickSort(arr, left, div - 1);
__QuickSort(arr, div + 1,right);
}
void QuickSort(int arr[], int size)
{
__QuickSort(arr, 0, size - 1);
}
歸併排序
演算法思想
這裡和快排一樣,老師依舊是直接打了100個星星,太重要啦。歸併排序和快排很容易混淆,因為歸併排序也用到了分治演算法的思想。思路是,現在對一個數組進行排序,我們把陣列分為兩份,如果左邊陣列是有序的陣列了,右邊也是一個有序的陣列了,那麼我們把兩個數組合並起來,整個陣列就有序了,如果這兩個陣列不是有序呢?那我們繼續分,分分分,直到小區間只剩下一個元素的時候,那麼整個小區間就是有序的了。
演算法圖解
演算法分析
時間複雜度:O(nlog2n)
空間複雜度:O(1)
穩定性:穩定
演算法實現
void Merge(int arr[], int left, int mid, int right,int extra[])
{
int left_i = left;
int i = left;
int right_i = mid;
while (left_i < mid&&right_i < right)
{
if (arr[left_i] <= arr[right_i])
{
extra[i++] = arr[left_i++];
}
else
{
extra[i++] = arr[right_i++];
}
}
while (left_i < mid)
{
extra[i++] = arr[left_i++];
}
while (right_i < right)
{
extra[i++] = arr[right_i++];
}
for (i = left; i < right; i++)
{
arr[i] = extra[i];
}
}
void __MergeSort(int arr[], int left, int right,int extra[])
{
if (left == right - 1)
{//區間只有一個元素,就代表有序
return;
}
if (left >= right)
{//區間沒有元素了
return;
}
int mid = left + (right - left) / 2;
__MergeSort(arr, left, mid,extra);
__MergeSort(arr, mid, right,extra);
Merge(arr, left, mid, right,extra);
}
void MergeSort(int arr[], int size)
{
int *extra = (int *)malloc(sizeof(int)*size);
__MergeSort(arr, 0, size,extra);
free(extra);
}
基數排序
演算法思想
基數排序是對雜湊演算法的運用,我們將一個數組,每一個數據,按照個位0-9存入雜湊桶,得到一個個位排序的數列,並在雜湊桶裡對重複數字進行標記。取出,然後在對十位進行同樣的操作,百位,千位。等等。直到最大的資料,的最高位。經過這無數次排序後,一個數列就有序了。
演算法圖解
演算法分析
時間複雜度:O(d(r+n))
空間複雜度:O(rd+n)
穩定性:穩定
演算法實現
int GetMaxDigit(int arr[], int size)
{
int digit = 1;
int base = 10;
int i = 0;
for (i = 0; i < size; i++)
{
while (arr[i] >= base)
{
++digit;
base *= 10;
}
}
return digit;
}
void RadixSort(int arr[], int size)
{
int i = 0;
int j = 0;
int k = 0;
int digit = GetMaxDigit(arr, size);
Node **array = (Node *)malloc(sizeof(Node)*size);
for (k = 0; k < digit; k++)
{
for (i = 0; i < size; i++)
{
array[i] = NULL;
}
for (i = 0; i < size; i++)
{
int index = (arr[i] / pow(10, digit)) % 10;
Node *node = (Node *)malloc(sizeof(Node));
node->Data = arr[i];
if (array[index] == NULL)
{
array[index] = node;
node->Next = NULL;
}
Node *cur = array[index];
while (cur->Next != NULL)
{
cur = cur->Next;
}
cur->Next = node;
node->Next = NULL;
}
for (i = 0; i < 10; i++)
{
Node *cur = array[i];
while (cur->Next != NULL)
{
arr[j++] = cur->Data;
cur = cur->Next;
}
}
}
free(array);
}
總結
演算法對比
再寫部落格期間也看了很多部落格,忘記從哪裡搞到一張排序的時間空間複雜度的對比圖(其實是作者懶癌發作)
再給大家一個高精度計時器的程式碼,可以自己測一下每種排序所消耗的時間。建議資料大一點,現在計算機跑這些500,1000個數據的程式碼,宛如張飛吃豆芽,大一點才能看到結果。
class HighPrecisionTimer
{
public:
HighPrecisionTimer(void)
{
QueryPerformanceFrequency(&CPU頻率);
}
~HighPrecisionTimer(void){}
void 開始()
{
QueryPerformanceCounter(&開始時間);
}
void 結束()
{
QueryPerformanceCounter(&結束時間);
間隔 = ((double)結束時間.QuadPart - (double)開始時間.Quadpart)/(double)CPU頻率.QuadPart;
}
double 間隔毫秒()const
{
return 間隔 *1000;
}
pricate:
double 間隔;
LARGE_INTEGER 開始時間;
LARGE_INTEGER 結束時間;
LARGE_INTEGER CPU頻率;
};
排序心得
寫大大的!快排,快排,快排!歸併!歸併!歸併!選擇選擇選擇!!
你們懂我意思吧!重中之重。
排序是資料結構中我認為最經典的東西。因為資料的有序,不僅對使用者來說,視覺的舒服。也方便我們再寫其他的功能使用。剛開始學習需要極度認真聽老師講。但是最重要的,是自己練,我加大一下字型
自己練
一定要動手,哪怕錯的,哪怕寫程式碼卡的再久再久,也要動手。不卡說明你沒理解,卡了才是真的思考。比如1.就是堆排序的選擇上。我卡了1天。基數排序,一開始思想錯的,卡了好久。但是卻能解決,寫了300多行沒用的程式碼。直到想明白了雜湊。瞬間寫出基數排序的程式碼。所以思考遠比程式碼重要(當然,該碼的你還得碼)