可遍历的全排列

本文介绍了一种不使用递归实现全排列遍历的方法,通过构建全排列树并利用迭代器接口提供下一排列结果,确保遍历过程不重复且有序。
全排列是一种比较烦人的东西。本文讨论如何在不使用递归的情况下,让全排列结果能够被遍历(也就是实现 Iterator 接口),使得全排列的提供者能随时提供“下一个”全排列结果,直到给出所有的排列。

本文发表于:http://yiding-he.iteye.com

通常全排列是不可排序的,因为不同的遍历方式会按不同的顺序得到结果(如交换法、滚动法等)。我们需要选择一种方式,每次枚举都能得到唯一的结果,绝不重复。因为只有这样的方式才能用来遍历。那就是:

全排列树。想象一下,树中的每个节点就是一个全排列结果,其子节点是将其交换变化而来的。举个例子,
根节点为 (1, 2, 3) 的树,其一级子节点就是 (1, 2, 3) (2, 1, 3) (3, 1, 2) 三个,分别是从排列中的 [0]-[0] 交换、[0]-[1] 交换和 [0]-[2]交换得来的。而二级子节点,排列中的位置 [0] 不变,从 [1] 开始和其他位置交换。(2, 1, 3) 的子节点为 (2, 1, 3) 和 (2, 3, 1) 两个。以此类推到下一级,直到无法交换位置为止。实际上,(1, 2, 3) 的全排列树一共就是三层。

你可能看到了:这里不是有重复的节点了吗?没关系。虽然枝节点有重复,但叶子节点是绝无重复的。我们只需要遍历这棵树的最底层,就能得到所有全排列。

遍历的时候,如何确定“上一个”和“下一个”叶子节点的位置呢?用一个很直观的表示法:每个节点的子节点都从 0 开始标上号,根节点就不用标号了,然后根据叶子节点的路径将标号组合成一个数组,就是每个节点的位置。例如上面那棵树中的叶子节点(2, 1, 3),位置就是 {1, 0}。它的下一个位置就是 {1, 1},再下一个位置是 {2, 0},不信把整个树画出来自己对对看。

实际上,因为叶子节点的位置都是绝对的,我就不一定非要从第一个结果来排起。给出任意一个排列结果,我都可以找到它在这棵树中的位置,然后直接求得下一个位置和下一个位置的排列结果。

基本的思路就是这些,然后就是用代码来实现。至于怎么思考实现的就略去了,下面是代码:
java 代码
 
  1. import sun.reflect.generics.reflectiveObjects.NotImplementedException;  
  2. import java.util.Iterator;  
  3.   
  4. /** 
  5.  * 全排列树 -- 可遍历的全排列 
  6.  */  
  7. class SortTree implements Iterator {  
  8.   
  9.     private int level;  
  10.   
  11.     private int[] defaultArr; // {1, 2, 3, 4, 5, ...} 这样一个数组  
  12.   
  13.     private int[] currentPosition;  
  14.   
  15.     public SortTree(int level) {  
  16.         this.level = level;  
  17.         init();  
  18.     }  
  19.   
  20.     private void init() {  
  21.         defaultArr = new int[level];  
  22.         for (int i = 0; i < level; i++) {  
  23.             defaultArr[i] = i;  
  24.         }  
  25.         currentPosition = getBeforeStartPosition();  
  26.     }  
  27.   
  28.     /** 
  29.      * 获得指定位置的全排列 
  30.      * 
  31.      * @param position 全排列在树中的位置 
  32.      * 
  33.      * @return 处于指定位置的全排列 
  34.      */  
  35.     public int[] getValue(int[] position) {  
  36.         int[] cloned = defaultArr.clone();  
  37.   
  38.         if (position.length != level - 1) {  
  39.             System.out.println("invalid position level");  
  40.             return new int[0];  
  41.         }  
  42.   
  43.         for (int i = 0; i < position.length; i++) {  
  44.             swap(cloned, i, i + position[i]);  
  45.         }  
  46.   
  47.         return cloned;  
  48.     }  
  49.   
  50.     /** 
  51.      * 获得指定的全排列的位置 
  52.      * 
  53.      * @param value 全排列中的一项 
  54.      * 
  55.      * @return 指定的项在全排列树中的位置 
  56.      */  
  57.     public int[] getPosition(int[] value) {  
  58.         int[] cloned = defaultArr.clone();  
  59.   
  60.         int[] position = new int[value.length - 1];  
  61.         for (int i = 0; i < value.length - 1; i++) {  
  62.             int pointer = 0;  
  63.             if (value[i] == cloned[i]) {  
  64.                 position[i] = 0;  
  65.             } else {  
  66.                 pointer++;  
  67.                 while (pointer < value.length) {  
  68.                     if (value[i] == cloned[pointer]) {  
  69.                         swap(cloned, i, pointer);  
  70.                         position[i] = pointer - i;  
  71.                     }  
  72.                     pointer++;  
  73.                 }  
  74.             }  
  75.         }  
  76.   
  77.         return position;  
  78.     }  
  79.   
  80.     /** 
  81.      * 获得下一个位置 
  82.      * 
  83.      * @param position 当前位置 
  84.      * 
  85.      * @return 下一个位置 
  86.      */  
  87.     public int[] getNextPosition(int[] position) {  
  88.         int[] result = position.clone();  
  89.         for (int i = result.length - 1; i >= 0; i--) {  
  90.             int upper = result.length - i;  
  91.             if (result[i] + 1 <= upper) {  
  92.                 result[i] += 1;  
  93.                 break;  
  94.             } else {  
  95.                 result[i] = 0;  
  96.             }  
  97.         }  
  98.         return result;  
  99.     }  
  100.   
  101.     /** 
  102.      * 是否还有下一个位置 
  103.      * 
  104.      * @param position 当前位置 
  105.      * 
  106.      * @return 如果还有则返回 true。 
  107.      */  
  108.     public boolean hasNextPosition(int[] position) {  
  109.         for (int i = 0; i < position.length; i++) {  
  110.             if (position[i] != defaultArr.length - i - 1) {  
  111.                 return true;  
  112.             }  
  113.         }  
  114.         return false;  
  115.     }  
  116.   
  117.     /** 
  118.      * 获得第一个位置之前的位置 
  119.      * 
  120.      * @return 第一个位置之前的位置 
  121.      */  
  122.     public int[] getBeforeStartPosition() {  
  123.         int[] position = new int[defaultArr.length - 1];  
  124.         position[position.length - 1] = -1;  
  125.         return position;  
  126.     }  
  127.   
  128.     private void swap(int[] ints, int a, int b) {  
  129.         int t = ints[a];  
  130.         ints[a] = ints[b];  
  131.         ints[b] = t;  
  132.     }  
  133.   
  134.     public boolean hasNext() {  
  135.         return hasNextPosition(currentPosition);  
  136.     }  
  137.   
  138.     public Object next() {  
  139.         currentPosition = getNextPosition(currentPosition);  
  140.         return getValue(currentPosition);  
  141.     }  
  142.   
  143.     public void remove() {  
  144.         throw new NotImplementedException();  
  145.     }  
  146. }  

使用方法:
java 代码
 
  1. SortTree tree = new SortTree(6);  
  2. int counter = 0;  
  3. while (tree.hasNext()) {  
  4.     int[] sort = (int[]) tree.next();  
  5.     printArray(sort);  
  6.     counter++;  
  7. }  
  8.   
  9. System.out.println("一共 " + counter + " 个结果");

代码仅用于检验思路,写法不甚严格。若要实用,最好能对参数加一些判断。
<think>我们参考引用的资料,可以看到全排列的递归实现方法。引用[1][3]都提到了递归方法,引用[5]则使用了标记数组的方法。 用户要求遍历每次全排列的值,也就是在生成每一个排列时,我们都需要输出或者保存这个排列。 常见的递归实现全排列有两种方式: 1. 交换法:通过交换数组中的元素来固定当前位置,然后递归处理后续位置,最后再交换回来(回溯)。 2. 标记法:使用一个标记数组记录哪些元素已经使用过,每次选择一个未使用的元素放入当前位置,然后递归处理后续位置,最后回溯标记。 根据引用[1]的代码,使用的是交换法。引用[5]使用的是标记法。 下面我们将分别用这两种方法来实现,并输出每次的全排列值。 方法一:交换法(基于引用[1]的代码,但注意原代码有误,我们修正并完善) 原引用[1]的代码中,permutation函数的循环条件有误(i < arr.size()-1),这样会漏掉最后一个元素。应该改为i < arr.size()。 方法二:标记法(基于引用[5]的代码) 由于用户要求遍历每次全排列的值,我们在递归的终止条件处输出当前排列即可。 下面分别给出两种方法的代码示例: 方法一:交换法(递归回溯) 步骤: 1. 从第一个位置开始(索引k),依次将每个位置的元素与后面的元素交换。 2. 固定第一个位置后,递归处理下一个位置(k+1)。 3. 当k到达最后一个位置时,表示一个排列完成,输出。 4. 递归返回后,需要交换回来(回溯)以保证下一次交换的正确性。 注意:这里我们传递的是数组的引用,所以需要回溯。 方法二:标记法(使用used数组) 步骤: 1. 使用一个辅助数组used来标记哪些元素已经被使用。 2. 使用一个结果数组arr(或者path)来保存当前已经选择的元素。 3. 递归函数中,从0到n-1遍历每个元素,如果该元素未被使用,则将其加入当前路径,标记为已使用,然后递归下一层。 4. 递归返回后,需要回溯:从当前路径移除该元素,并标记为未使用。 5. 当当前路径长度等于原数组长度时,输出排列。 我们以数组[1,2,3]为例,两种方法都应该输出6种排列。 由于用户要求遍历每次全排列的值,我们在递归的底部(即排列完成时)输出当前排列。 下面分别写出两种方法的代码实现: 注意:用户要求的是C++实现。 方法一:交换法(修正引用[1]的代码) 原引用[1]的代码中,主函数调用permutation(arr,1)是从索引1开始,这样会漏掉第一个元素。我们应该从0开始。 修改后的交换法代码: */ #include <iostream> #include <vector> using namespace std; // 打印数组 void print(vector<int> arr) { for (int i = 0; i < arr.size(); i++) { cout << arr[i] << ' '; } cout << endl; } void permutation(vector<int>& arr, int k) { // 当k到达最后一个元素的位置时,表示已经固定了所有位置,输出 if (k == arr.size()) { print(arr); return; } // 从当前位置k开始,将每个元素交换到位置k for (int i = k; i < arr.size(); i++) { swap(arr[k], arr[i]); // 固定第k位为arr[i] permutation(arr, k + 1); // 递归固定下一个位置 swap(arr[k], arr[i]); // 回溯,交换回来 } } int main() { vector<int> arr = {1,2,3}; // 测试数组 permutation(arr, 0); // 从索引0开始 return 0; } /* 方法二:标记法(参考引用[5]) 注意:引用[5]的代码是固定排列长度,我们这里用vector来动态保存排列。 但是,我们也可以用一个数组来保存当前排列,同时用一个标记数组记录哪些数字被使用过。 由于我们使用vector,可以动态添加删除。 标记法代码: */ #include <iostream> #include <vector> using namespace std; void permute(vector<int>& nums, vector<int>& current, vector<bool>& used) { // 如果当前排列长度等于原数组长度,则输出 if (current.size() == nums.size()) { for (int num : current) { cout << num << ' '; } cout << endl; return; } // 遍历每个元素 for (int i = 0; i < nums.size(); i++) { if (!used[i]) { // 如果该元素未被使用 used[i] = true; // 标记为已使用 current.push_back(nums[i]); // 加入当前排列 permute(nums, current, used); // 递归 current.pop_back(); // 回溯,移除最后一个元素 used[i] = false; // 回溯,标记为未使用 } } } int main() { vector<int> nums = {1,2,3}; vector<int> current; // 保存当前排列 vector<bool> used(nums.size(), false); // 初始化标记数组,全部未使用 permute(nums, current, used); return 0; } /* 两种方法都可以实现全排列,并遍历输出每个排列。 此外,C++标准库中也有全排列函数next_permutation,可以按字典序生成下一个排列。我们可以先对数组排序,然后循环调用next_permutation直到生成所有排列。 方法三:使用标准库函数next_permutation 示例代码: */ #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> arr = {1,2,3}; // 先排序,确保初始状态是最小字典序 sort(arr.begin(), arr.end()); do { for (int num : arr) { cout << num << ' '; } cout << endl; } while (next_permutation(arr.begin(), arr.end())); return 0; } /* 这种方法更简洁,但需要知道标准库函数的使用。 总结: 用户要求遍历每次全排列的值,我们可以用递归(交换法或标记法)或标准库函数实现。以上三种方法都可以实现。 根据用户引用的资料,主要关注递归方法,因此我们重点展示了递归的两种实现。 注意:递归方法在数组较大时可能会有栈溢出的风险,但对于较小的数组(如n<10)是可行的。 用户可以根据需要选择其中一种方法。 </think> 在C++中实现全排列遍历的常用方法有三种:递归交换法、回溯标记法标准库函数法。下面分别进行说明代码演示: ### 1. 递归交换法(基于元素交换) 通过不断交换数组元素位置生成排列,递归到边界时输出结果: ```cpp #include <iostream> #include <vector> using namespace std; void print(const vector<int>& arr) { for (int num : arr) cout << num << ' '; cout << endl; } void permutation(vector<int>& arr, int start) { if (start == arr.size()) { print(arr); // 排列完成,输出结果 return; } for (int i = start; i < arr.size(); i++) { swap(arr[start], arr[i]); // 固定当前位置 permutation(arr, start + 1); // 递归处理后续位置 swap(arr[start], arr[i]); // 回溯恢复 } } int main() { vector<int> arr = {1, 2, 3}; permutation(arr, 0); return 0; } ``` **输出示例**: ``` 1 2 3 1 3 2 2 1 3 2 3 1 3 2 1 3 1 2 ``` ### 2. 回溯标记法(使用访问标记) 借助标记数组记录元素使用状态,通过回溯构建排列: ```cpp #include <iostream> #include <vector> using namespace std; void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& path) { if (path.size() == nums.size()) { for (int num : path) cout << num << ' '; cout << endl; return; } for (int i = 0; i < nums.size(); i++) { if (!used[i]) { used[i] = true; // 标记已使用 path.push_back(nums[i]); // 选择当前元素 backtrack(nums, used, path); // 递归 path.pop_back(); // 回溯 used[i] = false; // 撤销标记 } } } int main() { vector<int> nums = {1, 2, 3}; vector<bool> used(nums.size(), false); vector<int> path; backtrack(nums, used, path); return 0; } ``` ### 3. 标准库函数法(推荐) 使用`<algorithm>`中的`next_permutation`函数: ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> arr = {1, 2, 3}; sort(arr.begin(), arr.end()); // 必须排序 do { for (int num : arr) cout << num << ' '; cout << endl; } while (next_permutation(arr.begin(), arr.end())); return 0; } ``` ### 关键特性对比 | 方法 | 时间复杂度 | 空间复杂度 | 是否保持原序 | 实现复杂度 | |--------------|------------|------------|--------------|------------| | 递归交换法 | $O(n!)$ | $O(n)$ | ❌ | 中等 | | 回溯标记法 | $O(n!)$ | $O(n)$ | ✔️ | 较高 | | 标准库函数 | $O(n!)$ | $O(1)$ | ❌ | 简单 | > **建议**:实际开发中优先使用`next_permutation`,面试或学习时建议掌握递归交换法[^1][^3][^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值