漸進分析
漸進分析是一種數學方法,漸進分析技術能夠在數量級上對算法進行精確度量。但是,數學不是萬能的,實際上,許多貌似簡單的算法很難用數學的精確性和嚴格性來分析,尤其分析平均情況。算法的實驗分析是一種事后計算的方法,通常需要將算法轉換為對應的程序並上機運行。
計數法是在算法中的適當位置插入一些計數器,來度量算法中基本語句的執行次數。生成合適的測試樣例作為測試的基准,並對輸入實例運行算法對應的程序,記錄得到的實驗數據。最后根據實驗得到的數據,結合實驗目的,對算法結果進行分析。
設計思路
實驗首先需要生成合適的測試樣例,為了盡可能追求實驗結果的一般性,將生成 3 組不同規模的實驗數據。每組數據在規模不同的基礎上,需要包含 3 種不同特點的數據。由於做的是排序算法時間復雜度的分析,因此 3 種不同情況分別為最好(正序)、最差(倒敘)和隨機情況。
接着需要編寫運行不同排序算法的程序,由於數據集的規模不同,使用純 C 語言的動態內存分配並不能很好地適應不同數據集。因此此處選擇使用 STL 庫中的 vector 容器來自動管理內存,依次適應不同規模的測試數據。通過添加計數變量來記錄基本語句的執行次數,在每個數據集運行完畢后輸出,進行實驗數據統計。
數據生成
數據生成腳本
由於實驗中的實驗數據打算從文件中讀取,因此生成數據時需要把數據保存在文件中。選擇使用 Python 腳本生成實驗所需數據集:
import random
filename = 'XXX.txt'
with open(filename, 'w') as file_object:
for i in range(10000):
file_object.write(str(random.randint(-10000,10000)) + '\n')
#file_object.write(str(i) + '\n')
#file_object.write(str(10000 - i) + '\n')
print("成功生成數據集" + filename)
數據集概況
數據集序號 | 數據集數據量(個) | 數據集特點 |
---|---|---|
1 | 100 | 正序自然數等差數列 |
2 | 100 | (-10000,10000)隨機數 |
3 | 100 | (-10000,10000)隨機數 |
4 | 100 | 逆序自然數等差數列 |
5 | 1000 | 正序自然數等差數列 |
6 | 1000 | (-10000,10000)隨機數 |
7 | 1000 | (-10000,10000)隨機數 |
8 | 1000 | 逆序自然數等差數列 |
9 | 10000 | 正序自然數等差數列 |
10 | 10000 | (-10000,10000)隨機數 |
11 | 10000 | (-10000,10000)隨機數 |
12 | 10000 | 逆序自然數等差數列 |
算法程序
主函數
首先編寫試驗所需的程序框架,即主函數。設計輸入數據集名時,程序接受文件名,然后把文件名交付給 file_Read()文件讀取函數進行讀取。使用 C++ STL 庫的 vector 容器進行存儲數據,因此 file_Read()函數的返回值應該是存儲文件中所有數據的 vector 容器。由於實驗實現2種排序算法,因此程序需要實現2種算法對應的函數。主函數調用排序算法進行排序並回顯基本語句數量,進行試驗數據記錄。最后使用迭代器遍歷排序完畢的 vector容器,輸出排序結果檢驗排序是否正確。
int main()
{
vector<int> dataset;
vector<int>::iterator it;
char file_name[10];
cin >> file_name;
dataset = file_Read(file_name);
dataset = BubbleSort(dataset);
//dataset = SelectSort(dataset);
/*for(it = dataset.begin(); it!= dataset.end(); it++)
{
cout << *it << endl;
}*/
return 0;
}
排序函數
選取冒泡排序法和選擇排序法進行分析,分別按照 2 種算法的實現方式編寫函數,注意要在基本語句——比較和交換語句處設置計數器。當算法執行完畢時輸出基本語句的執行次數,進行記錄。
vector<int> BubbleSort(vector<int> dataset) //冒泡排序
{
int temp;
int compare_count = 0;
int exchange_count = 0;
int exchange = dataset.size() - 1;
int bound;
while(exchange != 0)
{
bound = exchange;
exchange = 0;
for(int i = 0; i < bound; i++)
{
compare_count++;
if(dataset[i] > dataset[i + 1])
{
exchange = i;
temp = dataset[i];
dataset[i] = dataset[i + 1];
dataset[i + 1] = temp;
exchange_count += 3;
}
}
}
cout << "比較次數為:" << compare_count << endl;
cout << "交換次數為:" << exchange_count << endl;
return dataset;
}
vector<int> SelectSort(vector<int> dataset) //選擇排序
{
int idx;
int temp;
int compare_count = 0;
int exchange_count = 0;
for (int i = 0; i < dataset.size(); i++)
{
idx = i;
for (int j = i + 1; j < dataset.size(); j++)
{
compare_count++;
if (dataset[idx] < dataset[j])
{
idx = j;
}
}
temp = dataset[i];
dataset[i] = dataset[idx];
dataset[idx] = temp;
exchange_count += 3;
}
cout << "比較次數為:" << compare_count << endl;
cout << "交換次數為:" << exchange_count << endl;
return dataset;
}
記錄實驗數據
依次輸入 12 個數據集,分別運行冒泡排序法和選擇排序法,所獲取的實驗數據如下:
數據集序號 | 冒泡排序比較次數 | 冒泡排序交換次數 | 選擇排序比較次數 | 選擇排序交換次數 |
---|---|---|---|---|
1 | 99 | 0 | 4950 | 300 |
2 | 4895 | 7257 | 4950 | 300 |
3 | 4745 | 7668 | 4950 | 300 |
4 | 4950 | 14850 | 4950 | 300 |
5 | 999 | 0 | 499500 | 3000 |
6 | 495821 | 771342 | 499500 | 3000 |
7 | 496056 | 756987 | 499500 | 3000 |
8 | 499500 | 1498500 | 499500 | 3000 |
9 | 9999 | 0 | 49995000 | 30000 |
10 | 49931801 | 75580422 | 49995000 | 30000 |
11 | 49925382 | 74716506 | 49995000 | 30000 |
12 | 49995000 | 149985000 | 49995000 | 30000 |
實驗數據分析
首先對比較次數進行分析,3種規模的數據條形圖如下。可以明顯地看到,當數據已經基本有序時,冒泡排序算法能夠在很低的次數就完成排序。當數據完全失序或者處於較為隨機的狀態時,冒泡排序算法的比較次數略小於選擇排序,但是差別並不大。這個趨勢會隨着數據的規模增大而變得更加明顯。
接下來分析交換次數,3 種規模的數據條形圖如下。可以明顯地看出雖然在基本有序的情況下,冒泡排序的交換次數為 0。但是在其他情況下冒泡排序算法的交換次數遠大於選擇排序,尤其是在完全失序的情況下,冒泡排序算法的交換次數甚至是隨機情況下的 2 倍。而選擇排序的交換次數是固定的,是數據集數據量的 3 倍。
最后分析算法基本語句的總執行次數,3 種規模的數據條形圖如下。在數據基本有序的情況下,冒泡排序的基本語句執行次數遠小於選擇排序。但是在其他情況,冒泡排序的基本語句執行次數會是選擇排序的 2 倍以上,當完全失序時甚至能達到 3 倍以上。
時間復雜度
首先我們看冒泡排序。在最好情況下,也就是初始序列是一趟排序時,只需要進行一趟排序。排序過程中進行 n-1 次關鍵字比較,且不移動記錄。在初始序列為逆序的最壞情況下,需要進行 n-1 趟排序,總的比較次數 num1 為:
總的移動次數 num2 為:
所以在平均情況下,冒泡排序的關鍵字比較次數和記錄移動次數分別約為 n^2/4 和 3n^2/4,時間復雜度為 O(n^2) 。從上文的統計數據來看,冒泡排序的基本語句執行次數遠大於n的一次方階,遠小於n的三次方階,與平方階的數量級更為接近。其中最壞情況時間復雜度為 O(n^2),最好情況時間復雜度為
O(1)。
接下來看看選擇排序,選擇排序所需要進行移動的次數較少。最好情況,也就是數據集時正序序列時不需要移動。在逆序的最壞情況下,算法需要移動 3(n-1)次。無論記錄的初始排列如何,所需進行的關鍵字間比較次數相同,num 值都是:
因此選擇排序算法的時間復雜度也是 O(n^2)。從上文的統計數據來看,選擇排序的基本語句執行次數遠大於 n 的一次方階,遠小於 n 的三次方階,與平方階的數量級更為接近。其中最壞情況和最好情況的時間復雜度都是 O(n^2)。
參考資料
《數據結構(C語言版|第二版)》—— 嚴蔚敏 李冬梅 吳偉民 編著,人民郵電出版社
《算法設計與分析(第二版)》——王紅梅,胡明 編著,清華大學出版社
算法:排序
c++輸入文件流ifstream用法詳解
C++ stringstream介紹,使用方法與例子