【递归算法】子集

题目链接

文章摘要:

  • 题目要求返回不含重复数字数组的所有子集,包括空集。采用递归法解决,有两种思路:一是基于每个数字的"选与不选"构建决策树,递归遍历所有可能;二是按子集元素个数分类,通过循环和回溯生成所有组合。两种方法均使用全局变量记录结果和路径,并通过回溯恢复现场。代码实现简洁,时间复杂度为O(2ⁿ),空间复杂度为O(n)。

一、题目解析

题目给出一个不含重复数字的数组,要求我们返回该数组所有的子集。

这里的子集概念是高中知识,我们知道,集合是具有互异性和无序性的,即每个元素不相同、每个元素的位置可以互换。简单来说,就是必须不重复出现,虽数字的顺序不同但其实是同一个集合(如 1, 2 和 2, 1 是同一个集合),因此我们返回的结果也必须是不重复的。

注意,空集也是一个子集。

二、算法原理 + 代码实现

我们知道,一个集合的子集个数是 2ⁿ ,如果采用暴力穷举法那铁定超时了。

因此我们选择使用 递归法

递归解法一

决策树

我们使用递归来遍历数组nums,针对每一个数字,有 " 选择 " 和 " 不选择 "两个选项。

第一次递归,针对数字1,我们如果选择这个数字,那么就有一个结果 1 ,如果不选择这个数字,那么结果就是空集 ∅。再基于 " 选择了数字1 " 继续递归,若选择2,得到 1, 2 ,若不选择2,得到 1 ...

以此类推,我们可以得到决策树如下:

可以看到,结果都在叶子节点。

接下来我们根据决策树设计代码。

全局变量

这里需要两个全局变量,一个用于记录结果 ret,一个用于记录路径 path 。

由于不涉及到状态判断等操作,两个全局变量就足够了。

dfs 函数

函数头

我们这里思考一下每一次递归都会做的事情是什么?那就是 "是否选择当前数字",而数字从哪里来?从题目所给数组里来,因此将 int nums 作为参数传递。

由于是递归形式的遍历,我们需要让每一次的操作知道当前的数字是谁,因此要将下标设计成参数之一。

函数体

每一次递归都要具体做哪些事呢?是否选择当前数字:

  1. 若选择当前数字,就将数字添加到 path 当中,然后基于这个情况,继续递归,将所有情况都遍历到(注意回溯时要恢复现场)
  2. 若不选择当前数字,就直接递归去拿到下一个数字

细节问题

回溯

刚刚我们已经谈到,在 dfs 函数中选择了数字,在回溯的时候需要恢复现场------就是将已经添加到 path 中的数字再删掉。

剪枝

我们其实算是穷举了,把所有的情况都遍历到,因此不涉及到剪枝的问题,

递归出口

结果都在决策树的叶子节点,因此当递归时的下标与 nums 的长度一致时,就认为到达了决策树的叶子节点了,此时记录下结果然后回溯。

代码实现

Java 复制代码
class Solution {
    List<List<Integer>> ret = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        dfs(nums, 0);
        return ret;
    }

    private void dfs(int[] nums, int pos) {
        // 递归出口
        if (pos == nums.length) {
            ret.add(new ArrayList<>(path));
            return;
        }

        // 选择当前数字
        path.add(nums[pos]);
        dfs(nums, pos + 1);
        path.remove(path.size() - 1);  // 回溯时恢复现场

        // 不选择当前数字
        dfs(nums, pos + 1);
    }
}

递归解法二

第二个解决思路是 " 按照子集的元素个数 " 来作决策树。

决策树

  • 当子集中的元素个数是 0 ,结果只有一个空集 ∅;
  • 当子集中的元素个数是 1,结果有三个: 1 , 2 , 3 ;
  • 当子集中的元素个数是 2,结果有三个: 1, 2 , 1, 3 , 2, 3 ;
  • 当子集中的元素个数是 3,结果有一个: 1, 2, 3
    当列举的子集中的元素个数超过了所给数组 nums 的长度时,就说明结束了,此时得到的结果就是: \[ ∅ , 1 , 2 , 1, 2 , 3 , 1, 3 , 2, 3 , 1, 2, 3 ]

基于这个思路画出的决策树的每一个节点都是结果。

并且,每一次的基于某个数字的情况都是往该数字后面遍历。

比如此时子集当中只有一个元素且该元素是数字 1 的情况下,那么它基于数字 1 的所有组合情况都是往数字 1 后面遍历:遍历到 2,就组合成 1, 2 ,然后遍历到 3 ,就组合成 1, 3 ;对于子集当中只有一个元素且该元素是数字 2 的情况,它往后遍历只有数字 3,因此只能组成 2, 3 ;对于子集当中只有一个元素且该元素是数字 3 的情况,它的后面没有数字了,也就不再遍历了。

接下来根据决策树来设计代码。

全局变量

这里我们依然需要两个全局变量:path 记录路径,ret 记录结果。

dfs 函数

函数头

在这个思路中,我们的重复子问题是 " 往当前数字的后面遍历 ",因此首先需要知道当前数字是什么,而且我们是通过递归来实现遍历,自然是用到下标的,所以函数的参数应和解法一是一致的,int nums 和下标。

函数体

对于每一次递归所要做的事情,我们可以使用一个循环来遍历当前数字的后面数字,然后将数字添加到 path 中,接着基于这个再递归。

细节问题

回溯

在函数体中我们会基于当前数字往后遍历,因此会有回溯,也就必须要有恢复现场的操作。具体来说,就是当基于当前数字的情况递归回来之后,需要恢复现场,也就是将 path 当中最后的数字删掉。

剪枝

这里我们也不涉及到剪枝的操作。

递归出口

由于这个解法的决策树的每一个节点都是结果,因此我们一进入dfs函数就更新结果,但是不需要返回。出口主要由函数体中的循环来控制,当第一个递归中的循环结束了,递归也就结束了。

代码实现

Java 复制代码
class Solution {
    List<List<Integer>> ret = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        dfs(nums, 0);
        return ret;
    }

    private void dfs(int[] nums, int pos) {
        // 进入函数后直接更新结果
        ret.add(new ArrayList<>(path));

        // 从当前位置往后遍历
        for (int i = pos; i < nums.length; i++) {
            path.add(nums[i]);
            dfs(nums, i + 1);
            path.remove(path.size() - 1);  // 回溯时恢复现场
        }
    }
}

文章到这里就结束啦,若有错误请尽管指出~

完

相关推荐
非优秀程序员15 小时前
第05章 播放器大战,暴风、快播与那一夜|《互联网产品恩仇录》
算法
非优秀程序员15 小时前
第04章 邮箱大战,163 与 QQ 的暗战|《互联网产品恩仇录》
java·vue.js·架构
知已难寻16 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器
文丑颜不良啊18 小时前
LeetCode2481~2500题目解析
java·leetcode
Flynn Liang19 小时前
同一需求三套写法:用 Spring AI、LangChain4j、AgentScope Java 各实现一个带工具调用的 RAG 助手(实战对比)
java·人工智能·大模型·架构设计·spring ai·langchain4j·agentscope
聪明的笨猪猪20 小时前
实际工程问题:AI 生成 Spring Cache(@Cacheable / @CachePut / @CacheEvict)缓存注解踩坑
java·经验分享·面试·ai编程
愿有所息20 小时前
记一次 ARM64 + JDK8 上 fastjson2 的诡异数据错乱
java
秋饼20 小时前
MCP 2026-07-28 无状态协议大改版:Spring AI 2.1 多轮往返请求与网关路由落地
java·ai·技术分享·后端开发
SL_staff1 天前
JVS-BI 实战:业务人员如何用零代码 ELT + 可视化配置构建实时经营分析闭环
java·spring·全栈