本章將探討兩個高級主題:動態規劃和貪心算法 。javascript
使用遞歸去解決問題雖然簡潔,但效率不高 。包括 JavaScript 在內的衆多語言,不能高效地將遞歸代碼解釋爲機器代碼,儘管寫出來的程序簡潔,可是執行效率低下 。java
如下以計算斐波那契數列爲例進行說明。算法
斐波那契數列定義以下:數組
0 1 1 2 3 5 8 13 21 34 ……
複製代碼
斐波那契數列除了前兩項外,其餘值都是前兩項的和。使用遞歸計算斐波那契數列程序以下:瀏覽器
function recurFib(n) {
if (n < 2) {
return n
} else {
return recurFib(n - 1) + recurFib(n - 2)
}
}
console.log('recurFib(30):', recurFib(30)) // 832040
複製代碼
這裏咱們請求的是第30位的斐波那契數是什麼,這個還能計算出來,咱們若是改爲50的話,個人瀏覽器直接就崩潰了。數據結構
爲何會這樣子? 這是由於咱們使用了遞歸,而遞歸的效率是很低了,存在了大量的重複運算,咱們以獲取第5位的斐波那契數列爲例。函數
從這個圖中咱們能夠看到,咱們要計算斐波那契數列第5位是啥,咱們計算了2次第3位是啥,計算了3次第2位是啥……,咱們的程序中存在了大量重複運算。這就是遞歸效率低的緣由。學習
若是編譯器能夠將已經計算的值記錄下來,函數的執行效率就不會如此差。咱們能夠使用動態規劃的技巧來設計一個效率更高的算法 。測試
程序以下:ui
function dynFib(n) {
let valArr = new Array(n)
for (let i = 0; i <= n; i++) {
valArr[i] = 0
}
if (n === 0) {
return 0
}
if (n === 1 || n === 2) {
return 1
} else {
valArr[1] = 1
valArr[2] = 1
for (let i = 3; i <= n; i++) {
valArr[i] = valArr[i - 1] + valArr[i - 2]
}
return valArr[n - 1]
}
}
console.log('dynFib(50):', dynFib(50));
複製代碼
這時候咱們再計算斐波那契數列第50位是啥的時候,瀏覽器就不會卡死了。
使用for循環計算斐波那契數列
function iterFib(n) {
let last = 1
let nextLast = 1
let result = 1
for (let i = 2; i < n; i++) {
result = last + nextLast
nextLast = last
last = result
}
return result
}
console.log('iterFib(50):', iterFib(50))
複製代碼
另外一個適合使用動態規劃去解決的問題是尋找兩個字符串的最長公共子串。例如,在單詞「raven」和「havoc」中,最長的公共子串是「av」。尋找最長公共子串經常使用於遺傳學中,用於使用核苷酸中鹼基的首字母對 DNA 分子進行描述
動態規劃是更適合解決這個問題的方案。這個算法使用一個二維數組存儲兩個字符串相同位置的字符比較結果。初始化時,該數組的每個元素被設置爲 0。每次在這兩個數組的相同位置發現了匹配,就將數組對應行和列的元素加 1,不然保持爲 0 。
程序以下:
// 尋找最長公共子串
function lcs(word1, word2) {
// 公共子串的長途
let max = 0;
// 公共子串結束的索引
let index = 0
let lcsArr = new Array(word1.length + 1)
for (let i = 0; i <= word1.length; i++) {
lcsArr[i] = new Array(word2.length + 1)
for (j = 0; j <= word2.length; j++) {
lcsArr[i][j] = 0
}
}
for (let i = 0; i <= word1.length; i++) {
for (let j = 0; j <= word2.length; j++) {
if (i === 0 || j === 0) {
lcsArr[i][j] = 0
} else {
if (word1[i - 1] === word2[j - 1]) {
lcsArr[i][j] = lcsArr[i - 1][j - 1] + 1
} else {
lcsArr[i][j] = 0
}
}
if (max < lcsArr[i][j]) {
max = lcsArr[i][j]
index = i
}
}
}
let str = ''
if (max === 0) {
return str
} else {
for (let i = index - max; i <= max; i++) {
str += word2[i]
}
return str
}
}
複製代碼
看到這個程序,不得不感嘆,有些人真是太聰明瞭,居然能寫出這麼巧的程序。
測試:
console.log('lcs("abbcc","dbbcc"):', lcs("abbcc", "dbbcc"))
// lcs("abbcc","dbbcc"): bbcc
複製代碼
測試正常,說明咱們的程序沒有問題。
假設有個保險箱裏面有 5 件物品,它們的尺寸分別是 三、 四、 七、 八、 9,而它們的價值分別是 四、 五、 十、 十一、 13。咱們有個揹包,容積是16,咱們要從保險箱挑選一些東西放進揹包,咱們該怎樣裝才能使揹包的價值最大呢?
使用遞歸程序以下:
// 各個物品的價值
var value = [4, 5, 10, 11, 13]
// 各個物品的體積
var size = [3, 4, 7, 8, 9]
// 揹包容積
var capacity = 16
// 數組大小
var n = 5
function bag(capacity, size, value, n) {
if (n === 0 || capacity === 0) {
return 0
}
// 若是某一個物品的體積已經大於容積了,那這個物品確定就不行,則從下一個物品開始判斷
if (size[n - 1] > capacity) {
return bag(capacity, size, value, n - 1)
} else {
// 比較放入這個物品跟不放入這個物品,哪種狀況下揹包的價值最大
let add = value[n - 1] + bag(capacity - size[n - 1], size, value, n - 1);
let unAdd = bag(capacity, size, value, n - 1);
return Math.max(add, unAdd)
}
}
複製代碼
測試:
console.log(bag(capacity, size, value, n))
// 23
複製代碼
// 使用動態規劃方案
function dBag(capacity, size, value, n) {
let k = []
for (let i = 0; i <= capacity + 1; i++) {
k[i] = []
}
for (let i = 0; i <= n; i++) {
for (let j = 0; j <= capacity; j++) {
if (i === 0 || j === 0) {
k[i][j] = 0
} else if (size[i - 1] <= j) {
k[i][j] = Math.max(value[i - 1] + k[i - 1][j - size[i - 1]], k[i - 1][j])
} else {
k[i][j] = k[i - 1][j]
}
}
}
return k[n][capacity]
}
let result = dBag(capacity, size, value, n)
console.log('result:', result);
// 23
複製代碼
這程序實在是過高深了,這是怎麼想的呢?黑人問號臉.jpg。反正莫名其妙問題就解決了,爲啥要這樣子,我想了好久都沒明白,有明天的人請指教下。
貪心算法的一個經典案例是找零問題。你從商店購買了一些商品,找零 63 美分,店員要怎樣給你這些零錢呢?若是店員根據貪心算法來找零的話,他會給你兩個 25 美分、一個10 美分和三個 1 美分。在沒有使用 50 美分的狀況下這是最少的硬幣數量。
程序實現以下:
function makeChange(origAmt) {
let result = []
if (origAmt % 25 < origAmt) {
result[0] = parseInt(origAmt / 25)
origAmt = origAmt % 25
}
if (origAmt % 10 < origAmt) {
result[1] = parseInt(origAmt / 10)
origAmt = origAmt % 10
}
if (origAmt % 5 < origAmt) {
result[2] = parseInt(origAmt / 5)
origAmt = origAmt % 5
}
result[3] = origAmt
return result
}
function showChange(coins) {
if (coins[0] > 0) {
console.log("25 美分的數量 - " + coins[0] + " - " + coins[0] * 25);
}
if (coins[1] > 0) {
console.log("10 美分的數量 - " + coins[1] + " - " + coins[1] * 10);
}
if (coins[2] > 0) {
console.log("5 美分的數量 - " + coins[2] + " - " + coins[2] * 5);
}
if (coins[3] > 0) {
console.log("1 美分的數量 - " + coins[3] + " - " + coins[3] * 1);
}
}
let origAmt = 63;
showChange(makeChange(origAmt));
// 打印結果
25 美分的數量 - 2 - 50
10 美分的數量 - 1 - 10
1 美分的數量 - 3 - 3
複製代碼
自此本章就完了,數據結構與算法的javascript的部分也講解完了。讀這本書用了好幾個月,大概98%的都能看懂,目前就一個歸併排序和一個揹包問題,看不懂。留着之後慢慢想吧。
再次長出一口氣,終於完了,雖然我只是作筆記,敲敲別人的代碼,可是仍是深深的體會到了看完一本書是多麼不容易的一件事。學習之路真是艱難且長遠。加油,向大神進軍。