读【数据结构与算法】之高级算法

高级算法,本部分只包含两个主题:动态规划和贪心算法。

动态规划有时会被认为是一种与递归相反的技术。动态规划是从底部开始解决问题,将所有小问题解决掉,然后合并成一个整体的解决方案。

贪心算法是一种寻找“优质解”为手段而达成的整体解决方案算法。这些优质的解决方案称为局部最优解,将有希望得到正确的最终解决方案,也称为全局最优解。

动态规划

动态规划实例:计算斐波那契数列

利用递归实现的方式:

/**
 * 计算斐波那契数列(递归实现)
 * @param  {Number} n 指定序号
 * @return {Number}   指定序号的值
 */
function recurFib(n) {
	if (n < 2) {
		return n;
	} else {
		return recurFib(n - 1) + recurFib(n - 2);
	}
}

但是,你会发现这样的话效率会比较底下,因为每次计算都需要重新计算下之前已经计算过的值。如果把之前计算的值保存起来的话,就会得到很大提升。

使用动态规划设计算法是从他能够解决的最简单的子问题开始,继而通过得到解来解决其他更复杂的子问题。所有的子问题的解通常被存储在一个数组中以便于访问。

下边是优化后两个版本:

/**
 * 计算斐波那契数列(动态规划实现-数组)
 * @param  {Number} n 指定序号
 * @return {Number}   指定序号的值
 */
function dynFib(n) {
	var val = [], i = 0;
	for ( ; i <= n; i++) {
		val[i] = 0;
	}
	if (n === 1 || n === 2) {
		return 1;
	} else {
		val[1] = 1;
		val[2] = 2;
		for (i = 3; i <= n; i++) {
			val[i] = val[i - 1] + val[i - 2];
		}
		return val[n - 1];
	}
}

/**
 * 计算斐波那契数列(动态规划实现-整数)
 * @param  {Number} n 指定序号
 * @return {Number}   指定序号的值
 */
function iterFib(n) {
	var last = 1, nextLast = 1,
			result = 1, i = 2;
	for ( ; i < n; i++) {
		result = last + nextLast;
		nextLast = last;
		last = result;
	}
	return result;
}

寻找最长公共子串

使用动态规划来解决寻找最长公共子串是很适合的。该算法使用一个二维数组存储两个字符串相同位置的字符比较结果,初始化时,该数组的每一个元素设置为0,每次在这两个数组相同位置发现了匹配,就将数组对应的行和列元素加1,否则还为0。

基本实现:

/**
 * 寻找最长公共子串
 * @param  {String} word1 单词1
 * @param  {String} word2 单词2
 * @return {String}       单词1和单词2中最长公共子串
 */
function lcs(word1, word2) {
	var max = 0, // 出现公共字符的最大长度
			index = 0, // 出现最大长度时在word1中位置(从1开始计)
			len1 = word1.length,
			len2 = word2.length,
			lcsarr = new Array(len1 + 1),
			i = 0, j, ret = '';

	// 初始化二维数组每一个值都为0
	for ( ; i <= len1 + 1; i++) {
		lcsarr[i] = new Array(len2 + 1);
		for (j = 0; j <= len2 + 1; j++) {
			lcsarr[i][j] = 0;
		}
	}
	// 循环遍历所有的字母
	// 如果字母相等,那么对应的长度就是
	// 单词1和单词2上一个位置上所对应的
	// 长度值加1,否则就是当前的两个单词
	// 对应位置上的值不相等,长度值为0
	for (i = 0; i <= len1; i++) {
		for (j = 0; j <= len2; 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;
				}
			}
			// 如果出现了新的最长长度
			// 那么就对max重新赋值
			// 将index也重新赋值
			if (max < lcsarr[i][j]) {
				max = lcsarr[i][j];
				index = i;
			}
		}
	}

	if (max === 0) {
		ret = '';
	} else {
		// 从index - max的位置开始 取得单词2中
		// 到max长度位置的所有字母加起来就是
		// 需要的结果
		for (i = index - max; i <= max; i++) {
			ret += word2[i];
		}
	}
	return ret;
}

背包问题

经典背包问题:假如你是一个保险箱大盗,打开了一个满是宝物的保险箱,但是你必须挑选一些价值最高的东西放到你拿的小背包中,该怎么选择?

经典实现是使用递归来实现:

/**
 * 背包问题(递归)
 * @param  {Number} capacity 体积
 * @param  {Array}  size     尺寸组成数组
 * @param  {Array}  value    价值组成的数组
 * @param  {Number} n        当前用于计算长度
 * @return {Number}          最大价值
 */
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 Math.max(
			value[n - 1] + knapsack(capacity - size[n - 1], size, value, n - 1), knapsack(capacity, size, value, n - 1));
	}
}

下边看看利用动态规划来实现这个背包问题:

/**
 * 背包问题(动态规划)
 * @param  {Number} capacity 体积
 * @param  {Array}  size     尺寸组成数组
 * @param  {Array}  value    价值组成的数组
 * @param  {Number} n        当前用于计算长度
 * @return {Number}          最大价值
 */
function dKnapsack(capacity, size, value, n) {
	var K = [], i = 0, w = 0;
	for ( ; i <= capacity + 1; i++) {
		K[i] = [];
	}
	for (i = 0; i <= n; i++) {
		for (w = 0; w <= capacity; w++) {
			if (i === 0 || w === 0) {
				K[i][w] = 0;
			} else if (size[i - 1] <= w) {
				K[i][w] = Math.max(value[i - 1] + K[i - 1][w - size[i - 1]], K[i - 1][w]);
			} else {
				K[i][w] = K[i - 1][w];
			}
		}
	}
	return K[n][capacity];
}

贪心算法

贪心算法就是一种比较简单的算法,他总是会选择当下的最优解,而不去考虑这一次选择会不会对未来的选择造成什么影响。

贪心算法案例:找零问题

从商店里买了一些商品,找零63美分,店员要怎么样给你找零呢?根据贪心算法他会给你两个25美分一个10美分和三个1美分。当然,这是在没有使用50美分的情况下。

基本实现:

/**
 * 找零
 * @param  {Number} origAmt 总钱数
 * @return {Array}          需要找的不同面额钱个(张)数
 */
function makeChange(origAmt) {
	var remainAmt = 0;
	// 不同面额钱 25美分 10美分 5美分 1美分
	var money = [.25, .1, .05, .01];
	var i = 0, len = money.length;
	var ret = [], tmp;

	for ( ; i < len; i++) {
		if (origAmt === 0) break;
		tmp = money[i];
		// 按顺序 由小到大
		if (origAmt % tmp < origAmt) {
			ret[i] = parseInt(origAmt / tmp);
			origAmt = remainAmt = origAmt % tmp;
		}
	}
	return ret;
}

具体运行例子请看demo高级算法

发布于: 2014年 09月 26日