1.全排列
定义:从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。
公式:全排列数f(n)=n!(定义0!=1)
以上是对全排列的简单说明,我们先来看一个简单的例子:写出“ABCDE”的全排列:
【问题分析】我们用递归的方法来尝试,那么就需要将大问题转化为相似的小问题。当前字符串的所以排列的可能情况,就是遍历字符串,将当前的字符串作为开头,加上剩余的字符全排列情况之和。换句话说就是在“ABCDE”这个字符串的全排列情况,每一个字符都可以作为开头,我们要做的就是将每一个字符拿出来作为开头,然后将剩下的所有字符进行全排列!!!这样就实现了将问题转化为相似的小问题啦!!也就是递归的形式已经显然易见了!话不多说,具体细节在代码注释中
package demo01;
import java.util.List;
import java.util.Vector;
public class D2 {
//f方法就是用于求串str的全排列,返回的是一个list类型,存放串的所有排列情况
static List f(String str) {
List lst=new Vector();//首先要创建一个存放结果的list容器
if(str.length()==1) {//这是递归结束的条件,我们不断缩小递归的规模。当串只有一个字符时,递归结束
lst.add(str);//只有一个字符,这是全排列也就是它本身,直接添加到容器中
return lst;//返回容器,递归结束
}
for(int i=0;i<str.length();i++) {
char x=str.charAt(i);//取出串的每一个字符作为开头
List t=f(str.substring(0, i)+str.substring(i+1));
//将除去x字符外的所有字符的全排列的结果交给容器t
for(int k=0;k<t.size();k++) {//将容器t中的每一个串与x进行拼接,就是str串的全排列
lst.add(""+x+t.get(k));
}
}
return lst;//返回lst容器
}
public static void main(String []args) {
List lst=f("ABC");
for(int i=0;i<lst.size();i++) {
System.out.println(lst.get(i));
}
}
}
以上关于串的全排列是用Java来实现的,似乎对于c++而言,这种方法并没有太大的优势。在这里介绍最经典的全排列的递归算法,c++和Java都能轻松驾驭。为了更好的解释,我们以【1,2,3】的全排列为例
容易发现,它的全排列情况有【123】【132】【213】【231】【312】【321】
现将【123】的2/3位互换得【132】
2/3位互换恢复为【123】
将【123】的第一位分别于二三位互换分别得【213】【321】
再分别将其2/3位进行互换得【231】【312】
可能还是有点乱,总的来说,就是将某位于后面的每一位进行交换就得到了一组全排列
Java代码如下
public class D3 {
//a是考察的数组
//k是当前考虑的位置
static void f(char []aa,int k) {
if(k==aa.length-1) {//递归的出口,当进行到最后一位时,就没有可交换的元素了,直接出来吧!!
if(aa[0]=='A') {
System.out.println(String.valueOf(aa));
}
return;
}
for(int i=k;i<aa.length;i++) {
{char t=aa[i];aa[i]=aa[k];aa[k]=t;}//试探//将当前位于后面每一位进行交换
f(aa,k+1);//调用递归
{char t=aa[i];aa[i]=aa[k];aa[k]=t;}//回溯//必须恢复交换前的位置,基于该次序的交换不能乱!
}
}
public static void main(String []args) {
f("ABCDE".toCharArray(),0);
}
}
c++代码如下
#include <iostream>
using namespace std;
void prem(char a[],int begin,int end){
if(begin==end-1){
cout<<a<<endl;
return;
}
for(int i=begin;i<end;i++){
swap(a[begin],a[i]);//试探
prem(a,begin+1,end);
swap(a[begin],a[i]);//回溯
}
}
int main(){
char a[5]={'A','B','C','D','E'};
prem(a,0,5);
}
2.组合
我们先采用循环暴力求解的办法吧!!
假设从“ABCDE”中选3个字符的组合情况
循环暴力求解嘛!第一层循环,i可能从A取到E,第二层循环j,可能从A取到E,第三层循环k,可能从A取到E。三层循环后,满足i、j、k不相等的就是满足题目的组合情况。
仔细想一想??会不会有问题???是这样吗?组合对顺序并没有要求,所以在上面的循环暴力法之下,会出现ABC、BAC都是满足条件的,也就是会让同一个结果重复出现。那么我们该怎么样避免这个问题呢??
可不可以这样,我们将i、j、k的大小定下来,这样重复的项就只能出现一次(ABC、ACB、BCA……我们要求字典顺序i<j<k,那么就只有ABC一种情况会被保留下来)。这个时候,再用暴力的方法该怎么做呢?
第一层循环,i当然还是从A取到E,第二层循环,j从i的后一个开始取到E,第三层循环,k从j的后一个开始取到E,这样得到的三个字母组合一定是没有重复项的。仔细想想,是不是这个道理哈?!
来!上代码!
#include <iostream>
using namespace std;
int main(){
for(char i='A';i<='E';i++){
for(char j=i+1;j<='E';j++){
for(char k=j+1;k<='E';k++){
cout<<i<<j<<k<<endl;
}
}
}
return 0;
}
如果用递归呢???递归??没错!递归!
递归就是将问题转化为小规模的相似的问题~~自己做少量的工作,其余的工作交给别人来做!
从m个中取n个,怎么递归呢?我们设一个递归函数 int f(int m,int n);表示的就是从m个球中取n个球的组合数。我们就考虑抓取的第n次,对于某一个特定的球而言,这第n次抓取,要么取这个球,要么不取这个球。我们把取这个球和不取这个球的所有情况加起来不就是取n次的所有情况吗?是不是这个道理
代码如下(java实现)
public class D4 {
//m个中取n个
static int f(int m,int n) {
if(m==n)return 1;
if(n==0)return 1;
return f(m-1,n)+f(m-1,n-1);
}
public static void main(String []args) {
System.out.println(f(5,3));
}
}
我们都知道,循环和递归之间有着很神奇的联系。对于之前的循环暴力求解法,有没有相应的递归解法呢??当然是有的,思想是一样的,只是实现的手法不一样,直接上代码吧!
还是来说一下吧…………之所以刚刚能够轻松的用循环暴力法求解,那是因为字符串是固定的(“ABCDE”中取3个),取多少个也是固定的,所以循环的层数也就能确定下来。但是如果不确定呢??
在三层循环的时候,每一层循环做的都是相似的操作——从前一个已经确定的字符的后面任取一个字符!
那么我们自然就能想到用递归来代替循环求解。设一个函数static List f(String s,int n) ,该函数的作用,就是在字符串s中取n个的组合情况。在这个函数中,我们要做的就是在字符串中任取一个字符m,然后利用递归,在该字符后的剩下的字符中取n-1个字符。大概就是这个思想,现在看代码趴!
Java版本:
public class D5 {
static List f(String s,int n) {
List lst=new Vector();
if(n==0) {
lst.add("");//什么也不取,也是一种方法!
return lst;
}
for(int i=0;i<s.length();i++) {
char m=s.charAt(i);//取出第i位
List t=f(s.substring(i+1),n-1);//在取出的该字符的后面选出剩下的n-1个
for(int k=0;k<t.size();k++) {
lst.add(""+m+t.get(k));//加入到结果容器中
}
}
return lst;
}
public static void main(String[] args) {
List lst=f("ABCDE",3);
for(int i=0;i<lst.size();i++) {
System.out.println(lst.get(i));
}
}
}
c++版:
public class E2 {
//a用来存放原来的数组
//n位个数
//begin为开始的位置
//k为当前考虑的位置
//x数组用来存放个数
static void work(int []x) {
for(int i=0;i<x.length;i++) {
System.out.print(x[i]);
}
System.out.println();
}
static void f(int []a,int []x,int begin,int k,int n) {
if(k==x.length) {
if(n==0) {
work(x);
}
return;
}
// if(begin==a.length) {
// return;
// }
for(int i=begin;i<a.length;i++) {
x[k]=a[i];//试探
f(a,x,i+1,k+1,n-1);
x[k]=0;//回溯
}
}
public static void main(String []args) {
int []a= {1,2,3,4,5};
int []x=new int [3];
f(a,x,0,0,3);
}
}
3.有重复情况的排列组合
3.1有重复的全排列
在上面已经介绍过了,全排列的实际上就是遍历数组,将当前位置的元素与后面的元素进行交换。有重复项时,只与第一个进行交换,后面的不再进行交换。也就是多加一个判重的函数,代码如下,有一个相应的例题,就是扑克牌AA223344排序的问题
https://blog.csdn.net/jfwzy109127/article/details/87800495
#include <iostream>
#include <cstring>
using namespace std;
//第i个元素是否在前面元素[K……i-1]中出现过
int dup(char a[],int k,int i){
if(i>k){
for(int j=k;j<i;j++){
if(a[j]==a[i]){
return 0;
}
}
}
return 1;
}
void prem(char a[],int begin,int end){
if(begin==end-1){
cout<<a<<endl;
return;
}
for(int i=begin;i<end;i++){
if(dup(a,begin,i)){
swap(a[i],a[begin]);
prem(a,begin+1,end);
swap(a[i],a[begin]);
}
}
}
int main(){
string s="AA223344";
char s2[8];
strcpy(s2,s.c_str());
prem(s2,0,8);
}
3.2有重复项的组合问题
我还没想好怎么解释………………那就……emmm先看代码哈
通俗的将,就是将每一个元素能取到的值全部遍历一遍,满足条件的输出!
//AAABBC中任取3个
public class D62 {
static void work(int []x) {
for(int i=0;i<x.length;i++) {
for(int j=0;j<x[i];j++) {
System.out.print((char)(i+'A'));
}
}
System.out.println();
}
//a数组存放每个元素最多重复的次数
//x位实际上选取的每个元素的个数
//k为当前考虑的对象
//goal为目标的差值
static void f(int []a,int []x,int k,int goal) {
if(k==x.length) {
if(goal==0) {
work(x);
}
return;
}
for(int i=0;i<=Math.min(a[k], goal);i++) {
x[k]=i;//试探,假设第k为元素选取i次
f(a,x,k+1,goal-i);
x[k]=0;//回溯
}
}
public static void main(String[] args) {
int []a= {3,2,1};
int []x=new int [a.length];
f(a,x,0,3);
}
}