【leetcode】全排列问题+位运算+补码

文章目录

  • 说明:
  • 字符串的排列组合题型集合
    • 字符串的全排列
      • 题目
      • 算法思想
      • 递归方法1(July 方法):
      • 递归方法2:
    • 字符串的全组合
      • 方法1:递归
      • 方法2:位图
    • 有限制的组合 C(n,k)
      • 题目
      • 解题思路
  • 拓展:补码知识
  • 拓展:位操作
    • 1. 求整数的二进制表示中有多少个 1
      • 方法1
    • 2. 求NextN
      • 方法1: 简单枚举
      • 方法2: O(1)时间高效方法
    • 3. 超级棒的位运算总结

说明:

原文:July 算法习题 - 字符串4(全排列和全组合)
https://segmentfault.com/a/1190000002710424
【july的算法讲解及其推荐啊,非常透彻!】

字符串的排列组合题型集合

字符串的全排列

题目

设计一个算法,输出一个字符串字符的全排列。
比如,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
    【leetcode】全排列问题+位运算+补码_第1张图片
public static void Permutation(char[] s, int from, int to) {
       if(to<=1)
           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.
个人认为这个算法不如第一个递归方法,因为需要额外的空间;但是二者的时间复杂度是相同的,都是O(n!)。

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;
            }
        }
    }

字符串的全组合

输入三个字符 a、b、c,则它们的组合有a b c ab ac bc abc。当然我们还是可以借鉴全排列的思路,利用问题分解的思路,最终用递归解决

方法1:递归

方法2:位图

不过这里介绍一种比较巧妙的思路 —— 基于位图
假设原有元素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++){
            // 1<
                if( (i & (1<<j)) != 0) // 对应位上为1,则输出对应的字符
                {
                    sb.append(s[j]);
                }
            }
            System.out.print(sb + " ");
        }   
    }

有限制的组合 C(n,k)

题目

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<

	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
        // i = nextn(i)每次生成下一个组合二进制
        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;
    }

拓展:补码知识

补码表示法里10000000是-128,没有-0只有0。
【leetcode】全排列问题+位运算+补码_第2张图片

【可以看图计算 1+ (-128),结果就是-127】
127(0111,1111) + (-1)(1111,1111) = 126 ((1)0111,1110),计算过程中发生了溢出,符号位并没有特殊计算规则,就是自然进位的,溢出的直接截断】

作者:Huan Chen
链接:https://www.zhihu.com/question/21511392/answer/83131677
来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
、除了-0这个特例以外,符号位是否参与运算并不影响结果,而在补码中把-0剔除了,把10000000变成-128,其补码是128,溢出了。从最终结果看,可以认为是符号位参与了运算,但是也可以认为是128超出表示范围,结果未定义。总之,符号是否参与运算对结果没有影响,可以认为不存在-0。
作者:下愚
链接:https://www.zhihu.com/question/21511392/answer/18469269
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

拓展:位操作

1. 求整数的二进制表示中有多少个 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;
}

2. 求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)时间高效方法

用位运算生成下一个含有k个1的二进制数
https://www.cnblogs.com/one-piece/archive/2010/06/06/1752726.html
核心思想:
在草稿纸上随便举几个例子,规律很容易看出来。由于“1”的个数是固定的,为了让这个二进制数更大,我们必须把第一个出现在“1”左边的“0”改成“1”;同时,为了让这个二进制数尽可能小,我们必须把它右边那些“1”重新排到最低位去。

  1. 用x & -x可以提取最右边的那个“1”。
  2. 利用加法来消除连续的“1”时,把x & -x加到x上,利用二进制加法的进位把“…01111…”变成“…10000…”。
  3. 我们需要计算出刚才的操作中一共“跳过”了多少个“1”,换句话说现在的x的右起第一个“1”和原来的x的右起第一个“1”差了多少位。关键就在这里!我们可以用除法来完成这一步,例如100000除以100就相当于把被除数右移2位,得到的结果即可以表示两个数中的“1”差了多少位。
  4. 在最低位产生指定数量的“1”需要用到另一个技巧:减1操作可以把右边连续的“0”都变成“1”,即把…10000变成…01111。
b = x & -x;
t = x + b;
c = t & -t;
m = (c/b >> 1) - 1;
r = t | m; //最终结果

我们对上述算法做一个简单的说明:

操作 样例 说明
x 01011100 原数
-x 10100011 + 1 = 10100100 对应的复数补码(所有位取反+1)
b = x & -x 00000100 提取x的右起第一个“1”
t = x + b 01100000 把x的右起第一个位于某个“1”左边的“0”变成“1”,并把它右边的那些“1”都变为“0”
c = t & -t 00100000 提取t的右起第一个“1”
c / b 00001000 右移c中的那个“1”,其结果中最低位连续的“0”的个数正好是c和b中的“1”相差的距离
m = (c/b >> 1) - 1 00000011 在最低位产生数字“1”,其个数比上述的“距离”少1
r = t | m 01100011 最终结果
public int NextN(int n){
    int x = n&(-n);
    int t = n + x;
    int ans = t | ((n^t)/x)>>2;
    return ans;
}

3. 超级棒的位运算总结

位运算简介及实用技巧(一):基础篇
http://www.matrix67.com/blog/archives/263

  1. 取反想到异或
  2. x<< j数字右移j位
  3. 1<<(j-1), 只有第j位为1
  4. 或1 为1
C语言 Pascal语言
a & b a and b
a b
a ^ b a xor b
~a not a
a << b a shl b
a >> b a shr b
  1. and运算
    and运算通常用于二进制取位操作,例如一个数 and 1的结果就是取二进制的最末位。这可以用来判断一个整数的奇偶,二进制的最末位为0表示该数为偶数,最末位为1表示该数为奇数.

  2. or运算
    or运算通常用于二进制特定位上的无条件赋值,例如一个数or 1的结果就是把二进制最末位强行变成1。如果需要把二进制最末位变成0,对这个数or 1之后再减一就可以了,其实际意义就是把这个数强行变成最接近的偶数。

  3. xor运算
    xor运算通常用于对二进制的特定一位进行取反操作,因为异或可以这样定义:0和1异或0都不变,异或1则取反。
    xor运算的逆运算是它本身,也就是说两次异或同一个数最后结果不变,即(a xor b) xor b = a。xor运算可以用于简单的加密

  4. not运算
    not运算的定义是把内存中的0和1全部取反。使用not运算时要格外小心,你需要注意整数类型有没有符号。如果not的对象是无符号整数(不能表示负数),那么得到的值就是它与该类型上界的差,因为无符号类型的数是用$0000到$FFFF依次表示的。

  5. shl运算
    a shl b就表示把a转为二进制后左移b位(在后面添b个0)。例如100的二进制为1100100,而110010000转成十进制是400,那么100 shl 2 = 400。可以看出,a shl b的值实际上就是a乘以2的b次方,因为在二进制数后添一个0就相当于该数乘以2。
    通常认为a shl 1比a * 2更快,因为前者是更底层一些的操作。因此程序中乘以2的操作请尽量用左移一位来代替。
    定义一些常量可能会用到shl运算。你可以方便地用1 shl 16 – 1来表示65535。很多算法和数据结构要求数据规模必须是2的幂,此时可以用shl来定义Max_N等常量。

  6. shr运算
    和shl相似,a shr b表示二进制右移b位(去掉末b位),相当于a除以2的b次方(取整)。我们也经常用shr 1来代替div 2,比如二分查找、堆的插入操作等等。想办法用shr代替除法运算可以使程序效率大大提高。最大公约数的二进制算法用除以2操作来代替慢得出奇的mod运算,效率可以提高60%。

下面列举了一些常见的二进制位的变换操作。

功能 示例 位运算
去掉最后一位 (101101->10110) x >> 1
在最后加一个0 (101101->1011010) x << 1
将右起第一个1变成0,并将该位之后的连续0全部变成1,前面部分不变 1001,1000->1001,0111 x-1, 遇到右起第一个1,它的影响就停止了
将右起的第一个0变成1,并将该位之后的连续1全部变成0,前面部分不变 1001,0111->1001,1000 x+1,遇到右起第一个0,它的影响就停止了
在最后加一个1 (101101->1011011) x << 1+1 , << 的优先级更高
把最后一位变成1 (101100->101101) x | 1
把最后一位变成0 (101101->101100) x | 1-1
最后一位取反 (101101->101100) x ^ 1
把右数第k位变成1 (101001->101101,k=3) x | (1 << (k-1))
把右数第k位变成0 (101101->101001,k=3) x & ~(1 << (k-1))
右数第k位取反 (101001->101101,k=3) x ^ (1 << (k-1))
取末三位 (1101101->101) x & 7
取末k位 (1101101->1101,k=5) x & ((1 << k )-1), << 的优先级更高
取右数第k位 (1101101->1,k=4) x >> (k-1) &1
把末k位变成1 (101001->101111,k=4) x | (1 << k-1)
末k位取反 (101001->100110,k=4) x ^ (1 << k-1)
把右边连续的1变成0 (100101111->100100000) x & (x+1)
把右起第一个0变成1 (100101111->100111111) x | (x+1), 其他部分试图保持,则用或
把右边连续的0变成1 (11011000->11011111) x | (x-1)
取右边连续的1 (100101111->1111) (x ^ (x+1)) >> 1
去掉右起第一个1的左边 (100101000->1000) x & (x ^ (x-1)), 最后这一个在树状数组中会用到。

你可能感兴趣的:(#,算法刷刷更健康)