排序(冒泡,插入,希爾,歸併,快排,選擇,堆排序,桶排序,基數排序,雞尾酒排序)
穩定的排序 | 時間複雜度 | 空間複雜度 |
氣泡排序 | 最差、平均O(n^2),最好O(N) | 1 |
雞尾酒排序(雙向氣泡排序) | 最差、平均O(n^2),最好O(N) | 1 |
插入排序 | 最差、平均O(n^2),最好O(N) | 1 |
歸併排序 | 最差、平均、最好O(nlogn) | n |
桶排序 | n | k |
基數排序 | dn | n |
二叉樹排序 | nlogn | n |
圖書館排序 | nlogn | (1+e)n |
不穩定的排序 | 時間複雜度 | 空間複雜度 |
選擇排序 | 最差、平均n^2 | 1 |
希爾排序 | nlogn | 1 |
堆排序 | 最差、平均、最好O(nlogn) | 1 |
快速排序 | 平均nlogn,最壞n^2 | logn |
快速排序
分而治之,快排最好的情況,選主元每次正好中分,T(N) = O(NLogN),最壞情況,O(n*n)
1.選主元
取頭、中、尾的中位數
//選主元,取頭、中、尾的中位數 int median3(int A[], int left, int right) { int center = (left + right) / 2; if(A[left] > A[center]) swap(&A[left], &A[center]); if(A[left] > A[right]) swap(&A[left], &A[right]); if(A[center] > A[right]) swap(&A[center], &A[right]); //交換後,A[left]<=A[center]<=A[right] swap(&A[center], &A[right - 1]);//將pivot藏到右邊 //只需考慮A[left+1] ... A[right-2] return A[right - 1]; }
2.子集劃分
特殊情況:如果有元素正好等於pivot,停下來交換。這樣的話可以儘可能將兩邊的子集劃分的均衡
在小規模資料直接呼叫簡單排序,大規模資料就用快排
快排的過程如下:
快排就是在一趟排序後,主元到了它該去的位置,即,它左邊的數都是比它小的,右邊的數都是比它大的
#include<iostream> using namespace std; void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } //選主元,取頭、中、尾的中位數 int median3(int A[], int left, int right) { int center = (left + right) / 2; if(A[left] > A[center]) swap(&A[left], &A[center]); if(A[left] > A[right]) swap(&A[left], &A[right]); if(A[center] > A[right]) swap(&A[center], &A[right]); //交換後,A[left]<=A[center]<=A[right] swap(&A[center], &A[right - 1]);//將pivot藏到右邊 //只需考慮A[left+1] ... A[right-2] return A[right - 1]; } void quick_sort(int A[], int left, int right) { if(left >= right) return; int pivot = median3(A, left, right); int i = left; int j = right - 1; for(;;) { while(i<right-1 && A[++i] < pivot){//注意i的範圍 } while(j>-1 && A[--j] > pivot){//注意j的範圍,之前就就是因為i,j的範圍沒限定,導致一直有錯 } if(i < j) { swap(&A[i], &A[j]); } else { break; } } swap(&A[i], &A[right - 1]); quick_sort(A, left, i-1 ); quick_sort(A, i+1 , right); } int main() { int n; cout << "輸入元素個數: "; cin >> n; int *A = new int[n]; for(int i = 0; i < n; ++i) cin >> A[i]; quick_sort(A, 0, n-1); for(int i = 0; i < n; ++i) cout << A[i] << " "; cout << endl; delete[] A; return 0; }
快排的另一種寫法(主元是陣列第一個數)
#include<iostream>
using namespace std;
int partion(int A[],int low,int high)
{
int pivot = A[low];//用第一個數作為主元
while(low < high)
{
while(low < high && A[high] >= pivot)
--high;
A[low] = A[high];
while(low < high&&A[low] <= pivot)
++low;
A[high] = A[low];
}
A[low] = pivot;
return low;
}
void quick_sort(int A[], int low, int high)
{
if(low < high)
{
int pivot_pos = partion(A, low, high);
quick_sort(A, low, pivot_pos - 1);
quick_sort(A, pivot_pos + 1, high);
}
}
int main()
{
int n;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
quick_sort(A, 0, n - 1);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
氣泡排序
一趟排序後,將最大的元素放到了最後,然後再對最大元素之前的進行同樣的操作
最好情況,順序,T=O(N),最壞情況,逆序,T=O(N*N)
因為氣泡排序中,在前一個元素嚴格大於後一個元素時才交換,所以他是穩定的。
#include<iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void bubble_sort(int A[], int n)
{
for(int p = n - 1; p >= 0; p--)
{
int flag = 0;//用一個標記,如果沒改變的話,說明已經是排好序的,就不用再迴圈
for(int i = 0; i < p; ++i)
{
if(A[i]>A[i + 1])
{
swap(&A[i], &A[i + 1]);
flag = 1;
}
}
if(0 == flag)
break;
}
}
int main()
{
int n;
cout << "氣泡排序: " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
bubble_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
插入排序
把插入排序想象成摸撲克牌的過程,手裡首先有一張牌,A【0】,然後從A[1]開始摸,如果比前一張小,前一張就後移,直到遇到合適的位置,再將這張牌放入
最好情況,順序,T=O(N),最壞情況,逆序,T=O(N*N)
因為插入排序中,在嚴格大於時才後移,所以他是穩定的。
#include<iostream>
using namespace std;
void insertion_sort(int A[], int n)
{
for(int p = 1; p < n; p++)
{
int tmp = A[p];//現在摸到的牌,要將他放到它該在的位置
int i = p;
for(; i>0 && A[i - 1] > tmp; i--)
{
A[i] = A[i - 1];
}
A[i] = tmp;
}
}
int main()
{
int n;
cout << "插入排序 " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
insertion_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
希爾排序
希爾排序和插入排序有點像,不同在於插入時比較的數字之間的間隔不一樣
比如:81 94 11 96 12 35 17 95 28 58 41 75 15
先每隔5間隔的數做插入排序,那麼就是 81 35 41,他們就成了 35 41 81,在陣列中就是:
35 94 11 96 12 41 17 95 28 58 81 75 15
再對下一組“5-間隔”數 94 17 75
35 17 11 96 12 41 75 95 28 58 81 94 15
接下來同理,結束後,從【5-間隔】變成【3-間隔】,最後是【1-間隔】
增量序列,Dm > Dm-1 > ... > D1 =1
對每個Dk進行【Dk-間隔】的排序,Dk間隔有序,Dk-1間隔排序後,不會把Dk間隔排好的序給打亂,
原始希爾排序Dm = N/2 (取下界),Dk = (Dk+1) /2,(取下界),最壞時間T = (N^2)
缺陷:增量元素不互質,小增量可能不起作用。
改進:
Hibbard增量序列:Dk = 2^k -1,相鄰元素互質,最壞T = (n^1.5),平均時間O(n^1.25)
Sedgewick增量序列:{1,5,9,41,109,。。。} 9* 4^i - 9*2^i + 1 或 4^i - 3*2^i + 1,平均時間(n^7/6),最壞時間O(n^4/3)
#include<iostream>
using namespace std;
void shell_sort(int A[], int n)
{
for(int D = n / 2; D > 0; D = D / 2)//增量序列
{
for(int p = D; p < n; ++p)//插入排序
{
int tmp = A[p];
int i = p;
for(; i >= D && A[i - D]>tmp; i = i - D)//和插入排序不同的地方就是換成了D
{
A[i] = A[i - D];
}
A[i] = tmp;
}
}
}
int main()
{
int n;
cout << "希爾排序 " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
shell_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
歸併排序(遞迴和非遞迴)
歸併排序適用於外排序,它要另外開闢一個空間作為臨時的儲存,核心在於兩個有序子序列的合併。
遞迴:
#include<iostream>
using namespace std;
//核心,有序子列的歸併
void merge(int A[], int tmpA[], int l, int r, int rightend)
{
int leftend = r - 1;
int tmp = l;
int nums = rightend - l + 1;//元素個數
while(l <= leftend && r <= rightend)
{
if(A[l] <= A[r])
tmpA[tmp++] = A[l++];
else
tmpA[tmp++] = A[r++];
}
while(l <= leftend)
tmpA[tmp++] = A[l++];
while(r <= rightend)
tmpA[tmp++] = A[r++];
//把元素拷回A陣列
for(int i = 0; i < nums; i++, rightend--)
{
A[rightend] = tmpA[rightend];
}
}
//遞迴
void Msort(int A[], int tmpA[], int l, int rightend)
{
int center;
if(l < rightend){
center = (l + rightend) / 2;
Msort(A, tmpA, l, center);
Msort(A, tmpA, center+1, rightend);
merge(A, tmpA, l, center + 1, rightend);
}
}
void merge_sort(int A[], int n)
{
int *tmpA;
tmpA =(int*)malloc(n* sizeof(int));
if(tmpA != nullptr)
{
Msort(A, tmpA, 0, n - 1);
free(tmpA);
}
}
int main()
{
int n;
cout << "歸併排序(遞迴) " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
merge_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
非遞迴:
#include<iostream>
using namespace std;
//和merge不同在不用從tmpA拷回A
void merge1(int A[], int tmpA[], int l, int r, int rightend)
{
int leftend = r - 1;
int tmp = l;
int nums = rightend - l + 1;//元素個數
while(l <= leftend && r <= rightend)
{
if(A[l] <= A[r])
tmpA[tmp++] = A[l++];
else
tmpA[tmp++] = A[r++];
}
while(l <= leftend)
tmpA[tmp++] = A[l++];
while(r <= rightend)
tmpA[tmp++] = A[r++];
}
void merge_pass(int A[], int tmpA[], int n, int length)//length,當前子序列的長度
{
int i = 0;
for(; i <= n - 2 * length; i = i + 2 * length)
merge1(A, tmpA, i, i + length, i + 2 * length - 1);//A元素歸併到tmpA後,不拷回A
if(i + length < n)//歸併最後兩個序列
merge1(A, tmpA, i, i + length, n - 1);
else//只剩一個序列
{
for(int j = i; j < n; ++j)
tmpA[j] = A[j];
}
}
void merge_sort(int A[], int n)
{
int length = 1;
int *tmpA;
tmpA = (int*)malloc(n*sizeof(int));
if(tmpA != nullptr)
{
while(length < n)
{
merge_pass(A, tmpA, n, length);
length *= 2;
merge_pass(tmpA, A, n, length);//剛好歸併回A
length *= 2;
}
free(tmpA);
}
}
int main()
{
int n;
cout << "歸併排序(非遞迴) " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
merge_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
選擇排序
#include<iostream>
using namespace std;
void select_sort(int A[], int n)
{
for(int i = 0; i < n-1; ++i)
{
int min_index = i;
for(int j = i + 1; j < n; ++j)
{
if(A[j] < A[min_index])
min_index = j;
}
//從未排序的部分中找出值最小的,然後通過交換放到了有序部分的末尾
int temp = A[i];
A[i] = A[min_index];
A[min_index] = temp;
}
}
int main()
{
int n;
cout << "選擇排序 " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
select_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
堆排序
首先將陣列建成一個最大堆,然後將堆頂和陣列末尾交換,使最大的元素到末尾,再對剩下的部分調整成最大堆,再把堆頂交換到末尾,重複這個過程。
#include<iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void adjustheap(int A[], int parent, int n)
{
int child;
int temp = A[parent];//當前要調整的元素
//當前要調整的元素要和他的孩子相比較,但是不能越界,所以就有如下的迴圈條件
for(; parent*2+1 < n; parent = child)
{
child = parent * 2+1;
//如果有右孩子且右孩子的值大於左孩子
if((child +1 < n) && (A[child]) < A[child + 1])
child++;//找到了左右孩子中更大的
//如果temp更大,那麼現在這個位置他是可以“坐穩”的,跳出,否則就將更大的孩子提上來
if(temp >= A[child])
break;
else
A[parent] = A[child];
//一趟結束後,parent就去到更大的孩子的下標的位置
}
A[parent] = temp;
}
void heap_sort(int A[], int n)
{
for(int i = n / 2 - 1; i >= 0; i--)//從最後一個有孩子的結點開始調整,調整出一個最大堆
{
adjustheap(A, i, n);
}
//調整完之後,根結點就是最大值,將最大值交換到陣列的最後,然後對剩下的元素又重新調整成最大堆,重複這個過程
for(int i = n - 1; i >= 1; --i)
{
swap(&A[0], &A[i]);
adjustheap(A, 0, i);
}
}
int main()
{
int n;
cout << "堆排序 " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
heap_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
桶排序
桶排序需要知道輸入資料的範圍。假設N個學生,成績0~100,所以建立101個桶,M=101。掃描一遍所有學生的成績,將它放到對應的桶中。再掃面一遍 桶,輸出結果。時間O(M+N)。
#include<iostream>
using namespace std;
//假如是成績0~100,所以有101個桶
void bucket_sort(int A[], int n)
{
int grade[101] = { 0 };
for(int i = 0; i < n; ++i)
{
grade[A[i]]++;
}
for(int i = 0; i <= 100; ++i)
{
while(grade[i])
{
cout << i << " ";
grade[i]--;
}
}
}
int main()
{
int n;
cout << "桶排序 " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
bucket_sort(A, n);
delete[] A;
return 0;
}
基數排序
當資料範圍很大的時候,桶排序就不合適,所以用基數排序。基數排序分為最低位優先和最高位優先,以下程式碼用最低優先,即,先根據所有數的個位數排序,再對排了序的數根據十位數排序,再對排了序的數根據百位的數值大小排序……
以下程式碼基數為十進位制,最大為三位數
#include<iostream>
using namespace std;
//取一個數的個位,或者十位,或者百位……
//d=1,表示取個位,d=2取十位
int get_digit(int num, int d)
{
int val;
while(d--)
{
val = num % 10;
num = num / 10;
}
return val;
}
void radix_sort(int A[], int n)
{
int radix = 10;//十進位制
int *count = new int[radix];//存放對應數位是0,1,2,3...的數的個數
int *bucket = new int[n];//一個桶來存放A陣列的數,用於中轉
for(int d = 1; d <= 3; ++d)//最大的數有多少位,就要迴圈多少次,假設最大的數在這裡是3位數,所以外層迴圈3次
{
//初始化為0
for(int i = 0; i < radix; ++i)
count[i] = 0;
//統計數位是0,1,2,3...的數分別有多少個
for(int i = 0; i < n; ++i)
{
int j = get_digit(A[i], d);
count[j]++;
}
//意思是,數位為i的數,在它的前面最多有多少個數
for(int i = 1; i < radix; ++i)
count[i] = count[i] + count[i - 1];
//將陣列中的數從後往前裝入桶中,保證了穩定性
for(int i = n - 1; i >= 0; --i)
{
int j = get_digit(A[i], d);
bucket[count[j] - 1] = A[i];//根據這個數的數位上是多少,就放到桶的對應的位置
count[j]--;
}
//把桶中的數放回A中,完成一趟排序,下一趟就是比較A陣列各個數的十位/百位
for(int i = 0; i < n; ++i)
{
A[i] = bucket[i];
}
}
delete[] count;
delete[] bucket;
}
int main()
{
int n;
cout << "基數排序 " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
radix_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i]<<" ";
cout << endl;
delete[] A;
return 0;
}
雞尾酒排序(參考“程式設計師小灰”)
假設一個序列:
這就是雞尾酒排序的思路。排序過程就像鐘擺一樣,第一輪從左到右,第二輪從右到左,第三輪再從左到右......
雞尾酒排序的優點是能夠在特定條件下,減少排序的回合數,而缺點是程式碼量擴大一倍。
適用於【大部分元素已經有序】。
#include<iostream>
using namespace std;
void cock_tail_sort(int A[], int n)
{
int tmp = 0;
for(int i = 0; i < n/2; ++i)
{
//有序的標記,每一輪開始時都為true
bool flag = true;
//奇數輪從左往右進行比較和交換
for(int j = i; j < n - i - 1; ++j)
{
if(A[j]>A[j + 1])
{
tmp = A[j];
A[j] = A[j + 1];
A[j + 1] = tmp;
flag = false;
}
}
if(flag)
break;
//偶數輪,重新標記
flag = true;
//偶數輪,從右往左
for(int j = n - i - 1; j > i; --j)
{
if(A[j] < A[j - 1])
{
tmp = A[j];
A[j] = A[j - 1];
A[j - 1] = tmp;
flag = false;
}
}
if(flag)
break;
}
}
int main()
{
int n;
cout << "雞尾酒排序: " << endl;
cout << "輸入元素個數: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
cock_tail_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}