排序算法
- 大多數排序算法的性能和輸入模型有很大的關係,不同的算法適用於不同應用場景中的不同輸入
- 操作抽象:抽象less()和exch()等排序共同操作,有利於代碼理解和程序的可移植性
排序算法的共同操作
/*比較方法*/
static bool less(T a, T b) {
return a < b; //調用比較方法
}
/*交換方法*/
static void exchange(T a[], int i, int j) {
T temp = a[i];
a[i] = a[j];
a[j] = temp;
}
/*打印方法*/
static void show(T a[], int Len) {
cout << "數組元素:";
for (int i = 0; i < Len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
/*判斷有序方法*/
static bool isSorted(T a[], int Len) {
for (int i = 1; i < Len; i++) {
if (less(a[i], a[i - 1]))
return false;
}
return true;
}
初級排序算法
選擇排序
思想
- 找到數組中最小的元素,然後與未排序數組中的第一個元素交換
- 交換N-1次之後,排序完成
代碼實現
void SelectSort(T a[], int Len) {
int min;
for (int i = 0; i < Len - 1; i++) {
min = i;
for (int j = i + 1; j < Len; j++) {
if (less(a[j], a[min]))//比較函數
min = j;
}
exchange(a, i, min);//交換函數
}
}
特點
- 運行時間與輸入無關,即時間複雜度均是O(N*N)
- 在所有算法中,數據移動是最少的(交換次數與數組大小是線性關係)
- 比較次數:(N-1,N-2,N-3,…,1)==N*N/2
- 交換次數:N
適用情景
- 輸入數據量較少但是元素本身較大
插入排序
思想
- 將數組分爲已排序部分和未排序部分,每次從未排序部分中選擇第1個元素與已排序部分的元素比較,若比末尾元素大,就添加到已排序部分末尾,否則交換兩元素位置,繼續向前比較,直到比前一個元素大爲止。
代碼實現
//插入排序
void InsertSort(T a[],int Len){
int j;
for(int i=1;i<Len;i++){
j=i;
while(less(a[j],a[j-1])&&j>0){//比較操作
exchange(a,j, j-1);//交換操作
j--;
}
}
}
特點
-
運行時間與輸入元素的初始順序有關
-
比較次數:最好:N-1;最壞:N*N/2;平均:N*N/4
-
交換次數:最好:0;最壞:N*N/2;平均:N*N/4
-
倒置很少時,排序效率可能是算法中最好的
適用情景
- 面對部分有序或基本有序數組十分高效(利用算法的最好情況),也適合小規模數據
- 數組中每個元素離最終位置都不遠
- 一個有序的大數組+一個小數組
- 數組中只有幾個元素位置不確定
改進
不用進行邊界檢測的插入排序
- 將最小值放在最左邊來避免邊界檢測
- 改進效率:與簡單插入排序效率相當
void InsertSortNoCheck(T a[], int Len) {
//找到最小值
int min = 0;
for (int i = 1; i < Len; i++) {
if (less(a[i], a[min])) {
min = i;
}
}
exchange(a, 0, min); //將最小值交換到0處
//不用進行邊界檢測的插入排序
int j;
for (int i = 1; i < Len; i++) {
j = i;
while (less(a[j], a[j - 1])) { //比較操作
exchange(a, j, j - 1); //交換操作
j--;
}
}
}
帶檢查點且不用交換的插入排序
- 使用數組移位來代替交換,減少了數組訪問(內存引用)次數
- 改進效率:比原始插入排序大約快2倍
void InSortPandNoExch(T a[], int Len) {
//找到最小值
int min = 0, changes = 0;
for (int i = 1; i < Len; i++) {
if (less(a[i], a[min])) {
exchange(a, i, min);
changes++;
}
}
if (changes == 0)
return;
T temp;
int j;
for (int i = 2; i < Len; i++) {
temp = a[i];
j = i-1;
while (less(temp, a[j])) { //比較操作
a[j+1] = a[j]; //移動操作
j--;
}
a[j+1] = temp;
}
}
選擇排序與插入排序的比較
- 在隨機排序的無重複主鍵的數組中,插入排序比選擇排序大約快1倍
高級排序算法
希爾排序
思想
- 是一種基於插入排序的增量排序算法
- 對任意間隔爲k的子數組進行排序,當所有間隔爲k的子數組有序之後,對k進行增量縮小,繼續進行增量爲k的插入排序,直到k爲1。k=1時,數組現在已經變得基本有序,最後進行一次插入排序,排序完成
代碼實現
//希爾排序
void ShellSort(T a[],int Len){
int gap=1;//增量
while(gap<Len/3) gap=3*gap+1;//採用1,4,13,40,121,......的增量序列
while(gap>=1){
//按增量進行的插入排序
for(int i=gap;i<Len;i++){
for(int j=i;j>0&&less(a[j], a[j-gap]);j-=gap){
exchange(a, j, j-gap);
}
}
gap=gap/3;
}
}
特點
- 希爾排序的時間複雜度難以估量,大概在N^(3/2)
- 希爾排序是最簡單的高級排序算法
- 希爾排序的效率依賴於增量序列(可以選擇3*k+1,足以勝任大部分應用問題)
- 會破壞數組的穩定性
- 不需要額外的內存空間
適用情景
- 僅適用於線性表爲順序存儲
歸併排序
思想
- 基於分治思想的遞歸算法
- 分治核心:將1個大數組劃分爲2個小數組處理
- 遞歸核心:將2個有序數組合併成1個更大的有序數組
- 將數組不停地2分爲小數組,直到分爲2^n個僅含有1個元素的數組,然後對小數組依次進行合併成更大的有序數組
- 1/1合併爲2,2/2合併爲4,4/4合併爲8,直到合併爲N爲止
代碼實現
//歸併有序數組
static void Merge(T a[],T aux[],int lo,int mid,int hi){
//複製數據到輔助數組
for(int i=lo;i<=hi;i++){
aux[i]=a[i];
}
//歸併
int i=lo,j=mid+1;
for(int k=lo;k<=hi;k++){
if(i>mid) a[k]=aux[j++];
else if(j>hi) a[k]=aux[i++];
else if(less(aux[i], aux[j])) a[k]=aux[i++];
else a[k]=aux[j++];
}
}
/**
* 自頂向下的歸併排序
*/
static void MergeSort(T a[],T aux[],int lo,int hi){
if(hi<=lo) return ;
int mid=(lo+hi)/2;
MergeSort(a,aux, lo, mid);
MergeSort(a, aux,mid+1, hi);
if(a[mid]>a[mid+1]){
Merge(a, aux,lo, mid, hi);
}
}
/**
* 自底向上的歸併排序
* 效率:與自頂向下的歸併排序效率相當
*/
static void MergeSortBU(T a[],T aux[],int Len){
for(int aSize=1;aSize<Len;aSize*=2){
for(int lo=0;lo<Len;lo+=2*aSize){
int hi=lo+2*aSize-1,mid=lo+aSize-1;
hi=hi>(Len-1)? Len-1:hi;
Merge(a, aux, lo, mid, hi);
}
}
}
特點
- 歸併排序算法的時間複雜度NlgN,空間複雜度是N
- 是一個穩定的排序算法
- 是一種漸進最優的基於比較的算法
- 自底向上的歸併排序比較適合用鏈表組織的數據
算法改進
對小規模數組使用插入排序
- 一般數組中元素個數在5-15之間使用插入排序比較合適
- 效率大概能夠提升1個數量級
/*在指定區間上的插入排序*/
static void InsertSortLimit(T a[],int lo,int hi){
for(int i=lo+1;i<=hi;i++){
for(int j=i;less(a[j],a[j-1])&&j>lo;j--){
exchange(a, j-1, j);
}
}
}
//歸併·排序
static void MergeSort(T a[],T aux[],int lo,int hi){
if(hi<=lo+15){
InsertSortLimit(a,lo,hi);
return;//如果沒有return,程序就不會退出,陷入死循環
}
int mid=(lo+hi)/2;
MergeSort(a,aux, lo, mid);
MergeSort(a, aux,mid+1, hi);
if(a[mid]>a[mid+1]){
Merge(a, aux,lo, mid, hi);
}
}
歸併前判斷數組是否已經有序
- 對於有序數組,時間複雜度是N
- 修改判斷歸併數組1右端小與數組2左端及直接返回
- 改進效率:處理任意有序數組變爲線性時間
static void MergeSortBU(T a[],T aux[],int Len){
for(int aSize=1;aSize<Len;aSize*=2){
for(int lo=0;lo<Len;lo+=2*aSize){
int hi=lo+2*aSize-1,mid=lo+aSize-1;
hi=hi>(Len-1)? Len-1:hi;
if(a[mid]>a[mid+1]){
Merge(a, aux, lo, mid, hi);
}
}
}
}
快速排序
思想
- 一種基於分治思想的遞歸算法
- 分治核心:將1個大數組劃分爲2個小數組,減小處理的問題規模
- 遞歸核心:將大數組劃分爲2個小數組,不斷迭代至數組規模小到可以直接處理
- 每次迭代:選定一個切分元素v,以v將數組劃分爲2個子數組,num1均爲小於v的元素,num2均爲大於等於v的元素
- 對子數組繼續進行上面的劃分操作,直到數組不能再劃分
代碼實現
//切分方法
static int partition(T a[],int lo,int hi){
int i=lo,j=hi+1;//掃描指針
T part=a[lo];//切分元素
while(1){
while(less(a[++i],part)){
if(i==hi) break;
}
while(less(part, a[--j])){
}
if(i>=j) break;
exchange(a, i, j);
}
exchange(a, lo, j);
return j;
}
//快速排序遞歸
static void QuickSort(T a[],int lo,int hi){
if(lo>=hi) return;
int part=partition(a, lo, hi);
QuickSort(a, lo,part-1);
QuickSort(a, part+1, hi);
}
//快速排序調用
static void QuickSort(T a[],int Len){
QuickSort(a,0,Len-1);
}
特點
- 快速排序是原地排序,空間複雜度是1,時間複雜度是NlgN
- 但是快速排序的最差性能是N*N,關鍵在於如何選取切分元素
- 隨着數組規模增大,快排的運行時間會趨於平均運行時間NlgN
- 快速排序的內循環比較小,所以運行速度快
- 排序不穩定
- 快速排序一般比歸併排序,希爾排序都要快
算法改進
小數組切換成插入排序
- 改進效率:快了10倍以上,提升一個數量級
/*在指定區間上的插入排序*/
static void InsertSortLimit(T a[],int lo,int hi){
for(int i=lo+1;i<=hi;i++){
for(int j=i;less(a[j],a[j-1])&&j>lo;j--){
exchange(a, j-1, j);
}
}
}
static void QuickSort(T a[],int lo,int hi){
if(hi<=lo+15){
InsertSortLimit(a, lo, hi);
return;//需要返回,不然會無窮遞歸
}
int part=partition(a, lo, hi);
QuickSort(a, lo,part-1);
QuickSort(a, part+1, hi);
}
採用三取樣切分選取切分元素
- 改進效率:相比於原始快排,使用三取樣切分快了10倍以上
static void QuickSortSub3(T a[],int lo,int hi){
if(lo>=hi) return;
int part=partition3(a, lo, hi);
QuickSortSub3(a, lo,part-1);
QuickSortSub3(a, part+1, hi);
}
//三取樣切分
static T sub3partition(T a[],int lo,int hi){
T part;
if(hi-lo<2){
return a[lo];
}
T v1=a[lo],v2=a[lo+1],v3=a[lo+2];
int index;
//獲取中位數:2次比較
if( (v1-v2)*(v2-v3)>0 )
index=lo+1;
else if( (v2-v1)*(v1-v3)>0 )
index=lo;
else index=lo+2;
part=a[index];
exchange(a,lo, index);
return part;
}
static int partition3(T a[],int lo,int hi){
int i=lo,j=hi+1;//掃描指針
T part=sub3partition(a, lo,hi);
while(1){
while(less(a[++i],part)){
if(i==hi) break;
}
while(less(part,a[--j])){ }
if(i>=j) break;
exchange(a,i,j);
}
exchange(a,lo,j);
return j;
}
- 取3個數字的中位數代碼
//選取中位數:一行代碼
//index= v1 > v2 ? (v2 > v3 ? lo+1 : (v1>v3? lo+2:lo)) : (v1 > v3 ? lo: (v2>v3? lo+2:lo+1));
//獲取中位數:2次比較
if( (v1-v2)*(v2-v3)>0 )
index=lo+1;
else if( (v2-v1)*(v1-v3)>0 )
index=lo;
else index=lo+2;
採用三向切分
- 適用於存在大量重複的數據
- 改進效率:對於包含大量重複元素的輸入,時間複雜度可以達到N
void QuickSortManyRepeat(T a[],int lo,int hi){
if(hi<=lo) return;
int lt=lo,i=lo+1,gt=hi;
T v=a[lo];
while(i<=gt){
if(a[i]<v) exchange(a,lt++,i++);
else if(a[i]>v) exchange(a,gt--,i);
else i++;
}
QuickSortManyRepeat(a, lo, lt-1);
QuickSortManyRepeat(a, gt+1, hi);
}
優先隊列
API
class PriorityQueue{
private:
//隊列最大長度
int maxSize;
//隊列長度
int N=0;
//優先隊列
T* pq;
//元素比較
bool less(int i,int j){}
//元素交換
void exchange(int i,int j){}
//上浮操作
void swim(int k){}
//下沉操作
void sink(int k){}
//動態調整隊列長度
void reSize(int Len){}
public:
//創建最大容量爲max的優先隊列
MaxPQ(int max){}
//向優先隊列中插入一個元素
void insert(T v){}
//返回最大元素
T max(){}
//刪除並返回最大元素
T delMax(){}
//返回隊列是否是空
bool isEmpty(){}
//返回優先隊列中元素的數目
int size(){}
}
實現堆的操作
//上浮:
void swim(int k){
while(k>1&&less(k/2,k)){
exchange(k/2,k);
k=k/2;
}
}
//下沉:
void sink(int k){
while(2*k<=N){
int j=2*k;
if(j<N&&less(j,j+1)) j++;
if(!less(k,j)) break;
exchange(k,j);
k=j;
}
}
基於堆的優先隊列的實現
- 對於插入元素和刪除元素都是lgN的時間
template <class T>
class MaxPQ{
private:
//隊列最大長度
int maxSize;
//隊列長度
int N=0;
//優先隊列
T* pq;
//元素比較
bool less(int i,int j){
return pq[i]<pq[j];
}
//元素交換
void exchange(int i,int j){
T temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
//上浮操作
void swim(int k){
while(k>1&&less(k/2,k)){
exchange(k/2,k);
k=k/2;
}
}
//下沉操作
void sink(int k){
while(2*k<=N){
int j=2*k;
if(j<N&&less(j,j+1)) j++;
if(!less(k,j)) break;
exchange(k,j);
k=j;
}
}
//動態調整隊列長度
void reSize(int Len){
T* temp=new T[N+1];
for(int i=1;i<=N;i++){
temp[i]=pq[i];
}
pq=new T[Len];
for(int i=1;i<=N;i++){
pq[i]=temp[i];
}
delete [] temp;
}
public:
//創建最大容量爲max的優先隊列
MaxPQ(int max){
pq=new T[max];
maxSize=max;
}
//向優先隊列中插入一個元素
void insert(T v){
pq[++N]=v;
swim(N);
//動態調整數組大小
if(N==maxSize){
reSize(2*N);
}
}
//返回最大元素
T max(){
T *result=NULL;
if(N!=0){
return pq[1];
}
return *result;
}
//刪除並返回最大元素
T delMax(){
T max=pq[1];
exchange(1,N--);
pq[N+1]=NULL;//防止越界
sink(1);
//動態調整數組大小
if(N<=maxSize/4){
reSize(2*N);
}
return max;
}
//返回隊列是否是空
bool isEmpty(){
return N==0;
}
//返回優先隊列中元素的數目
int size(){
return N;
}
};
優先隊列的應用場景
- 任務調度問題
- TopM問題:從輸入流中找到最大的M個元素
- Multiway問題:將M個輸入流歸併爲有序的輸入流
堆排序
堆排序的實現
- 主要分爲兩個階段:1.構造堆階段 2.依次下沉排序階段
/**
* 堆排序
*/
template <class T>
class HeapSort{
private:
//元素比較
bool less(T a,T b){
return a<b;
}
//元素交換
void exchange(T pq[],int i,int j){
T temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
//下沉操作
void sink(T a[],int k,int N){
//從根節點開始向下尋找最大的元素
while(2*k<=N){
int j=2*k;
//找到左右子節點的最大值
if(j<N&&less(a[j-1],a[j])) j++;
//如果父節點大於葉子節點的最大值就退出,否則就交換
if(!less(a[k-1],a[j-1])) break;
exchange(a,k-1,j-1);
//以最大的葉節點繼續向下找
k=j;
}
}
public:
HeapSort(T a[],int Len){
int N=Len;
//構造堆
for(int k=N/2;k>=1;k--){
sink(a,k,N);
}
while(N>1){
exchange(a,1-1,N-1);
N--;
sink(a,1,N);
}
}
};
堆排序的特點
- 時間複雜度是NlgN,空間複雜度是常數
- 堆排序是唯一能夠同時最優地利用時間和空間的算法
- 缺點是:無法利用緩存,因爲不是在相鄰元素之間進行比較
排序算法之間的比較
初級排序之間的比較
插入排序與選擇排序的比較
- 一般情況下,插入排序比選擇排序快1倍左右
- 而且插入排序是穩定排序算法
簡單插入排序與優化的插入排序之間的比較
- inert:簡單插入排序
- insertNoCheck:不用進行邊界檢測的插入排序
- insertNoExchange:不用進行元素交換的插入排序
- insertNoCheck與insert效率相當
- insertNoExchange比insert快大概1倍
高級排序算法之間的比較
快速排序與歸併排序,希爾排序,堆排序之間的比較
- 一般情況下,快速排序比歸併排序、希爾排序要快
- 雖然都是NlgN級別算法,但是快排的常數c往往要小一些,因爲它的內循環比較小
- 相比於堆排序和希爾排序,快排是順序訪問元素,可以利用緩存
- 快速排序是最快的通用排序算法
- 如果需要穩定性排序,一般選擇歸併排序
總結
- 排序算法一般是所有應用的基礎
- 不同的應用場景需要選擇不同的排序算法