本章将会学习递归、动态规划和贪心算法。

第十一章 算法模式

递归

递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题。递归通常涉及函数调用自身。

递归函数是像下面能够直接调用自身的方式或函数

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

能够像下面这样间接调用自身的函数,也是递归函数

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

假设现在必须要执行 recursiveFunction ,结果是什么?单单上述情况而言,它会一直执行下去。因此,每个递归函数都必须有边界条件,即一个不再递归调用的条件(停止点),以防无限递归。

JavaScript 调用栈大小的限制

如果忘记加上用以停止函数递归调用的边界条件,会发生什么呢?递归并不会无限执行下去,浏览器会抛出错误,也就是所谓的栈溢出错误(stack Overflow error)

每个浏览器都有自己的上限,可以用一下代码测试。

var i = 0;
function recursiveFn(){
    i++;
    recursiveFn();
}
try{
   recursiveFn();
}catch(e){
    console.log('i='+i+' error:'+e);
}
// 谷歌:i=15706 error:RangeError: Maximum call stack size exceeded
// 360:i=31470 error:RangeError: Maximum call stack size exceeded
// 火狐:i=40687 error:InternalError: too much recursion

根据操作系统和浏览器的不同,具体的数值也会有所不同,但区别不大。

ES6 有尾调用优化(tail call optimazation)。如果函数内最后一个操作是调用函数,会通过“跳转指令(jump)”而不是“子程序调用(subroutine call )”来控制。也就是说,ES6中,这里的代码会一直执行下去。所以,具有停止递归的边界条件很重要。

尾调用 点击看看,阮一峰老师的。

斐波那契数列

斐波那契数列的定义如下:

  • 1 和 2 的斐波那契数是1
  • n(n>2)的斐波那契数是(n-1)加上(n-2)的斐波那契数。

实现

function fibonacci(num){
    if(num === 1 || num ===2){
        return 1;
    }
    return arguments.callee(num - 1) + arguments.callee(num - 2);
}

让我们试着找出6的斐波那契数,其会产生如下函数调用

我们也可以用非递归的方法实现

function fib(num){
    var n1 = 1,
    n2 = 1,
    n = 1;
    for(var i = 3; i <= num; i++){
        n = n1 + n2;
        n1 = n2;
        n2 = n;
    }
    return n;
}

为什么要用递归?是因为更快吗?其实并不,反而更慢。递归的好处在于更容易理解,并且它所需的代码量更少。然后在ES6中,因为有尾调用,可以加快递归的速度。总而言之,我们用递归,通常是因为它更容易解决问题。

动态规划

动态规划(Dynamic Programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。与分而治之不同的是,动态规划是将问题分解成相互依赖的子问题。

用动态规划解决问题,要遵循三个步骤:

  1. 实现子问题。
  2. 实现要反复执行来解决子问题的部分
  3. 识别并求解出边界条件

可以用动态规划解决一些著名的问题如下:

  • 背包问题:给出一组项目,各自有值和容量,目标是要找出总值最大的项目的集合。这个问题的限制是,总容量必须小于等于“背包”的容量。
  • 最长公共子序列:找出一组序列的最长公共子序列(可由另一序列删除元素但不改变)
  • 矩阵链相乘:给出一系列矩阵,目标是找到这些矩阵相乘的最高效办法(计算次数尽可能少)。相乘操作不会进行,解决方案是找到这些矩阵各自相乘的顺序。
  • 硬币找零:给出面额为d1...dn的一定数量的硬币和要找零的钱数,找出有多少种找零的方法。
  • 图的全源最短路径:对所有顶点对(u,v),找出顶点u到顶点v的最短路径。

最少硬币找零问题

最少硬币找零问题是硬币问题的一个变种。硬币找零问题是给出要找零的钱数,以及可以用的硬币面额d1...dn 及其数量,找出有多少种找零方法。最少硬币找零问题是要给出要找零的钱数以及可用的硬币面额d1...dn及其数量,找出所需的最少硬币个数。

例如,美国有一下面额(硬币):d1=1,d2=5,d3=10,d4=25

如果要找36美分的零钱,我么可以用1个25美分,1个10美分和一个便士(1美分)

如何将这个解答转化成算法?

最少硬币找零的解决方案是找到n所需的最小硬币数。但要做到这一点,首先得找到对每个x<n的解。然后,我们将解建立在更小的值的基础上。

function MinCoinChange(coins){
    var coins = coins; // 零钱的面额
    var cache = {}; // 缓存
    this.makeChange = function(amount){ // 递归函数
        var me = this;
        if(!amount){ // 若金额总额小于0则返回空数组
            return [];
        }
        if(cache[amount]){ // 若缓存中已有该计算结果,则直接返回
            return cache[amount];
        }
        var min = [],newMin,newAmount;
        for(var i = 0; i < coins.length; i++){
            var coin = coins[i];
            newAmount = amount - coin;
            if(newAmount >= 0){
                newMin = me.makeChange(newAmount);
            }
            if(newAmount >= 0 && (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount)){
                min = [coin].concat(newMin);
                console.log('new Min '+ min + 'for '+ amount);
            }
        }
        return (cache[amount] = min);
    }
    this.getCache = function(){
        console.log(cache);
    }
}

测试

const minCoinChange = new MinCoinChange([1,5,10,25]);
console.log(minCoinChange.makeChange(36));
/*
new Min 1,1,1,1,1for 5
new Min 5for 5
new Min 1,5for 6
new Min 1,1,5for 7
new Min 1,1,1,5for 8
new Min 1,1,1,1,5for 9
new Min 1,1,1,1,1,5for 10
new Min 5,5for 10
new Min 10for 10
new Min 1,10for 11
new Min 1,1,10for 12
new Min 1,1,1,10for 13
new Min 1,1,1,1,10for 14
new Min 1,1,1,1,1,10for 15
new Min 5,10for 15
new Min 1,5,10for 16
new Min 1,1,5,10for 17
new Min 1,1,1,5,10for 18
new Min 1,1,1,1,5,10for 19
new Min 1,1,1,1,1,5,10for 20
new Min 5,5,10for 20
new Min 10,10for 20
new Min 1,10,10for 21
new Min 1,1,10,10for 22
new Min 1,1,1,10,10for 23
new Min 1,1,1,1,10,10for 24
new Min 1,1,1,1,1,10,10for 25
new Min 5,10,10for 25
new Min 25for 25
new Min 1,25for 26
new Min 1,1,25for 27
new Min 1,1,1,25for 28
new Min 1,1,1,1,25for 29
new Min 1,1,1,1,1,25for 30
new Min 5,25for 30
new Min 1,5,25for 31
new Min 1,1,5,25for 32
new Min 1,1,1,5,25for 33
new Min 1,1,1,1,5,25for 34
new Min 1,1,1,1,1,5,25for 35
new Min 5,5,25for 35
new Min 10,25for 35
new Min 1,10,25for 36
(3) [1, 10, 25]
*/
minCoinChange.getCache();
// {1: Array(1), 2: Array(2), 3: Array(3), 4: Array(4), 5: Array(1), 6: Array(2), 7: Array(3), 8: Array(4), 9: Array(5), 10: Array(1), 11: Array(2), 12: Array(3), 13: Array(4), 14: Array(5), 15: Array(2), 16: Array(3), 17: Array(4), 18: Array(5), 19: Array(6), 20: Array(2), 21: Array(3), 22: Array(4), 23: Array(5), 24: Array(6), 25: Array(1), 26: Array(2), 27: Array(3), 28: Array(4), 29: Array(5), 30: Array(2), 31: Array(3), 32: Array(4), 33: Array(5), 34: Array(6), 35: Array(2), 36: Array(3)}

const minCoinChange1 = new MinCoinChange([1,3,4]);
console.log(minCoinChange1.makeChange(6));
/*
 new Min 1for 1
 new Min 1,1for 2
 new Min 1,1,1for 3
 new Min 3for 3
 new Min 1,3for 4
 new Min 4for 4
 new Min 1,4for 5
 new Min 1,1,4for 6
 new Min 3,3for 6
 (2) [3, 3]
*/
minCoinChange1.getCache();  // {1: Array(1), 2: Array(2), 3: Array(1), 4: Array(1), 5: Array(2), 6: Array(2)}

背包问题

背包问题是一个组合优化问题。它可以描述如下:给定一个固定大小、能够携带W的背包,以及一组有价值和重量的物品,找出一个最佳解决方案,使得装入背包的物品总重量不超过W,且总价值最大。

下面是一个例子:

物品 重量 价值
1 2 3
2 3 4
3 4 5

考虑背包能够携带的重量只有5。对于这个例子,我们可以说最佳解决方案就是往背包里装入物品1和物品2,这样,总重量为5,总价值为7。

背包算法:

function knapSack(capacity,weights,values,n){
    var i,w,a,b,kS = [];
    for(i = 0; i <= n; i++){
        kS[i] = [];
    }
    for(i = 0; i <= n; i++){
        for(w = 0; w <= capacity; w++){
            if(i == 0 || w == 0){
                kS[i][w] = 0;
            }else if(weights[i-1] <= w){
                a = values[i - 1] + kS[i - 1][w - weights[i-1]];
                b = kS[i-1][w];
                kS[i][w] = (a > b) ? a : b;
            }else{
                kS[i][w] = kS[i-1][w];
            }
        }
    }
    return kS[n][capacity];
}

工作原理

  • 首先,初始化将用于寻找解决方案的矩阵ks[n+1][capacity+1]
  • 忽略矩阵的第一列和第一行,只处理索引不为0的列和行
  • 物品i的重量必须小于约束(capacity)才有可能成为解决方案的一部分。否则,总重量就会超出背包能够携带的重量。发生这种情况的话,就采用之前的值。
  • 当找到可以构成解决方案的物品时,选择价值最大的那个
  • 问题的解决方案就在二维表格右下角的最后一个格子里面

测试

var values = [3,4,5],
weights = [2,3,4],
capacity = 5,
n = values.length;
console.log(knapSack(capacity,weights,values,n)); // 7

上面的算法只输出背包携带物品价值的最大值,而不列出实际的物品。我们可以增加下面的附加函数来找出构成解决方案的物品:

function findValues(n,capacity,kS,weights,values){
    var i = n, k = capacity;
    console.log('解决方案包含以下物品: ');
    while( i > 0 && k > 0 ){
        if(kS[i][k] !== kS[i-1][k]){
            console.log('物品' + i + ',重量:' + weights[i-1] + ',价值:'+values[i - 1] );
            i--;
            k = k - kS[i][k];
        }else{
            i--;
        }
    }
}

输出结果:

解决方案包含以下物品:
物品2,重量:3,价值:4
物品1,重量:2,价值:3

最长公共子序列

另一个经常被当做编程挑战问题的动态最长公共子序列(LCS):找出两个字符串序列的最长子序列的长度。最长子序列是指,在两个字符串序列中以相同顺序出现,但不要求连续(非字符串子串)的字符串序列。

考虑如下的例子:

字符串 元素
字符串1 a c b a e d
字符串2 a b c a d f

LCS:长度为4的‘’acad“

下面的算法

function lcs(wordX,wordY){
    var m = wordX.length,
    n = wordY.length,
    l = [],
    solution = [],
    i, j, a, b;
    for(i = 0; i <= m; ++i){
        l[i] = [];
        solution[i] = [];
        for(j = 0; j <= n; ++j){
            l[i][j] = 0;
            solution[i][j] = '0';
        }
    }
    for(i = 0; i <= m; i++){
        for(j = 0; j <= n; j++){
            if(i == 0 || j == 0){
                l[i][j] = 0;
            }else if(wordX[i-1] == wordY[j-1]){
                l[i][j] = l[i-1][j-1] + 1;
                solution[i][j] = 'diagonal';

            }else{
                a = l[i-1][i];
                b = l[i][j-1];
                l[i][j] = a > b ? a : b;
                solution[i][j] = l[i][j] == l[i-1][j] ? 'top' : 'left';
            }
        }
    }
    printSolution(solution,l,wordX,wordY,m,n);
    return l[m][n];
}

function printSolution(solution,l,wordX,wordY,m,n){
    var a = m,
        b = n,
        i,
        j,
        x = solution[a][b],
        answer = '';
        while(x !== '0'){
            if(solution[a][b] === 'diagonal'){
                answer = wordX[a-1] + answer;
                a--;
                b--;
            }else if(solution[a][b] === 'left'){
                b--;
            }else if(solution[a][b] === 'top'){
                a--;
            }
            x = solution[a][b];
        }
        console.log('lcs:' + answer);
}

矩阵链相乘(未完成)

贪心算法(未完成)

最少硬币找零问题

背包问题

函数式编程简介

借助ES6的能力,JavaScript 也能够进行函数式编程

函数式编程和命令式编程

以函数式方式进行开发并不简单。

假如我们想打印一个数组中所有的元素。我们可以用命令式编程,声明的函数如下:

var printArray = function(array){
    for(var i = 0; i < array.length; i++){
        console.log(array[i]);
    }
}
printArray([1,2,3,4,5]);

在上面的代码中,我们迭代数组,打印每一项。

现在,我们试着将这个例子转换成函数式编程。在函数式编程中,我们关注的重点是需要描述什么,而不是如何描述

var forEach = function(array,action){
    for(var i = 0; i < array.length; i++){
        action(array[i]);
    }
}

接着我们需要创建另一个元素负责把数组元素打印到控制台的函数(考虑为回调函数),如下

var logItem = function(item){
    console.log(item);
}

最后,像下面这样使用函数

forEach([1,2,3,4,5],logItem);

几点需要注意:

  • 主要目标是描述数据,已经要对数据应用的转换
  • 程序执行顺序的重要性很低,而在命令式编程中,步骤和顺序是非常重要的
  • 函数和数据结合是函数式编程的核心
  • 在函数式编程中,我们可以使用和滥用函数和递归,而在命令式编程中,则使用循环、赋值、条件和函数。

另外一个例子,考虑我们要找数组中最小的值。用命令式编程完成这个任务,只要迭代数组,检查当前的最小值是否大于数组元素,如果是,就更行最小值。

var findMinArray = function(array){
    var minValue = array[0];
    for(var i = 1; i < array.length; i++){
        if(minValue > array[i]){
           minValue = array[i];
        }
    }
    return minValue;
}
console.log(findMinArray([8,6,4,5,9])); // 4

用函数式编程完成相同的任务,可以使用Math.in 函数,传入所有要比较的数组元素。我们可以像下面的例子里这样,使用ES2015 的解构操作符(...),把数组转换成单个元素:

const min_ = function(array){
    return Math.min(...array);
}
console.log(min_([8,6,4,5,9])); // 4

使用箭头函数,简化代码

const min_ = arr => Math.min(...arr);

JavaScript函数式工具箱——map、filter 和 reduce

map、filter和reduce函数是函数式编程的基础

我们可以使用map函数 ,把一个数据集合转换成映射成另一个数据集合。先看一个命令式编程的例子:

var daysOfWeek = [
    { name: 'Monday', value: 1 },
    { name: 'Tuseday', value: 2 },
    { name: 'Webnesday', value: 7 },
]
var daysOfWeekValues_ = [];
for(var i = 0; i < daysOfWeek.length; i++ ){
    daysOfWeekValues_.push(daysOfWeek[i].value);
}

再以函数式编程来考虑同样的例子,代码如下:

var daysOfWeekValues = daysOfWeek.map(function(day){
    return day.value;
})
console.log(daysOfWeekValues);

我们可以使用 filter 函数过滤一个集合的值。来看一个例子

var positiveNumbers_ = function(array){
    var positive = [];
    for(var i = 0; i < array.length; i++){
        if(array[i] >= 0){
           positive.push(array[i]);
        }
    }
    return positive;
}
console.log(positiveNumbers_([-1,1,2,-2])); // (2) [1, 2]

改成函数式

var positiveNumbers = function(array){
    return array.filter(function(num){
        return num >= 0;
    });
}
console.log(positiveNumbers([-1,1,2,-2])); // (2) [1, 2]

也可以使用reduce函数,把一个集合归纳成一个约定的值。比如,对一个数组中的值求和:

var sumValues = function(array){
    var total = array[0];
    for(var i = 1; i < array.length; i++){
        total += array[i];
    }
    return total;
}
console.log(sumValues([1,2,3,4,5])); // 15

上面的代码也可以写成这样的:

var sum_ = function(array){
    return array.reduce(function(a,b){
        return a + b;
    });
}
console.log(sum_([1,2,3,4,5])); // 15

再看另外一个例子,考虑我们需要写一个函数,把几个数组连接起来。为此,可以创建另外一个数组,用于存放其他数组的元素。我们可以执行以下命令式的代码

var mergeArrays = function(arrays){
    var count = arrays.length,
        newArray = [],
        k = 0;
    for(var i = 0; i < count; i++){
        for(var j = 0; j < arrays[i].length; j++){
            newArray[k++] = arrays[i][j];
        }
    }
    return newArray;
}
console.log(mergeArrays([[1,2,3],[4,5],[6]])); // (6) [1, 2, 3, 4, 5, 6]

在这个例子,我们声明了变量,还使用了循环。现在,我们用JavaScript 函数式编程把上面的代码重写如下:

var mergeArraysConcat = function(arrays){
    return arrays.reduce(function(p,n){
        return p.concat(n);
    });
}
console.log(mergeArraysConcat([[1,2,3],[4,5],[6]])); // (6) [1, 2, 3, 4, 5, 6]

箭头函数简写

const mergeArrays = (...arrays) => [].concat(...arrays);
console.log(mergeArrays([1,2,3],[4,5],[6])); // (6) [1, 2, 3, 4, 5, 6]

小结

在本章中,你了解了更多的递归的知识,已经它帮助我们解决一些动态规划问题。我们介绍了最著名的动态规划问题,如最少硬币找零、背包问题、最长公共子序列和矩阵链相乘(后面补)。

还学习了贪心算法,已经如何用贪心算法解决最少硬币找零和背包问题。

还学习了函数式编程,并通过一些例子了解了如何以这种范式使用JavaScript 的功能。

书籍链接: 学习JavaScript数据结构与算法

为什么我要放弃javaScript数据结构与算法(第十一章)—— 算法模式的更多相关文章

  1. 为什么我要放弃javaScript数据结构与算法(第十章)—— 排序和搜索算法

    本章将会学习最常见的排序和搜索算法,如冒泡排序.选择排序.插入排序.归并排序.快速排序和堆排序,以及顺序排序和二叉搜索算法. 第十章 排序和搜索算法 排序算法 我们会从一个最慢的开始,接着是一些性能好 ...

  2. 为什么我要放弃javaScript数据结构与算法(第九章)—— 图

    本章中,将学习另外一种非线性数据结构--图.这是学习的最后一种数据结构,后面将学习排序和搜索算法. 第九章 图 图的相关术语 图是网络结构的抽象模型.图是一组由边连接的节点(或顶点).学习图是重要的, ...

  3. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  4. 为什么我要放弃javaScript数据结构与算法(第七章)—— 字典和散列表

    本章学习使用字典和散列表来存储唯一值(不重复的值)的数据结构. 集合.字典和散列表可以存储不重复的值.在集合中,我们感兴趣的是每个值本身,并把它作为主要元素.而字典和散列表中都是用 [键,值]的形式来 ...

  5. 为什么我要放弃javaScript数据结构与算法(第六章)—— 集合

    前面已经学习了数组(列表).栈.队列和链表等顺序数据结构.这一章,我们要学习集合,这是一种不允许值重复的顺序数据结构. 本章可以学习到,如何添加和移除值,如何搜索值是否存在,也可以学习如何进行并集.交 ...

  6. 为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张. 本章内容 链表数据结构 向链表添加元素 从链表移除元素 使用 LinkedList 类 ...

  7. 为什么我要放弃javaScript数据结构与算法(第四章)—— 队列

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第四章 队列 队列数据结构 队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序 ...

  8. 为什么我要放弃javaScript数据结构与算法(第三章)—— 栈

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第三章 栈 栈数据结构 栈是一种遵循后进先出(LIFO)原则的有序集合.新添加的或待删除的元素都保存在栈的同一端,称为栈顶,另一 ...

  9. 为什么我要放弃javaScript数据结构与算法(第二章)—— 数组

    第二章 数组 几乎所有的编程语言都原生支持数组类型,因为数组是最简单的内存数据结构.JavaScript里也有数组类型,虽然它的第一个版本并没有支持数组.本章将深入学习数组数据结构和它的能力. 为什么 ...

随机推荐

  1. iOS自动更新如何实现

    APP检测更新可以使用两种方法.第一种是和安卓等系统一样,获取自己服务器的APP版本号与已安装的APP版本号比较:第二种是根据已发布到app store上的应用版本号与已安装的APP版本号比较更新.第 ...

  2. Ubuntu下Apache的安装

    Ubuntu下可快速安装LAMP server(Apache+MySQL+PHP5),参见<Ubuntu下快速安装LAMP server>一文. 也可以手动安装Apache.本文介绍如何手 ...

  3. Android性能优化方法(七)

    Java从JDK1.2版本开始,就把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期.这四种级别由高到低依次为:强引用.软引用.弱引用和虚引用. 这里重点介绍一下软引用和弱引用. 如果 ...

  4. 媒体对象 - Media Objects(摘录)

    原文链接:http://www.jianshu.com/p/6443be21efbd 一个媒体对象由以下及部分组成 父容器 .media 媒体部分 .media-left 或者 .media-righ ...

  5. 第一个Struts2程序

    Struts2.3.16, Tomcat6.0.37,Java8 /web.xml <?xml version="1.0" encoding="UTF-8" ...

  6. 【教程】发布NAServer到ArcGIS Server 10.4上[超详细]

    前阵子对ArcGIS API For JavaScript的网络分析有兴趣,但是不知道其数据是如何获取的. 查阅API知道,AJS的网络分析只有三个功能:最短路径(RouteTask).最近设施点(C ...

  7. MTK(android init.rc) 写一个开机启动的服务

    在开机往往我们想要做一些初始化的操作,这时候可以使用init.rc文件来实现,当然书写一个开机启动的服务也需要规范的. 假设是C程序,则需要准备C程序的源码,在我的例程中是一个在开机完成(androi ...

  8. 《Spring_Four》第三次作业——基于Jsoup的大学生考试信息展示系统的原型设计与开发

    <Spring_Four团队>第三次团队项目——基于Jsoup的大学生考试信息展示系统的原型设计与开发 一.实验目的与要求 (1)掌握软件原型开发技术: (2)学习使用软件原型开发工具:本 ...

  9. spark遇到的一些问题及其解决办法

    1.报错:ERROR storage.DiskBlockObjectWriter: Uncaught exception while reverting partial writes to file ...

  10. TypeScript学习笔记(一) - 环境搭建

    本篇将简单介绍一下TypeScript,并记录开发环境的搭建.使用Visual Studio Code进行一个简单的Demo开发过程. 第一部分.简介 TypeScript是一种由微软开发的自由和开源 ...