我們通常說的排序算法指的是內部排序算法,即數據在內存中進行排序。
首先先來看一下我們學過的排序都有什麼?
排序可以大的方面分爲比較排序和非比較排序?
比較排序有:
1、冒泡排序
2、選擇排序
3、插入排序
(1)二分插入排序
(2)希爾排序
4、歸併排序
5、堆排序
6、快速排序
非比較排序有:
1、計數排序
2、基數排序
3、桶排序
我們先對各種排序進行對比:
我們看到有一個列名叫做穩定性,穩定性的概念是這樣定義的:
如果Ai==Aj;排序前Ai在Aj之前,排序後Ai還在Aj之前,則稱這種排序算法是穩定的。通俗的講就是保證排序前後兩個相等的數的相對順序保持不變
排序算法穩定性的好處:排序算法如果是穩定的,那麼從一個鍵上排序然後再從另一個鍵排序的結果可以爲後一個鍵排序所用。
下面開始逐一進行介紹:
1、冒泡排序
冒泡排序是一個很簡單的排序方法,它重複走過要排序的元素,依次比較相鄰兩個元素,如果順序錯誤就把它們交換,然後經過數次交換之後越大(越少)的數會由交換慢慢浮到數列的頂端
冒泡排序的算法:
1、比較相鄰的元素,如果前一個比後一個大,就把它們交換位置
2、對每一對相鄰元素作相同的操作,從開始的一對到結束的一對,最後的元素就是最大的數
3、針對所有的元素作相同的工作,除了最後一個
4、持續每次對越來越少的元素進行如上操作,直到沒有一對數字需要比較
代碼如下:
#include<stdio.h>
void BubbleSort(int A[],int size)
{
for (int j= 0; j < size - 1; j++)//控制每次循環最大元素浮動到數組最後
{
for (int i = 0; i < size - 1- j; i++)//控制左右相鄰的比較
{
if (A[i]>A[i + 1])
{
int tmp = A[i];
A[i] = A[i + 1];
A[i + 1] = tmp;
}
}
}
}
int main()
{
int A[] = {2,6,4,8,1,5,9};
int size = sizeof(A) / sizeof(A[0]);
BubbleSort(A, size);
for (int i = 0; i < size ; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果如下:
(1)冒泡排序的改進:雞尾酒排序
雞尾酒排序也叫定向冒泡排序,是冒泡排序的一種改進,不同點是此算法從低到高然後從高到低,而冒泡排序僅僅從低到高去比較序列中的每個元素,此算法相比而言效率更高。
#include<stdio.h>
void Swap(int A[], int i, int j)
{
int tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
void BubbleSort(int A[], int size)
{
int left = 0;
int right = size - 1;//對邊界進行初始化
while (left < right)
{
for (int i = 0; i < right; i++)//前半段控制將最大的元素放在後面
{
if (A[i]>A[i + 1])
{
Swap(A, i, i + 1);
}
}
right--;
for (int j = right; j > left; j--)//後半段控制將最小元素放在最前面
{
if (A[j - 1] > A[j])
{
Swap(A, j - 1, j);
}
}
left++;
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
BubbleSort(A, size);
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
2、選擇排序
選擇排序也是一種非常簡單的排序算法,工作原理是:初始時在序列中找到最小(最大)元素,放在序列的初始位置作爲已排序序列,然後再從剩餘未排序序列中找最小(最大)元素,放在已排序序列的末尾,一直持續,直到所有元素排序完畢
選擇排序和冒泡排序的區別是:冒泡排序通過依次變換兩個相鄰元素順序不合法的位置,從而將當前最小(最大)元素放到合適的位置,而選擇排序是每遍歷依次都記住當前最小(最大)元素的位置,最後僅需一次操作就可以將元素放在合適的位置。
代碼如下:
#include<stdio.h>
void Swap(int A[], int i, int j)
{
int tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
void SelectSort(int A[], int size)
{
for (int i = 0; i < size - 1; i++)
{
int min = i;
for (int j = i + 1; j < size - 1; j++)
{
if (A[min] > A[j])
min = j;
}
if (min != i)
Swap(A, min, i);
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
SelectSort(A, size);
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
3、插入排序
插入排序是一種非常直觀的排序算法,它的工作原理類似於玩撲克牌。
對於未排序數據,在已排序序列中找到相應的位置並插入
具體算法:
(1)從第一個元素開始,該元素可以認爲已經被排序
(2)取出下一個元素,在已經排序的元素序列中從後向前掃描
(3)如果該元素大於新元素,將該元素移到下一位置
(4)重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
(5)將新元素插在該位置之後
(6)重複步驟2-5
代碼如下:
#include<stdio.h>
void InsertSort(int A[], int size)
{
for (int i = 0; i < size; i++)
{
int get = A[i];
int j = i - 1;
while (j >= 0 && A[j]>get)
{
A[j+1 ] = A[j];
j -- ;
}
A[j + 1] = get;
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
InsertSort(A, size);
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
插入排序不適合對數據量比較大的排序應用,但是如果排序的數據量很小,比如數據量小於千,那麼插入排序還是一個不錯的選擇,stl中的sort和stdlib中的qsort都將插入排序作爲快速排序的補充
4、插入排序的改進二分插入排序
對於插入排序,如果比較操作的代價比交換操作大的話,可以先採用二分查找來減少比較操作的次數,我們稱爲二分插入排序
代碼如下:
#include<stdio.h>
void HalfInsertSort(int A[], int size)
{
for (int i = 1; i < size; i++)
{
int get = A[i];//右手抓到一張撲克牌
int left = 0;//左手上的牌是已排序的
int right = i - 1;//手牌左右邊界進行初始化
while (left <= right)//二分法確定新牌的位置
{
int mid = left + ((right - left) >> 1);
if ( A[mid] > get)
right = mid - 1;
else
left = mid + 1;
}
for (int j = i - 1; j >= left; j--)//將新牌吃哈如位置\
後面的元素依次向後移動一個位置
{
A[j + 1] = A[j];
}
A[left] = get;
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
HalfInsertSort(A, size);
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
當較大時,二分插入排序的比較次數比直接插入排序的最差情況好的多,但比直接插入排序的最好情況要差,所以當以元素初始序列已經接近升序時,直接插入排序比二分插入排序比較次數少。
5、插入排序的更高效改進:希爾排序
希爾排序又叫做遞減增量排序,是插入排序的一種更高效的改進版本,希爾排序是不穩定的排序算法
希爾排序是基於插入排序的以下兩種性質而提出改進辦法的
(1)插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
(2)但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動移一位
希爾排序通過將比較的全部元素分爲幾個區域來提升插入排序的性能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步,然後算法再取越來越小得步長,進行排序,算法的最後一步就是普通的插入排序,但是到了這一步,需要排序的元素幾乎是已經排好的了
代碼如下:
#include<stdio.h>
void ShellSort(int A[], int size)
{
int h = 0;
while (h <= size)//確定初始增量
{
h = h * 3 + 1;
}
while (h >= 1)
{
for (int i = h; i < size; i++)
{
int j = i - h;
int get = A[i];
while (j >= 0 && A[j]>get)
{
A[j + h] = A[j];
j = j - h;
}
A[j + h] = get;
}
h = (h - 1) / 3;//遞減增量
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
ShellSort(A, size);
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
6、歸併排序
歸併排序是創建在歸併操作上的一種有效的排序算法,效率爲O(nlogn)
歸併排序的實現分爲遞歸實現和非遞歸實現。遞歸實現的歸併排序是算法設計中分治策略的典型應用,我們將一個大問題分成若干個小問題分別解決,用小問題的大單來解決整個大問題。非遞實現的歸併排序首先進行的是兩兩歸併,然後是四四歸併,然後是八八歸併,一直下去直到歸併了整個數組
歸併排序算法主要依賴歸併操作。歸併操作指的是將兩個已經排序的序列合併成一個序列的操作,歸併操作步驟如下:
(1)申請空間,使其大小爲兩個已排序序列之和,該空間用來存放合併後的序列
(2)設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
(3)比較兩個指針所指向的元素,選擇相對於小得元素放入到合併空間,並移動指針到下一位置
(4)重複步驟3直到某一指針到達序列尾
(5)將另一序列剩下的所有元素直接複製到合併序列尾
#include<stdio.h>
#include<malloc.h>
void Merge(int A[], int left, int mid, int right)
{
int size = right - left+1;
int *tmp = (int *)malloc(size*sizeof(int));
int i = left;
int j = mid + 1;
int index = 0;
while (i <= mid&&j <= right)
{
tmp[index++] = A[i] < A[j] ? A[i++] : A[j++];
}
while (i <= mid)
{
tmp[index++] = A[i++];
}
while (j <= right)
{
tmp[index++] = A[j++];
}
for (int k = 0; k < size; k++)
{
A[left++] = tmp[k];
}
}
void MergeSort(int A[], int left, int right)
{
if (left == right)
return;
int mid = left + ((right - left) >> 1);
MergeSort(A, left, mid);
MergeSort(A, mid + 1, right);
Merge(A, left, mid, right);
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
MergeSort(A, 0,size-1);
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
7、堆排序
堆排序是指利用堆這種數據結構所設計的一種選擇排序法。堆是一種近似完全安全二叉樹的結構(通常堆是通過一維數組來實現的),並滿足性質:以最大堆(也叫大根堆,大頂堆)爲例,其中父節點的值總是大於它的孩子節點。
堆排序的過程:
(1)由輸入的無序數組構造一個最大堆,作爲初始的無序區
(2)把堆頂元素(最大值)和堆尾元素互換
(3)把堆(無序區)的尺寸縮小1,並調用heapify(A,0)從新的堆頂元素開始進行堆調整
(4)重複步驟2,直到堆的尺寸爲1
代碼如下:
#include<stdio.h>
void Swap(int A[], int i, int j)//交換函數
{
int tmp = A[i];
A[i]=A[j];
A[j] = tmp;
}
void HeapModify(int A[], int i, int size)//從A[i]向下進行調整
{
int left_child = i * 2 + 1;//左孩子索引
int right_child = i * 2 + 2;//右孩子索引
int max = i;//選出當前節點與其左右孩子三者中最大值
if (left_child<size&&A[left_child] > A[max])
max = left_child;
if (right_child<size&&A[right_child]>A[max])
max = right_child;
if (max != i)
{
Swap(A, i, max);//將當前節點與其最大子節點進行交換
HeapModify(A, max, size);//遞歸調用,繼續從當前節點向下進行堆調整
}
}
int BuildHeap(int A[], int size)//創建堆,時間複雜度0(n)
{
int Heap_size = size;
for (int i = Heap_size/2-1; i >= 0; i--)//從每一個非葉子節點,\
開始向下進行堆調整
{
HeapModify(A, i, Heap_size);
}
return Heap_size;
}
void HeapSort(int A[], int size)
{
int Heap_size = BuildHeap(A,size);//建立一個最大堆
while(Heap_size > 1)//堆元素個數大於1,未完成排序
{
//將堆頂元素與堆的最後一個元素互換,並從堆中去掉最後一個元素
//此處交換操作很有可能把後面的元素的穩定性打亂,所以堆排序是不穩定的排序算法
Swap(A, 0, --Heap_size );
HeapModify(A,0, Heap_size);//從新的堆頂元素開始向下進行堆調整,時間複雜度O(logn)
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
HeapSort(A, size);
printf("堆排序\n");
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果如下:
8、快速排序
快速排序共有三種方法:基準值法,挖坑法和兩個指針法
下面依次進行介紹:
(1)基準值法,取第一個或最後一個作爲基準值(可以用三數取中法進行優化),定義兩個標記left和right,left指向起始位置,right指向結束位置,right從後往前找直到找到比基準值小的停下來,left從前往後找,找到比基準值大的停下來,然後交換arr[left]和arr[right]的值,重複上述動作直到left>=right然後將基準值和當前停下來的位置進行交換,這樣整個數組玖分成兩半,左一半是比基準值小得,右一半是比基準值大的,然後遞歸處理整個數組就可以了。
代碼如下:
#include<stdio.h>
void Swap(int A[], int i, int j)
{
int tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
int position(int A[], int left, int right)
{
int key = left;
while (left < right)
{
while (left < right&& A[key] > A[left]){
++left;
}
while (left < right&& A[key] < A[right])
{
--right;
}
if (left < right)
{
Swap(A, left, right);
}
}
Swap(A, key, left);
return left;
}
void QuickSort(int A[], int left, int right)
{
if (left < right)
{
int n =position(A, left, right);
QuickSort(A, left, n - 1);
QuickSort(A, n+1 , right);
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
QuickSort(A, 0,size-1);
printf("快速排序\n");
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
(2)挖坑法
算法描述:這個算法也是從基準值法演變而來的,首先取首元素爲基準值,用變量key來保存就相當於被挖走了,就形成了一個坑,然後從後往前找比key值小的填到第一個的位置,那麼array[right]這裏又是一個坑了,所以再讓left從左往右找比key大的元素,(然後將array[right])的坑填了,然後讓right繼續從後面找填上一個坑,以此類推,直到left==right此時此處必定爲一個坑,然後將key值填進去就好了,這個時候數組就分爲兩組和上述情況一樣,然後遞歸調用就可以了
代碼如下:
#include<stdio.h>
int position(int A[], int left, int right)
{
int key = A[left];
while (left < right)
{
while (left < right&&key < A[right])
{
right--;
}
if (left < right)
{
A[left] = A[right];
}
while (left<right&&key>A[left])
{
left++;
}
if (left < right)
A[right] = A[left];
}
A[left] = key;
return left;
}
void QuickSort(int A[], int left, int right)
{
if (left < right)
{
int n = position(A, left, right);
QuickSort(A, left, n - 1);
QuickSort(A, n + 1, right);
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
QuickSort(A, 0, size - 1);
printf("快速排序2\n");
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果:
3、兩個指針法
算法描述:同樣取最後一個元素爲基準值,然後定義兩個指針(指向下標的所以這裏說是指針),fast指向left,slow指向left-1,然後fast開始走,找到比array[right]小得元素則slow開始走,當slow走完以後判斷fast是否等於slow如果不等於交換arr[slow] 和arr[fast],相等的話不交換,然後fast繼續走,如果所指的數都大於arr[right]則slow不動當fast==slow時循環結束,此時slow++然後交換arr[slow]和arr[right],此時數組被分爲兩部分遞歸調用。
代碼如下:
#include<stdio.h>
void Swap(int A[],int i, int j)
{
int tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
void TreeNumberGetMid(int A[], int left, int right)
{
int mid = left + ((right - left) >> 1);
if (A[mid] > A[left] && A[mid] < A[right])
{
Swap(A,mid,right);
return;
}
if (A[left]>A[mid] && A[right] < A[right])
{
Swap(A,left, right);
return;
}
if (A[right]>A[mid] && A[right] < A[left])
return;
}
int position(int A[], int left, int right)
{
TreeNumberGetMid(A, left, right);
int fast = left;
int slow = left - 1;
while (fast < right)
{
if (A[fast] < A[right])
{
++slow;
if (slow != fast)
Swap(A,fast, slow);
}
fast++;
}
++slow;
Swap(A,slow, right);
return slow;
}
void QuickSort(int A[], int left, int right)
{
if (left < right)
{
int n = position(A, left, right);
QuickSort(A, left, n - 1);
QuickSort(A, n + 1, right);
}
}
int main()
{
int A[] = { 2, 6, 4, 8, 1, 5, 9 };
int size = sizeof(A) / sizeof(A[0]);
QuickSort(A, 0, size - 1);
printf("快速排序3\n");
for (int i = 0; i < size; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
運行結果如下:
下面來介紹非比較排序:
1、計數排序
首先遍歷一遍原數組,找到最大的數和最小的數,將每個數出現的次數用一個大小充足的數組進行保存,然後遍歷這個標記數組對數組進行還原,就得到排好序的序列
#include<iostream>
using namespace std;
void CountSort(int arr[], int size)
{
int max = arr[0];
int min = arr[0];
for (int i = 1; i < size; i++)
{
if (arr[i]>max)
max = arr[i];
if (arr[i] < min)
min = arr[i];
}
int _size = max - min + 1;
int *temp = new int[_size];
memset(temp, 0x00, _size * 4);
for (int i = 0; i < size; i++)
{
temp[arr[i] - min]++;
}
int index = 0;
for (int i = 0; i < _size; i++)
{
while (temp[i]--)
{
arr[index++] = i + min;
}
}
delete[] temp;
}
int main()
{
int arr[] = { 0, 2, 5, 6, 9, 8, 8, 6, 7, 3 };
int size = sizeof(arr) / sizeof(arr[0]);
CountSort(arr, size);
cout << "計數排序" << endl;
for (int i = 0; i < size; i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
運行結果:
2、基數排序:
先按照個位數將數據放進對應的桶裏面,然後將數據按照順序寫回原數組,然後再按照十位數將數據放進對應的桶裏面,然後按照順序寫回原數組,一次類推直到所有位數都比較完成。
代碼如下:
void BaseSort(int arr[], int size)
{
int count = 1;//位數
int elem = 1;
int radix = 10;
int *temp = new int[size];
for (int i = 0; i < size; i++)//求數組中最大數的位數
{
while (arr[i]>radix)
{
count++;
radix *= 10;
}
}
for (int i = 0; i < count; i++)
{
int count[10] = { 0 };
for (int j = 0; j < size; j++)
{
count[arr[j] / elem % 10]++;
}
int position[10] = { 0 };
for (int j = 1; j < 10; j++)
{
position[j] = position[j - 1] + count[j - 1];
}
for (int j = 0; j < size; j++)
{
int ret = arr[j] / elem % 10;
temp[position[ret]++] = arr[j];
}
memcpy(arr, temp, sizeof(arr[0])*size);
elem *= 10;
}
delete[] temp;
}
int main()
{
int arr[] = {2,3 , 5,2,6,1,0,8};
int size = sizeof(arr) / sizeof(arr[0]);
BaseSort(arr,size);
cout << "基數排序" << endl;
for (int i = 0; i < size; i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
運行結果如下:
3、桶排序
桶排序也叫箱排序。工作的原理是將數組元素映射到有限數量個桶裏,利用計數排序可以定位桶的邊界,每個桶再各自進行桶內排序
代碼如下;
#include<iostream>
using namespace std;
const int bn = 5;//桶的·個數
int c[bn];//計數數組
int MapToBucket(int x)//映射函數
{
return x / 10;
}
void CountSort(int arr[], int size)//找邊界
{
for (int i = 0; i < size; i++)
{
c[i] = 0;
}
for (int i = 0; i < size; i++)
{
c[MapToBucket(arr[i])]++;
}
for (int i = 1; i < size; i++)
{
c[i] = c[i] + c[i - 1];
}
int *B = new int[size];
for (int i = size - 1; i >= 0; i--)
{
int b = MapToBucket(arr[i]);
B[--c[b]] = arr[i];
}
for (int i = 0; i < size; i++)
{
arr[i] = B[i];
}
delete[] B;
}
void InsertSort(int arr[], int left, int right)
{
for (int i = left + 1; i <= right; i++)
{
int get = arr[i];
int j = i - 1;
while (j >= left&&arr[j] > get)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = get;
}
}
void BucketSort(int arr[], int size)
{
CountSort(arr, size);
for (int i = 0; i < bn; i++)
{
int left = c[i];
int right = (i == bn - 1 ? size - 1 : c[i + 1] - 1);
if (left < right)
{
InsertSort(arr, left, right);
}
}
}
int main()
{
int arr[] = { 2, 3, 5, 0, 8, 9, 4 };
int size = sizeof(arr) / sizeof(arr[0]);
BucketSort(arr, size);
cout << "桶排序" << endl;
for (int i = 0; i < size; i++)
{
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
運行結果: