算法 | 一文玩转全排列(附 实际案例分析)

一、全排列初识

从 n 个不同元素中任取 m(m≤n)个元素,按照一定的顺序排列起来,叫做从 n 个不同元素中取出 m 个元素的一个排列。当 m=n 时所有的排列情况叫全排列。

二、问题引入

看了上面的定义还是一脸糊涂?不要紧,先看看下面的例子,你就会对全排列概念有个具体的认识:

对于一个给定的序列 a = [a1, a2, a3, … , an],请设计一个算法,用于输出这个序列的全部排列方式。
例如:a = [1, 2, 3]
输出
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 2, 1]
[3, 1, 2]

这就是一个简单的全排列问题,单纯看上面的例子,貌似看起来很简单,用眼盯着可能都能算对,但是如果数组a的值个数增加到十个百个呢,那手算出来的概率就变得很低很低了,这时候就要利用算法来解决这一类问题。

三、递归实现

我们先考虑两种情况,

1. 数组元素互不相同

如果数组a中所有元素都不相同,每个数值都是唯一的,这时候问题就简单了,只需要进行dfs算法深度优先搜索,就可以实现,看代码:

import java.util.Arrays;

public class 全排列 {
	public static void main(String[] args) {
        int[] a = {1, 2, 3};//无重复元素
        allrange(a, 0, a.length - 1);

    }

    //全排列(递归回溯)
    private static void allrange(int[] a, int cursor, int end) {
        // 递归终止条件
        // 已经到序列结尾了 
        if (cursor == end) {
            System.out.println(Arrays.toString(a));
        }
        
        //初始 i=游标,因为 游标 之前的顺序已经确定了,不需要再排列了
        for (int i = cursor; i <= end; i++) {
        	
            swap(a, cursor, i);//固定游标,让 i 值不断变化,去输出当前后面的各种顺序的排列
            allrange(a, cursor + 1, end);     
            swap(a, cursor, i);// 回到交换之前的序列,这里可以理解为回溯,保证下一个值不受上一个值的影响(记忆抹去)
        }
    }

    private static void swap(int[] a, int cursor, int i) {
        int temp = a[cursor];
        a[cursor] = a[i]; 
        a[i] = temp;
    }
}

输出

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 2, 1]
[3, 1, 2]

2. 数组内有重复元素

如果数组内有重复元素,我们就要让这几个重复的元素不进行互相交换,如果它们互相交换,可以很容易得知,他们交换后输出的数组的顺序都是一样的,所以交换并没有改变输出顺序,这个是无意义的。所以我们必须排除这种不必要的交换,可以将这个功能封装成一个方法完成,整个代码如下:

import java.util.Arrays;

public class 全排列 {
	public static void main(String[] args) {
	        int[] a = {1, 2, 3,2};//有重复元素
	        allrange(a, 0, a.length - 1);
	
	    }

   //全排列(递归回溯)
    private static void allrange(int[] a, int cursor, int end) {
        // 递归终止条件
        // 已经到序列结尾了 
        if (cursor == end) {
            System.out.println(Arrays.toString(a));
        }
        
        //初始 i=游标,因为 游标 之前的顺序已经确定了,不需要再排列了
        for (int i = cursor; i <= end; i++) {
        	//如果不进行这一步,则有重复元素的数组可能会出现输出两个相同的情况(比如[1,2,2,3]输出两次)
            //但是对于没有重复元素的数组,这一步可有可无
        	if (!judgeSwap(a, cursor, i)) {//看cursor到i之间是否有两个数组值相同,如果有,进入for让i递增,此i就不进行交换   (有相等,就不交换,就i递增进入下一项)  
                continue;
            }
            swap(a, cursor, i);//固定游标,让 i 值不断变化,去输出当前后面的各种顺序的排列
            allrange(a, cursor + 1, end);     
            swap(a, cursor, i);// 回到交换之前的序列,这里可以理解为回溯,保证下一个值不受上一个值的影响(记忆抹去)
        }
    }

    private static void swap(int[] a, int cursor, int i) {
        int temp = a[cursor];
        a[cursor] = a[i]; 
        a[i] = temp;
    }
   
   
   	//判断是否需要进行交换
   	//看cursor到i之间是否有两个数组值相同,如果有,返回 false,不交换,直接continue
     private static boolean judgeSwap(int[] a, int cursor, int i) {
        for (int j = cursor; j < i; j++) {
        	
        	//若同一个数组出现两个相同的值,如果还交换的话,那就相当于会输出两次,也就是说相同的值会输出两次,重复了
                        if (a[j] == a[i]) { //有相等,就不交换
                return false;
            }
        }
       return true;
    }
    
 }

输出
[1, 2, 3, 2]
[1, 2, 2, 3]
[1, 3, 2, 2]
[2, 1, 3, 2]
[2, 1, 2, 3]
[2, 3, 1, 2]
[2, 3, 2, 1]
[2, 2, 3, 1]
[2, 2, 1, 3]
[3, 2, 1, 2]
[3, 2, 2, 1]
[3, 1, 2, 2]

上面已经将两种情况都用算法描述好了,看到这里,相信应该对全排列的方法基本掌握了,下面通过一道有关全排列的算法题对这一知识点进行巩固吧。

四、实例分析

问题描述
100 可以表示为带分数的形式:100 = 3 + 69258 / 714。
还可以表示为:100 = 82 + 3546 / 197。
注意特征:带分数中,数字 1~9 分别出现且只出现一次(不包含 0)。
类似这样的带分数,100 有 11 种表示法。
输入格式
从标准输入读入一个正整数 N (N<1000*1000)
输出格式
程序输出该数字用数码 1~9 不重复不遗漏地组成带分数表示的全部种数。
注意:不要求输出每个表示,只统计有多少表示法!
样例输入 1
100
样例输出 1
11
样例输入 2
105
样例输出 2
6

分析:

1)先写出全排列模板;(dfs)

2)再划分 " + 前,/前,/后 ",三个区域,然后对这三个区域进行计算。

  • 划分区域需要把数组中的值取出来toInt()
  • 计算(从后往前)(注意:res+=arr[i]*t; t*=10;,arr[i] 乘 10的n次方(arr[i]*10, arr[i]*100…) )

具体的解析过程在代码中都有体现,上代码:

public class 带分数 {
	static int input;
	static int count;
	//static int res;错误(教训:少定义全局变量)
	public static void main(String[] args) {
		int []arr={1,2,3,4,5,6,7,8,9};
		Scanner sc = new Scanner(System.in);
		input=sc.nextInt();
		dfs(arr,0);//因为已知是9位数,所以不用再定义end参数传递进去了
		System.out.println(count);
	}
	
	static void dfs(int []arr,int k){	//因为已知是9位数,所以不用再定义end参数传递进去了
		//因为这里不可重复,所以不用考虑判断是否交换(相当于刚刚的第一种情况:数组元素不重复)
		
		if(k==9) {
			check(arr);
			//return;
		}
		for (int i = k; i <9; i++) {//i=k
			swap(arr,k,i);//这里必须传递arr进去,否则是值参数,返回过来不改变实际值
			dfs(arr,k+1);
			swap(arr,k,i);	//swap(arr[k],arr[i]);错误写法(必须传入 arr)
		}
		
		
		
	
	}
	
	
	private static void check(int[] arr) {
		//+前面的数最多有7个(最少留2位)		//	100 = 3 + 69258 / 714
		for(int i=0;i<=6;i++){//统一从0开始(避免后面计算数组值时混淆)
			int beforeAdd=toInt(arr,0,i+1);//i+1 保证至少 +前 至少有1位
			if(beforeAdd>=input) continue;//优化(效率)
			
			//   /前面的数 (包括 + 前面的数)( 7-i = 8-i-1 )共8位(0开始算)(最后至少留一位,-1)
			for (int j = 0; j <=7-i; j++) {
				int beforeMul=toInt(arr,i+1,j);//第三个参数是长度(i+1 ~ i+1 + j)
				int afterMul=toInt(arr,i+j+1,8-i-j);
				if(beforeAdd+beforeMul/afterMul==input && beforeMul%afterMul==0){	//计算
					count++;
				}
			}
		}
		
		
	}
	private static int toInt(int[] arr, int start, int len) {
		int t=1;
		//教训:少定义全局变量
		int res=0;//res必须在这里定义,不能定义成全局static,因为每个数的res都不同,所以每次都要重新初始化
		for (int i = start+len-1; i >= start; i--) {	//下标,所以-1
			res+=arr[i]*t;//arr[i] 乘 10的n次方(arr[i]*10, arr[i]*100, arr[i]*1000...)
			t*=10;
		}
		return res;
	}
	
	static void swap(int []arr,int k, int i) {
		int t=arr[k];
		arr[k]=arr[i];
		arr[i]=t;
		
	}
	
	
	
}


到这里,相信对于全排列算法的理解一定有更进一步的提升,全排列算法在算法题中,包括各种竞赛题和面试题,也频繁出现,但全排列有一个相对固定的模板,而具体的案例都是基于模板进一步展开的,所以只要将全排列的模板牢记心中,大部分题目拿起来都会有思路进行求解的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值