本文始发于个人公众号: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,组合挑战,你能想出不用递归的解法吗?的更多相关文章

  1. Java实现 LeetCode 77 组合

    77. 组合 给定两个整数 n 和 k,返回 1 - n 中所有可能的 k 个数的组合. 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], ...

  2. LeetCode 77. 组合(Combinations)

    题目描述 给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合. 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], ...

  3. Leetcode之回溯法专题-77. 组合(Combinations)

    Leetcode之回溯法专题-77. 组合(Combinations)   给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合. 示例: 输入: n = 4, k = 2 输 ...

  4. LeetCode 75,90%的人想不出最佳解的简单题

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode专题的44篇文章,我们一起来看下LeetCode的75题,颜色排序 Sort Colors. 这题的官方难度是Medi ...

  5. LeetCode:组合总数III【216】

    LeetCode:组合总数III[216] 题目描述 找出所有相加之和为 n 的 k 个数的组合.组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字. 说明: 所有数字都是正整数. ...

  6. LeetCode:组合总数II【40】

    LeetCode:组合总数II[40] 题目描述 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candi ...

  7. leetcode排列组合相关

    目录 78/90子集 39/40组合总和 77组合 46/47全排序,同颜色球不相邻的排序方法 78/90子集 输入: [1,2,2] 78输出: [[], [1], [2], [1 2], [2], ...

  8. LeetCode算法题-First Bad Version(Java实现-三种解法)

    这是悦乐书的第200次更新,第210篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第66题(顺位题号是278).您是产品经理,目前领导团队开发新产品.不幸的是,您产品的最 ...

  9. LeetCode 94 | 基础题,如何不用递归中序遍历二叉树?

    今天是LeetCode专题第60篇文章,我们一起来看的是LeetCode的94题,二叉树的中序遍历. 这道题的官方难度是Medium,点赞3304,反对只有140,通过率有63.2%,在Medium的 ...

随机推荐

  1. Pyqt5_QWidget

    QWidget常用方法: Qwidget.size()#获得客户区的大小 QWidget.width().QWidget.height()#获得客户区的宽度与高度 #设置不可以变宽.高 QWidget ...

  2. 初探Redis-基础类型List

    Redis存在五种基础类型:字符串(String).列表(List).哈希(Hash).集合(Set).有序集合(Sorted Set).本次列举出List的常用操作. Redis官网:https:/ ...

  3. Redis详解(十)------ 从零开始搭建集群

    在上一篇博客我们介绍了------Redis哨兵(Sentinel)模式,哨兵模式主要是解决高可用问题,在master节点宕机时,slave节点能够自动切换成为master节点 本篇博客我们来介绍Re ...

  4. iOS开发添加新手引导

    往往项目中经常出现此类需求 用户通过点击引导按钮可响应页面附带按钮的点击事件. // // gzhGuideView.h // GuideView // // Created by 郭志贺 on 20 ...

  5. python调用大漠插件教程03窗口绑定实例

    怎样利用注册好的大漠对象来绑定窗口? 直接上代码,根据代码分析 from win32com.client import Dispatch import os from win32gui import ...

  6. 分布式事务解决方案Seata

    Seata全称是Simple Extensible Autonomous Transaction Architecture,是由阿里巴巴开源的具有高性能和易用性的分布式事务解决方案. 微服务中的分布式 ...

  7. 关于hexo中plugins博客配置对无法生成index.html文件的影响

    用hexo搭建的博客网站在访问时出现403错误,经调查后发现是public文件夹下的index.html文件丢失. 在csdn上搜了一下发现大家都是查看是否有一下hexo的插件未安装,将未安装插件安装 ...

  8. 网络编程-TCP长连接和短连接

    TCP是一个面向连接的协议.无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接.下面会介绍一个TCP连接是如何建立的以及通信结束后是如何终止的. 一.TCP连接的建立与终止 1.1 建立连 ...

  9. 学习scrapy框架爬小说

    一.背景:近期学习python爬虫技术,感觉挺有趣.由于手动自制爬虫感觉效率低,了解到爬虫界有先进的工具可用,尝试学学scrapy爬虫框架的使用. 二.环境:centos7,python3.7,scr ...

  10. Rocket - util - ECC

    https://mp.weixin.qq.com/s/yato1PrnHe517J8twgZFOg   介绍ECC(Error Correcting Code/Error Checking and C ...