大道理什麼的我已經不想多講,直接上一些讓自己重新理解問題的筆記纔是硬核之事。今天我寫的代碼是排序問題,這是一個很古老,也是算法開課繞不開的問題,有很多種形態各樣的排序算法——冒泡排序、插入排序、歸併排序,快速排序、堆排序、希爾排序等等。
今天我首先想接觸的是插入排序和歸併排序兩種。首先來看一下插入排序——
插入排序
主要思路是將一個新的數據插入到一個已經排好序的數組中,然後依次進行最終完成排序,這個算法的時間複雜度是,主要的問題存在於,由於數組是連續的空間,因此插入操作,實際上是兩個相鄰的數組之間比較大小然後交換位置的過程的重複。
——我先寫出僞代碼
## 輸入:數組A
## 輸出:排好序的數組A
for i in 2 to n:
for j in i to 0:
if A[j] < A[j - 1] ## 根據結果從大到小還是從小到大決定大於小於號
swap(A[j - 1], A[j])
以下是插入排序的C++代碼實現:
#include <iostream>
#define Length(A, B) (sizeof(A) / sizeof(B))
void insert_sort(int *, int);
void swap(int &, int &);
void display(int *, int);
int main(int argc, char const *argv[])
{
int A[] = {3, 2, 5, 4, 8, 7, 6};
int length = Length(A, int);
display(A, length);
insert_sort(A, length);
display(A, length);
return 0;
}
void insert_sort(int A[], int length)
{
if (A == nullptr)
return;
if (length <= 0)
return;
for (int i = 1; i < length; ++ i)
{
for (int j = i; j > 0; --j)
{
if (A[j] > A[j - 1])
swap(A[j], A[j - 1]);
}
}
}
void swap(int &num1, int &num2)
{
int temp_num = num1;
num1 = num2;
num2 = temp_num;
}
void display(int A[], int length)
{
for (int i = 0; i < length; ++i)
{
std::cout << A[i] <<std::endl;
}
}
P.S. 在這裏我強調一個小問題,就是在計算給定數組的長度的時候,我們採用的方法是這樣的
int A[] = {a1, ..., an};
int length = sizeof(A) / sizeof(A[0]);
但是如果,我們的數組是一個參數傳來的,那麼這樣做就會出現問題:
int A[] = {a1, ..., an};
int length(int A[])
{
if (A == nullptr) return 0;
return sizeof(A) / sizeof(A[0]);
}
實際上,上述第一段代碼的運行結果是n, 而第二段代碼的運行結果是2。那麼這是怎麼回事呢?——實際上,對於數組而言,數組名可以看成是指針,但是他卻和指針並不能完全等同起來。在c++中進行參數傳遞,數組名傳入的實際上是自己的首地址,因此此時進行sizeof得到的其實是整型數組的長度即8,然後再取除以sizeof(int)這樣得到的結果就是2。
歸併排序
歸併排序算是一個非常典型的分治法的具體使用案例了,總體上說來就是將原數組分成長度相等的兩部分,然後分別對他們進行排序,最後將排好序的兩個子序列進行合併,(快排是將數組按大小分成兩部分,然後進行迭代),依次遞歸程序,就實現了排序算法。
歸併排序的時間複雜度滿足以下的時間遞推式:
因此他的時間複雜度爲
——我先寫出僞代碼
## 輸入:數組A,數組的起始位置,數組長度length
## 輸出:排好序的數組A
def merge_sort(A, left, length):
interval = length / 2
merge_sort(A, left, interval)
merge_sort(A, left + interval, length - interval)
merge(A_left, A_right)
以下我寫出他的c++代碼實現:
void merge_sort(int A[], int left, int length)
{
if (length == 1 || A == nullptr)
return;
int interval = length / 2;
/***
* 將原數組等距離二分
*/
int n_left = left, n_right = left + interval;
merge_sort(A, n_left, interval);
merge_sort(A, n_right, length - interval);
/***
* 將原數組二分完成之後,再對兩部分已排好序的數組進行合併
*/
int *temp_array = new int[length];
int count = 0;
/***
* 構造臨時數組,先存在臨時數組,然後再複製給原來的數組
*/
while (n_right < length + left && n_left < interval + left)
{
temp_array[count ++] = A[n_left] <= A[n_right] ? A[n_left ++] : A[n_right ++];
}
/***
* 若左邊數組的數組已經完成合並,右邊的數組還未合併完
* 這樣就可以直接將臨時數組的前count個數複製回原來的數組
*/
if (n_left == interval + left && n_right <= left + length)
{
for (int i = 0; i < count; ++ i)
{
A[i + left] = temp_array[i];
}
}
/***
* 若右邊數組的數組已經完成合並,左邊的數組還未合併完
* 這樣就先將左邊的最大數先放到原數組的右邊,
* 然後將臨時數組的前count個數複製回原來的數組
*/
if (n_left <= interval + left && n_right == left + length)
{
for (int i = left + interval - 1; i >= n_left; -- i)
{
A[length + i - interval] = A[i];
}
for (int i = 0; i < count; ++ i)
{
A[i + left] = temp_array[i];
}
}
delete []temp_array;
}
P.S.還有一個版本的歸併排序,只是它的參數需要的少一點,我也把它貼在下面:
void merge_sort(int A[], int length)
{
if (A == nullptr || length <= 1)
return;
int interval = length / 2;
merge_sort(A, interval);
merge_sort(A + interval, length - interval);
int left = 0, right = interval;
int *temp_array = new int[length];
int count = 0;
while (left < interval && right < length)
temp_array[count ++] = A[left] <= A[right] ? A[left ++] : A[right ++];
if (left == interval && right <= length)
{
for (int i = 0; i < count; ++ i)
{
A[i] = temp_array[i];
}
}
if (right == length && left <= interval)
{
for (int i = interval - 1; i >= left ; -- i)
{
A[i + length - interval] = A[i];
}
for (int i = 0; i < count; ++ i)
{
A[i] = temp_array[i];
}
}
delete []temp_array;
}
思路同上,那麼接下來我依然是進行總結。
總體來說,歸併排序的思路很清晰,很明瞭,但爲什麼我的代碼還是寫了這麼久?!原因很長時間是因爲我再處理邊界的時候除了問題,沒有想明白一個邊界問題。因此在這裏我記錄下來,du對於邊界問題一定要仔細考慮,千萬不可隨意書寫,到最後debug太難受了。
P.S.其次,我想說明,對於缺少左邊界參數歸併版本,實際上就是將數組的偏移直接作爲參數傳入函數進行處理,這樣就能直接將左邊界寫死,完成代碼的實現。另外通過指針訪問數組,實際上只需要在首地址的基礎之上加上偏移,然後解引用便獲得相應下標得值。eg:A[] = {1, 2, 3, 4, 5}想要訪問A[3]得途徑有A[3]、*(A + 3)——