JavaScript算法入门2-数组类(LeetCode)

503 阅读7分钟

序章


我们把字符串数组正则排序递归归为简单算法。接下来系列里,将系列文章里将为大家逐一介绍。
本章节主要介绍数组的这几个问题:电话号码的组合(公式运算)、卡牌分组(归类运算)、种花问题(筛选运算)和格雷编码(二进制运算)。

数组

电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number

步骤一:寻找规律

先分割输入split,然后映射字母。先俩个组合运算,完了和新的继续组成俩个运算。俩个俩个的运算。运用递归。

步骤二:伪代码实现

for(i=0;i<arr[0].length;i++)
    for(j=0;j<arr[1].length;j++)
        result.push(arr[0][i]arr[1][j])
    arr.splice(0,2,result)
    if(arr.length>1)
        递归
    else 
        return result

步骤三:计算子串代码演示
代码思路整理:

  • 建立电话号码键盘映射
  • 把输入字符串根据单个字符分割为数组,例如'2345'=>[2,3,4,5]
  • 保存键盘映射后的字母内容,例如'23'=>['abc','def']
  • 创建递归函数:如果俩个数组组合完后,arr的length变为1,直接输出;如果大于1,继续执行递归。
  • 调用递归函数:如果输入为1,则直接返回code;如果输入为一个数字,则直接split后形成新的数组输出。
/**
 * @param {string} digits
 * @return {string[]}
 */
var letterCombinations = function(digits) {
    let numberMap = ['',1,'abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'],
        numInputArr = digits.split(''),
        code = [];
    numInputArr.forEach(item => {
        if(numberMap[item]) {
            code.push(numberMap[item])
        }
    })

    let comb = (arr) => {
        // 临时变量用来保存前俩个组合的结果
        let temp = [];
        for(i=0,il=arr[0].length;i<il;i++) {
            for(j=0,jl=arr[1].length;j<jl;j++) {
                temp.push(`${arr[0][i]}${arr[1][j]}`)
            }
        }
        arr.splice(0,2,temp)
        if(arr.length > 1) {
            comb(arr);
        } else {
            return temp
        }
        return arr[0]
    }
    if(code.length>1) {
        return comb(code)
    } else if(code.length===1) {
        return code[0].split('')
    } else {
        return code
    }
};

小结:上述做法涉及到的知识点如下所示。

Array.prototype.split

卡牌分组

给定一副牌,每张牌上都写着一个整数。

此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:

每组都有 X 张牌。 组内所有的牌上都写着相同的整数。 仅当你可选的 X >= 2 时返回 true。

示例 1:

输入:[1,2,3,4,4,3,2,1]
输出:true
解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]

示例 2:

输入:[1,1,1,2,2,2,3,3]
输出:false

示例 3:

输入:[1]
输出:false
解释:没有满足要求的分组。

示例 4:

输入:[1,1]
输出:true
解释:可行的分组是 [1,1]

示例 5:

输入:[1,1,2,2,2,2]
输出:true
解释:可行的分组是 [1,1],[2,2],[2,2]

提示:

1 <= deck.length <= 10000 0 <= deck[i] < 10000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/x-of-a-kind-in-a-deck-of-cards

步骤一:寻找规律

  • 给的卡牌毫无规律,首先需要排序
  • 寻找最大公约数,没有最大公约数,就不符合题目要求。比如示例2是(3,3,2),没有最大公约数,不符合。示例5是(2,4)最大公约数是2,符合题目要求,可以拆分
  • 如何算俩个数之间的最大公约数GCD
a. 穷举法:从1开始往上遍历,找到一个数能被俩个数同时整除,就是最大公约数。此方法比较消耗性能。  
b. 辗转相除法:辗转相除法又称为欧几里得算法,该方法依托于一个定理:  
gcd(a, b) = gcd(b, a mod b)  
其中,a mod b是a除以b所得的余数。也就是说a,b的最大公约数就是b,a mod b的最大公约数。  
原理参考链接:https://www.jianshu.com/p/25d0ca88a4a1

步骤二:卡牌组合代码演示

/**
 * @param {number[]} deck
 * @return {boolean}
 */
var hasGroupsSizeX = function(deck) {
    // 卡牌排序
    let str = deck.sort().join('');
    // 分组(单张或着多张分组):(\d)\1+ 指的是寻找相同数字的牌,或者只有1张牌 \d
    let group = str.match(/(\d)\1+|\d/g)
    // 求俩个数的最大公约数:辗转相除法
    let gcd = (a,b) => {
        if(b===0){
            return a
        } else {
            return gcd(b, a%b)
        }
    }
    while(group.length>1){
        let a = group.shift().length;
        let b = group.shift().length;
        let v = gcd(a,b)
        if(v === 1) {
            return false
        } else {
            group.unshift('0'.repeat(v) )
        }
    }
    return group.length ? group[0].length > 1 : false
};

代码里用正则去匹配的时候,字符串比较大的时候,总是不能匹配成功。

详情查看:leetcode-cn.com/submissions…

代码优化

/**
 * @param {number[]} deck
 * @return {boolean}
 */
var hasGroupsSizeX = function(deck) {
    let group = [];
    let tmp = {};
    // 计算每张相同卡牌的数量
    deck.forEach(item => {
        tmp[item] = tmp[item] ? tmp[item] + 1 : 1
    })
    // 保存数量,例如实例2为(3,3,2)
    for (let v of Object.values(tmp)) {
        group.push(v)
    }
    // 求俩个数的最大公约数:辗转相除法
    let gcd = (a,b) => {
        if(b===0){
            return a
        } else {
            return gcd(b, a%b)
        }
    }
    while(group.length>1){
        let a = group.shift();
        let b = group.shift();
        let v = gcd(a,b)
        if(v === 1) {
            return false
        } else {
            group.unshift(v)
        }
    }
    return group.length ? group[0] > 1 : false
};

种花问题

假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。

示例 1:

输入: flowerbed = [1,0,0,0,1], n = 1
输出: True

示例 2:

输入: flowerbed = [1,0,0,0,1], n = 2
输出: False
注意:
数组内已种好的花不会违反种植规则。
输入的数组长度范围为 [1, 20000]。
n 是非负整数,且不会超过输入数组的大小。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/can-place-flowers

解法一

解题思路

因为是线性问题,所以可以用贪心。
因为javascript的数组的特性,如果对应的下标没有值,都返回undefined
所以 !flowerbed[i] 即可以代表为 i位置为0的情况,也可以代表超出边界的情况

代码演示

/**
 * @param {number[]} flowerbed
 * @param {number} n
 * @return {boolean}
 */
var canPlaceFlowers = function (flowerbed, n) {
  let count=0
  for (let i = 0; i < flowerbed.length; i++) { 
    if (flowerbed[i] === 0) { 
      if (!flowerbed[i - 1] && !flowerbed[i + 1]) {
        count++
        flowerbed[i]=1
      }
    }
  }
  return count >= n
};

解法二

解题思路

首先,为了避免首位和尾位的特殊情况,可在首位和尾位都填上一个0。
然后,找到满足左右两边都是0即可的目标值,目标值个数即为可插花朵的最大值。

代码演示

/**
 * @param {number[]} flowerbed
 * @param {number} n
 * @return {boolean}
 */
var canPlaceFlowers = function(flowerbed, n) {
    let cnt=0;
    flowerbed=[0,...flowerbed,0];
    for(i=1,len=flowerbed.length-1;i<len;i++) {
        if(flowerbed[i-1]===0 && flowerbed[i]===0 && flowerbed[i+1]===0){
                cnt++;
                i+=1;
            }
    }
  return cnt >= n
};

格雷编码

格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。

给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。格雷编码序列必须以 0 开头。

示例 1:

输入: 2
输出: [0,1,3,2]
解释:
00 - 0
01 - 1
11 - 3
10 - 2

对于给定的 n,其格雷编码序列并不唯一。
例如,[0,2,3,1] 也是一个有效的格雷编码序列。

00 - 0
10 - 2
11 - 3
01 - 1

示例 2:

输入: 0
输出: [0]
解释: 我们定义格雷编码序列必须以 0 开头。
     给定编码总位数为 n 的格雷编码序列,其长度为 2n。当 n = 0 时,长度为 20 = 1。
     因此,当 n = 0 时,其格雷编码序列为 [0]。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/gray-code

步骤一: 寻找规律

  • 总共是2的n次方个
  • 输出的高位部分,一半是0一半是1(二进制,不是0就是1)
  • 输出的低位部分,一半是前一个的输出,一半是前一个输出的对称。

步骤二: 代码实现

/**
* @param {number} n
* @return {number[]}
*/
var grayCode = function(n) {
   //递归函数
   let make = (n) => {
       if(n === 1) {
           return ['0', '1'];
       } else {
           let result = [];
           let preResult = make(n-1);
           let max = Math.pow(2,n)-1;
           for(let i=0,len=preResult.length;i<len;i++) {
               result[i]= `0${preResult[i]}`
               result[max-i] = `1${preResult[i]}`;
           }
           // console.log(result)
           return result
       }
   }
   if(n===0) {
       return ['0'];
   } else {
       return make(n).map(value => parseInt(value,2));
   }
};