【資料結構】常見的排序方法的實現以及效能對比
前言:
排序演算法在筆試和麵試的時候經常出現,有時候面試官可能不止問我們排序的寫法,還會問相關演算法的一些效能的比較。平時學習時,由於排序演算法較多,如果不做一些歸納總結的話,很容易混為一談,並且沒有對每個排序有很明確的定位。就會導致在面試的時候啞口。
一、插入排序
在我看來插入排序的思路簡單,效率一般,最好的情況是O(N)即有序或接近有序的時候效率還是很高的,最壞的情況是O(N^2)即逆序時的情況,穩定。就效率而言算不上很優。下面看下插入排序的思路。
1.思路分析
插入排序指的是在陣列中取一個數,拿這個數跟前面的數比較,如果比前面的數小,那麼把當前的這個數往後挪一下,把後面的值挪到前面來,然後依次跟前面的依次比較。
//插入排序
void InsertSort(int* a, int size)
{
for (int i = 0; i<size - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0 && a[end] > a[end + 1])
{
a[end + 1] = a[end];
a[end] = tmp;
--end;
}
}
}
二、shell排序
shell排序的最好情況為O(N)即有序的情況,最壞情況為O(N^2)即逆序的情況,但是shell排序的時間複雜度平均值為O(N^1.3)。
1.思路分析
其實shell排序差不多可以理解為插入排序的優化,每次調整序列使之接近有序然後做和插入排序相類似的操作,然後實現shell排序。
//希爾排序 void shellsort1(int a[], int n) { int gap = n; while (gap > 1) { gap = gap / 3 + 1; for (int i = 0; i<n-gap; i++)//小於n-gap的沒一個數都比較一次 { int end = i; int tmp = a[end + gap]; while (end >= 0 && a[end]>a[end + gap]) { a[end + gap] = a[end]; a[end] = tmp; end = end - gap; } a[end + gap] = tmp; } } }
三、選擇排序
選擇排序的最好情況為O(N^2),最壞情況也為O(N^2),所以對於選擇排序是排序中比較差的那一種。
1.思路分析
實現思路就是每次選擇一個最小的放到前面,或者選擇一個最大的放到最後面(這裡是升序排序的),但是有優化就是選同時選一個最小最大的,放到最前和最後,這樣能夠提高一一倍的效率。
//選擇排序
void ChoiceSort(int* a,int size)
{
assert(a);
int min, max;
for (int i = 0; i < size/2; i++)
{
min = i;
max = size - 1 - i;
for (int j = i+1; j < size; j++)//可能最小的一個值在最大值裡面
{
if (a[min] > a[j])
min = j;
if (a[max] < a[size-j-1])
max = size-j-1;
}
if (min != i)
{
swap(a[i], a[min]);
if (max == i)//如果max在第一個位置就會被換走,所以需要重新換回來,重定向到交換後的位置
{
max = min;
}
}
if (max !=size-1-i)
swap(a[max], a[size - 1 - i]);
}
}
四、堆排序
堆排序的最好情況是O(n*lgN),最壞也如此,因為堆排序是根據他的高度來決定的。
1.思路分析
堆排序的實現思路就是先建好一個大堆或者小堆,然後每次拿堆頭和最後一個葉子節點交換值,取出來以後放到最後,然後縮小區間調整堆。
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
//找子節點中的最大值
if (child + 1 < size&&a[child + 1] > a[child])
{
++child;
}
//如果孩子節點大於父節點交換
if (a[parent] < a[child])
{
swap(a[parent], a[child]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* a, int size)
{
//建堆
for (int i = (size-2)/2; i >= 0; i--)
{
AdjustDown(a, size, i);
}
//堆排序
for (int i = 0; i < size; i++)
{
swap(a[0], a[size - 1 - i]);
AdjustDown(a, size - 1 - i, 0);
}
}
五、氣泡排序
氣泡排序的最好情況是O(N),最壞是O(N^2),效率不是特別高
1.思路分析
氣泡排序的實現就是前後比較選出一個最大或最小的數放到最後,兩重迴圈控制就能實現了。但是有優化,只需要設定個標誌位判斷是否已經完成排序。
//優化的氣泡排序
void Bubble(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
bool judge = false;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
swap(a[j], a[j + 1]);
judge = true;
}
}
if (!judge)
{
return;
}
}
}
六、快速排序
快速排序就是個比較優的排序了,雖然他的最壞情況是O(N^2),但是他的總體情況是O(N*lgN)的,總體而言效率還是很高,再加上一些優化,效能還是很不錯的
1.思路分析
挖坑法:選定最右邊的值作為坑,從左邊開始找比選定的值大的,然後把值寫入這個位置,然後從右邊選一個小的,填入到剛剛轉移出去的位置,一直找直到左右指標相等,然後把選定的值填入中間位置就完成了。
prev和cur法:prev一開始賦值為-1,cur賦值為0。cur遇到比選定的右邊的值大的話就往後走,遇到小的就停下來,然後++prev再交換prev和cur的值,使小的上前大的往後走,prev始終指向的是大的值的前一個。
交換法:同樣選定右邊的值作為中間值,從左邊找一個大的停下來,從右邊找一個小的停下來,然後交換左右兩邊的值,這樣就實現了大的往後小的往前的目的。
//快速排序--》挖坑法
void QuickSort(int* a, int left,int right)
{
if (left < right)
{
int low = left;
int high = right;
int key = a[low];
while (low < high)
{
//從右邊找到第一個小於基數的位置
while (low< high && a[high]> key)
high--;
a[low] = a[high];
//從左邊找到第一個大於技術的位置
while (low < high && a[low] < key)
low++;
a[high] = a[low];
}
a[low] = key;
QuickSort(a, left, low - 1);
QuickSort(a, low + 1, right);
}
}
int PartSort(int* a, int left, int right)//prev和cur法
{
assert(a);
if (left < right)
{
int cur = left;
int prev = left - 1;
int key = a[right];
while (cur < right)
{
if (a[cur] < key)
{
prev++;
if (cur != prev)
swap(a[cur], a[prev]);
}
cur++;
}
swap(a[++prev], a[right]);
return prev;
}
return left;
}
void QuickSort_NonR(int* a, int left, int right)//非遞迴實現
{
assert(a);
stack<int> s;
if (left < right)
{
//先壓入左,再壓入右,取的時候先取右,再取左,每次去左右區間排序下,然後一直迴圈下去,藉助棧,跟棧的原理差不多
int mid = PartSort(a, left, right);
if (left < (mid - 1))
{
s.push(left);
s.push(mid - 1);
}
if (mid + 1 < right)
{
s.push(mid + 1);
s.push(right);
}
while (!s.empty())
{
int high = s.top();
s.pop();
int low = s.top();
s.pop();
if (high - low < 13)
InsertSort(a, high -low+1);
mid = PartSort(a, low, high);
if (low < mid - 1)
{
s.push(low);
s.push(mid - 1);
}
if (mid + 1 < high)
{
s.push(mid + 1);
s.push(high);
}
}
}
}
七、歸併排序
歸併排序的最好最壞情況都為O(N*lgN),穩定
1.思路分析
歸併排序的思路就是先開闢一塊空間,把原來的序列每次分一半,一直分到只有一個值為止,然後一個一個合併,先把排序好的值拷到開闢的空間中,然後再拷會原陣列,區間每次增加一半,直到增回原來的值。
void MergrPartSort(int* a, int first, int mid, int end, int* tmp)
{
assert(a);
int begin1 = first;
int begin2 = mid + 1;
int last1 = mid;
int last2 = end;
int i = first;
while (begin1 <= last1 && begin2 <= last2)
{
if (a[begin1] <= a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
}
while (begin1 <= last1)
tmp[i++] = a[begin1++];
while (begin2 <= last2)
tmp[i++] = a[begin2++];
for (int j = first; j <= end; j++)
{
a[j] = tmp[j];
}
}
void MergrSort(int *a, int first, int end, int* tmp)
{
assert(a);
if (first < end)
{
int mid = first + (end - first) / 2;
//一直遞迴到只有一個
MergrSort(a, first, mid, tmp);
MergrSort(a, mid + 1, end, tmp);
//遞迴到最後一種情況的時候,做合併處理(相當於連結串列的合併)
MergrPartSort(a, first, mid, end, tmp);
}
}
void MergrSort_R(int* a,int n)
{
assert(a);
//開闢一塊空間,用於歸併排序時,將空間先在陣列中排序好,然後在放回原陣列
int* tmp = new int[n];
//如果開闢不成功,返回
if (tmp == NULL)
return ;
MergrSort(a, 0,n-1,tmp);
delete[] tmp;
}
//非遞迴寫法
void sort(int *a, int *tmp, int begin, int mid, int end){
int begin1 = begin, begin2 = mid + 1, k=begin;
while (begin1<=mid && begin2<=end)
{
if (a[begin1] <= a[begin2])
{
tmp[k++] = a[begin1++];
}
else
tmp[k++] = a[begin2++];
}
while (begin1 <= mid)
{
tmp[k++] = a[begin1++];
}
while (begin2 <= end)
{
tmp[k++] = a[begin2++];
}
/*for (int i = begin; i <= end; i++)
{
a[i] = tmp[i];
}*/
}
void Merge(int* a, int* tmp, int k, int n)
{
int i = 0, j;
while (i + 2*k <= n)//按照間隔為k,每一次分類2*k個區間,直到所有的都分類完
{
sort(a, tmp, i, i + k - 1, i + 2 * k - 1 );//為什麼中間值是i+k-1;因為這是做一個預設處理,將前面區間的最後一個值作為中間值,因為都是雙倍的
i += 2 * k;
}
if (i+ k + 1 < n)//如果剩餘的個數比一個k長度還多,那麼就在進行一次合併
sort(a, tmp, i, i + k - 1, n - 1);
else//其他情況說明這一塊是已經排序好了的,直接拷貝過去就行了,為什麼會排序好,因為前面排序小區間時排序好了
{
for (j = i; j < n; j++)
{
tmp[j] = a[j];
}
}
for (i = 0; i < n; i++)//將值拷回原陣列
{
a[i] = tmp[i];
}
}
//歸併排序的非遞迴寫法
void MergeSort_NonR(int* a, int n)
{
int * tmp = new int[n];
int i = 1;
while (i < n)
{
Merge(a, tmp, i, n);
i *= 2;//合併時每次翻一倍,1 2 4 8 16
}
delete[] tmp;
}
void testMerge_NonR()
{
int a[] = { 5, 5, 3, 4, 7, 5, 2, 5, 1, 5, 0, 5, 5 };
MergeSort_NonR(a, sizeof(a) / sizeof(a[0]));
Print(a, sizeof(a) / sizeof(a[0]));
}