详细总结组合排列的十余种算法实现

 

目录

•写在前面

•问题引入

•暴力枚举

循环枚举

递归枚举

回溯枚举

•深度优先搜索

前序遍历

中序遍历

后序遍历

•字典序

•二进制位运算

•带重复数字

•总结


•写在前面

排列组合的问题,如果没有合适的算法去解决,时间复杂度会相当的大,毕竟阶乘的时间复杂度不仅让人头大,也让他计算机欲罢不能,而且我们遇到排列组合的相关问题的概率相当的大,所以非常有必要掌握排列组合相关的算法,碰到很多问题,我们心里就有些底气了。我这里例举几种算法,其中想要特别强调二进制的相关解法,非常有趣。

•问题引入

我们把实际问题抽象一下,就是从一个集合中取出任意元素,形成唯一的组合。如 [a,b,c] 可组合为 [a]、[b]、[c]、[ab]、[bc]、[ac]、[abc]。我们既然说的是排列组合,当然就有区分这个集合里面,是否存在重复的元素的问题,如 [a,a,b,c] 可组合为 [a]、[b]、[c]、[ab]、[aa]、[bc]、[ac]、[abc]、[aab]等等。针对是否存在重复的元素问题,我们自然需要不同的解决方案,不过有些方法思路存在共性,接下来我们以不同算法为基础,结合不同的问题进行讲解,完成思路的理解(我们的重点是思路,有了思路,实现代码的时候就简单多了)。这里我们抛出一个问题,即如下

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。输入: nums = [1,2,3]
输出:
[[3],[1],[2],[1,2,3],[1,3],[2,3],[1,2],[]
]

我们先按照不含重复元素来进行思考,然后附上含重复元素的思路。

•暴力枚举

对于这种排列组合的问题,首先想到的当然是穷举。由于排列的要求较少,实现更简单一些,如果我先找出所有排列,再剔除由于位置不同而重复的元素,即可实现需求。假设需要从 [A B C D E] 五个元素中取出所有组合,那么我们先找出所有元素的全排列,然后再将类似 [A B] 和 [B A] 两种集合去重即可。当然,我们可以在枚举的时候进行一些剪枝,即在枚举个过程中进行一些判断,也可以当做一种优化。总结就是,逐个枚举,空集的幂集只有空集,每增加一个元素,让之前幂集中的每个集合,追加这个元素,就是新增的子集。还是不知道啥意思?没关系,我么来看思路和代码实现,这里我们分为两种枚举方式。

循环枚举

循环枚举的思路其实特别简单(暴力的思路就是人类的直觉,思路当然简单啦,哈哈哈),我们先创建结果集(res),然后往结果集里面添加一个空集,现在完成结果集的初始化之后(初始化的结果集中只有一个空集),我们开始在集合(nums)中开始循环遍历所有的元素,每次遍历一个元素,我们就把这个元素添加在当前结果集的每一个子集后,并形成新的子集,添加到结果集中,思路非常的简单暴力,代码吐下:

public static List<List<Integer>> enumerate(int[] nums) {List<List<Integer>> res = new ArrayList<List<Integer>>();res.add(new ArrayList<Integer>());for (Integer n : nums) {int size = res.size();for (int i = 0; i < size; i++) {List<Integer> newSub = new ArrayList<Integer>(res.get(i));newSub.add(n);res.add(newSub);}}return res;
}

递归枚举

递归枚举的总体思路和循环枚举一致,只不过我们的实现方式是使用递归,代码的变化其实就是把循环枚举里面的最外面那层遍历集合(nums)的循环,使用递归来代替,很简单的,看代码你就明白了。

public static void recursion(int[] nums, int i,List<List<Integer>> res) {if (i >= nums.length) return;int size = res.size();for (int j = 0; j < size; j++) {List<Integer> newSub = new ArrayList<Integer>(res.get(j));newSub.add(nums[i]);res.add(newSub);}recursion(nums, i + 1, res);
}

回溯枚举

回溯枚举就和上面的两种枚举思路有点不一样了,这种枚举思路就好像是我们人的思路,进行一个一个拼凑,只要符合我们子集的条件,就将这个子集添加进结果集,代码如下:

public static void backtrack(int[] nums, int i, List<Integer> sub, List<List<Integer>> res) {for (int j = i; j < nums.length; j++) {if (j > i && nums[j] == nums[j - 1]) continue;sub.add(nums[j]);res.add(new ArrayList<Integer>(sub));backtrack(nums, j + 1, sub, res);sub.remove(sub.size() - 1);}
}

•深度优先搜索

集合中每个元素其实就两种状态,就是选和不选,我们通过这两种状态,可以构成了一个满二叉状态树,比如,左子树是不选,右子树是选,从根节点、到叶子节点的所有路径,构成了所有子集。可以有前序、中序、后序的不同写法,结果的顺序不一样。本质上,其实是比较完整的中序遍历。我这样光说,可能比较抽象,我这里通过画的一张中序遍历的图进行讲解,如下。

前序遍历

public static void preOrder(int[] nums, int i, ArrayList<Integer> subset, List<List<Integer>> res) {if (i >= nums.length) return;subset = new ArrayList<Integer>(subset);res.add(subset);preOrder(nums, i + 1, subset, res);subset.add(nums[i]);preOrder(nums, i + 1, subset, res);
}

中序遍历

public static void inOrder(int[] nums, int i, ArrayList<Integer> subset, List<List<Integer>> res) {if (i >= nums.length) return;subset = new ArrayList<Integer>(subset);inOrder(nums, i + 1, subset, res);subset.add(nums[i]);res.add(subset);inOrder(nums, i + 1, subset, res);
}

后序遍历

public static void postOrder(int[] nums, int i, ArrayList<Integer> subset, List<List<Integer>> res) {if (i >= nums.length) return;subset = new ArrayList<Integer>(subset);postOrder(nums, i + 1, subset, res);subset.add(nums[i]);postOrder(nums, i + 1, subset, res);res.add(subset);
}

•字典序

如果你足够理解了前面的思路,其实你应该会发现前面本质思路可以分为两类,暴力、递归、回溯一类,以及深度优先搜索一类,分类的依据是什么呢?其实就是思考的角度不一样,前面的那一类,我们思考的对象是每个子集,我们对每个子集进行相关算法的实现,而深度优先搜索开始,我们将关注的点放在集合(nums)中的每个元素的状态,我们集合中的每个元素在子集中只有两个状态,也就是存在和不存在。现在我们将关注的点转到了元素的状态上,即01状态,上面的深度优先搜索的实现只是一个过渡,本质上和递归等效率差不了多少,因为01状态是二进制的天下,我们自然使用二进制来代替,效率很高。利用二进制的思想去解决这个问题,就很简单了,值得一提的是,我们在使用二进制思想解决问题的时候,并不一定说使用位运算,而这里要讲的字典序,就不适用位运算来实现二进制思路。为了更好的理解思路,我们暂时先将问题简化为:

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

算法其实非常的直观,不过有一个值得注意的是,我们在子集的最后需要放一个标志位(也就是所说的哨兵),在每次循环的时候,我们只要找到nums中的第一个满足 nums[j] + 1 != nums[j + 1]的元素,并将其加一nums[j]++ 以转到下一个组合。这种思想代替了位运算,完成了二进制的实现

public List<List<Integer>> combine(int n, int k) {LinkedList<Integer> nums = new LinkedList<Integer>();for(int i = 1; i < k + 1; ++i)nums.add(i);nums.add(n + 1);List<List<Integer>> output = new ArrayList<List<Integer>>();int j = 0;while (j < k) {output.add(new LinkedList(nums.subList(0, k)));j = 0;while ((j < k) && (nums.get(j + 1) == nums.get(j) + 1))nums.set(j, j++ + 1);nums.set(j, nums.get(j) + 1);}return output;
}

•二进制位运算

很多算法都能通过位运算巧秒地解决,其优势主要有两点:一者位运算在计算机中执行效率超高,再者由于位运算语义简单,算法大多直指本质。组合算法也能通过位运算实现,而且代码实现之后,简直令人回味无穷。我们将问题回归到最开始的问题,现在我们假设,从 M 个元素中取任意个元素形成组合,组合内元素不能重复、元素位置无关。我们使用状态的思想,对于每个元素来说,要么被放进组合,要么不放进组合。每个元素都有这么两种状态。我们这里举个例子,如果从 5 个元素中任意取 N 个元素形成组合的话,用二进制位来表示每个元素是否被放到组合里,就是:

每种组合都可以拆解为 N 个二进制位的表达形式,而每个二进制组合同时代表着一个十进制数字,所以每个十进制数字都就能代表着一种组合。十进制数字的数目我们很简单就能算出来,从00000... 到 11111... 一共有种,排除掉全都不被放进组合这种可能,结果有几种。

public List<List<String>> combination(String[] m) {List<List<String>> result = new ArrayList<>();for (int i = 1; i < Math.pow(2, m.length) - 1; i++) {List<String> eligibleCollections = new ArrayList<>();for (int j = 0; j < m.length; j++) {if ((i & (int) Math.pow(2, j)) == Math.pow(2, j)) {eligibleCollections.add(m[j]);}}result.add(eligibleCollections);}return result;
}

当然,还想更秀操作一点,我们在第一和二层循环那里,使用位移运算,来完成与运算,本质是一样的,代码如下

public static List<List<Integer>> binaryBit(int[] nums) {List<List<Integer>> res = new ArrayList<List<Integer>>();for (int i = 0; i < (1 << nums.length); i++) {List<Integer> sub = new ArrayList<Integer>();for (int j = 0; j < nums.length; j++)if (((i >> j) & 1) == 1) sub.add(nums[j]);res.add(sub);}return res;
}

当然,如果我们还有一种稍微绕一点点的二进制实现方式,就是数1的个数,这种思路有点绕,不过不管怎么样都还是二进制的一种实现方式,带着二进制的特色,我这里就大概的提一下数二进制中1的个数的实现方式,具体的问题解决代码,感兴趣的可以自己去实现以下,数二进制中1的个数的代码如下,想要关于数二进制的个数的其他算法,可以看我另一篇文章

int BitCount1(unsigned int n)
{unsigned int c =0 ; // 计数器for (c =0; n; n >>=1) // 循环移位c += n &1 ; // 如果当前位是1,则计数器加1return c ;
}

•带重复数字

我们在集合中带有重复数字,这样的集合和不带重复数字有什么区别呢?我们按照这个思路,其实可以使用不重复数字的求解方式,在求解带重复数字集合的过程中,进行去重即可。有了这种思路,我们去设计算法其实就不难了,在暴力、递归、回溯、深搜的方法中,我们很容易的将算法进行改进,我在这里就不多提了,这里要说的是二进制的算法改进。你仔细想一下我们怎么样才能在二进制的算法中进行相应的去重,这其实不容易想到的。

我们来假设nums中有[1,2,3],那么它的结果集以及对应的二进制形如下面这这样

1 2 3
0 0 0 -> [     ]
0 0 1 -> [    3]
0 1 0 -> [  2  ]   
0 1 1 -> [  2 3]  
1 0 0 -> [1    ]
1 0 1 -> [1   3] 
1 1 0 -> [1 2  ]
1 1 1 -> [1 2 3]

但是如果有了重复数字,很明显就行不通了。例如对于 nums = [ 1 2 2 2 3 ]。

1 2 2 2 3
0 1 1 0 0  -> [  2 2  ]
0 1 0 1 0  -> [  2 2  ]
0 0 1 1 0  -> [  2 2  ]

上边三个数产生的数组重复的了。三个中我们只取其中 1 个,取哪个呢?我们仔细想一下应该好想到,就是我们只要去重复数字的开头的序列就可以了,比如重复了三个2,那么我们只要分别取一个2开头即“2”,两个2开头即“22”以及三个2开头即“222”就可以了,这样就可以避免重复,更形象的解释,我们举个五个2的例子,然后例举出他们不重复的序列,像下面这样

2 2 2 2 2 
1 0 0 0 0 -> [  2         ]
1 1 0 0 0 -> [  2 2       ]
1 1 1 0 0 -> [  2 2 2     ]
1 1 1 1 0 -> [  2 2 2 2   ]
1 1 1 1 1 -> [  2 2 2 2 2 ] 

那么这个时候,我们就可以将问题转成,我们怎么去取不同个数的2在一起,其实仔细思考也不难理解,我们只要对二进制稍微研究一下就知道了,我们先拿两个2来举例子,在五位二进制中,有两个2开头的二进制序列有哪些?

2 2 2 2 2 
1 1 0 0 0 -> [  2 2       ]
1 0 1 0 0 -> [  2 2       ]
0 1 1 0 0 -> [  2 2       ]
0 1 0 1 0 -> [  2 2       ]
0 0 0 1 1 -> [  2 2       ]

上面所有两个2的情况,我们只需要去其中一种,那么我们应该取哪种?怎么取呢?这个时候我们观察一下他们是否存在不同点,而且其中存在一个和其他情况都不同的形式,我们来看一下出现了重复数字,并且当前是 1 的前一个的二进位。

对于 1 1 0 0 0 ,是 1。对于 1 0 1 0 0 , 是 0。对于 0 1 1 0 0 ,是 0。对于 0 1 0 1 0 ,是 0。对于 0 0 0 1 1 ,是 0。

可以看到只有第一种情况对应的是 1 ,其他情况都是 0。其实除去从开头是连续的 1 的话,就是两种情况。第一种就是,占据了开头,类似于这种 10...1....。第二种就是,没有占据开头,类似于这种 0...1...。这两种情况,除了第一位,其他位的 1 的前边一定是 0。所以的话,我们的条件是看出现了重复数字,并且当前位是 1 的前一个的二进位。有了这种思路,我们只需要在不重复的代码中,进行一个判断就可以了,代码如下

public static List<List<Integer>> binaryBit(int[] nums) {List<List<Integer>> res = new ArrayList<List<Integer>>();for (int i = 0; i < (1 << nums.length); i++) {List<Integer> sub = new ArrayList<Integer>();boolean illegal=false;for (int j = 0; j < nums.length; j++)if (((i >> j) & 1) == 1) {if(j>0&&nums[j]==nums[j-1]&&(i>>(j-1)&1)==0){illegal=true;break;}else{sub.add(nums[j]);}}if(!illegal){res.add(sub);}}return res;
}

•总结

二进制是真的很美妙,我们的很多问题都可以通过二进制来解决,所以我们需要慢慢适应并融入二进制的世界,思考问题的角度和 思路也将变得更加开阔。

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

论坛的头像

做了个论坛的头像,相当的简单 也就只能看看啦 菜啊

论坛头像编辑 html,spu头像编辑.html

&#xfeff;SPU头像编辑 $axure.utils.getTransparentGifPath function() { return resources/images/transparent.gif; }; $axure.utils.getOtherPath function() { return resources/Other.html; }; $axure.utils.getReloadPath function() { return resources/reload.ht…

牛客网项目---1.7.账号设置(上传头像、修改密码)

7、账号设置/setting 上传头像将头像保存到本地/七牛云服务器&#xff0c;随机生成字符串作为图片的名称&#xff0c;表单提交再返回该界面。获取头像时&#xff0c;在本地获取&#xff0c;通过输入输出流&#xff0c;先读到缓冲区&#xff0c;再从缓冲区写出&#xff1b;在七…

Discuz论坛无法上传头像/ 企业邮箱被归为垃圾邮件的问题

背景&#xff1a; 安装了Discuz论坛程序&#xff0c;更改个人头像的时候&#xff0c;点击上传按钮没有反应基于阿里云企业邮箱配置了论坛的邮件发送服务&#xff0c;但是邮箱被列入垃圾邮件拒收 解决方法&#xff1a; 上传头像失败&#xff1a;ucenter和全局域名要加https&a…

flask更改用户头像

目录结构图: 配置app 导包及配置 import os from flask import Flask, flash, request, redirect, url_for,Request,render_template from werkzeug.utils import secure_filenameUPLOAD_FOLDER 文件下载的绝对路径 # 允许的下载类型 ALLOWED_EXTENSIONS {txt, pdf, png, j…

discuz论坛用户--设置--修改头像不显示的解决方法

本文参考&#xff1a;https://www.2cto.com/kf/201601/478810.html 和https://jingyan.baidu.com/article/ceb9fb10ab73b68cad2ba092.html 先登陆discuz管理员后台admin.php&#xff0c;找到“站长”>>“UCenter设置”&#xff0c;其它的默认的地方就不要动了。关键要改…

关于Discuz 出现上传头像失败的问题

今天测试了论坛上传头像时出现 网上大部分的解决方案是配置UCenter Access denied for agent changed 头像无法更新 配置文件&#xff1a; config下config_global.php&#xff0c;config_ucenter.php uc_server\data下的config.inc.php 具体可参考此处&#xff1a;Discuz配…

内部论坛系统

开发环境/技术&#xff1a; Linux&#xff0c;MyEclipse&#xff0c;JDK1.7&#xff0c;MySql&#xff0c;JS&#xff0c;jQuery&#xff0c; ajax&#xff0c;Tomcat&#xff0c;CSS样式&#xff0c;Struts框架等 项目描述/功能&#xff1a; 项目主要实现了用户登陆注册&#…

Vue 彩色头像|一个有趣的头像生成器 附源码

前言 这是一款矢量风格的头像生成器&#xff0c;您可以搭配不同的素材组件&#xff0c;生成属于您自己的个性化头像。 介绍 您可能感兴趣的功能&#xff1a; 可视化组件配置栏随机生成头像重做/撤消国际化批量生成多个头像 在线体验 Vue 彩色头像https://www.shserve.cn/…

PHP 头像上传到mysql数据库

准备环境 window2008 phpStudy Mysql数据库 mysql数据库 第一步&#xff1a;用户登录成功 该用户已经存在数据库 进入个人中心&#xff0c;查找到数据库中的个人信息 进行图片上次 <?php include "../mysqlcon/dblink.php"; //导入数据库 ?><html> …

用php做论坛头像代码,详细介绍PHP针对多用户实现头像更换代码示例

一个网站&#xff0c;其实说白了就是某几个特定功能的组合&#xff0c;而更换用户头像就在这些功能之中。今天就来做个测试&#xff0c;针对不同的用户&#xff0c;实现头像上传功能。 成品图 思路针对不同的用户上传头像&#xff0c;我们要为每一个已登录的用户创建一个文件夹…

教你用go freetype根据用户昵称生成头像

最近需要为用户服务添加一些新功能&#xff0c;其中就包括在注册时根据用户昵称生成头像这一点。 由于用户服务是用golang写的&#xff0c;google来google去都只找到freetype一个比较简单好用的库&#xff0c;其他比如ImageMagicK之类api都过于低层不适合我们这样相对简单的图…

java ajax 更改头像_ajax+node实现头像更改

好久没有更新博客了&#xff0c;这几天在写文件上传的时候遇到了一个新的问题&#xff0c;就是关于ajax实现文件上传的问题 这几天在做一个小的demo&#xff0c;类似于论坛的一个东西&#xff0c;基于jqueryexpressmongo的一个小的案例&#xff0c;在做到关于设置个人头像的时候…

DISCUZ论坛插件h5手机电脑头像上传3.7.1带扩展插件【收集免费分享】

一个支持电脑和手机h5技术头像上传的插件。 说明&#xff1a;本插件h5电脑版和h5手机版为自主全新开发的触屏版头像上传&#xff0c;体验好&#xff0c;性能好&#xff0c;绿色。 主要特点&#xff1a;支持H5电脑版和H5手机版头像上传。 支持鼠标和触屏操作&#xff0c;支持图…

discuz 头像html5上传,discuz更新H5头像上传

越来越多的浏览器可以慢慢的不在支持flash,对应一些discuz论坛的老的版本来说就需要进行升级操作了,接下来吾爱编程为大家介绍一下discuz头像上传flash改为h5上传的方法,有需要的小伙伴可以参考一下: 1、准备工作: 根据自己的网站编码格式下载对应的最新的版本代码,然后解…

DISCUZ 如何为主题帖列表页添加头像,显示发帖者头像

只显示名字的代码 <em style" font-size:14px;"><!--{if $thread[authorid] && $thread[author]}--><a href"home.php?modspace&uid$thread[authorid]" c"1"{if $groupcolor[$thread[authorid]]} style"color…

Roop:单图离线版软件包及使用方法!

你们要的“单图换脸”离线一键运行版来了。Roop发布几十个小时后&#xff0c;马不停蹄地搞了Colab在线版。其实这东西都挺好的&#xff0c;又快又方便&#xff0c;几乎没有任何硬件要求&#xff0c;点一点就可以搞定了。但是它有一个问题&#xff0c;就是没有“魔法” 就没法使…

被迫在小公司熬了2年,现在我终于进了腾讯测试岗...

其实两年前校招的时候就往腾讯投了一次简历&#xff0c;结果很明显凉了&#xff0c;随后这个理想就被暂时放下了&#xff0c;但是这个种子一直埋在心里&#xff0c;想着总有一天会再次挑战的。 其实这两年除了工作以外&#xff0c;其余时间基本上都在学习&#xff0c;打磨自己…

Oracle-Linux修改字符集

Oracle-修改字符集 连接查询字符集立即关闭数据库并终止所有用户会话开启挂载启用受限会话设置作业队列进程数为0设置 AQ 时间管理进程数为 0打开&#xff08;Open&#xff09;一个已经挂载&#xff08;Mount&#xff09;的数据库修改数据库字符集为AL32UTF8立即关闭数据库并终…

win10/win11更新后没有声音,音频服务未响应

确保扬声器硬件没问题 检查扬声器驱动 打开设备管理器 选择声音、视频和游戏控制器 3.右键属性 4. 更新驱动 5. 从我的电脑 6. 选择最初的驱动