LeetCode 77,组合挑战,你能想出不用递归的解法吗?
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是LeetCode第46篇文章,我们一起来LeetCode中的77题,Combinations(组合)。
这个题目可以说是很精辟了,仅仅用一个单词的标题就说清楚了大半题意了。这题官方难度是Medium,它在LeetCode当中评价很高,1364人点赞,只有66个反对。通过率53.6%。
题意
题目的题意很简单,给定两个整数n和k。n表示从1到n的n个自然数,要求随机从这n个数中抽取k个的所有组合。
样例
Input: n = 4, k = 2
Output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
全排列的问题我们已经很熟悉了,那么获取组合的问题怎么做呢?
递归
这是一个全组合问题,实际上我们之前做过全排列问题。我们来分析一下排列和组合的区别,可能很多人知道这两者的区别,但是对于区别本身的理解和认识不是非常深刻。
排列和组合有一个巨大的区别在于,排列会考虑物体摆放的顺序。也就是说同样的元素构成,只要这些元素一些交换顺序,那么就会被视为是不同的排列。然而对于组合来说,是不会考虑物体的摆放顺序的。只要是这些元素构成,无论它们怎么调换摆放顺序,都是同一种组合。
我们获取全排列的时候用的是回溯法,我们当然也可以用回溯法来获取组合。但问题是,我们怎么保证获取到的组合都是元素的组成不同,而不是元素之间的顺序不同呢?
为了保证这一点,需要用到一个惯用的小套路,就是通过下标递增来控制拿取元素的顺序。如果我们限定了拿取元素的下标是递增的,那么就可以保证每一次拿取到的组合都是独一无二的。所以我们就把这一点加在回溯法上即可,只要理解了,并不难实现。
在代码的实现当中,我们用上了闭包,省略了几个参数的传递,整体上来说编码的难度降低了一些。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def dfs(start, cur):
# 如果当前已经拿到了K个数的组合,直接加入答案
# 注意要做深拷贝,否则在之后的回溯过程当中变动也会影响结果
if len(cur) == k:
ret.append(cur[:])
return
# 从start+1的位置开始遍历
for i in range(start+1, n):
cur.append(i+1)
dfs(i, cur)
# 回溯
cur.pop()
ret = []
dfs(-1, [])
return ret
迭代
这题并不是只有一种做法,我们也可以不用递归实现算法。不用递归意味着没有系统帮助我们建栈存储中间信息了,需要我们自己把迭代过程当中所有变量的关系整理清楚。
我们假设n=8,k=3,那么在所有合法的组合当中,最小的组合一定是[1,2,3],最大的组合一定是[6,7,8]。如果我们保证组合当中的元素是有序排列的,那么组合之间的大小关系也是可以确定的。进而我们可以思考设计一种方案,使得我们可以从最小的组合[1,2,3]一直迭代到[6,7,8],并且我们还要保证在迭代的过程当中,组合当中元素的顺序不会被打乱。
我们可以想象成这n个数在一根“直尺”上排成了一行,我们有k个滑动框在上面移动。这k个滑动框取值的结果就是n个元素中选取k个的组合,并且由于滑动框之间是不能交错的,所以保证了这k个值是有序的。我们要做的就是设计一种移动滑动框的算法,使得能够找到所有的组合情况。
我们可以想象一下,一开始的时候滑动框都聚集在最左边,我们要移动只能移动最右侧的滑动框。我们把滑动框从k移动到了k+1,那么这个时候它的右侧有k-1个滑动框,一共有k个位置。
那么这个问题其实转化成了k个元素当中取k-1个组合的子问题。我们把1-k的这个部分看成是新的“直尺”,我们要在其中移动k-1个滑动框获取所有的组合。首先,我们需要把这k-1个滑动框全部移动到左侧,然后再移动其中最右侧的滑动框。然后循环往复,直到所有的滑动框都往右移动了一格为止,这其实是一个递归的过程。
我们不去深究这个递归的整个过程,我们只需要理解清楚其中的几个关键点就可以了。首先,对于每一次递归来说,我们只会移动这个递归范围内最右侧的滑动框,其次我们清楚每一次递归过程中的起始状态。开始状态就是所有的滑动框全部集中在“直尺”的最左侧,结束状态就是全部集中在最右侧。
我们把上面的逻辑整理一下,假设我们经过一系列操作之后,m个滑动框全部移动到了长度为n的直尺的最右侧。这就相当于的组合都已经获取完了。如果n+1的位置还有滑动框,并且它的右侧还可以移动,那么我们需要将它往右移动一个,到n+2的位置。这个时候剩下的局面就是
,为了获取这些组合,我们需要把这m个滑动框全部再移动到直尺的最左侧,重新开始移动。
我们在实现的时候当然没有滑动框,我们可以用一个数组记录滑动框当中的元素。
我先用递归写一下这段逻辑:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def comb(window, m, ret):
ret.append(window[:-1])
# 如果第m位的滑动框不超过直尺的范围并且m右侧的滑动框
while window[m] < min(n - k + m + 1, window[m+1] - 1):
# 向右滑动一位
window[m] += 1
# 如果m左侧还有滑动框,递归
if m > 0:
# 把左侧的滑动框全部移动到最左侧
window[:m] = range(1, m+1)
comb(window, m-1, ret)
else:
# 否则记录答案
ret.append(window[:-1])
ret = []
window = list(range(1, k+1))
# 额外多放一个滑动框作为标兵
window.append(n+1)
comb(window, k-1, ret)
return ret
这种解法的速度比上面正规递归的速度快了许多,因为我们递归的过程当中做了诸多限制,剪掉了很多无关的情况,相当于做了极致的剪枝。
最关键的是上面的这段逻辑我们是可以用循环实现的,所以我们可以用循环来将递归的逻辑展开,就得到了下面这段代码。
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
# 构造滑动框
window = list(range(1, k + 1)) + [n + 1]
ret, j = [], 0
while j < k:
# 添加答案
ret.append(window[:k])
j = 0
# 从最左侧的滑动框开始判断
# 如果滑动框与它右侧滑动框挨着,那么就将它移动到最左侧
# 因为它右侧的滑动框一定会向右移动
while j < k and window[j + 1] == window[j] + 1:
window[j] = j + 1
j += 1
# 连续挨着最右侧的滑动框向右移动一格
window[j] += 1
return ret
这段代码虽然非常精炼,但是很难理解,尤其是你没能理解上面递归实现的话,会更难理解。所以我建议,先把递归实现的滑动框的方法理解了,再来理解不含递归的这段,会容易一些。
总结
我们通过回溯法求解组合的方法应该是最简单也是最基础的,难度也不大。相比之下后面一种方法则要困难许多,我们直接去啃,往往不得要领。既会疑惑为什么这样可以保证能获得所有的组合,又会不明白其中具体的实现逻辑。所以如果想要弄明白第二种方法,一定要从滑动框这个模型出发。
从代码实现的角度来说,滑动框方法的递归解法比非递归的解法还要困难。因为递归条件以及逻辑都比较复杂,还涉及到存储答案的问题。但是从理解上来说,递归的解法更加容易理解一些,非递归的算法往往会疑惑于j这个指针的取值。所以如果想要理解算法的话,可以从递归的代码入手,想要实现代码的话,可以从非递归的方法入手。
这道题目非常有意思,值得大家细细思考。
如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。
本文使用 mdnice 排版
LeetCode 77,组合挑战,你能想出不用递归的解法吗?的更多相关文章
- Java实现 LeetCode 77 组合
77. 组合 给定两个整数 n 和 k,返回 1 - n 中所有可能的 k 个数的组合. 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], ...
- LeetCode 77. 组合(Combinations)
题目描述 给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合. 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], ...
- Leetcode之回溯法专题-77. 组合(Combinations)
Leetcode之回溯法专题-77. 组合(Combinations) 给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合. 示例: 输入: n = 4, k = 2 输 ...
- LeetCode 75,90%的人想不出最佳解的简单题
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode专题的44篇文章,我们一起来看下LeetCode的75题,颜色排序 Sort Colors. 这题的官方难度是Medi ...
- LeetCode:组合总数III【216】
LeetCode:组合总数III[216] 题目描述 找出所有相加之和为 n 的 k 个数的组合.组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字. 说明: 所有数字都是正整数. ...
- LeetCode:组合总数II【40】
LeetCode:组合总数II[40] 题目描述 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candi ...
- leetcode排列组合相关
目录 78/90子集 39/40组合总和 77组合 46/47全排序,同颜色球不相邻的排序方法 78/90子集 输入: [1,2,2] 78输出: [[], [1], [2], [1 2], [2], ...
- LeetCode算法题-First Bad Version(Java实现-三种解法)
这是悦乐书的第200次更新,第210篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第66题(顺位题号是278).您是产品经理,目前领导团队开发新产品.不幸的是,您产品的最 ...
- LeetCode 94 | 基础题,如何不用递归中序遍历二叉树?
今天是LeetCode专题第60篇文章,我们一起来看的是LeetCode的94题,二叉树的中序遍历. 这道题的官方难度是Medium,点赞3304,反对只有140,通过率有63.2%,在Medium的 ...
随机推荐
- OpenResty高性能web平台
openresty高性能web平台安装使用 简介:OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库.第三方模块以及大多数的依赖项.用 ...
- web项目——org.apache.jasper.JasperException: /WEB-INF/content/mainForm.jsp (line: 3, column: 62) File "/WEB-INF/c.tld" not found
报错信息: HTTP Status 500 – Internal Server Error Type Exception Report Message /WEB-INF/content/mainFor ...
- 使用websocket开发智能聊天机器人
前面我们学习了异步web框架(sanic)和http异步调用库httpx,今天我们学习websocket技术. websocket简介 我们知道HTTP协议是:请求->响应,如果没有响应就一直等 ...
- [JavaWeb基础] 003.JAVA访问Mysql数据库
上面两篇讲解了简单的JSP + Servlet的搭建和请求,那么后面我们肯定要用到数据交互,也就是操纵数据库的数据,包括对数字的增加,删除,修改,查询.我们就用简单的MySql来做例子 我们需要引入驱 ...
- 08 . Nginx状态码
HTTP状态码 本篇文章主要介绍运维过程中经常遇到的状态码,并通过业界流行的Nginx进行模拟实现. 2XX状态码 2XX类型状态码表示一个HTTP请求成功,最典型的就是200 # 200状态码 # ...
- Alpha冲刺 —— 5.6
这个作业属于哪个课程 软件工程 这个作业要求在哪里 团队作业第五次--Alpha冲刺 这个作业的目标 Alpha冲刺 作业正文 正文 github链接 项目地址 其他参考文献 无 一.会议内容 1.展 ...
- 【Linux】CentOS7安装tomcat8.5.45,这方法也太简单了吧!
1.下载tomcat https://tomcat.apache.org/download-80.cgi 选择tar.gz.下载完大概9495kb大小的压缩包 2.将文件从Windows复制到Cent ...
- Java实现 LeetCode 699 掉落的方块(线段树?)
699. 掉落的方块 在无限长的数轴(即 x 轴)上,我们根据给定的顺序放置对应的正方形方块. 第 i 个掉落的方块(positions[i] = (left, side_length))是正方形,其 ...
- Java实现 LeetCode 664 奇怪的打印机(DFS)
664. 奇怪的打印机 有台奇怪的打印机有以下两个特殊要求: 打印机每次只能打印同一个字符序列. 每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符. 给定一个只包含小写英文字母的字符 ...
- Java实现 LeetCode 面试题62. 圆圈中最后剩下的数字(约瑟夫环)
面试题62. 圆圈中最后剩下的数字 0,1,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字.求出这个圆圈里剩下的最后一个数字. 例如,0.1.2.3.4这5个数字组成一个圆 ...