《Algorithms Unlocked》是 《算法導論》的合著者之一 Thomas H. Cormen 寫的一本算法基礎。
書中沒有涉及編程語言,直接用文字描述算法,我用 JavaScript 對書中的算法進行描述。
循環和查找
首先是三個簡單的查找。目的是從數組中查找一個特定的值。
array: 一個數組
x: 要查找的值
// 簡單的線性查找
function linearSearch(array, x) {
let answer = 'NOT-FOUND';
for (let i = 0; i < array.length; i++) {
if (array[i] === x) {
// 雖然找到了i, 但沒有返回繼續查找,直到 for 結束
answer = i;
}
}
console.log(answer);
return;
}
雖然找到了目標值,但for循環依然繼續遍歷直到結束,下面是優化
// 優化的查找,找到目標後立刻返回
function betterLinearSearch(array, x) {
for (let i = 0; i < array.length; i++) {
if (array[i] === x) {
// 直接返回
console.log(i);
return;
}
}
console.log('NOT-FOUND');
return;
}
還有一個問題是:假如直到最後都沒有找到目標值,將試圖訪問越過數組末尾的元素。書上說:“在計算機程序中,當你試圖訪問越過數組末尾的元素時,結果通常是糟糕的。你的程序可能會崩潰,也可能會損壞數據。”
寧可信其有,不可信其無啊。繼續優化。
// 更優的寫法
// 總是讓 for 循環可以結束
function sentinelLinearSearch(array, x) {
let n = array.length - 1; // 最後一個元素
// 把數組最後一個值保存到last變量中
let last = array[n]
// 把數組最後一個值替換成目標值
array[n] = x;
// 判斷數組中是否有目標值x,即使沒有,數組的最後一個值也一定是目標值,避免越過數組末尾的訪問
let i = 0;
while (array[i] !== x) {
i++;
}
//如果i小於數組長度,或者最後一個值爲目標值x,則返回i
array[n] = last;
if (i < n || last === x) {
console.log(i);
return;
}
return 'NOT-FOUND';
}
第三個方案在進行循環遍歷的時候只進行了一個判斷——array[i]是否等於x,而上面的兩種方案在進行for循環時都要進行i是否大於length的判斷和array[i]是否等於x兩個判斷。所以當數組大到一定程度的時候,第三個方案效率大於上面兩個方案。
遞歸
遞歸是指在函數中對函數自身進行調用。
遞歸有兩個特性:
- 必須有一個或對個基礎情況,它是指不用遞歸而直接計算出結果。比如下面例子中:當 n=0 時,基礎情況發生,f(0) = 1;
- 程序中的每個遞歸調用一定是通過一系列關於同一個問題的子問題的求解而最終迭代到基礎情況。
下面是一個經典的遞歸例子,計算階乘。
當n=0時,n! = 1 且 n! = n(n-1)(n-2)...3•2•1 (n≥0)
比如:5! = 5•4•3•2•1 = 120
// 階乘
function factorial(n) {
if (n >= 0) {
if (n === 0) {
return 1;
};
return n * factorial(n - 1);
};
}
之前的查找算法也可以寫成遞歸風格
// 線性查找的遞歸風格
function recursiveLinearSearch(array, i, x) {
if (i < array.length) {
if (array[i] === x) {
console.log(i);
return;
}else {
return recursiveLinearSearch(array, i+1, x);
}
}
console.log('NOT-FOUND');
return;
}