常見比較排序演算法的實現(歸併排序、快速排序、堆排序、選擇排序、插入排序、希爾排序)
阿新 • • 發佈:2019-01-09
這篇部落格主要實現一些常見的排序演算法。例如:
//氣泡排序
//選擇排序
//簡單插入排序
//折半插入排序
//希爾排序
//歸併排序
//雙向的快速排序(以及快速排序的非遞迴版本)
//單向的快速排序
//堆排序
對於各個演算法的實現原理,這裡不再多說了,程式碼中註釋較多,結合註釋應該都能理解演算法的原理,讀者也可自己google一下。另外,註釋中有很多點,比如邊界條件、應用場景等已經用 * 標記,* 越多,越應該多注意。
下面是實現:
//氣泡排序
void BubbleSort(int *arr, int n)
{
if(NULL == arr || n < 2)
return ;
for(int i = 0; i < n; ++i) //i 趟數
{
for(int j = 0; j < n-i-1; ++j) //j 第i趟需要比較的次數, 一定是從頭開始比較,因為最後一個元素已經有序
{
if(arr[j] > arr[j+1]) //*
{
std::swap(arr[j], arr[j+1]); //如果前面的一個元素較大,就交換
}
}
}
}
//選擇排序
void SelectSort(int *arr, int n)
{
if(NULL == arr || n < 2)
return ;
for(int i = 0; i < n; ++i) //每次從當前位置往後找一個最小的值,放在當前位置
{
int minIndex = i; //找最小值時用來記錄下標
for(int j = i; j < n; ++j) //從i位置開始往後 找到一個最小值
{
if(arr[j] < arr[minIndex])
{
minIndex = j; //找到比arr[i]小的時候,記錄最小值下標
}
}
if(minIndex != i)
{
std::swap(arr[i], arr[minIndex]); //把最小值放到i位置
}
}
}
//簡單插入排序
void SimpleInsertSort(int *arr, int n)
{
if(NULL == arr || n < 2)
return ;
int i = 0, j = 0;
int save = 0;
for(i = 1; i < n; ++i) //預設第一個元素已經有序,從第二個元素開始,把每個元素插入到前面有序的合適位置
{
save = arr[i]; //儲存當前的值,如果移動元素,可能會被覆蓋
for(j = i-1; j >= 0; j--)
{
if(arr[j] < save)
break;
arr[j+1] = arr[j]; //移動元素
}
if(j+1 != i)
arr[j+1] = save; //在合適位置放上之前儲存的數
}
}
//折半插入排序
void BinaryInsertSort(int *arr, int n)
{
if(NULL == arr || n < 2)
return ;
int i = 0, j = 0;
int save = 0;
for(i = 1; i < n; ++i)
{
save = arr[i];
int low = 0, high = i - 1;
int mid = 0;
while(low <= high) //找一個位置 放要插入的數, 迴圈結束後,low > high
{ //折半插入比簡單插入的優點就在這裡,能很快找到要插入的位置,減少了比較次數
mid = low + (high-low)/2;
if(arr[mid] < save)
low = mid + 1;
//else if(arr[mid] > save)
else
high = mid - 1;
}
for(j = i; j > low; --j) //移動元素,arr[low]是比save大的第一個數字,拷貝到這個數為止,並不能減少移動元素的次數
{
arr[j] = arr[j-1];
}
arr[j] = save; //arr[low]放上save
}
}
//希爾排序
//嚴蔚敏版本,實現的不太好,裡面自己指定了 步長(用step陣列儲存,一定要保證陣列最後一個元素為1, 原因下面有解釋)
//void ShellInsert(int *arr, int n, int gap) //這個函式說白了就是插入排序,只不過是把插入排序中的步長1換成了gap
//{
// assert(arr);
//
// int i = 0, j = 0;
// int save = 0;
// for(i = 0+gap; i < n; i+=gap) //每隔一個步長的所有數做一次插入排序
// {
// save = arr[i];
// for(j = i-gap; j >= 0; j-=gap) //找個合適的位置放待插入的數
// {
// if(arr[j] < save)
// break;
// arr[j+gap] = arr[j];
// }
// if(j+gap != i)
// arr[j+gap] = save; //放數
// }
//}
//
//void ShellSort(int *arr, int n, int *step, int t) //step裡面存放的是每次希爾排序的步(t是step的長度), step一定是降序排列的,最後一個步長一定為1
//{
// if(NULL == arr || NULL == step)
// return ;
//
// for(int i = 0; i < t; ++i)
// {
// ShellInsert(arr, n, step[i]); //每次找一個步長進行插入排序
// }
//}
void ShellSort(int *arr, int len)
{
assert(arr && len>0);
int gap = len; //gap是每次希爾插入的步長
while(gap > 1)
{
gap = gap/3 + 1; //最後加1能保證gap的最後一個值一定是1,因為之前gap大於1的過程都是為最後一個簡單插入做準備(稱為預處理)
int cur = gap;
for(cur = gap; cur < len; ++cur) //下面就是簡單插入排序,只不過插入排序的步長為gap
{
int tmp = arr[cur];
int findIndex = cur - gap;
while(findIndex >= 0 && arr[findIndex] > tmp)
{
arr[findIndex+gap] = arr[findIndex];
findIndex -= gap;
}
arr[findIndex+gap] = tmp;
}
}
}
//歸併排序
void Merge(int *arr, int begin, int mid, int end) // 把arr中的 [begin, mid]、 [mid+1, end] 兩個有序片段排成一個有序序列
{
assert(arr);
int *brr = new int[end+1-begin]; //臨時陣列,用來儲存臨時有序序列
int i = 0, j = 0; //兩個有序片段的指標
int k = 0;
for(i = begin, j = mid+1; i<=mid && j<=end; )
{
if(arr[i] <= arr[j]) //如果兩個數相等,預設先放前面一段的數i指向的片段,這也保證了歸併排序是穩定的
brr[k++] = arr[i++];
else
brr[k++] = arr[j++];
}
while(i <= mid)
brr[k++] = arr[i++]; //如果子陣列還有剩餘元素沒有插入到臨時陣列中,直接拷貝全部元素到臨時陣列中
while(j <= end)
brr[k++] = arr[j++];
for(i = begin; i <= end; ++i) //轉移臨時陣列到原陣列中 ***
arr[i] = brr[i-begin];
delete []brr; //釋放臨時陣列
}
void MergeSort(int *arr, int left, int right) //對[left, right]進行排序
{
if(NULL == arr || right-left < 1)
return ;
int mid = left + (right-left)/2; //找一箇中間值,用來分段歸併
MergeSort(arr, left, mid); //歸併左半段
MergeSort(arr, mid+1, right); //歸併右半段
Merge(arr, mid, right); //左半段、右半段已經有序,只需要合併就行
}
//雙向的快速排序
int partition(int *arr, int begin, int end) //[begin, end] ***
{
int key = arr[begin]; //樞軸預設取第一個元素
int save = arr[begin];
int left = begin, right = end;
while(left < right)
{
while(left<right && arr[right] >= key)
--right;
arr[left] = arr[right];
while(left<right && arr[left] <= key)
++left;
arr[right] = arr[left];
}
arr[left] = save; //left是樞軸元素所在處,left前面的元素都比arr[left]小,後面的元素都比arr[left]大
return left;
}
//雙向的快速排序,(partition)需要前後指標往中間遍歷
void QuickSort_TwoWay(int *arr, int begin, int end) // [begin, end]
{
if(NULL == arr || begin >= end)
return ;
if(begin < end)
{
int partiIndex = partition(arr, begin, end);
QuickSort_TwoWay(arr, begin, partiIndex-1); // [begin, partiIndex-1]
QuickSort_TwoWay(arr, partiIndex+1, end); // [partiIndex+1, end]
}
}
//快速排序的**非遞迴版本**,實現起來不難,主要是研究其演算法原理 ******
void QuickSort_NoR(int *arr, int left, int right)
{
if (NULL == arr || right - left < 1)
return ;
stack<int> st;
st.push(right); //先把左右區間壓入棧中
st.push(left);
while (!st.empty())
{
left = st.top(); //出棧得到要排序的邊界值,先出來的是左邊界
st.pop();
right = st.top(); //後出來的是右邊界
st.pop();
//對[left, right]進行一次partition,之後arr[mid]大於[left, mid-1],arr[mid]小於[mid+1, right]
int mid = partition(arr, left, right);
//把右子區間的邊界壓棧,為排序右子陣列做準備,相當於遞迴排序右子陣列
if (right > mid + 1)
{
st.push(right);
st.push(mid + 1);
}
//把左子區間的邊界壓棧,為排序左子陣列做準備,相當於遞迴排序左子陣列
if (mid-1 > left)
{
st.push(mid - 1);
st.push(left);
}
}
}
//單向的快速排序,只需要一個指標從前往後掃描, 該方法特別適合連結串列的排序 ******
void QuickSort_OneWay(int *arr, int begin, int end) // [begin, end]
{
if(NULL == arr || begin >= end)
return ;
int index = begin+1; //往後找比key小的數******
int key = arr[begin]; //相當於樞軸
int mid = begin; //相當於partition,用於標記左右有序的分界******
for(index=begin+1 ; index <= end; ++index)
{
if(arr[index] < key) //找小
{
if(++mid != index) //防止自己跟自己交換
std::swap(arr[index], arr[mid]);
}
}
std::swap(arr[begin], arr[mid]); //mid位置處放入樞軸
QuickSort_OneWay(arr, begin, mid-1); //遞迴排序左半部分
QuickSort_OneWay(arr, mid+1, end); //遞迴排序左半部分
}
快排的優化:
(1)key值的選取:
- 在[left, right]中隨機選擇一個值作為key值,假設選取的值下標為keyIndex, 這時,把arr[left]與arr[keyIndex]交換,因為key值只能選取開頭或者結尾的值;
- 在arr[left],arr[mid],arr[right]中選擇一個大小處於中間值的數作為key值, 同樣還需要把arr[left]與arr[midIndex]交換。
(2)當子陣列長度很短時,也需要執行快排,進行遞迴壓棧,這樣會嚴重降低快排的效能:
- 子陣列的長度小於一定值時,選擇插入排序(插入排序非常適合待排序陣列基本有序時的情況), 這樣可以降低遞迴壓棧的時間開銷。
//堆排序 方法1:常規情況
void AdjustDown(int *arr, int len, int root) //調整以root為根的子樹滿足堆的特點(這裡實現的是大堆, 下面還有優化)
{
int parent = root;
int child = 2*root + 1; //預設root的左子樹比右子樹大
while(child < len)
{
if(child+1 < len && arr[child+1] > arr[child]) //如果右子樹大,調整child
++child;
if(arr[parent] < arr[child]) //如果子樹比根節點大,交換
{
std::swap(arr[parent], arr[child]);
parent = child; //交換完成後,以child為根的子樹可能不滿足堆的特點,需要向下重新調整(AdjustDown)
child = 2*parent + 1;
}
else
break;
}
}
void HeapSort(int *arr, int len)
{
assert(arr && len>0);
for(int root = len/2-1; root >= 0; --root) //建堆 len/2 - 1是第一個非葉子節點的下標
{
AdjustDown(arr, len, root); //從第一個非葉子節點一直調整到根節點(下標為0)
}
for(int i = 0; i < len-1; ++i)
{
std::swap(arr[0], arr[len-1-i]); //根節點是最大的元素,根節點和最後一個元素交換,最大元素處於最後
AdjustDown(arr, len-1-i, 0); //重新調整樹滿足堆,但是節點個數要減1(因為最後一個元素已經有序)
}
}
//堆排序 方法2:使用仿函式+模板函式
template<class T>
class Great
{
public:
bool operator() (const T& left, const T& right) //物件過載了(), 可以像函式一樣使用,例如: great(3, 1) 返回true
{
return left > right;
}
};
template<class T>
class Less
{
public:
bool operator() (const T& left, const T& right)
{
return left < right;
}
};
//***
//該方法實現為模板函式,通過給函式傳進一個Great或Less的物件,從而動態實現大堆或小堆
template<class Compare>
void AdjustDown(int *arr, int len, int root, Compare com) //用法 AdjustDown(arr, len, root, Great<int>())
{
int parent = root;
int child = 2*root + 1;
while(child < len)
{
if(child+1 < len && com(arr[child+1], arr[child]))
++child;
if(com(arr[child], arr[parent]))
{
std::swap(arr[parent], arr[child]);
parent = child;
child = 2*parent + 1;
}
else
break;
}
}
//堆排序 方法3:使用仿函式+模板類
template<class T, template<typename T> class Compare = Less >
class Heap
{
public:
Heap(T *arr, int sz)
:_arr(arr)
,_size(sz)
{
//建堆
for(int root = _size/2 - 1; root >= 0; --root)
AdjustDown(_size, root);
}
~Heap()
{
//神馬都不用做
}
//功能同上面的AdjustDown
void AdjustDown(int len, int root)
{
Compare<T> com;
int parent = root;
int child = 2*parent + 1;
while(child < len)
{
if(child+1 < _size && com(_arr[child+1], _arr[child]))
++child;
if(com(_arr[child], _arr[parent]))
{
std::swap(_arr[parent], _arr[child]);
parent = child;
child = 2*parent + 1;
}
else
break;
}
}
void PrintArray()
{
for(int i = 0; i < _size; ++i)
cout<<_arr[i]<<" ";
cout<<endl;
}
//protected:
public:
T *_arr;
int _size;
};
template<class T>
void HeapSort(T *arr, int len)
{
Heap<T, Great> hp(arr, len);
for(int i = 0; i < len; ++i)
{
std::swap(hp._arr[0], hp._arr[len-1-i]);
hp.AdjustDown(len-1-i, 0);
}
}
常見的排序就是這麼多了。。。
下面附上一張各種排序的時間複雜度,空間複雜度,以及穩定性的比較: