【递归算法】子集

题目链接

文章摘要:

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

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

相关推荐
qq_3331209714 小时前
C++高并发内存池的整体设计和实现思路_C 语言
java·c语言·c++
mh_f14 小时前
33.批量通过GET链接下载图片到指定文件夹下
java
金銀銅鐵14 小时前
[Java] 如何理解 class 文件中方法的 access flags?
java·后端
智研数智工坊14 小时前
SpringBoot4.0.6 + Security7.x + JWT 最新完整实战|无状态权限认证、统一异常处理、可直接落地
java·spring boot·spring security·jwt·权限认证
DIY源码阁14 小时前
JavaSwing宿舍管理系统 - MySQL版
java·数据库·mysql·eclipse
Han_han91914 小时前
递归相关题目:
java
yuan1999715 小时前
基于 MATLAB PSO 工具箱的函数寻优算法
开发语言·算法·matlab
YUANQIANG202415 小时前
博弈论中势函数与势博弈构造:为什么看似 “先射箭后画靶”
算法·信息与通信
kTR2hD1qb15 小时前
Claude Code Skill的介绍与使用
java·前端·数据库·人工智能
WBluuue15 小时前
Codeforces 1096 Div3(ABCDEFGH)
c++·算法