字符串的全排列
设计一个算法,输出一个字符串字符的全排列。
比如,String = "abc"
输出是"abc","bac","cab","bca","cba","acb"
算法思想
从集合依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,如此递归处理;
比如:首先我要打印abc的全排列,就是第一步把a 和bc交换(得到bac,cab),这需要一个for循环,循环里面有一个swap,交换之后就相当于不管第一步了,进入下一步递归,所以跟一个递归函数, 完成递归之后把交换的换回来,变成原来的字串
递归方法1(July 方法):
abc 为例子:
1. 固定a, 求后面bc的全排列: abc, acb。 求完后,a 和 b交换; 得到bac,开始第二轮
2. 固定b, 求后面ac的全排列: bac, bca。 求完后,b 和 c交换; 得到cab,开始第三轮
3. 固定c, 求后面ba的全排列: cab, cba
即递归树:
str: a b c
ab ac ba bc ca cb
result: abc acb bac bca cab cba
public static void Permutation(char[] s, int from, int to) {
if(to<0)
return;
if(from == to){
System.out.println(s);
}
else{
for(int i=from;i<=to;i++){
swap(s,i,from);
Permutation(s,from+1,to);
swap(s,from,i);
}
}
}
public static void swap(char[] s, int i, int j) {
char temp = s[i];
s[i] = s[j];
s[j] = temp;
}
递归方法2:
与上面算法区别:
本算法需要一个额外的存储空间存放结果(buffer),固定第一个位置是哪个元素的时候,是通过一个循环,然后看原始字符串上,每一个位置是什么元素。July的做法没有结果的buffer,都是在一个字符串上进行的操作。第一个swap的作用就是,依次拿起始字符和后面的每一个字符交换,这样就能遍历第一个位置上的所有可能字符
n个数的全排列,一共有n!
种情况. (n个位置,第一个位置有n种,当第一个位置固定下来之后,第二个位置有n-1种情况...)
全排列的过程:
-
选择第一个字符
-
获得第一个字符固定下来之后的所有的全排列
-
选择第二个字符
-
获得第一+ 二个字符固定下来之后的所有的全排列
-
从这个过程可见,这是一个递归的过程。
还有一点需要注意是:
之前递归过程选择的字符,下一次不能再被选: 第一个位置选了a, 其他位置就不能选a了
解决方法是1. 扫描之前选择的字符 或者 2.创建一个与字符串等长的boolean数组,标记该位置对于的字符是否已经选择。若选择,则标记true; 若未选择,则标记false.
public class Permutation {
public static void permute(String str){
int length = str.length();
boolean[] used = new boolean[length];
StringBuffer output = new StringBuffer(length);
permutation(str,length,output,used,0);
}
// @para
// position : 下一个放置的元素位置,所以调入时候是0
//
static void permutation(String str, int length, StringBuffer output, boolean[] used, int position){
// end of the recursion
if(position == length){
System.out.println(output.toString());
return;
}
else{
for(int i=0;i<length;i++){
// skip already used characters
if(used[i])
continue;
// add fixed character to output, and mark it as used
output.append(str.charAt(i));
used[i] = true;
// permute over remaining characters starting at position+1
// recursion
permutation(str,length,output,used,position+1);
// remove fixed character from output and unmark it
output.deleteCharAt(output.length()-1);
used[i] = false;
}
}
}
个人认为这个算法不如第一个递归方法,因为需要额外的空间;但是二者的时间复杂度是相同的,都是O(n!)
。
字符串的全组合
输入三个字符 a、b、c
,则它们的组合有a
b
c
ab
ac
bc
abc
。当然我们还是可以借鉴全排列的思路,利用问题分解的思路,最终用递归解决。不过这里介绍一种比较巧妙的思路 —— 基于位图。
假设原有元素n
个,最终的组合结果有2^n - 1
. 可以使用2^n - 1
个位,1表示取该元素,0表示不取。 所以a
表示001
,取ab
是011。
001,010,011,100,101,110,111
。对应输出组合结果为:a,b,ab,c,ac,bc,abc
。
因此可以循环 1~2^n-1
(字符串长度),然后输出对应代表的组合即可。
public static void Combination(char [] s){
if(s.length == 0){
return;
}
int len = s.length;
int n = 1<<len;
//从1循环到2^len-1
for(int i=0;i<n;i++){
StringBuffer sb = new StringBuffer();
//查看第一层循环里面的任意一种取值当中的哪一位是1[比如ab,011], 如果是1,对应的字符就存在,打印当前组合。
for(int j=0;j<len;j++){
if( (i & (1<<j)) != 0) // 对应位上为1,则输出对应的字符
{
sb.append(s[j]);
}
}
System.out.print(sb + " ");
}
}
for(int j=0;j<len;j++){
if( (i & (1<<j)) != 0)
}
j = 0, 1<<j 为将第一位置1
j = 1, 1<<j 为将第二位置1
j = 2, 1<<j 为将第三位置1
有限制的组合
Leetcode
Given two integers n and k, return all possible combinations of k numbers out of 1 ... n.
For example,
If n = 4 and k = 2, a solution is:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
解题思路
基于位操作,这里我们主要借助一个二进制操作 “ 求最小的、比 x 大的整数 M,使得 M 与 x 的二进制表示中有相同数目的 1”,如果这个操作已知,那么我们可以设置一个初始整数 bit,bit 的低位第 1~k 个二进制位为 1,其余二进制位为 0,bit 的二进制表示一种组合,然后调用上述操作求得下一个 bit,bit 的最大值为:bit 从低位起第 n-k+1~n 位等于 1,其余位等于 0,即 (1<<n) - (1<<(n-k)
public static List<List<Integer>> combine(int n, int k) {
if(n == 0 | k>n){
return null;
}
int len = n;
int nbit = 1<<len;
int kbit = 1<<k;
int inbit = 1<<n - 1<<(n-k);
List<List<Integer>> result = new ArrayList<List<Integer>>();
//从1循环到2^len-1
for(int i=kbit-1; i<= inbit; i = nextn(i)){
List<Integer> list = new ArrayList<Integer>();
for(int j=0;j<len;j++){
if( (i & (1<<j)) != 0) // 对应位j上为1,则输出对应的字符
{
list.add(j+1);
}
}
result.add(list);
}
return result;
}
// 返回最小的,比N大的整数M,使M与N的二进制有相同数目的1
public static int nextn(int k){
int x = k & (-k);
int t = k+x;
return t | ((k^t)/x)>>2;
}
附录: 位操作
求整数的二进制表示中有多少个 1
方法1
应用了n&=(n-1)
能将 n 的二进制表示中的最右边的 1 翻转为 0 的事实。只需要不停地执行 n&=(n-1)
,直到 n 变成 0 为止,那么翻转的次数就是原来的 n 的二进制表示中 1 的个数,其代码如下:
public int count1Bits(int n){
int count = 0;
while(n!=0){
count++;
n = n & (n-1);
}
return count;
}
NextN
给定一个正整数 N,求最小的、比 N 大的正整数 M,使得 M 与 N 的二进制表示中有相同数目的 1
方法1: 简单枚举
从 N+1 开始枚举,对每个数都测试其二进制表示中的 1 的个数是否与 N 的二进制表示中 1 的个数相等,遇到第一次相等时就停止
public int GetNextN(int n){
int k = count1Bits(n);
do{
n++;
}while(count1Bits(n) != k);
return n;
}
方法2: O(1)
时间高效方法
public int NextN(int n){
int x = n&(-n);
int t = n + x;
int ans = t | ((n^t)/x)>>2;
return ans;
}