全排列算法分析(原创方法/一般方法/字典序法)

全排列算法即对给定的一个序列,输出其所有不同的(n!种)排列,例如:

给定序列{1, 2, 3}有{1, 2, 3}、{1, 3, 2}、{2, 1, 3}、{2, 3, 1}、{3, 1, 2}、{3, 2, 1}这6种排列

好像很容易就能写出来,对于更长的序列也只是时间问题,最终肯定能够用笔一一列出来

但是要用程序实现的话,可能让人有点无从下手(乍看好像很简单),下面给出三种不同的解全排列的方法:

-------

一.原创方法

所谓的原创方法就是不考虑算法的效率及其他因素,完全为了解决问题而自己去构造一种可行的方法(不一定好,但能解决问题)

首先,让我们想想自己是如何写出上面的6个序列的

1.确定第一位上的元素,有三种可能:1, 2, 3

2.确定第二位上的元素(以1为例),有两种可能:2, 3

3.确定第三位上的元素(以2为例),只有一种可能:3。这时我们就得到了一个序列:1, 2, 3

接下来,让我们理一理思路,想想用程序如何实现

对于一个长度为n的序列来说,计算机要做的就是

1.确定第(i)位上的元素,有(n + 1 - i)种可能,依次选择一种可能作为第i位的元素,保存当前已经确定的序列(长度为i)

2.确定第(i+1)位上的元素,有(n + 1 - i - i)种可能,依次选择一种可能作为第i + 1位的元素,保存当前已经确定的序列(长度为i + 1)

……

n.确定第(n)位上的元素,只有一种可能,将唯一的可能元素作为第n位上的元素,输出已经确定的序列

显然,这是一种递归的方法

Java代码如下:

public class FullPermutation {

	static int count = 0;

	/**
	 * 递归实现全排列
	 * <br />程序思路是:依次确定第一位到最后一位,与人的一般思维方式一致
	 */
	public static void main(String[] args) {
		String str = "13234";
		str = check(str);//去除重复元素
		fullPermutate(0, "", str);
		System.out.print(count);
	}

	/**
	 * @param index 本次调用确定第index位
	 * @param path 已经确定顺序的串
	 * @param string 待全排列的串
	 */
	static void fullPermutate(int index, String path, String string)
	{
		String restStr = strSub(string, path);
		if(index == string.length())
		{
			System.out.println(path + restStr);
			count++;//
			return;
		}
		else
		{
			for(int i = 0;i < string.length() - index;i++)
				fullPermutate(index + 1, path + restStr.charAt(i), string);
		}
	}

	/**
	 * @param full 完整的串
	 * @param part 部分子串
	 * @return rest 返回full与part的差集
	 */
	static String strSub(String full, String part)
	{
		String rest = "";

		for(int i = 0;i < full.length();i++)
		{
			String c = full.charAt(i) + "";
			if(!part.contains(c))
				rest += c;
		}

		return rest;
	}

	/**
	 * @param str 待检查的串
	 * @return 返回不含重复元素的串
	 */
	static String check(String str)
	{
		for(int i = 0;i < str.length() - 1;i++)
		{
			String firstPart = str.substring(0, i + 1);
			String restPart = str.substring(i + 1);
			str = firstPart + restPart.replace(str.charAt(i) + "", "");
		}

		return str;
	}
}

P.S.至于上面的代码中为什么要去除参数中的重复元素,是为了增强程序的鲁棒性,经典的排列问题是针对不含重复元素的序列而言的,含重复元素的序列我们将在后面展开讨论

二.一般算法(最常见的,也是最经典的全排列算法)

核心思想是交换,具体来说,对于一个长度为n的串,要得到其所有排列,我们可以这样做:

1.把当前位上的元素依次与其后的所有元素进行交换

2.对下一位做相同处理,直到当前位是最后一位为止,输出序列

[需要注意的一点:我们的思想是“交换”,也就是直接对原数据进行修改,那么在交换之后一定还要再换回来,否则我们的原数据就发生变化了,肯定会出错]

如果觉得上面的解释还是很难懂的话,那么记住这句话:核心思想就是让你后面的所有人都和你交换一遍(而你是一个指针,从前向后按位移动...)

C代码如下:(摘自http://www.cnblogs.com/nokiaguy/archive/2008/05/11/1191914.html

#include <stdio.h>  

int n = 0;  

void swap(int *a, int *b)
{
    int m;
    m = *a;
    *a = *b;
    *b = m;
}
void perm(int list[], int k, int m)
{
    int i;
    if(k > m)
    {
        for(i = 0; i <= m; i++)
            printf("%d ", list[i]);
        printf("\n");
        n++;
    }
    else
    {
        for(i = k; i <= m; i++)
        {
            swap(&list[k], &list[i]);
            perm(list, k + 1, m);
            swap(&list[k], &list[i]);
        }
    }
}
int main()
{
    int list[] = {1, 2, 3, 4, 5};
    perm(list, 0, 4);
    printf("total:%d\n", n);
    return 0;
}

原文也给出了一点解释,但如果还是不能理解的话,不妨输出一下运行轨迹,有助于理解,或者用笔画一画,多看几遍就明白了,直接看代码的话确实不好理解

三.字典序法

其实在本文开头给出的例子中就用了字典序,人写全排列或者其它类似的东西的时候会不自觉的用到字典序,这样做是为了防止漏掉序列

既然如此,用字典序当然也能实现全排列,对于给定序列{3, 1, 2},我们理一理思路,想想具体步骤:

1.对给定序列做升序排序,得到最小字典序{1, 2, 3}

2.对有序序列求下一个字典序,得到{1, 3, 2}

3.如果当前序列没有下一个字典序(或者说当前序列是最大字典序,如{3, 2, 1}),则结束

显然字典序法的核心是:求下一个字典序,要充分理解这里的“下一个”,有两层意思:

1.该序列在字典中是排在当前序列后面的

2.该序列是字典中最靠近当前序列的

字典序有严格的数学定义,按照定义就能求出一个序列的下一个字典序,具体做法不在此展开叙述(下面的代码中有细致的解释)

Java代码如下:

public class DictionaryOrder {

	/**
	 * 按字典序输出全排列
	 * <br />按照字典序可以得到已知序列的下一个序列,可以用于不需要得到所有全排列的场合(例如数据加密)
	 */
	public static void main(String[] args) {
		int arr[] = new int[]{4, 3, 1, 2};
		/*
		boolean exist = nextPermutation(arr);
		if(exist)
		{
			for(int value : arr)
				System.out.print(value);
			System.out.println();
		}
		else
			System.out.println("当前序列已经是最大字典序列");
		*/
		///*
		//对给定序列排序(升序)
		sort(arr);
		for(int value : arr)
			System.out.print(value);
		System.out.println();
		//求全排列并输出
		int count = 1;//第一个已经在上面输出了
		while(nextPermutation(arr))
		{
			for(int value : arr)
				System.out.print(value);
			System.out.println();
			count++;
		}
		System.out.println("共 " + count + " 个");
		//*/
	}

	/**
	 * @param arr 当前序列
	 * @return 字典序中的下一个序列,没找到则返回false
	 */
	public static boolean nextPermutation(int[] arr)
	{
		int pos1 = 0, pos2 = 0;
		//1.从右向左找出满足arr[i] < arr[i + 1]的i
		//(就是找出相邻位中满足前者小于后者关系的前者的位置)
		boolean find = false;
		for(int i = arr.length - 2;i >= 0;i--)
			if(arr[i] < arr[i + 1])
			{
				pos1 = i;
				find = true;
				break;
			}
		if(!find)//若没找到,说明当前序列已经是最大字典序了
			return false;
		//2.从pos1向后找出最小的满足arr[i] >= arr[pos1]的i
		//(就是找出pos1后面不小于arr[pos1]的最小值的位置)
		int min = arr[pos1];
		for(int i = pos1 + 1;i < arr.length;i++)
		{
			if(arr[i] >= arr[pos1])
			{
				min = arr[i];
				pos2 = i;
			}
		}
		//3.交换pos1与pos2位置上的值
		int temp = arr[pos1];
		arr[pos1] = arr[pos2];
		arr[pos2] = temp;
		//4.对pos1后面的所有值做逆序处理(转置)
		int i = pos1 + 1;
		int j = arr.length - 1;
		for(;i < j;i++, j--)
		{
			temp = arr[i];
			arr[i] = arr[j];
			arr[j] = temp;
		}
		return true;
	}

	/**
	 * 对给定数组做升序排序(冒泡法)
	 * @param arr 待排序数组
	 */
	public static void sort(int[] arr)
	{
		for(int i = 0;i < arr.length - 2;i++)
			for(int j = 0;j < arr.length - i - 1;j++)
				if(arr[j] > arr[j + 1])
				{
					int temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
	}
}

-------

算法细节就到这里,下面讨论几个无关紧要问题:

1.给定序列中存在重复元素

经典的全排列问题不讨论这个问题,但实际应用中可能会遇到,我们有两个选择:

i.对原数据(给定序列)进行处理,对于重复元素只保留一个,或者对重复元素做替换,例如对{1, 1, 2, 3, 2},我们建立一张替换表,a = 1, b = 2,原数据处理结果为{1, a, 2, 3, b},至此我们就消除了重复元素

ii.对算法做修改,检测重复元素并做相应处理,需要结合具体数据特征做处理

2.算法效率问题

如果复杂问题中需要用到全排列,那么不得不考虑算法效率问题了,上面给出的算法中,前两种时间复杂度相同,都是n层递归,每层n-i + 1个循环

第三种算法的时间复杂度主要集中在了排序上,如果给定序列已经有序,那么此时第三种算法无疑是最佳的

另外,还有一种新颖的全排列算法,有兴趣的话也可以试一试,原文链接:

http://supershll.blog.163.com/blog/static/37070436201171005758332/

全排列算法分析(原创方法/一般方法/字典序法)

时间: 2024-08-03 07:37:04

全排列算法分析(原创方法/一般方法/字典序法)的相关文章

[全排列]基于逆序列的字典序法的改进

摘要: 字典序法是生成全排列的经典算法.本文在对字典序法进行分析的基础上,提出了一种基于逆序列的改进字典序全排列生成算法.通过与传统的四种全排列生成算法进行对比,本文方法可以大大提高全排列的生成效率.关键词:全排列;字典序;逆序列 基于逆序列的字典序法的改进 code

字典序法生成全排列算法的证明

引言 对一个给定数据进行全排列,在各种场合经常会用到.组合数学中,生成全排列的方法有很多,卢开澄老师的<组合数学>中就介绍了三种:序数法,字典序法,临位互换法等.其中以字典序法由于算法简单,并且使用的时候可以依照当前状态获取下一个状态,直到所有排列全部完成,方便在程序中随要随用,应用比较广泛,STL中的Next_permutation也是使用此法. 算法定义 首先看什么叫字典序,顾名思义就是按照字典的顺序(a-z, 1-9).以字典序为基础,我们可以得出任意两个数字串的大小.比如 "

训练创新思维的方法:曼陀罗思考法

回顾10多年来走过的软件之路除了在经验上有一点积累.掌握了不少的技术之外似乎仍然一无所有,我并不是在传播负能量,这种一无所有指的并不是物质或是生活上的,而是在事业道路上.软件发展在于创新而这么多年来的工作却一直只是在跟随,自己意淫一下觉得比别人好的也只不过是跟随的速度会比较认识的人快那么一点点,时光推移到底什么东西是完全属于自己的呢 ?我也曾与不少至友讨论此问题,貌似也让大家陷入一些思考中.我们不缺技术.也不缺乏经验缺的可能是一种思想和意识那就是“持续创新的思维”.以前总认为自己在这方面是缺根筋

全排列算法(字典序法、SJT Algorithm 、Heap&#39;s Algorithm)

一.字典序法 1) 从序列P的右端开始向左扫描,直至找到第一个比其右边数字小的数字,即. 2) 从右边找出所有比大的数中最小的数字,即. 3) 交换与. 4) 将右边的序列翻转,即可得到字典序的下一个排列. 5) 重复上面的步骤,直至得到字典序最大的排列,即左边数字比右边的大的降序排列. 二.SJT Algorithm 初始状态为. 1) 找到最大的可移动数m(当一个数指向一个比它小的数是,该数就是可移动数) 2) 交换m和m所指向的数 3) 改变所有比m大的数的方向 4) 重复上面的步骤,直至

PHP_字典序法获得排列组合

前段时间一次聚会闲聊时聊到一个问题,就是给你一排数组,例如1,2,3,4,5,如何能高效的获取上述数列的所有排列组合,正巧没事,研究了一下,一开始以为是个很简单的问题,就直接开始写代码了,后来发现怎么循环也不理想,基本上都有一些不必要的消耗,百度一下看到一个不错的算法,字典序法,顺便学习一下,然后记录之. 摘一段算法思想: 设P是[1,n]的一个全排列. P=P1P2-Pn=P1P2-Pj-1PjPj+1-Pk-1PkPk+1-Pn , j=max{i|Pi<Pi+1}, k=max{i|Pi>

[LeetCode] Permutations 排列生成算法之字典序法

字典序排序生成算法 字典序法就是按照字典排序的思想逐一产生所有排列. 例如,由1,2,3,4组成的所有排列,从小到大的依次为: 1234, 1243, 1324, 1342, 1423, 1432, 2134, 2143, 2314, 2341, 2413, 2431, 3124, 3142, 3214, 3241, 3412, 3421, 4123, 4132, 4213, 4231, 4312, 4321. 分析这种过程,看后一个排列与前一个排列之间有什么关系? 再如,设有排列(p)=276

跟着百度学PHP[4]OOP面对对象编程-15-魔术方法__call方法

在程序开发中,如果在使用对象调用对象内部方法时候,调用的这个方法不存在那么程序就会出错,然后程序退出不能继续执行.那么可不可以在程序调用对象内部 不存在的方法时,提示我们调用的方法及使用的参数不存在,但程序还可以继续执行,这个时候我们就要使用在调用不存在的方法时自动调用的方 法“__call()”.  THE END 跟着百度学PHP[4]OOP面对对象编程-15-魔术方法__call方法

static方法和普通方法----调用方法

package cep10; public class Static1 { void fun1(){ System.out.println("这是一个普通方法"); } static void fun2(){ System.out.println("这是一个静态方法"); } @SuppressWarnings("static-access") /* J2SE 提供的最后一个批注是 @SuppressWarnings. * 该批注的作用是给编译器

C#Lambda表达式的理解:谓词方法 匿名方法 使用Lambda

Lambda表达式 "Lambda表达式"是一个匿名函数,是一种高效的类似于函数式编程的表达式,Lambda简化了开发中需要编写的代码量.它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型,支持带有可绑定到委托或表达式树的输入参数的内联表达式.所有Lambda表达式都使用Lambda运算符=>,该运算符读作"goes to".Lambda运算符的左边是输入参数(如果有),右边是表达式或语句块. 下面三个方法会帮你会容易理解到Lambda表达式的好处,