非递归的方式实现排列和组合类DFS
欢迎来到『九章算法班 2021 版』的第36节课,今天我们一起来学习『非递归的方式实现排列和组合类DFS』
在前面的学习中我们学习了递归实现排列组合问题,在本章中我们将学习非递归的实现。
本章关键字:Combination(组合),Permutations(排列),Binary(二进制)。
用非递归(Non-recursion / Iteration)的方式实现全子集问题,有两种方式:
进制转换(binary)
宽度优先搜索(Breadth-first Search)
基于进制转换的方法
思路就是使用一个 正整数的二进制表示 的第 i 位是 1 还是 0 来代表集合的第 i 个数取或者不取。因为从 0 到 2^n - 1 总共 2^n 个整数,正好对应集合的 2^n 个子集。
比如 {1,2,3} 的子集可以用 0 到 7 来表示。
0 -> 000 -> {}
1 -> 001 -> {3}
2 -> 010 -> {2}
3 -> 011 -> {2,3}
4 -> 100 -> {1}
5 -> 101 -> {1,3}
6 -> 110 -> {1,2}
7 -> 111 -> {1,2,3}
参考代码:
java代码:
class Solution {
/**
* @param S: A set of numbers.
* @return: A list of lists. All valid subsets.
*/
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<List<Integer>>();
int n = nums.length;
Arrays.sort(nums);
for (int i = 0; i < (1 << n); i++) {
List<Integer> subset = new ArrayList<Integer>();
for (int j = 0; j < n; j++) {
if ((i & (1 << j)) != 0) {
subset.add(nums[j]);
}
}
result.add(subset);
}
return result;
}
}
C++代码:
class Solution {
public:
/**
* @param nums: A set of numbers
* @return: A list of lists
*/
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> result;
const int n = nums.size();
sort(nums.begin(), nums.end());
for (int i = 0; i < (1 << n); i++) {
vector<int> subset;
for (int j = 0; j < n; j++) {
if ((i & (1 << j)) != 0) {
subset.push_back(nums[j]);
}
}
result.push_back(std::move(subset));
}
return result;
}
};
python代码:
class Solution:
def subsets(self, nums):
result = []
n = len(nums)
nums.sort()
for i in range(1 << n):
subset = []
for j in range(n):
if (i & (1 << j)) != 0:
subset.append(nums[j])
result.append(subset)
return result
基于 BFS 的方法
在 BFS 那节课的讲解中,我们很少提到用 BFS 来解决找所有的方案的问题。事实上 BFS 也是可以用来做这件事情的。
用 BFS 来解决该问题时,层级关系如下:
第一层: []
第二层: [1] [2] [3]
第三层: [1, 2] [1, 3], [2, 3]
第四层: [1, 2, 3]
每一层的节点都是上一层的节点拓展而来。
参考代码:
java代码:
public class Solution {
/*
* @param nums: A set of numbers
* @return: A list of lists
*/
public List<List<Integer>> subsets(int[] nums) {
// List vs ArrayList (google)
List<List<Integer>> results = new LinkedList<>();
if (nums == null) {
return results; // 空列表
}
Arrays.sort(nums);
// BFS
Queue<List<Integer>> queue = new LinkedList<>();
queue.offer(new ArrayList<Integer>());
while (!queue.isEmpty()) {
List<Integer> subset = queue.poll();
results.add(subset);
for (int i = 0; i < nums.length; i++) {
if (subset.size() == 0 || subset.get(subset.size() - 1) < nums[i]) {
List<Integer> nextSubset = new ArrayList<Integer>(subset);
nextSubset.add(nums[i]);
queue.offer(nextSubset);
}
}
}
return results;
}
}
C++代码:
class Solution {
public:
/*
* @param nums: A set of numbers
* @return: A list of lists
*/
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> results;
sort(nums.begin(), nums.end());
// BFS
queue<vector<int>> que;
que.push({});
while (!que.empty()) {
vector<int> subset = que.front();
que.pop();
results.push_back(subset);
for (int i = 0; i < nums.size(); i++) {
if (subset.size() == 0 || subset.back() < nums[i]) {
vector<int> nextSubset = subset;
nextSubset.push_back(nums[i]);
que.push(nextSubset);
}
}
}
return results;
}
};
python代码:
class Solution:
def subsets(self, nums):
results = []
if not nums:
return results
nums.sort()
# BFS
queue = deque()
queue.append([])
while queue:
subset = queue.popleft()
results.append(subset)
for i in range(len(nums)):
if not subset or subset[-1] < nums[i]:
nextSubset = list(subset)
nextSubset.append(nums[i])
queue.append(nextSubset)
return results
下面让我们利用学到的新实现方法,来解决全子集问题。
Subsets
本题目是一道LintCode编程题目,请先完成LintCode账号的绑定,再点击“刷新”按钮查看本题目。
在学习全排列的解决方法之前,我们先来学习如何求 下一个排列。
问题:给定一个若干整数的排列,给出按整数大小进行字典序从小到大排序后的下一个排列。若没有下一个排列,则输出字典序最小的序列。
从末尾往左走,如果一直递增,例如 {...9,7,5} ,那么下一个排列一定会牵扯到左边更多的数,直到一个非递增数为止,例如 {...6,9,7,5} 。对于原数组的变化就只到 6 这里,和左侧其他数再无关系。6 这个位置会变成6右侧所有数中比 6 大的最小的数,而 6 会进入最后 3 个数中,且后 3 个数必是升序数组。
所以算法步骤如下:
从右往左遍历数组 nums,直到找到一个位置 i ,满足 nums[i] > nums[i - 1] 或者 i 为 0 。
i 不为 0 时,用j再次从右到左遍历 nums ,寻找第一个 nums[j] > nums[i - 1] 。而后交换 nums[j] 和 nums[i - 1] 。注意,满足要求的 j 一定存在!且交换后 nums[i] 及右侧数组仍为降序数组。
将 nums[i] 及右侧的数组翻转,使其升序。
可能会有同学有些疑问:
Q:i为0怎么办?
A:i为0说明整个数组是降序的,直接翻转整个数组即可。
Q:有重复元素怎么办?
A:在遍历时只要严格满足 nums[i] > nums[i - 1] 和 nums[j] > nums[i - 1] 就不会有问题。
Q:元素过少是否要单独考虑?
A:当元素个数小于等于1个时,可以直接返回。
java参考代码:
public class Solution {
/**
* @param nums: A list of integers
* @return: A list of integers that's next permuation
*/
// 用于交换nums[i]和nums[j]
public void swapItem(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// 用于翻转nums[i]到nums[j],包含两端的这一小段数组
public void swapList(int[] nums, int i, int j) {
while (i < j) {
swapItem(nums, i, j);
i ++;
j --;
}
}
public void nextPermutation(int[] nums) {
int len = nums.length;
if ( len <= 1) {
return;
}
int i = len - 1;
while (i > 0 && nums[i] <= nums[i - 1]) {
i --;
}
if (i != 0) {
int j = len - 1;
while (nums[j] <= nums[i - 1]) {
j--;
}
swapItem(nums, j, i-1);
}
swapList(nums, i, len - 1);
}
}
python参考代码:
class Solution:
# 用于翻转nums[i]到nums[j],包含两端的这一小段数组
def swapList(self, nums, i, j):
while i < j:
nums[i], nums[j] = nums[j], nums[i]
i += 1
j -= 1
"""
@param nums: An array of integers
@return: nothing
"""
def nextPermutation(self, nums):
n = len(nums)
if n <= 1:
return
i = n-1
while i > 0 and nums[i] <= nums[i-1]:
i -= 1
if i != 0:
j = n-1
while nums[j] <= nums[i-1]:
j -= 1
nums[j], nums[i-1] = nums[i-1], nums[j]
self.swapList(nums, i, n-1)
next-permutation
本题目是一道LintCode编程题目,请先完成LintCode账号的绑定,再点击“刷新”按钮查看本题目。
在学习了 下一个排列 的算法之后,对于全排列问题,我们只需要不断调用这个算法的函数就可以啦。
一些可以做得更细致的地方:
为了确定何时结束,建议在迭代前,先对输入nums数组进行升序排序,迭代到降序时,就都找完了。有心的同学可能还记得在 nextPermutation 当中,当且仅当数组完全降序,那么从右往左遍历的指针 i 最终会指向 0 。所以可以为 nextPermutation 带上布尔返回值,当 i 为 0 时,返回 false,表示找完了。要注意,排序操作在这样一个 NP 问题中,消耗的时间几乎可以忽略。
当数组长度为 1 时,nextPermutation 会直接返回 false ;当数组长度为 0 时, nextPermutation 中 i 会成为 -1 ,所以返回 false 的条件可以再加上 i 为 -1 。
Java中,如果输入类型是 int[] ,而输出类型是 List<List> ,要注意,并没有太好的方法进行类型转换,这是由于 int 是基本类型。建议还是自行手动复制,实际工作中还可使用 guava 库。
java示例代码:
public class Solution {
/*
* @param nums: A list of integers.
* @return: A list of permutations.
*/
public List<List<Integer>> permute(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
boolean next = true; // next 为 true 时,表示可以继续迭代
while (next) {
List<Integer> current = new ArrayList<>(); // 进行数组复制
for (int num : nums) {
current.add(num);
}
result.add(current);
next = nextPermutation(nums);
}
return result;
}
// 用于交换nums[i]和nums[j]
public void swapItem(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// 用于翻转nums[i]到nums[j],包含两端的这一小段数组
public void swapList(int[] nums, int i, int j) {
while (i < j) {
swapItem(nums, i, j);
i ++;
j --;
}
}
public boolean nextPermutation(int[] nums) {
int len = nums.length;
int i = len - 1;
while (i > 0 && nums[i] <= nums[i - 1]) {
i--;
}
if (i <= 0) {
return false;
}
int j = len - 1;
while (nums[j] <= nums[i - 1]) {
j--;
}
swapItem(nums, j, i - 1);
swapList(nums, i, len - 1);
return true;
}
}
python示例代码:
class Solution:
"""
@param: nums: A list of integers.
@return: A list of permutations.
"""
def permute(self, nums):
# write your code here
nums.sort()
result = []
hasNext = True # hasNext 为 true 时,表示可以继续迭代
while hasNext:
current = list(nums) # 进行数组复制
result.append(current)
hasNext = self.nextPermutation(nums)
return result
def swapList(self, nums, i, j):
while i < j:
nums[i], nums[j] = nums[j], nums[i]
i += 1
j -= 1
def nextPermutation(self, nums):
n = len(nums)
if n <= 1:
return
i = n - 1
while i > 0 and nums[i] <= nums[i-1]:
i -= 1
if i <= 0:
return False
j = n-1
while nums[j] <= nums[i-1]:
j -= 1
nums[j], nums[i-1] = nums[i-1], nums[j]
self.swapList(nums, i, n-1)
return True
Permutations
本题目是一道LintCode编程题目,请先完成LintCode账号的绑定,再点击“刷新”按钮查看本题目。
最后我们来研究下如何求一个排列是第几个。
题目:给出一个不含重复数字的排列,求这些数字的所有排列按字典序排序后该排列的编号,编号从1开始。例如排列 [1, 2, 4] 是第 1 个排列。
算法描述
只需计算有多少个排列在当前排列A的前面即可。如何算呢?举个例子,[3,7,4,9,1],在它前面的必然是某位置i对应元素比原数组小,而i左侧和原数组一样。也即 [3,7,4,1,X] , [3,7,1,X,X] , [3,1或4,X,X,X] , [1,X,X,X,X] 。
而第 i 个元素,比原数组小的情况有多少种,其实就是 A[i] 右侧有多少元素比 A[i] 小,乘上 A[i] 右侧元素全排列数,即 A[i] 右侧元素数量的阶乘。 i 从右往左看,比当前 A[i] 小的右侧元素数量分别为 1,1,2,1,所以最终字典序在当前 A 之前的数量为 1×1!+1×2!+2×3!+1×4!=39 ,故当前 A 的字典序为 40。
具体步骤:
用 permutation 表示当前阶乘,初始化为 1,result 表示最终结果,初始化为 0 。由于最终结果可能巨大,所以用 long 类型。
i从右往左遍历 A ,循环中计算 A[i] 右侧有多少元素比 A[i] 小,计为 smaller ,result += smaller * permutation。之后 permutation *= A.length - i ,为下次循环 i 左移一位后的排列数。
已算出多少字典序在 A 之前,返回 result + 1 。
参考代码
java代码:
public class Solution {
/**
* @param A: An array of integers
* @return: A long integer
*/
public long permutationIndex(int[] A) {
// write your code here
long permutation = 1;
long result = 0;
for (int i = A.length - 2; i >= 0; --i) {
int smaller = 0;
for (int j = i + 1; j < A.length; ++j) {
if (A[j] < A[i]) {
smaller++;
}
}
result += smaller * permutation;
permutation *= A.length - i;
}
return result + 1;
}
}
python代码:
class Solution:
"""
@param A: An array of integers
@return: A long integer
"""
def permutationIndex(self, A):
permutation = 1
result = 0
for i in range(len(A) - 2, -1, -1):
smaller = 0
for j in range(i + 1, len(A)):
if A[j] < A[i]:
smaller += 1
result += smaller * permutation
permutation *= len(A) - i
return result + 1
常见QA:
Q:为了找寻每个元素右侧有多少元素比自己小,用了O(n^2)的时间,能不能更快些?
A:可以做到O(nlogn),但是很复杂,这是另外一个问题了,可以使用BST,归并排序或者线段树:http://www.lintcode.com/zh-cn/problem/count-of-smaller-number-before-itself/
Q:元素有重复怎么办?
A:好问题!元素有重复,情况会复杂的多。因为这会影响 A[i] 右侧元素的排列数,此时的排列数计算方法为总元素数的阶乘,除以各元素值个数的阶乘,例如 [1, 1, 1, 2, 2, 3] ,排列数为
6! ÷ (3! × 2! × 1!) 。为了正确计算阶乘数,需要用哈系表记录 A[i] 及右侧的元素值个数,并考虑到 A[i] 与右侧比其小的元素 A[k] 交换后,要把 A[k] 的计数减一。用该哈系表计算正确的阶乘数。而且要注意,右侧比 A[i]小 的重复元素值只能计算一次,不要重复计算!
permutation-index
本题目是一道LintCode编程题目,请先完成LintCode账号的绑定,再点击“刷新”按钮查看本题目。
本章内容的文字内容相对较多,希望同学可以多加阅读,多加实践,来达到最佳的学习效果。
本章节的学习已经结束了,请完成章节的教学服务评价后进入下一个章节的学习,或者新建学习记录重新学习本章节。