1. 程式人生 > >C語言實現八大排序演算法詳解及其效能之間的

C語言實現八大排序演算法詳解及其效能之間的

  • 概述

排序是資料結構中的重要一節,也是演算法的重要組成部分。主要分為內部排序以及外部排序,今天我們講內部排序,也就是八大排序。

插入排序

直接插入排序

演算法思想

名字已經暴露了他的演算法,就是往裡面插入資料,就拿我們生活中的例子來說,打撲克牌。我們往手裡碼牌的時候,是一張一張的碼,先碼一張,抓手心,不需要修改位置,因為只有一張牌,一定是有序的。再接一張,和手裡的牌對比大小,調整位置,選擇放在它的左邊或者右邊。然後接著碼,又接到一張牌,拿到先和右邊的牌比,比右邊還大就放到最右邊,如果比右邊這張小呢,在和左邊這張比。同樣,我們這裡也是這樣的,首先我們預設第一個元素,一定是有序,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的值。就好了。

演算法圖解

  • Hover法

在這裡插入圖片描述

  • 挖坑法

在這裡插入圖片描述

演算法分析

時間複雜度: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多行沒用的程式碼。直到想明白了雜湊。瞬間寫出基數排序的程式碼。所以思考遠比程式碼重要(當然,該碼的你還得碼)

以上是我對外部八大排序的理解,如果有什麼需要改進的地方,希望個位大佬指點。