排序算法小結
定義
排序算法的核心是比較和移動,排序算法分類爲內部排序和外部排序,區別的要點是排序過程是否需要外部的內存交換過程;也可以按照算法的思路分爲比較排序和非比較排序;
排序算法的穩定性,若排序對象中存在多個關鍵字相同的記錄,經過排序後,相同關鍵字的記錄之間的相對次序保持不變,則該排序方法是穩定的,若次序發生變化(哪怕只有兩條記錄之間),則該排序方法是不穩定的;
時間複雜度,一般情況下,算法中基本操作重複執行的次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得T(n)/f(n)的極限值(當n趨近於無窮大時)爲不等於零的常數,記T(n)=O(f(n))T(n),O(f(n))爲算法的漸進時間複雜度,簡稱時間複雜度;
空間複雜度,空間複雜度(Space Complexity)是對一個算法在運行過程中臨時佔用存儲空間大小的量度,它是問題規模n
n的函數,記做S(n)=O(f(n))。比如直接插入排序的時間複雜度是O(n2),空間複雜度是O(1)。而一般的遞歸算法就要有O(n)的空間複雜度了,因爲每次遞歸都要存儲返回信息,需要輔助空間的大小隨着n的增大線性增大。
1、冒泡排序
基本原理:
N個元素,只剩一個首元素的時候,不需要比較,總共需要循環比對N-1次,每一次選出其中最大或者最小的數冒出到尾端;
在每一個循環 i 中,從當前循環的第一元素開始比較到最後一個,因此共有N-1-i次內部循環,內部進行比較操作,符合條件的兩數,則移動雙方位置。
改進:
1、如果某次冒泡不存在數據交換,則說明已經排序好了,可以直接退出排序
2、頭尾進行冒泡,每次把最大的沉底,最小的浮上去,兩邊往中間靠1
~~雞尾酒排序與冒泡排序~~ 的不同處在於排序時是以首尾雙向在序列中進行排序。
先對數組從左到右進行升序的冒泡排序;
再對數組進行從右到左的降序的冒泡排序;
以此類推,持續的、依次的改變冒泡的方向,並不斷縮小沒有排序的數組範圍。
2、直接插入排序
基本原理:
將第1和第2元素排好序,然後將第3個元素插入到已經排好序的元素中,依次類推,直到輪詢到最後一個元素。
for (int i = 1; i < n; i++)//從第二個元素開始,依次與前一個元素比大小
{
if (a[i] < a[i - 1])//若前面的元素大於後面的元素,則尋找後面元素本應正確的位置
{
temp = a[i];
for (j = i - 1; j >= 0 && a[j] > temp; j--)
{
a[j + 1] = a[j];
}
a[j + 1] = temp;//將無序元素放入到正確的位置
}
}
3、希爾排序
基本原理:
插入排序每次只能操作一個元素,效率低。元素個數N,取奇數k=N/2,將下標差值爲k的數分爲一組(一組元素個數看總元素個數決定),在組內構成有序序列,再取k=k/2,將下標差值爲k的數分爲一組,構成有序序列,直到k=1,然後再進行直接插入排序。
設置一個最小增量dk,剛開始dk設置爲n/2。進行插入排序,隨後再讓dk=dk/2,再進行插入排序,直到dk爲1時完成最後一次插入排序,此時數組完成排序。
例如,現在要對序列{ 1,8,3,9,5,7 }進行希爾排序
第一步,選擇增量爲3,進行排序:
則產生三個分組(括號爲數組元素下標)
{ 1(0),9(3) }
{ 8(1),5(4) }
{ 3(2),7(5) }
然後對該所有分組進行直接插入排序,得到新的分組(注意:此時下標是不會變的,只是用直接插入排序進行數組排序)
{ 1(0),9(3) }
{ 5(1),8(4) }
{ 3(2),7(5) }
因此按照下標,我們可以寫出第一趟按照增量爲3的希爾排序得到的序列:{ 1,5,3,9,8,7 }
第二步,選擇增量爲1,進行排序
這一步,直接按照直接插入排序的方法,進行直接插入排序,得到有序序列:{ 1,3,5,7,8,9 }
// 初始時從dk開始增長,每次比較步長爲dk
void Insrtsort(int *a, int n,int dk) {
for (int i = dk; i < n; ++i) {
int temp = a[i];
for(j = i-dk; j >=0 && a[j] > temp; j=j-dk)
{
a[j+dk] = a]j];
}
a[j+dk] = tmp; // 插入tmp
}
}
}
void ShellSort(int *a, int n) {
int dk = n / 2; // 設置初始dk
while (dk >= 1) {
Insrtsort(a, n, dk);
dk /= 2;
}
}
4、簡單選擇排序
基本原理:
每次循環選出當前未排序的列表中最小的數,因此每次排序完後的,分爲有序組和無序組,無序組的容量會逐漸減少,外部循環N-1次。每一次循環內,並不是比較大小出現衝突就換,而是記錄下min的下標位置,一次循環完後才移動一次位置。
for (int i = 0; i < n; i++)
{
int key = i; // 臨時變量用於存放數組最小值的位置
for (int j = i + 1; j < n; j++) {
if (a[j] < a[key]) {
key = j; // 記錄數組最小值位置
}
}
if (key != i)
{
int tmp = a[key];
a[key] = a[i];
a[i] = tmp; // 交換最小值
}
}
5、堆排序
推薦博文:
堆排序詳解
圖解堆排序過程
詳細的遞歸代碼
【基本原理】
先把數組構造成一個大頂堆(父親節點大於其子節點),然後把堆頂(數組最大值,數組第一個元素)和數組最後一個元素交換,這樣就把最大值放到了數組最後邊。把數組長度n-1,再進行構造堆,把剩餘的第二大值放到堆頂,輸出堆頂(放到剩餘未排序數組最後面)。依次類推,直至數組排序完成。
堆分爲大根堆和小根堆 | 完全二叉樹 |
---|---|
大根堆 | 任意一個父節點的值大於等於其子節點的值【用於升序排列】 |
小根堆 | 任意一個父節點的值小於等於其子節點的值【用於降序排列】 |
【大根堆】
以升序排序爲例,利用大根堆的性質(堆頂元素最大)不斷輸出最大元素,直到堆中沒有元素
1.構建大根堆
2.輸出堆頂元素與堆底元素交換
3.將堆低元素放一個到堆頂,再重新構造成大根堆,再輸出堆頂元素,以此類推
【代碼思路】
○ ○ ○ ○ ○ ○ ○
根 左 右
1、若根的索引爲0開始,最遠的非葉子節點(最後的父節點)按照層次排列:i = n/2 -1 2、(n爲數目,i爲索引)——其左孩紙(2i+1) 右孩子(2i+2)
若根的索引爲1開始,則最遠的非葉子節點(最後的父節點)按照層次排列:i = n/2
——其左孩紙(2i) 右孩子(2i+1)
#include <stdio.h>
#include <stdlib.h>
void swap(int* a, int* b)
{
int temp = *b;
*b = *a;
*a = temp;
}
//從頂到尾迭代實現每一個父節點和子結點的比較和交換
void max_heapify(int arr[], int start, int end)
{
//建立父節點指標和子節點指標
int dad = start;
int son = dad * 2 + 1;
while (son <= end) //若子節點指標在範圍內才做比較
{
if (son + 1 <= end && arr[son] < arr[son + 1])
//先比較兩個子節點大小,選擇最大的
son++;
if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數
return;
else //否則交換父子內容再繼續子節點和孫節點比較
{
swap(&arr[dad], &arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len)
{
int i;
//初始化,i從最後一個父節點開始調整
//建立一個初始化的大頂堆
for (i = len / 2 - 1; i >= 0; i--)
//迭代實現堆的構建
max_heapify(arr, i, len - 1);
//先將第一個元素和已排好元素前一位做交換,再重新調整,直到排序完畢
for (i = len - 1; i > 0; i--)
{
swap(&arr[0], &arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main() {
int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
6、快速排序
基本原理:
選擇一個基準元素,比基準元素小的放基準元素的前面,比基準元素大的放基準元素的後面,這種動作叫分區,每次分區都把一個數列分成了兩部分,每次分區都使得一個數字有序,然後將基準元素前面部分和後面部分繼續分區,一直分區直到分區的區間中只有一個元素的時候。
快速排序的一趟算法思想是:
1.設置兩個變量 i , j ,令 i = 0 ;j = n - 1 ;
2.以第一個數組元素作爲關鍵數據,賦值給key,即 key = A[0];
3.從 j 開始向前搜索,即由後開始向前搜索(j- -),找到第一個小於key的值A[j],將A[j]和A[i]互換;
4.從 i 開始向後搜索,即由前開始向後搜索(i++),找到第一個大於key的值A[i],將A[i]和A[j]互換;
5.重複第3、4步,直到 i = j 結束。
領域:
原本在正確位置的元素是不會移動,每一次前後的指針相同時,表示一次排序完成,接着以該位置分爲前後兩個部分,重複操作。
//快速排序
void QuickSort(int a[], int left, int right)
{
if (left >= right) {
return;
}
// 定義兩個標識
int i = left;
int j = right;
int key = a[left];//將序列的第一個元素設爲基數
int temp;
while (i < j)
{
while (i < j && a[j] >= key)
{
j--;
}
if (i != j)//如果i j沒有相遇,交換位置
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
while (i < j && a[i] <= key)
{
i++;
}
if (i != j)//如果i j沒有相遇,交換位置
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
else
{
printArr(a, 8);//打印數組
}
}
QuickSort(a, left, i - 1);
QuickSort(a, i + 1, right);
}
7、歸併排序
基本原理:分治思想
將一個無序的數列一直一分爲二,直到分到序列中只有一個數的時候,這個序列肯定是有序的,因爲只有一個數,然後將兩個只含有一個數字的序列合併爲含有兩個數字的有序序列,這樣一直進行下去,最後就變成了一個大的有序數列
// 合併兩個已排好序的數組
void Merge(int a[], int left, int mid, int right)
{
int len = right - left + 1; // 數組的長度
int *temp = new int[len]; // 分配個臨時數組
int k = 0;
int i = left; // 前一數組的起始元素
int j = mid + 1; // 後一數組的起始元素
while (i <= mid && j <= right)
{
// 選擇較小的存入臨時數組
temp[k++] = a[i] <= a[j] ? a[i++] : a[j++];
}
while (i <= mid)
{
temp[k++] = a[i++];
}
while (j <= right)
{
temp[k++] = a[j++];
}
for (int k = 0; k < len; k++)
{
a[left++] = temp[k];
}
}
// 遞歸實現的歸併排序
void MergeSort(int a[], int left, int right)
{
if (left == right) //結束的標誌
return;
int mid = (left + right) / 2;
MergeSort(a, left, mid);
MergeSort(a, mid + 1, right);
Merge(a, left, mid, right);
}
8、桶排序
桶排序算法詳解
1、注意桶範圍的選取、排序數值的最小值和最大值的差、或者將原數按照相同的過程進行處理
2、注意相同數的排序
#include <stdio.h>
int main()
{
int book[1001],i,j,t,n;
for(i=0;i<=1000;i++)
book[i]=0;
scanf("%d",&n);//輸入一個數n,表示接下來有n個數
for(i=1;i<=n;i++)//循環讀入n個數,並進行桶排序
{
scanf("%d",&t); //把每一個數讀到變量t中
book[t]++; //進行計數,對編號爲t的桶放一個小旗子
}
for(i=1000;i>=0;i--) //依次判斷編號1000~0的桶
for(j=1;j<=book[i];j++) //出現了幾次就將桶的編號打印幾次
printf("%d ",i);
getchar();getchar();
return 0;
}