数据结构与算法JavaScript描述(13) —— 高级算法(Algorithm)

1. 动态规划

与递归相反的技术。
递归是从顶部开始将问题分解,通过解决掉所有分解出小问题的方式,来解决整个问题。代码简洁,但效率不高

如计算斐波那契数列,存在很多值在递归调用中被重复计算

动态规划方案底部开始解决问题,将所有小问题解决掉,然后合并成一个整体解决方案,从而解决掉整个大问题。

动态规划方案通常会使用一个数组来建立一张表,用于存放被分解成众多子问题的解。
当算法执行完毕,最终的解将会在这个表中很明显的地方被找到。

(1) 例一: 计算斐波那契数列

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...

递归方案实现:

function recurFib(n) {
    if (n < 2) {
        return n
    } else {
        return recurFib(n - 1) + recurFib(n - 2)
    }
}

动态规划方案实现:

function dynFib(n) {
    if (n === 0) return 0
    if (n <= 2) return 1
    const rs = []
    for (let i = 0; i <= n; i++) {
        rs[i] = 0
    }
    rs[1] = 1
    rs[2] = 1
    for (let i = 3; i <= n; i++) {
        // 当前值为前两个值之和
        rs[i] = rs[i - 1] + rs[i - 2]
    }
    return rs[n]
}

测试:

console.time('递归计算斐波那契数列')
var result1 = recurFib(40)
console.log(result1)
console.timeEnd('递归计算斐波那契数列')

console.time('动态规划斐波那契数列')
var result2 = dynFib(40)
console.log(result2)
console.timeEnd('动态规划斐波那契数列')

多执行几次,得出测试结果:

递归计算斐波那契数列: 1730.80322265625ms
动态规划斐波那契数列: 0.30810546875ms

递归计算斐波那契数列: 2223.50390625ms
动态规划斐波那契数列: 0.34814453125ms

递归计算斐波那契数列: 1730.210693359375ms
动态规划斐波那契数列: 0.176025390625ms

可以看出:动态规划斐波那契数列递归计算斐波那契数列效率高很多。

(2) 例二: 寻找两个字符串的公共子串(匹配同一位置)

暴力方式实现:

给出两个字符串A和B,
我们可以通过从A的第一个字符开始与B对应的每一个字符进行比对的方式找到他们的最长公共子串;
如果此时没有找到匹配的字母,则移动到A的第二个字符处,然后从B的第一个字符开始匹配,依次类推。

function lcs(str1, str2) {
    const len1 = str1.length
    const len2 = str2.length
    let rs = ''
    for (let i = 0; i < len1; i++) {
        if (str1[i] === str2[i]) {
            let m = i
            let str = ''
            while (m < len1 && m < len2 && str1[m] === str2[m]) {
                str += str1[m]
                m++
            }
            if (rs.length < str.length) {
                rs = str
            }
        }
    }
    return rs
}

动态规划方式实现:

function dynLcs(str1, str2) {
    const len1 = str1.length
    const len2 = str2.length
    let maxLen = 0  // 存储最长子串的长度
    let index = 0   // 存储最长子串的最后一个字符的索引
    const lcsarr = new Array(len1 + 1) // 初始化一个二维数组,来分别存储两个字符串的索引值
    for (let i = 1; i < len1 + 1; i++) {
        lcsarr[i] = new Array(len2 + 1) 
        for (let j = 1; j < len2 + 1; j++) {
            lcsarr[i][j] = 0
            if (str1[i - 1] !== str2[j - 1] || i !== j) {
                continue
            }
            // 如果两个字符串相应位置的字符进行了匹配,当前数组元素的值将被设置为前一次循环中数组元素保存的值加一
            lcsarr[i][j] = lcsarr[i - 1][j - 1] + 1
            if (maxLen < lcsarr[i][j]) {
                maxLen = lcsarr[i][j]
                index = i
            }
        }
    }

    // 构建最长子串
    let str = ''
    for (let i = 0; i < maxLen; i++) {
        str += str2[index - maxLen + i]
    }
    return str
}

测试:

// 模拟数据
const arr = "abcdefghijklmnopqrstuvwxyz".split('')
function initData (nums) {
    let str = []
    for (let i = 0; i < nums; i++) {
        index = Math.floor(Math.random() * (arr.length + 1))
        str += arr[index] || arr[index - 1]
    }
    return str
}
var str1 = initData(1000)
var str2 = initData(1000)

// test
console.time('自定义lcs')
var result1 = lcs(str1, str2)
console.log(result1)    // ye
console.timeEnd('自定义lcs')

console.time('动态规划lcs')
var result2 = dynLcs(str1, str2)
console.log(result2)    // ye
console.timeEnd('动态规划lcs')

多执行几次,得出测试结果:

自定义lcs: 0.76904296875ms
动态规划lcs: 46.73095703125ms

自定义lcs: 0.77197265625ms
动态规划lcs: 44.8828125ms

自定义lcs: 0.296142578125ms
动态规划lcs: 59.943115234375ms

可以看出:自定义lcs动态规划lcs效率高很多。
注: 查了很多资料是说动态规划lcs要比自定义lcs效率高,但是经过自己测试后却是反的,不晓得是不是哪里出问题了ㄟ( ▔, ▔ )ㄏ

(3) 例三: 背包问题

试想你是一个保险箱大盗,打开了一个装满奇珍异宝的保险箱,但是你必须将这些宝贝放入你的一个小背包中
保险箱中的物品规格和价值不同,你希望自己的背包装进的宝贝总价值最大
关键思路:计算装入背包的每一个物品的最大价值,直到背包装满

递归方法实现:

function knapsack(capacity, size, value, n) {
    if (capacity === 0 || n === 0) {
        return 0
    }
    if (size[n - 1] > capacity) {
        return knapsack(capacity, size, value, n - 1)
    } else {
        const val1 = value[n - 1] + knapsack(capacity - size[n - 1], size, value, n - 1)
        const val2 = knapsack(capacity, size, value, n - 1)
        return val1 > val2 ? val1 : val2
    }
}

动态规划方法实现:

function dKnapsack(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++) {
            // j 为背包容量
            if (i === 0 || j === 0) {
                // 数组的第一个元素总被设置为0
                K[i][j] = 0
            } else if (size[i - 1] <= j) {
                const val1 = value[i - 1] + K[i - 1][j - size[i - 1]]
                const val2 = K[i - 1][j]
                K[i][j] = val1 > val2 ? val1 : val2
            } else {
                K[i][j] = K[i - 1][j]
            }
        }
    }
    return K[n][capacity]
}

测试:

const capacity = 16 // 背包容积
const n = 5     // 保险箱中的物品数
const size = [3, 4, 7, 8, 9]    // 保险箱里的物品尺寸
const value = [4, 5, 10, 11, 13]    // 保险箱里的物品价值

console.time('递归解决knapsack')
const maxValue1 = knapsack(capacity, size, value, n)
console.log(maxValue1)  
console.timeEnd('递归解决knapsack')

console.time('动态规划解决knapsack')
const maxValue2 = dKnapsack(capacity, size, value, n)
console.log(maxValue2)  
console.timeEnd('动态规划解决knapsack')

得出测试结果:

递归解决knapsack: 0.77392578125ms
动态规划解决knapsack: 0.39697265625ms

可以看出:动态规划解决knapsack效率高于递归解决knapsack

2. 贪心算法

总是选择当下的最优解,而不去考虑这一次的选择会不会对未来的选择造成影响。
使用贪心算法通常表明,实现者希望做出的这一系列局部“最优”选择能够带来最终整体“最优”选择
如果是这样的话,该算法将会产生一个最优解,否则,则会得到一个次优解。

(1) 例一: 找零问题

你从商店购买了一些商品,找零 63 美分,店员要 怎样给你这些零钱呢

function mackChange(origAmt) {
    const coins = []
    if (origAmt % .25 < origAmt) {
        coins[3] = parseInt(origAmt / .25)
        origAmt = origAmt % .25
        console.log(`25 美分的数量 - ${coins[3] } - $${coins[3] * .25}`)
    }
    if (origAmt % .1 < origAmt) {
        coins[2] = parseInt(origAmt / .1)
        origAmt = origAmt % .1
        console.log(`10 美分的数量 - ${coins[2] } - $${coins[2] * .1}`)
    }
    if (origAmt % .05 < origAmt) {
        coins[1] = parseInt(origAmt / .05)
        origAmt = origAmt % .05
        console.log(`5 美分的数量 - ${coins[1] } - $${coins[1] * .05}`)
    }
    coins[0] = parseInt(origAmt / .01)
    console.log(`1 美分的数量 - ${coins[0] } - $${coins[0] * .01}`)
}

// test
mackChange(.63)
// 25 美分的数量 - 2 - $0.5
// 10 美分的数量 - 1 - $0.1
// 1 美分的数量 - 3 - $0.03

(2) 例二: 贪心算法解决背包问题

function ksack(capacity, size, value, n) {
    let load = 0    // 已放进背包的容量
    let i = 0       // 放进背包的物品个数
    let maxValue = 0    // 最大价值
    while (load < capacity && i < n) {
        if (size[i] <= (capacity - load)) {
            maxValue += value[i]
            load += size[i]
        } else {
            let r = (capacity - load) / size[i]
            maxValue += r * value[i]
            load += size[i]
        }
        i++
    }
    return maxValue
}

// test
const capacity = 16 // 背包容积
const n = 5     // 保险箱中的物品数
const size = [3, 4, 7, 8, 9]    // 保险箱里的物品尺寸
const value = [4, 5, 10, 11, 13]    // 保险箱里的物品价值
const maxValue = ksack(capacity, size, value, n)     
console.log(maxValue) // 21.75

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 1.把二元查找树转变成排序的双向链表 题目: 输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。 要求不...
    曲终人散Li阅读 3,238评论 0 19
  • 原文: 常用的算法设计思想主要有动态规划、贪婪法、随机化算法、回溯法等等,这些思想有重叠的部分,当面对一个问题的时...
    josephok阅读 1,044评论 0 3
  • 一个字符串的子串是字符串中连续的一个序列,而一个字符串的子序列是字符串中保持相对位置的字符序列,譬如,"adi"可...
    AwesomeAshe阅读 1,134评论 0 0
  • 抱怨 ,指心中怀有不满,责怪他人。在一份关于“为何要抱怨”的网络调查中,有74.7%的人表示为了发泄内心的苦闷...
    森岚2017阅读 157评论 0 1
  • [连续签到第6天] 2048-1-14 周日快乐 鉴峰笔记之如果: 如果有一天, 你我不再要求得到, 只是去付出;...
    鉴峰笔记阅读 119评论 0 0