算法-经典递归解决排列组合

文章目录

    • 前言
    • 1. 获取字符串的所有字串
    • 2. 数组的子集(无重复)
    • 3. 数组的子集(有重复)
    • 4. 字符大小写全排列
    • 5. 全排列(无重复)
    • 6. 全排列(有重复)

前言

如何正确的处理递归

所有的递归都分为带路径的递归和不带路径的递归, 我们之前学二叉树的时候基本上都是带路径的递归, 所有的递归其实都是DFS(深度优先搜索), 对于如何用递归解决问题这件事, 我们需要把递归想象为一个"黑匣子" , 在解决问题的时候尝试寻找子问题, 然后复用该函数, 新手在处理递归问题的时候, 通常喜欢对递归问题进行展开, 固然, 对递归问题进行全部展开是一种直观的理解递归问题的方式, 但是对递归问题进行全部的展开, 往往会很难以理解, 下次遇到问题的时候还是会一头雾水, 所以我建议, 在用递归处理问题的时候, 我们先去抽象的理解递归问题, 相信递归函数一定能处理这个问题, 然后在深入递归函数的本质, 这样理解起来往往会比较简单…, 下面的经典递归问题的解析我们都将用这种方式来解决…

如何理解递归与回溯

递归回溯其实是相辅相成的, 递归的过程中天然就带有回溯, 但是有时候我们不去用到这个点, 其实就是一种恢复现场的技巧, 个人认为恢复现场这个词更能体现出回溯的真正含义

1. 获取字符串的所有字串

题目解析

这道题的题目是获取字符串的所有字串, 比如 “abcdefg” 字串的数量是 2^7 == 128个, 其实就是我们高中所学的集合相关的知识, 求集合的所有子集的问题(二项式定理), 上面的字符串太长不好举例子, 我们用 “abc” 举例子, 该字符串的字串有8个, 分别是
“a”, “b”, “c”, “ab”, “ac”, “bc”, “abc”, " "

递归分析

首先把我们的函数想象为一个黑盒, 我们的函数为func, 在经过func的作用之后就可以得到所有的字串的集合, 所以出现了问题的复现
求"abcd"所有字串 = 带有"a" + 求"bcd"的所有字串
求"abcd"所有字串 = 不带有"a" + 求"bcd"的所有字串
注意这里的不带有"a", 其实就是我们等下需要进行恢复现场的点…
现在看我们的代码实现

public class Main{public static void main(String[] args) {//输入程序Scanner in = new Scanner(System.in);String s = in.next();//参数处理与函数调用char[] chars = s.toCharArray();StringBuilder sp = new StringBuilder();HashSet<String> set = new HashSet<>();getSubStrings(chars, 0, sp, set);//迭代器遍历输出Iterator<String> it = set.iterator();while (it.hasNext()) {System.out.print(it.next() + "/ ");}}public static void getSubStrings(char[] chars, int index, StringBuilder sp, HashSet<String> set) {//递归的终止条件if (index == chars.length) {String temp = sp.toString();if (!set.contains(temp)) {set.add(temp);}return;}//加上当前字符的情况sp.append(chars[index]);getSubStrings(chars, index + 1, sp, set);//下面的这一步, 就是我们所说的回溯, 因为想要得到不含有该字符的所有字串就需要删除该字符sp.deleteCharAt(sp.length() - 1);getSubStrings(chars, index + 1, sp, set);}
}

上述代码的实现逻辑就是通过sp不断拼接, 当我们的下标是终止位置的时候就进行输出
代码执行结果

在这里插入图片描述
代码的调用逻辑图, 由于该代码的调用逻辑比较复杂, 我们把递归调用的逻辑图用下图来表示("abc为例子), 图中的黑线是DFS向下递归的过程, 我们的(√ / ×) 代表的是该位置的字符时候进行拼接, 我们向上返回的时候正好是我们的该字符删除的时期, 还是那句话, 递归的逻辑图一般比较复杂, 我们理解递归的方式应该是抽象与具体相结合…
在这里插入图片描述
下面的代码是对我们的上面的代码的改进, 通过一个char数组和一个size来管控path的控制, 其实就是子集手动的模拟了一个栈的结构

	public static String[] generatePermutation2(String str) {char[] s = str.toCharArray();HashSet<String> set = new HashSet<>();f2(s, 0, new char[s.length], 0, set);int m = set.size();String[] ans = new String[m];int i = 0;for (String cur : set) {ans[i++] = cur;}return ans;}public static void f2(char[] s, int i, char[] path, int size, HashSet<String> set) {if (i == s.length) {set.add(String.valueOf(path, 0, size));} else {path[size] = s[i];f2(s, i + 1, path, size + 1, set);f2(s, i + 1, path, size, set);}}

2. 数组的子集(无重复)

在这里插入图片描述

题目描述就是上面描述的那样, 其实就类似于我们上面的寻找字符串的所有子序列一样, 我们而且这个是没有相同的元素的, 所以最后也不涉及去重的相关操作, 我们快速过掉这个题

class Solution {List<List<Integer>> res = new ArrayList<>();public List<List<Integer>> subsets(int[] nums) {getSubSets(nums,0,new ArrayList<>());return res;}private void getSubSets(int[] nums,int index,List<Integer> list){//递归的终止条件if(index == nums.length){List<Integer> temp = new ArrayList<>();for(int elem : list){temp.add(elem);}res.add(temp);}else{list.add(nums[index]);getSubSets(nums,index + 1,list);//回溯过程list.remove(list.size() - 1);getSubSets(nums,index + 1,list);}}
}

3. 数组的子集(有重复)

这个问题跟上面的问题一样, 但是不一样的是, 我们的内部元素使用重复的, 这会导致我们最终的结果出先问题, 这时候我们就想, 那这个题我们还按照之前字符串子集的那个思路, 最后用HashSet去重一下就好了么, 实则不然, 我们看下面的这个例子
假如我们的集合是 [ 4 , 4 , 1 , 4 ]
我们如果用字符串去重的方式写这一道题, 我们就会出现下面的bug
我们的集合的子集可能会有(√是有,x是无)
[√, √, √, x] —> [ 4 , 4 , 1 ]
[√, x,√, √ ] —> [ 4 , 1 , 4 ]
这两个集合明显都是子集, 如果用HashSet去重的话二者都会被添加进结果, 但显然这个结果是不对的, 根据我们这道题目的要求, 我们的每个子集的元素的数目如果一致也是认为是一样的, 也就是
4 1 4 和 4 4 1 其实只能添加一个, 那如何解决这个问题呢

常规排序 + DFS展开

用我们之前的思路解决这道题是很容易的, 我们只需要先将数组进行排序, 就可以用常规的HashSet进行去重操作, 因为排序过了之后, 之前的重复的组别(比如 441 和 414 )就会被排序颠覆给自然的擦除掉, 代码实现如下

class Solution {List<List<Integer>> res = new ArrayList<>();public List<List<Integer>> subsetsWithDup(int[] nums) {//我们的常规解法就是之前学的那个返回字符串的所有子集的思路ArrayList<Integer> list = new ArrayList<>();HashSet<List<Integer>> set = new HashSet<>();Arrays.sort(nums);func(nums,0,list,set);return res;}private void func(int[] nums,int index,ArrayList<Integer> list,HashSet<List<Integer>> set){//递归的终止条件if(index == nums.length){List<Integer> temp = (List<Integer>)list.clone();if(!res.contains(temp)) res.add(temp);}else{list.add(nums[index]);func(nums,index+ 1 ,list,set);list.remove(list.size() - 1);func(nums,index + 1,list,set);}}}

排序 + 剪枝 + DFS展开

显然, 上面的解法是不够好的, 时间复杂度是 2^n * n, 那有没有一种更好的方式来解决这个问题呢
我们拿下面的这个例子举例
假如一个集合排序之后的结果是 [ 1 1 1 1 1 2 2 2 2 4 4 5 5 6 6 8 8 ]
我们尝试用分组的策略来简化递归的过程
我们的问题被拆解为 :
全集合的子集 = n 个 1 + [ 2 2 2 2 4 4 5 5 6 6 8 8 ] 子集的数量
其中 n 的值为 [ 0, 5 ]

剪枝的原理实现

为什么这个逻辑就可以简化代码, 是因为如果不进行这样设计的话, 我们原来的 5 个 1 会进行全部展开, 一共的数目情况是 2 ^ 5 == 32, 但是如果用这种分组的角度思考的话, 我们就只有6种情况(1的个数), 所以自然会加快了递归的过程, 代码实现如下

class Solution {public List<List<Integer>> subsetsWithDup(int[] nums) {// 先对数组进行排序Arrays.sort(nums);List<List<Integer>> res = new ArrayList<>();ArrayList<Integer> arr = new ArrayList<>();func(nums, 0, arr, res);return res;}private void func(int[] nums, int index, ArrayList<Integer> arr, List<List<Integer>> res) {// 递归的终止条件if (index == nums.length) {ArrayList<Integer> tempList = new ArrayList<>();for (int elem : arr) {tempList.add(elem);}res.add(tempList);} else {int j = index + 1;// 把j下标移动到不同的第一个元素的位置while (j < nums.length && nums[index] == nums[j])j++;int sz = 0;for (sz = 0; sz <= (j - index); ++sz) {//进行不同数目的组内数字添加for (int k = 0; k < sz; ++k) {arr.add(nums[index]);}func(nums, j, arr, res);//回溯的过程, 要恢复现场for (int k = 0; k < sz; ++k) {arr.remove(arr.size() - 1);}}}}
}

在这里插入图片描述

4. 字符大小写全排列

在这里插入图片描述

这道题我们插到这里作为一个练习, 本题的思路就是上面的DFS求子序列的思路(注意回溯的逻辑)

class Solution {public List<String> letterCasePermutation(String s) {List<String> res = new ArrayList<>();char[] sc = s.toCharArray();StringBuilder sp = new StringBuilder();HashSet<String> set = new HashSet<>();letterSubPath(sc, 0, sp, res, set);return res;}public void letterSubPath(char[] chars, int index, StringBuilder sp, List<String> res, HashSet<String> set) {// 递归的终止条件if (chars.length == index) {String temp = sp.toString();if (!set.contains(temp)) {set.add(temp);res.add(temp);}} else {if (chars[index] >= '0' && chars[index] <= '9') {sp.append(chars[index]);letterSubPath(chars, index + 1, sp, res, set);sp.deleteCharAt(sp.length() - 1);} else {if (chars[index] >= 'A' && chars[index] <= 'Z') {sp.append(chars[index]);letterSubPath(chars, index + 1, sp, res, set);sp.deleteCharAt(sp.length() - 1);sp.append((char) (chars[index] + 32));letterSubPath(chars, index + 1, sp, res, set);sp.deleteCharAt(sp.length() - 1);} else {sp.append(chars[index]);letterSubPath(chars, index + 1, sp, res, set);sp.deleteCharAt(sp.length() - 1);sp.append((char) (chars[index] - 32));letterSubPath(chars, index + 1, sp, res, set);sp.deleteCharAt(sp.length() - 1);}}}}
}

5. 全排列(无重复)

这个题的意思就是说对于一个集合来说, 求他的所有的排列
比如一个集合[ 3 1 2 ], 一共 n! 种情况
我们可能的排列是[ 3 1 2 ] [ 3 2 1 ] [ 2 3 1 ] [ 2 1 3 ] [ 1 2 3 ] [ 1 3 2 ]

递归的逻辑分析

那么这道题如何用递归来理解呢, 我们的函数的func是来求全部的排列, 所以得到下面的关系
全部元素的全排列 = 不同的元素在0下标的全排列 + 剩余元素的全排列
子问题的递推出现了复现, 所以就可以写出下面的递归的逻辑

class Solution {public List<List<Integer>> permute(int[] nums) {List<List<Integer>> res = new ArrayList<>();permute(nums, 0, res);return res;}private void permute(int[] nums, int index, List<List<Integer>> res) {//递归的终止条件if (index == nums.length) {List<Integer> temp = new ArrayList<>();for (int elem : nums) {temp.add(elem);}res.add(temp);} else {for (int j = index; j < nums.length; ++j) {swap(nums, index, j);permute(nums, index + 1, res);//一定要回溯恢复现场(不然会有重复的情况存在)swap(nums, index, j);}}}private void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}
}

复杂度是 n! * n

6. 全排列(有重复)

这个题目比上面的就只有一点, 就是加一个HashSet去重
代码实现如下
在这里插入图片描述

class Solution {private void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}public List<List<Integer>> permuteUnique(int[] nums) {List<List<Integer>> res = new ArrayList<>();permuteUnique(nums, 0, res);return res;}private void permuteUnique(int[] nums, int index, List<List<Integer>> res){if(index == nums.length){List<Integer> temp = new ArrayList<>();for (int elem : nums) {temp.add(elem);}res.add(temp);}else{HashSet<Integer> set = new HashSet<>();for (int j = index; j < nums.length; ++j) {if(!set.contains(nums[j])){set.add(nums[j]);swap(nums, index, j);permuteUnique(nums, index + 1, res);swap(nums, index, j);}}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/47803.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HBuilder x 使用Git管理项目,配合easy-git插件管理项目代码配置git和推送/拉取使用教程

文章目录 目录 文章目录 使用流程 小结 概要安装流程技术细节小结 概要 克隆/拉取项目代码到本地电脑教程 HBuilder x 使用Git管理项目&#xff0c;配合easy-git插件 克隆项目代码到本地教程-CSDN博客 电脑环境已安装好Hbuilder x工具 如果没有安装可以参考以下先安装好Hbui…

银发经济发展需要什么支持

随着社会老龄化的加剧&#xff0c;“银发经济”应运而生&#xff0c;成为社会发展的新动向。银发经济指的是针对老年人群体的经济活动&#xff0c;包括健康护理、休闲旅游、教育文化等多方面内容。这一现象不仅体现了社会对老年群体的关注和尊重&#xff0c;同时也为经济发展提…

HarmonyOS NEXT零基础入门到实战-第一部分

构建节页面思路&#xff1a; 1、排版 (分析布局) 2、内容&#xff08;基础组件&#xff09; 3、美化&#xff08;属性方法&#xff09; 设计资源-svg图标 界面中展示图标 ->可以使用svg图标&#xff08;任意放大缩小不失真&#xff0c;可以改颜色&#xff09; 使用方式&a…

重磅活动推荐:2024 CLK 大会启动中,承办单位开放报名

中国 Linux 内核开发者大会&#xff08;简称“CLK 大会”&#xff09;是中国 Linux 内核领域最具影响力的峰会之一&#xff0c;由清华大学、英特尔、富士通南大、IBM、阿里云、华为、腾讯等企业支持主办。大会秉承“自由、协作、创新”理念&#xff0c;以推动和普及开源技术为使…

java中Hashcode的作用【详解版】

一 HashCode作用 1.1 HashCode作用 hashCode是object类的一个方法&#xff0c;用于哈希表结构&#xff0c;主要是用来获取哈希值&#xff0c;用于确定对象在哈希表中的位置&#xff0c;如果两个对象的hashcode相同&#xff0c;那么他们可能被放在哈希表同一个位置(这取决于哈…

【每天值得看】文章获得《每天值得看》人工智能板块推荐第三名!为自己点个赞!!!

[2024-07-19]&#xff5c;CSDN每天值得看&#xff5c;人工智能 ① 【机器学习】Grid Search: 一种系统性的超参数优化方法&#xff08;鑫宝Code:[博客] [成就]&#xff09; [质量分&#xff1a;97&#xff1b;难度等级&#xff1a;未知&#xff1b;新鲜技术&#xff1a;99] 摘…

辅助类BigDecima/BigInteger

** 大数据的运算** 编号1方法解释1add2subtract-3multiply*4divide/

如何发一篇顶会论文? 涉及3D高斯,slam,自动驾驶,三维点云等等

SLAM&3DGS 1&#xff09;SLAM/3DGS/三维点云/医疗图像/扩散模型/结构光/Transformer/CNN/Mamba/位姿估计 顶会论文指导 2&#xff09;基于环境信息的定位&#xff0c;重建与场景理解 3&#xff09;轻量级高保真Gaussian Splatting 4&#xff09;基于大模型与GS的 6D pose e…

AutoMQ 生态集成 Redpanda Console

通过 Kafka Web UI 更加便利地管理 Kafka/AutoMQ 集群 随着大数据技术的飞速发展&#xff0c;Kafka 作为一种高吞吐量、低延迟的分布式消息系统&#xff0c;已经成为企业实时数据处理的核心组件。然而&#xff0c;Kafka 集群的管理和监控却并非易事。传统的命令行工具和脚本虽…

C++从入门到起飞之——this指针 全方位剖析!

个人主页&#xff1a;秋风起&#xff0c;再归来~ C从入门到起飞 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 目录 1、this指针 2、C和C语⾔实现Stack对⽐ C实现Stack代码 C实现Stack代…

c# listview控件调整标题显示顺序

右键点击listview,选择编辑列 修改DisplayIndex listview在成员位置点击上下箭头移动后&#xff0c;实际显示不会改变&#xff0c;因为DisplayIndex没有改变

【Git】(基础篇四)—— GitHub使用

GitHub使用 经过上一篇的文章&#xff0c;相信大家已经对git的基本操作熟悉了&#xff0c;但哪些使用git的方法只是在本地仓库进行&#xff0c;本文介绍如何使用git和远程仓库进行连接使用。 Github和Gitee 主要用到的两个远程仓库在线平台是github和gitee GitHub GitHub …

STM32第十九课:FreeRTOS移植和使用

目录 需求一、FreeRtos概要二、移植FreeRtos1.复制源码2.内存空间分配和内核相关接口3.FreeRTOSConfig.h4.在工程中添加.c.h 三、任务块操作1.创建任务2.任务挂起&#xff0c;恢复&#xff0c;删除 四、需求实现代码 需求 1.将FreeRtos&#xff08;嵌入式实时操作系统&#xf…

若依框架中Spring Cloud版本启动失败问题

RuoYiSystemApplication启动不了 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.ruoyi.system.mapper.SysConfigMapper.selectConfigList 该问题是因为在我们拉取若依代码到本地之后&#xff0c;没有对配置作改动&#xff0c;而且若依…

服务器基础1

服务器基础复习01 1.环境部署 系统&#xff1a;华为欧拉系统 网络简单配置nmtui 因为华为欧拉系统密码需要复杂度 所以我们可以进入后更改密码 echo 123 | passwd --stdin root也可以 echo "root:123" | chpasswd2.关闭防火墙&#xff0c;禁用SElinux 首先先关…

纠正和防止机器学习中的不公平现象

「AI秘籍」系列课程&#xff1a; 人工智能应用数学基础人工智能Python基础人工智能基础核心知识人工智能BI核心知识人工智能CV核心知识AI 进阶&#xff1a;企业项目实战 预处理、处理中、后处理方法和非定量方法 机器学习中的公平性是一个复杂的问题。更糟糕的是&#xff0c;负…

加密传输及相关安全验证:

1.1. 加密&#xff1a; 1.1.1. 对称加密&#xff1a; 特点&#xff1a;加解密用一个密钥&#xff0c;加解密效率高&#xff0c;速度快&#xff0c;有密钥交互的问题问题&#xff1a;双方如何交互对称密钥的问题&#xff0c;用非对称密钥的公钥加密对称密钥的混合加密方式常用…

[数据分析]脑图像处理工具

###############ATTENTION&#xff01;############### 非常需要注意软件适配的操作系统&#xff01;有些仅适用于Linux&#xff0c;可以点进各自软件手册查看详情。 需要自行查看支持的影像模态。 代码库和软件我没有加以区分。 不是专门预处理的博客&#xff01;&#xf…

C语言 底层逻辑详细阐述指针(一)万字讲解 #指针是什么? #指针和指针类型 #指针的解引用 #野指针 #指针的运算 #指针和数组 #二级指针 #指针数组

文章目录 前言 序1&#xff1a;什么是内存&#xff1f; 序2&#xff1a;地址是怎么产生的&#xff1f; 一、指针是什么 1、指针变量的创建及其意义&#xff1a; 2、指针变量的大小 二、指针的解引用 三、指针类型存在的意义 四、野指针 1、什么是野指针 2、野指针的成因 a、指…

《基于 Kafka + Quartz 实现时限质控方案》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…