版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2017.08.11 |
前言
將數(shù)據(jù)結(jié)構(gòu)和算法比作計(jì)算機(jī)的基石毫不為過,追求程序的高效是每一個(gè)軟件工程師的夢想。下面就是我對算法方面的基礎(chǔ)知識(shí)理論與實(shí)踐的總結(jié)。感興趣的可以看上面幾篇。
1. 算法簡單學(xué)習(xí)(一)—— 前言
插入排序 insertion - sort
插入排序是一個(gè)針對少量元素進(jìn)行排序的有效算法,下面我們先看一下插入排序的模型。
1. 問題描述
輸入數(shù)據(jù):n個(gè)數(shù) (a1, a2, ... , an)
輸出數(shù)據(jù):對這n個(gè)數(shù)據(jù)重新排序,使得a1' ≤ a2' ≤ ... ≤ an'
。這里待排序的數(shù)也稱為關(guān)鍵字。
2. 算法模型
開始打牌時(shí),左邊的手是空著的,牌面朝下放在桌子上,接著,一次從桌子上摸起一張牌,并將它插入左手一把牌中的正確位置,為了找到這個(gè)牌的正確位置,要將它與手中已有從右向左進(jìn)行比較,如上圖所示。無論什么時(shí)候左手中的牌都是排序好的,而這些牌原先都是桌子上那副里最頂上的一些牌。
3. 算法偽代碼
下面我們就看一下算法偽代碼。
//insertion sort
1 for j ← 2 to length[A]
2 do key ← A[j]
3 //insert A[j] into the sorted sequence A[1 ... j - 1]
4 i ← j - 1
5 while i > 0 and A[i] > key
6 do A[i + 1] ← A[i]
7 i ← i - 1
8 A[i + 1] ← key
4. 循環(huán)不變式與插入算法的正確性
下面看這個(gè)數(shù)組 A = [5, 2, 4, 6, 1, 3]
,下標(biāo)j指示了待插入到手中的當(dāng)前牌,在外層for循環(huán)(循環(huán)變量為j)的每一輪迭代的開始,包含元素A[1 ... j - 1]
的子數(shù)組構(gòu)成了左手中當(dāng)前已經(jīng)排好的序的一手牌,元素A[j + 1 ... n]
對應(yīng)于桌子上的那堆沒有抓起來的牌。
下面以循環(huán)不變式(loop invariant)
的形式,來表達(dá)A[1 ... j - 1]
的這些性質(zhì)。在過程第1 ~ 8
行偽代碼中,在每一輪迭代的開始,子數(shù)組A[1 ... j - 1]
中包含了最初位于A[1 ... j - 1]
但是目前已經(jīng)排好序,循環(huán)過程如下所示。
上圖中黑色方框里面的值取自A[j]
的關(guān)鍵字值,在過程第5行的測試中,將它與左邊陰影框中的各個(gè)值進(jìn)行比較。
循環(huán)不變式的三個(gè)性質(zhì):
- 初始化: 它在循環(huán)的第一輪迭代開始之前,應(yīng)該是正確的。
- 保持 : 如果在循環(huán)的某一次迭代開始之前它是正確的,那么,在下一次迭代開始之前,它也應(yīng)該保持正確。
- 終止 : 當(dāng)循環(huán)結(jié)束時(shí),不變式給我們有一個(gè)有用的性質(zhì),有助于表明算法是正確的。
下面就證明一下這些性質(zhì)的正確性。
初始化 : 首先看第一輪迭代開始之前的正確性,此時(shí)
j = 2
,而子數(shù)組為A[1 ... j - 1]
,則只包含一個(gè)元素A[1]
,實(shí)際上就是最初在A[1]
中的那個(gè)元素,這個(gè)子數(shù)組已經(jīng)是排序好的了,證明第一次迭代開始之前是成立的。保持 :看第二個(gè)性質(zhì),證明每一輪循環(huán)都使循環(huán)不變式保持成立,從非形式化的意義上看,外層for循環(huán)的循環(huán)體中,要將
A[j - 1]
,A[j - 2]
,A[j - 3]
等元素向右移動(dòng)一個(gè)位置,直到找到A[j]的適當(dāng)位置時(shí)為止(第4 ~ 7
)行,這時(shí)將A[j]
的值插入(第 8 行)。如果要證明這個(gè)性質(zhì)不成立就需要證明while
循環(huán)有一個(gè)循環(huán)不成立,這里就不證明了。終止: 當(dāng)
j = n + 1
的時(shí)候外層for循環(huán)結(jié)束,將j
替換為n + 1
,就有子數(shù)組A[1..n]
包含了原先A[1..n]
中的元素,但是現(xiàn)在已經(jīng)排好序了,就已經(jīng)是整個(gè)數(shù)組了,也就意味這個(gè)數(shù)組是正確的。
5. 算法分析
算法分析即指對一個(gè)算法所需要的資源進(jìn)行預(yù)測,內(nèi)存、通信寬帶或計(jì)算機(jī)硬件等資源偶爾會(huì)是我們主要關(guān)心的,但是通常,資源就是指我們希望測度的計(jì)算時(shí)間。在分析一個(gè)算法之前,要建立有關(guān)實(shí)現(xiàn)技術(shù)的模型,包括描述所用資源及代價(jià)的模型。
插入排序過程的時(shí)間開銷和輸入有關(guān),不同個(gè)數(shù)的排序時(shí)間消耗一定不同。即使輸入個(gè)數(shù)相同,時(shí)間也不一定相同,還和它們已排列的程度有關(guān)。
可以將運(yùn)行時(shí)間表示為運(yùn)行時(shí)間
和輸入規(guī)模
的函數(shù)。
- 輸入規(guī)模:這個(gè)因素與具體問題有關(guān)。
- 運(yùn)行時(shí)間:指在特定輸入時(shí),所執(zhí)行的基本操作數(shù)或步數(shù),可以很方便的定義獨(dú)立于具體機(jī)器的步驟概念。目前先假定執(zhí)行一行偽代碼花費(fèi)的時(shí)間都是常量
ci
。
對 j = 2, 3, ..., n , n = length[A]
,設(shè)tj
為第 5
行中while
的測試次數(shù),循環(huán)體要比測試少一次,即不滿足條件退出,所得的占用時(shí)間如下所示。
下面我們把這些時(shí)間相加,可得。
上面的表達(dá)式就是運(yùn)行時(shí)間的總和。
從上面式子可以看到,即使規(guī)模相同,那么時(shí)間長度也與該規(guī)模下數(shù)據(jù)的輸入情況有關(guān)。下面我們看幾種特殊情況。
- 假定輸入數(shù)組已經(jīng)排好序
這種情況對于 j = 2, 3, ... , n
中的每一個(gè)值,都會(huì)發(fā)現(xiàn),在第 5 行匯總,當(dāng) i 取其初始值j - 1
時(shí),都有A[i] ≤ key
,于是對應(yīng)j = 2, 3, ... , n
中有tj = 1
,則可以簡化最佳運(yùn)行時(shí)間為:
這個(gè)時(shí)候時(shí)間就可以簡化為T(n) = an + b
,其中a, b都是與ci
有關(guān)的常數(shù)。
- 假定輸入數(shù)組是逆序排列
這個(gè)時(shí)候就需要我們將每一個(gè)元素A[j]與整個(gè)已排序的子數(shù)組A[1 ... j - 1]
中的每一個(gè)元素進(jìn)行比較,因此,對于 j = 2, 3, ... , n
有tj = j
,那么時(shí)間函數(shù)變化為:
我們簡化為 T(n) = an ^ 2 + bn + c
,a, b, c
均為與ci
有關(guān)的常數(shù),整個(gè)函數(shù)是關(guān)于n的二次函數(shù)。
6. 插入排序的最壞情況和平均情況分析
上面我們可以看到最壞情況就是輸入的是逆序的情況,最好的情況就是輸入的是排好順序的情況,我們一般考慮的就是最壞的運(yùn)行時(shí)間。
- 知道一個(gè)算法的運(yùn)行最壞情況,就是知道了算法的時(shí)間上限,就不用擔(dān)心它會(huì)變的更壞了。
- 對于某些算法來說,最壞的情況出現(xiàn)的情況還是相當(dāng)頻繁的,例如當(dāng)我們在數(shù)據(jù)庫里面檢索一條不存在的信息時(shí),就是出現(xiàn)了算法的最壞情況。
- 這里還要說一下平均情況,這里的平均情況就是數(shù)組中
A[1 ... j - 1]
中一半元素小于A[j]
,另外一半元素大于A[j]
,因此,這里有tj = j / 2
,在計(jì)算時(shí)間復(fù)雜度你就會(huì)發(fā)現(xiàn)它仍然是一個(gè)二次函數(shù),與最壞情況下的運(yùn)行時(shí)間是一樣的。
7. 增長的量級(jí)
下面我們對上面提出的時(shí)間表達(dá)式進(jìn)一步進(jìn)行首相,得到的就是運(yùn)行時(shí)間的增長率 (rate of growth)
,或稱為增長的量級(jí)(order of growth)
,這樣我們只考慮最高次項(xiàng)而忽略其他項(xiàng),同時(shí)也忽略最高次項(xiàng)的系數(shù),所以上面我們總結(jié)的最壞情況的表達(dá)式就為O(n^2)
。
一般認(rèn)為,如果一個(gè)算法最壞情況運(yùn)行時(shí)間要比另外一個(gè)算法的低,就認(rèn)為它的效率更高。
8. 代碼實(shí)現(xiàn)
下面我們就看一下代碼實(shí)現(xiàn)。
先建立一個(gè)C
工程。
下面我們看一下代碼實(shí)現(xiàn)。
亂序
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, const char * argv[])
{
int a[6] = {5, 2, 4, 6, 1, 3};
int length = sizeof(a)/sizeof(a[0]);
clock_t startTime, endTime;
startTime = clock();
for (int j = 1; j <= length - 1; j++) {
int i = j - 1;
int key = a[j];
while (i >= 0 && a[i] > key) {
a[i + 1] = a[i];
i --;
a[i + 1] = key;
}
}
endTime = clock();
for (int k = 0; k < length; k++) {
printf("%d\n",a[k]);
}
printf("運(yùn)行時(shí)間為%ld\n", endTime - startTime);
return 0;
}
下面看輸出結(jié)果
1
2
3
4
5
6
運(yùn)行時(shí)間為3
Program ended with exit code: 0
順序
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, const char * argv[])
{
int a[6] = {1, 2, 3, 4, 5, 6};
int length = sizeof(a)/sizeof(a[0]);
clock_t startTime, endTime;
startTime = clock();
for (int j = 1; j <= length - 1; j++) {
int i = j - 1;
int key = a[j];
while (i >= 0 && a[i] > key) {
a[i + 1] = a[i];
i --;
a[i + 1] = key;
}
}
endTime = clock();
for (int k = 0; k < length; k++) {
printf("%d\n",a[k]);
}
printf("運(yùn)行時(shí)間為%ld\n", endTime - startTime);
return 0;
}
下面看一下輸出結(jié)果
1
2
3
4
5
6
運(yùn)行時(shí)間為1
Program ended with exit code: 0
倒序
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, const char * argv[])
{
int a[6] = {6, 5, 4, 3, 2, 1};
int length = sizeof(a)/sizeof(a[0]);
clock_t startTime, endTime;
startTime = clock();
for (int j = 1; j <= length - 1; j++) {
int i = j - 1;
int key = a[j];
while (i >= 0 && a[i] > key) {
a[i + 1] = a[i];
i --;
a[i + 1] = key;
}
}
endTime = clock();
for (int k = 0; k < length; k++) {
printf("%d\n",a[k]);
}
printf("運(yùn)行時(shí)間為%ld\n", endTime - startTime);
return 0;
}
下面看輸出結(jié)果
1
2
3
4
5
6
運(yùn)行時(shí)間為3
Program ended with exit code: 0
后記
未完,待續(xù)~~~