其他的一些算法概念

其他的一些算法概念

原地算法

一句话总结就是: 原地算法不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入的一种算法操作。

假设要将具有 n 项内容的数组 a 翻转过来。一种看似简单的方法是创建一个大小相等的新数组,用适当的顺序填充副本,然后再删除:

 function reverse(a[0..n-1])
     allocate b[0..n-1]  # 额外设定一个数组
     for i from 0 to n-1 # 从 0 到 n-1 遍历数组 a
         b[n -1 - i] := a[i] 
     return b

这种方法虽然简单,但是需要 O(n) 的额外空间以使数组 a 和 b 同时可用。此外,分配存储空间和释放存储空间通常是缓慢的操作。

加入我们不需要a数组的话,我们直接将a当作输出直接操作。可使用原地算法,用它自己翻转的内容来覆盖掉原先的内容。这样,无论数组有多大,它都只需要辅助变量 i 和 tmp:

 function reverse_in_place(a[0..n-1])
     for i from 0 to floor((n-2)/2)
         tmp := a[i]
         a[i] := a[n − 1 − i]
         a[n − 1 − i] := tmp

只使用了极少的辅助空间,这样是我们节省了大量的空间。

动态规划

动态规划(Dynamic Programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化算法。下面有一些用动态规划来解决实际问题的算法:

最少硬币找零

给定一组硬币的面额,以及要找零的钱数,计算出符合找零钱数的最少硬币数量。例如,美国硬币面额有1、5、10、25这四种面额,如果要找36美分的零钱,则得出的最少硬币数应该是1个25美分、1个10美分和1个1美分共三个硬币。这个算法要解决的就是诸如此类的问题。我们来看看如何用动态规划的方式来解决。

对于每一种面额,我们都分别计算所需要的硬币数量。具体算法如下:

  1. 如果全部用1美分的硬币,一共需要36个硬币
  2. 如果用5美分的硬币,则需要7个5美分的硬币 + 1个1美分的硬币 = 8个硬币
  3. 如果用10美分的硬币,则需要3个10美分的硬币 + 1个5美分的硬币 + 1个1美分的硬币 = 5个硬币
  4. 如果用25美分的硬币,则需要1个25美分的硬币 + 1个10美分的硬币 + 1个1美分的硬币 = 3个硬币

对应的示意图如下:

img

方案4的硬币总数最少,因此为最优方案。

具体的代码实现如下:

function minCoinChange(coins, amount) {
    let result = null;
    if (!amount) return result;

    const makeChange = (index, value, min) => {
        let coin = coins[index];
        let newAmount = Math.floor(value / coin);
        if (newAmount) min[coin] = newAmount;
        if (value % coin !== 0) {
            makeChange(--index, value - coin * newAmount, min);
        }
    };

    const arr = [];
    for (let i = 0; i < coins.length; i++) {
        const cache = {};
        makeChange(i, amount, cache);
        arr.push(cache);
    }

    console.log(arr);
    let newMin = 0;
    arr.forEach(item => {
        let min = 0;
        for (let v in item) min += item[v];
        if (!newMin || min < newMin) {
            newMin = min;
            result = item;
        }
    });
    return result;
}

函数minCoinChange()接收一组硬币的面额,以及要找零的钱数。我们将上面例子中的值传入:

const result = minCoinChange2([1, 5, 10, 25], 36);
console.log(result);
[
  { '1': 36 },
  { '1': 1, '5': 7 },
  { '1': 1, '5': 1, '10': 3 },
  { '1': 1, '10': 1, '25': 1 }
]
{ '1': 1, '10': 1, '25': 1 }

贪心算法

贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择,从而达到全局的最优。它不像动态规划算法那样计算更大的格局。

最少硬币找零

我们来看看如何用贪心算法解决前面提到过的最少硬币找零问题。

function minCoinChange(coins, amount) {
    const change = [];
    let total = 0;
    for (let i = coins.length - 1; i >= 0; i--) {
        const coin = coins[i];
        while (total + coin <= amount) {
            change.push(coin);
            total += coin;
        }
    }
    return change;
}

const result = minCoinChange([1, 5, 10, 25], 36);
console.log(result); // [ 25, 10, 1 ]

前提是coins数组已经按从小到大排好序了,贪心算法从最大值开始尝试,如果该值不满足条件(要找零的钱数),则继续向下找,直到找到满足条件的所有值。以上算法并不能满足所有情况下找出最优方案,例如下面这种情况:

const result = minCoinChange([1, 2, 5, 9, 10], 18);
console.log(result); // [ 10, 5, 2, 1 ]

给出的结果[10, 5, 2, 1]并不是最优方案,最优方案应该是[9, 9]。

与动态规划相比,贪心算法更简单、效率更高。但是其结果并不总是最理想的。但是综合看来,它相对执行时间来说,输出一个可以接受的结果。

回溯算法

如果要求输出所有可能的解,往往都是要用深度优先搜索。如果是要求找出最优的解,或者解的数量,往往可以使用动态规划。

首先,回溯法所用到的核心思想就是递归法,虽然其过程逻辑很清楚,而且执行效率很高。

描述:

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

其实我总结起来就3点:

1 出口。一个递归算法一定要有出口,否则就是一个死循环了。出口语句一般都挺好写的,但 是出口语句该放在哪儿了,这个就是关键了,这儿容许我先卖个关子。

2 递归函数的参数。一般情况下,递归函数是要带参数的,因为递归操作都是用来处理下一次的过程,如果没有参数的话,那么就很难从下一次的操作回溯到当前操作了。这么说,可能会有点迷糊,别急,后面我会举例子,这儿还是卖个关子。

3 递归函数的处理过程。这个自不必多说,重中之重,需要好好理解其过程

这儿容许我先卖个关子。

2 递归函数的参数。一般情况下,递归函数是要带参数的,因为递归操作都是用来处理下一次的过程,如果没有参数的话,那么就很难从下一次的操作回溯到当前操作了。这么说,可能会有点迷糊,别急,后面我会举例子,这儿还是卖个关子。

3 递归函数的处理过程。这个自不必多说,重中之重,需要好好理解其过程

上面3点就是我总结的关于回溯法的关键点了,我觉得只要真正的把这3步吃透,一般的回溯法题目是ok的(这可不是我吹牛哈)下面我就这3点仔细讲讲,大家可要竖起耳朵通清楚了哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值