【前端面试常见算法题系列】77. 组合(中等)

91 阅读1分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

一、题目描述

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

题目来源:77. 组合

二、思路分析

这类题放在生活中,就是一道排列组合的数学题,我们要解题,自然会考虑到数字间的组合,一般是从小到大进行配对,穷举直到最大的数字为止。

按照这样的思路,我们可以将其转化为代码,就是循环里面再循环,也就是递归。难点在于,当我们配对成功后,怎么继续往下配对,因为当前配对成功只是答案的其中一个。其实就是需要将配对的那个数字删除(假如配对两个数),这样只剩一个的那个数字就能继续往下配对。

这道题的思路分析,我想自顶向下讲解,先贴 AC 代码,然后讲解其中思路。

首先在本地新建 test.js 文件,用于调试代码:

var combine = function(n, k) {
  const res = [], track = [];
  const backtrack = (n, k, start, track) => {
    
    console.log('res', res)
    console.log('track', track)
    
    if (track.length === k) {
    
      console.log('track.length === k')
    
      res.push(Array.from(track));
      return;
    }
    for (let i = start; i <= n; i++) {
      
      console.log('push', i)
      
      track.push(i);
      backtrack(n, k, i + 1, track);
      track.pop();
      
      console.log('pop', i)
    }
  }
  backtrack(n, k, 1, track);
  return res;
};
combine(4, 2);

运行 node test.js 后,可以看到如下打印信息:

res []
track []
push 1
res []
track [ 1 ]
push 2
res []
track [ 1, 2 ]
track.length === k
pop 2
push 3
res [ [ 1, 2 ] ]
track [ 1, 3 ]
track.length === k
pop 3
push 4
res [ [ 1, 2 ], [ 1, 3 ] ]
track [ 1, 4 ]
track.length === k
pop 4
pop 1
push 2
res [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ] ]
track [ 2 ]
push 3
res [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ] ]
track [ 2, 3 ]
track.length === k
pop 3
push 4
res [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 2, 3 ] ]
track [ 2, 4 ]
track.length === k
pop 4
pop 2
push 3
res [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 2, 3 ], [ 2, 4 ] ]
track [ 3 ]
push 4
res [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 2, 3 ], [ 2, 4 ] ]
track [ 3, 4 ]
track.length === k
pop 4
pop 3
push 4
res [ [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 2, 3 ], [ 2, 4 ], [ 3, 4 ] ]
track [ 4 ]
pop 4

按照前文说的,从最小的数字开始匹配,不满足条件则往下走,若满足条件则将其存入答案中,同时将最近的那个数字删除,然后接着往下匹配。

由于是递归,需要在递归函数最开始定义 bad case ,才能退出递归,这里的 bad case 就是 track.length === k ,这很好理解,配对的数量达到要求后,自然就能存入答案中了,然后 return 出去。return 之后,会退回到上一个递归函数中,从递归的地方继续往下走,也就是会执行 track.pop() ,这就是我说的 “满足条件则将其存入答案中,同时将最近的那个数字删除”

三、AC 代码

/**
 * @param {number} n
 * @param {number} k
 * @return {number[][]}
 */
var combine = function(n, k) {
    const res = [], track = [];
    const backtrack = (n, k, start, track) => {
        if (track.length === k) {
            res.push(Array.from(track));
            return;
        }
        for (let i = start; i <= n; i++) {
            track.push(i);
            backtrack(n, k, i + 1, track);
            track.pop();
        }
    }
    backtrack(n, k, 1, track);
    return res;
};