JavaScript數據結構之 —— 11動態規劃(實例)

遞歸

遞歸是一種解決問題的方法,通常涉及函數調用自身。我們使用遞歸,並不是因爲它運行速度更快,而是因爲它更利於理解,代碼也少。

能夠像下面這樣直接調用自身的方法或函數,是遞歸函數:

var recursiveFunction = function(someParam){
	recursiveFunction(someParam);
};

能夠像下面這樣間接調用自身的函數,也是遞歸函數:

var recursiveFunction1 = function(someParam){
	recursiveFunction2(someParam);
};
var recursiveFunction2 = function(someParam){
	recursiveFunction1(someParam);
};

棧溢出錯誤

遞歸併不會無限地執行下去;瀏覽器會拋出錯誤,也就是所謂的棧溢出錯誤(stack overflow error)
每個瀏覽器都有自己的上限,可用以下代碼測試:

var i = 0;
function recursiveFn () {
	i++;
	recursiveFn();
}
try {
	recursiveFn();
} catch (ex) {
	console.log('i = ' + i + ' error: ' + ex);
}

在這裏插入圖片描述

計算斐波那契數列

遞歸方法

n = (n-1) + (n-2)
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, …

function fibonacci(num){
	if (num < 2) {
		return num;
	}
	return fibonacci(num - 1) + fibonacci(num - 2);
}

上面這個函數的問題在於它的執行效率非常低,有太多的值在遞歸調用中被重新計算,如果編譯器可以將已經計算的值記錄下來,函數的執行效率就不會如此差。我們可以使用動態規劃的技巧來設計一個效率更高的算法。

假如需要找出第六個位置的斐波那契數,如下所示,僅僅是第三個斐波那契數就計算了三次,求值越大,計算的次數會變得更多:
在這裏插入圖片描述

動態規劃

使用遞歸去解決問題雖然簡潔,但效率不高,儘管寫出來的程序簡潔,但是執行效率低下。許多使用遞歸去解決的編程問題,可以重寫爲使用動態規劃的技巧去解決。動態規劃方案通常會使用一個數組來建立一張表,用於存放計算過程中的值,可以避免重新計算。

function dynFib(n) {
	if (n < 2) {
		return n;
	}
	else {
		var val = [0,1]; // 初始化 作爲保存中間結果的數組
		for (var i = 2; i <= n; ++i) {
			val[i] = val[i-1] + val[i-2];
		}
		return val[n];
	}
}

比較一下時間:

var start = new Date().getTime();
console.log(fibonacci(40));
var stop = new Date().getTime();
console.log(" 遞歸計算耗時 - " + (stop-start) + " 毫秒");

start = new Date().getTime();
console.log(dynFib(40));
stop = new Date().getTime();
console.log(" 動態規劃耗時 - " + (stop-start) + " 毫秒");

在這裏插入圖片描述
由下圖可以看出,動態規劃的上限是非常高的:
在這裏插入圖片描述

看到這裏我們可以輕易地發現,上面的數組,只是存儲值的一種方式,實際上我們也可以不使用數組來實現這個功能,如下所示:

function iterFib(n) {
	// 聲明前三個值
	var last = 1;
	var nextLast = 0;
	var result = 1;
	
	for (var i = 2; i <= n; ++i) {
		result = last + nextLast;
		nextLast = last;
		last = result;
	}
	return result;
}

雖然教材上說這個版本的函數在計算斐波那契數列時和動態規劃版本的效率一樣。但是經過測試我們發現,這種方式比使用數組實際上還要更快一點。
在這裏插入圖片描述

要注意動態規劃和分而治之(歸併排序和快速排序算法中用到的那種)是不同的方法。分而治之方法是把問題分解成相互獨立的子問題,然後組合它們的答案,而動態規劃則是將問題分解成相互依賴的子問題。

動態規劃尋找最長公共子串

例如,在單詞“raven”和“havoc”中,最長的公共子串是“av”。

function lcs(word1, word2) {
	var max = 0;
	var index = 0;
	// 聲明一個二維數組,長寬分別爲兩個比較的字符串的長度,數組中所有內容爲 0
	var lcsarr = new Array(word1.length + 1);
	for (var i = 0; i <= word1.length + 1; ++i) {
		lcsarr[i] = new Array(word2.length + 1);
		for (var j = 0; j <= word2.length + 1; ++j) {
			lcsarr[i][j] = 0;
		}
	}

	// 對角線匹配,如果遇到相等的,就對角線加一
	for (var i = 0; i <= word1.length; ++i) {
		for (var 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] = word1[i-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;
			}
		}
	}
	console.log(lcsarr )
	var str = "";
	console.log(index,max)
	if (max == 0) {
		return "";
	} else {
		return word1.substr(index-max,max);
	}
}

實現原理如下圖所示:
在這裏插入圖片描述
爲了方便理解,我們先實現前面兩步,打印出連續的元素有哪些:

function lcs(word1, word2) {
	var max = 0;
	var index = 0;
	// 聲明一個二維數組,長寬分別爲兩個比較的字符串的長度,數組中所有內容爲 0
	var lcsarr = new Array(word1.length + 1);
	for (var i = 0; i <= word1.length + 1; ++i) {
		lcsarr[i] = new Array(word2.length + 1);
		for (var j = 0; j <= word2.length + 1; ++j) {
			lcsarr[i][j] = 0;
		}
	}

	// 對角線匹配,如果遇到相等的,就對角線加一
	for (var i = 0; i <= word1.length; ++i) {
		for (var 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] = word1[i-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-1;
			}
		}
	}
	return lcsarr;
}

在這裏插入圖片描述
然後將打印出來的字符換爲遞增數字,並保留最後匹配到的位置,使用如下方法返回,即可打印出結果:

return {
	'最長公共子串長度':max,
	'最長公共子串內容':word1.substr(index-max,max)
};

在這裏插入圖片描述

揹包問題

揹包問題是算法研究中的一個經典問題。試想你一些物品放入你的一個小揹包中。物品的尺寸和價值不同。你希望自己的揹包裝進的寶貝總價值最大。
如果在我們例子中的保險箱中有5 件物品,它們的尺寸分別是3、4、7、8、9,而它們的價值分別是4、5、10、11、13,且揹包的容積爲16,那麼恰當的解決方案是選取第三件物品和第五件物品,他們的總尺寸是16,總價值是23。

遞歸解決方案

function max(a, b) {
	return (a > b) ? a : b;
}

function knapsack(capacity, size, value, n) {
	if (n == 0 || capacity == 0) {
	// 如果揹包容量或者物品總數爲零,返回總價值 零
		return 0;
	}
	if (size[n - 1] > capacity) {
	// 從後往前,如果物品尺寸大於揹包容量,就跳過
		return knapsack(capacity, size, value, n - 1);
	} else {
	// 如果物品尺寸小於揹包容量,就放進揹包,計算物品價值
		return max(value[n - 1] +
		knapsack(capacity - size[n - 1], size, value, n - 1),
		knapsack(capacity, size, value, n - 1));
	}
}

函數 knapsack 接收四個參數,分別爲 揹包容量、物品尺寸、物品價值和物品數量,物品根據物品價值排序。
驗證:

var value = [4, 5, 10, 11, 13];
var size = [3, 4, 7, 8, 9];
var capacity = 16;
var n = 5;
knapsack(capacity, size, value, n);

在這裏插入圖片描述

動態規劃解決方案

待補充

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章