JS算法相关|青训营笔记

127 阅读4分钟

这是我参与「第四届青训营 」笔记创作活动的的第8天

JS算法相关

大家好,这里是Vic,终于到了JS部分的最后一个篇章,这个篇章我把总结为JS的几道经典算法题,根据月影老师的讲述,深刻理解了这几道题,面试部分的算法就差不多了。话不多说,让我们开始正文

14CC5905.gif

判断是否4的幂

首先我们来看第一道题目,也是一道很经典的题目,判断四的幂。这道题目其实挺简单的,新手的话一般会想到使用一个while循环然后进行判断,代码如下:

function isPowerOfFour(num){
  num = parseInt(num);
  
  while(num>1){
    if(num % 4) return false;
    num /= 4;
  }
  // 为什么这里是 num === 1
  // 因为要考虑输入数字小于1的情况
  return num === 1
}

我们也可以将这个方法装换为二进制运算操作

function isPowerOfFour(num) {
  num = parseInt(num);
  
  while(num > 1) {
    // 从后向前每两位进行判断是否都为0
    // 这个操作就是判断能否整除4的运算
    if(num & 0b11) retrun false;
    // 相当于除以4
    num >>>= 2;
  }
  return num === 1;
}

那么有没有什么比较帅气的方法呢?在这里,我们可以使用下面的这种方法,这也是力扣上遇到这种题目的常见方法。代码如下:

function isPowerOfFour(num) {
  num = parseInt(num);
  
  return num > 0 &&
         (num & (num - 1)) === 0 &&
         (num & 0xAAAAAAAAAAAAA) === 0;
}

新手看到这段代码可能会觉得头大,因此做一下讲解,这里利用了JS语言的特性,当使用&&的时候逐个条件进行确定,当有条件不符合的时候直接为false,不进行下面条件的判断。这段代码中有三个条件:第一个条件是判断是否为正数,第二个条件是判断是否为2的幂(这是由于如果是二的幂必定是首位为1,后面都为0这样的形式),第三个条件是在判断二进制形式下偶数位都为0(在之前为2的幂的基础上进行筛选,这样就得到了4的幂)。

这样一看是不是就简单了很多。

没关系,我们还有这道题目的终极解法:正则表达式

function isPowerOfFour(num) {
  num = parseInt(num).toString(2);
  
  return /^1(?:00)*$/.test(num);
}

对正则表达式不熟悉的同学,建议补充一下正则表达式的相关知识,在实际开发中,我们使用正则表达式的场景还是很多的。

洗牌算法

说到洗牌算法,一般来说,我们的第一反应就是使用sort在列表中进行重新排序,其对应的方法如下所示:

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function shuffle(cards) {
  return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}

console.log(shuffle(cards));

其结果如下所示:

screenshot-20220801-145913.png

看上去我们似乎实现了一个随机的洗牌算法,但事实上真的是这样吗?让我们用函数验证一下:

const result = Array(10).fill(0);

for(let i = 0; i < 1000000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}

console.table(result);

结果如下:

screenshot-20220801-150230.png

似乎这个洗牌算法并没有实现随机,我们发现在这个算法中小的数字更容易放到前面,这显然是不行的。

因此,我们需要使用另一个基于随机采样方法,其代码如下:

function shuffle(cards) {
  const c = [...cards];
  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
  }
  return c;
}

这段代码其实很好理解,使用扑克牌做例子进行讲解,我们在这段函数中做的操作就是每次取出牌堆最底下的一张牌然后与随机位置的一张牌交换位置,之后再取出倒数第二张牌与其上面的随机位置交换顺序,如此直至每个位置都交换过。

让我们来看看这段代码的随机性如何:

screenshot-20220801-152040.png

可以看到这段代码是满足随机性的要求的。为了在实际中更好地使用,我们将其改为一个生成器函数:

function * draw(cards){
  const c = [...cards];

  for(let i = c.length; i > 0; i--) {
    const pIdx = Math.floor(Math.random() * i);
    [c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
    yield c[i - 1];
  }
}

分红包算法

分红包方法有两种方法,一种是切西瓜法,一种是抽牌法。

首先介绍切西瓜法

切西瓜法的原理就是每次选出最大的一块进行分割,如此循环,直至分完。其代码如下:

function generate(amount, count){
  let ret = [amount];
  
  while(count > 1){
    // 选择最大的一块
    let cake = Math.max(...ret),
        idex = ret.indexOf(cake),
        // 保证最小为 1
        part = 1 + Math.floor((cake / 2) * Math.random()),
        rest = cake - part;
    ret.splice(idex, 1, part, rest);
    count --;
  }
  return ret
}

这个算法没什么好说的,思想就是将金额存在列表中,然后每次找最大的金额切分,用切分得到的数值替换原来的数值。

我们来看看其运行的结果:

screenshot-20220801-162547.png

然而这个算法存在着一个问题,他分出的数字过于分均,当我们的实际场景中需要增加一些趣味性时,这个算法就不能满足了。在这个场景下我们需要使用第二种方法抽牌法

抽牌法是基于我们之前写的洗牌算法的。其代码如下:

function generate(amount, count){
  // 不足两人的时候,直接拿到所有金额
  if (count <= 1) return [amount];
  
  const cards = Array(amount - 1).fill(0).map((_, i) => i + 1);
  const pick = draw(cards);
  const result = [];
  // 洗牌之后取出目标数量的卡片
  for(let i = 0; i < count; i++) {
    result.push(pick.next().value);
  }
  result.sort((a, b) => a - b);
  result.push(amount);
  // 相减得到每个人的红包金额
  for(let i = result.length - 1; i > 0; i--) {
    result[i] = result[i] - result[i - 1];
  }
  return result;
}

结果如图所示:

screenshot-20220801-170018.png