本文共 6469 字,大约阅读时间需要 21 分钟。
可以直觉地知道,只要把数组任意相邻的两个元素交换位置,就可以得到一个新的排列。例如把数组 [1,2,3,4,5] 的 5 和 4 交换位置就得到 [1,2,3,5,4],再把 5 和 3 交换位置就得到[1,2,5,3,4]……这样不停地交换就能得到所有的(不重复的)排列吗?这里有两个问题: 1)怎么知道交换相邻的两个元素就能得到所有的排列(还是说有时候也需要交换不相邻的元素)? 2)要以何种顺序交换元素才能保证每次都得到新的(不重复)的排列呢?把5依次与前面的4、3、2、1交换位置其实等于把 5 插入到子数组 [1,2,3,4] 的所有可能的位置上得到新的排列。如果我们事先已经知道子数组 [1,2,3,4] 的所有排列,就可以把 5 插入到这些排列的所有可能的位置上得到数组 [1,2,3,4,5] 的所有排列。那么如何知道子数组 [1,2,3,4] 的所有排列呢?我们同样可以把 [1,2,3,4] 分解为 4 和子数组 [1,2,3]……这样一直分解到子数组只剩一个元素时为止。按照这个思路,我们将得到一个普通的递归生成全排列的算法。不过邻位对换法使用的是另一种思路:为每个元素附加一个移动方向。
源码如下。
class Program
{ static void Main(string[] args) { string[] source = new string[] { "1", "2", "3", "4", "5" }; foreach (IList<string> p in SwapPermutation(source)) { Console.WriteLine(p.Montage(t => t, " ")); } } // 使用邻位对换法生成全排列 static IEnumerable<IList<string>> SwapPermutation(string[] source) { yield return source.ToList(); // 第一个排列就是数组的初始顺序 LinkedList<Item> s = Item.Create(source); // 初始化 Item max = null; while ((max = FindMaxMovableItem(s)) != null) // 寻找最大的可移元素 max { max.Move(); // 把 max 与移动方向所指向的那个邻位交换 yield return s.ToList(t => t.Value); // 交换后产生了一个新的排列 // 把所有比 max 大的元素的移动方向反转 foreach (Item item in s) { if (item > max) item.ReverseDirection(); } } } // 寻找最大的可移元素,找不到时返回null static Item FindMaxMovableItem(LinkedList<Item> s) { Item max = null; foreach (Item item in s) { if (item.IsMovable() && (max == null || item > max)) max = item; } return max; } } Item的完整代码如下。 // 带有方向的元素 [DebuggerDisplay("Value = {Value} Direction={Direction} Index = {Index}")] class Item { public Item(string value) { Value = value; Direction = ItemDirection.Left; // 初始时方向默认指向左边}
// 元素的值 public string Value { get; set; }// 元素的移动方向
public ItemDirection Direction { get; set; } // 在链表中的节点 public LinkedListNode<Item> Node { get; set; } // 初始创建 public static LinkedList<Item> Create(string[] source) { LinkedList<Item> result = new LinkedList<Item>(); for (int i = 0; i < source.Length; i++) { Item item = new Item(source[i]); // 增加对链表中节点的反向引用,以便能够知道前一个和后一个节点是什么 item.Node = result.AddLast(item); } return result; }// 反转元素的移动方向
public void ReverseDirection() { if (Direction == ItemDirection.Left) Direction = ItemDirection.Right; else Direction = ItemDirection.Left; } // 返回元素是否可移 public bool IsMovable() { // 如果一个元素的移动方向所指向的那个邻位比它小,此元素就是可移的; // 相反,如果一个元素的移动方向所指向的那个邻位比它大,此元素就是不可移的。 // 如果一个元素的移动方向上没有邻位,此元素也是不可移的。 if (Direction == ItemDirection.Left) // 移动方向向左时 { if (Node.Previous == null) // 已经是最左侧元素时 return false; else return Node.Previous.Value < this; } else // 移动方向向右时 { if (Node.Next == null) // 已经是最右侧元素时 return false; else return Node.Next.Value < this; } } // 与移动方向所指向的那个邻位交换 public void Move() { if (Direction == ItemDirection.Left) // 移动方向向左时 { // 与左侧元素交换位置 Item temp = Node.Previous.Value; Node.Previous.Value = this; Node.Value = temp; temp.Node = Node; Node = Node.Previous; } else // 移动方向向右时 { // 与右侧元素交换位置 Item temp = Node.Next.Value; Node.Next.Value = this; Node.Value = temp; temp.Node = Node; Node = Node.Next; } } public static bool operator <(Item lhs, Item rhs) { if (lhs == null || rhs == null) return false; else return lhs.Value.CompareTo(rhs.Value) < 0; } public static bool operator >(Item lhs, Item rhs) { if (lhs == null || rhs == null) return false; else return lhs.Value.CompareTo(rhs.Value) > 0; } } enum ItemDirection { Left = 0, Right = 1 }注意:当最大可移元素是整个数组里最大的那个元素时(本例中是5),可以省略“把所有比 max 大的元素的移动方向反转”那一步,而且只要5是可移的它就一定是最大可移元素,所以可以一直移动5直到不能移动再调用FindMaxMovableItem() 。上面的源码为了简单起见没有做这些工作。
邻位对换法的原理 邻位对换法的规则乍一看既简单又奇妙,其实它与本文开篇所提到的递归生成全排列的算法本质上是相同的。在5从最右侧移动到最左侧之后,一定要先使得子数组[1,2,3,4]变成下一个排列[1,2,4,3],再让5从最左侧移动到最右侧。对于子数组[1,2,3,4]来说,同样是先把4从子数组[1,2,3]的最右侧移动的最左侧,然后让子数组[1,2,3]变成下一个排列[1,3,2],再让4从它的最左侧移动到最右侧……只不过如果没有递归时的堆栈来保存每一层进行到哪里了,怎么知道每一步应该移动5还是4抑或是3呢?这里必须要先观察得到2个重要的规律, 1)只有元素从子数组的一侧移动到另一侧之后才需要把子数组变换成下一个排列。例如只有当4从子数组[1,2,3]的最右侧移动的最左侧之后,才需要让子数组[1,2,3]变成下一个排列[1,3,2]。 2)元素一定比子数组中的任何一个元素都大。例如5比子数组[1,2,3,4]的每一个元素都大;4比子数组[1,2,3]的每一个元素都大。 接着我们列出需要知道的3个信息, 1)每个元素下一步的移动方向。 2)每个元素是否已经移动到了子数组的最左侧或最右侧。 3)每一步应该移动哪个元素。 根据上面的2个规律,我们只要为每一个元素增加一个移动方向属性就可以知道这3个信息。首先,当一个元素的移动方向上的邻位比它大或者没有元素时,说明此元素已经到了边界,它暂时不可移,“递归”要下降一层。如果下一层的最大元素也到了边界,就再下降一层。当把子数组变换成新的排列时,再从最上层开始移动元素。“移动方向”属性既保存了移动方向信息,同时也可用于判断元素是否已经移动到了边界。 思考题 请比较邻位对换法里的“递归”与真正的递归方法的区别,然后看看能否写出一个不一样的《盗梦空间》出来。/*
函数名称:Permutation 函数功能:排列邻位对换法:输出n个数的所有全排列 输入变量:int n:1,2,3,...,n共n个自然数 输出变量:无 */ void Permutation(int n) { int *a = new int[n]; //用来存储n个自然数 bool *p = new bool[n]; //用来存储n个元素的指向:向左为false,向右为true for (int i=0; i<n; i++) //存储全排列的元素值,并计算全排列的数量 { a[i] = i + 1; p[i] = false; //开始均指向左侧 } do { Print(a, n); //输出第一个全排列 if (n == a[n-1])//若n在最右侧,将其逐次与左侧的元素交换,得到n - 1个新的排列 { for (int i=n-1; i>0; i--) { int temp = a[i]; a[i] = a[i-1]; a[i-1] = temp; bool flag = p[i]; p[i] = p[i-1]; p[i-1] = flag; Print(a, n); } } else //若n在最左侧,将其逐次与右侧的元素交换,得到n - 1个新的排列 { for (int i=1; i<n; i++) { int temp = a[i]; a[i] = a[i-1]; a[i-1] = temp; bool flag = p[i]; p[i] = p[i-1]; p[i-1] = flag; Print(a, n); } } } while (Move(a, p, n)); delete []a; delete []p; } /* 函数名称:Move 函数功能:寻找最大可移数,可移数m,将m与其箭头所指的邻数互换位置, 并将所得新排列中所有比m大的数p的方向调整 输入变量:int a[]:存储了1,2,3,...,n共n个自然数的数组 bool p[]:存储了n个元素的指向的数组:向左为false,向右为true int n:数组a[]的长度 输出变量:排列中存在最大可移数,则做了相关操作后返回真,否则直接返回假 */ bool Move(int a[], bool p[], int n) { int max = 1; int pos = -1; for (int i=0; i<n; i++) { if (a[i] < max) continue; if ((p[i] && i < n-1 && a[i] > a[i+1]) || //指向右侧 (!p[i] && i > 0 && a[i] > a[i-1])) //指向左侧 { max = a[i]; pos = i; } } if (pos == -1) //都不能移动 return false; //与其箭头所指的邻数互换位置 if (p[pos]) //指向右侧 { int temp = a[pos]; a[pos] = a[pos+1]; a[pos+1] = temp; bool flag = p[pos]; p[pos] = p[pos+1]; p[pos+1] = flag; } else //指向左侧 { int temp = a[pos]; a[pos] = a[pos-1]; a[pos-1] = temp; bool flag = p[pos]; p[pos] = p[pos-1]; p[pos-1] = flag; } //将所得排列中所有比max大的数p的方向调整 for (int i=0; i<n; i++) { if (a[i] > max) p[i] = !p[i]; } return true; }http://www.cnblogs.com/1-2-3/archive/2011/05/23/generate-permutation-part3.html
http://www.360doc.com/content/13/0426/16/12132058_281101494.shtml