前 n 个数字二进制中 1 的个数 | 循序递进---@二十一画

题目:

给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。

示例 1:

输入: n = 2
输出: [0,1,1]
解释: 
0 --> 0
1 --> 1
2 --> 10
示例 2:

输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101

说明 :

0 <= n <= 100000

进阶:

给出时间复杂度为 O(n*sizeof(integer)) 的解答非常容易。但你可以在线性时间 O(n) 内用一趟扫描做到吗?
要求算法的空间复杂度为 O(n) 。
你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount )来执行此操作。

分析:

题目中说明部分给了一种思路,那么就是实现时间复杂度为 O(n*sizeof(integer)) 的解答,不妨来看下,虽然题目说这种方式简单,但是依旧需要思考,优化的过程是在基础进行,不能直接跳过最简单的解法去思考高难度的解法,这样是不现实的。

代码:

第一版:暴力法

image.png

第一版中三种方式都运行效率基本一样:因为基本都是题目说的时间复杂度最大的那几种。

实现题目中的解法,对于遍历到的每一个数字n,都单独进行count --1个数的操作。以下是几种实现的方式,使用firstX来分别区分。

class Solution {
    public int[] countBits(int n) {

      	// 下面两个结果都是答案,是分别不同的方式求解得出
        int[] res01 = first01(n);
      	int[] res02 = first02(n);
        int[] res03 = first03(n);
        
    }

    public static int[] first01(int n){

        int one_cnt[] = new int[n+1];

        for(int i=0;i<=n;i++){
            one_cnt[i] = calOneCntVer01(i);
        }
        return one_cnt;
    }
		
    public static int calOneCntVer01(int i){
				// 使用int类型转二进制-->String 【本题i<= 100000,符合int范围,可以直接调用】
        // 根据String的replace方法得到1的个数
        String binStr = Integer.toBinaryString(i);
        return binStr.length()-binStr.replace("1","").length();
    }

		
    //使用遍历32位进制,得到1的个数
    public static int[] first02(int n){

        int one_cnt[] = new int[n+1];
				
       	// 01) 因为本题n<=100000,所以先求最高进制位,比如3的表示是 0011,那么求每一个位上是否为1只需要从倒数第二位开始即可,因为第二位左面的位上肯定都是0,可以不管
        int x = 100000;
        int posCount = 0;
        for(int i=30;i>=0;i--){
            if(x>=(1<<i)){
                posCount = i+1;  // 这里就得到了n的最高有效位,再往左就都是0了【这里要注意得到结果+1,就类似货装卡车的问题,一卡车装1t,货物总共4.3t,也需要5辆卡车才能完全涵盖4.3t货物】
                break;
            }
        }
				// 02) 遍历每一个n的取值  调用calOneCntVer02方法
        for(int i=0;i<=n;i++){
    				// 03) 直接从posCount开始判断是否为1即可
            one_cnt[i] = calOneCntVer02(i,posCount);
        }
        return one_cnt;
    }
	
    public static int calOneCntVer02(int n,int posCount){

        int oneCount = 0;

        // 01)从第posCount 向右边的二进制位移动,表示在i上就是i的递减
        for(int i=posCount;i>=0;i--){
            if(n>=(1<<i)){
              // 02) 如果n>=(1<<i)  证明第i位上肯定=1
              //  n-=(1<<i)  的意思就是 将i位的1 归0 ,然后继续递减判断
                n-=(1<<i); 
              // 每次 归0,就是一个1的消失,这里需要记录一共存在几个1
                oneCount++;
            }
        }
        return oneCount;
    }
  
  // 直接使用java内置函数  Integer.bitCount  直接得出结果  【本题n符合int范围可以使用】
  public static int[] first03(int n){

        int one_cnt[] = new int[n+1];

        for(int i=0;i<=n;i++){
            one_cnt[i] = Integer.bitCount(i);
        }
        return one_cnt;
    }
}
第二版:Brian Kernighan 算法

这个方法应该少数人可以了解,大部分可能都是不太熟悉【包括我😂】。

Brian Kernighan 算法的原理是:对于任意整数 x,令 x=x&(x-1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x 的「一比特数」。

在代码实现前,我想解释下该算法的正确性,防止大家使用了该原理,但是不知道为什么该原理为什么正确,那就很尴尬了。

1.如果x是偶数,那么x的二进制表示低位一定是0. 【举例 12-> …1100,38->0010 0110】

x12(1100)38(0010 0110)126(0111 1110)
x-111(1011)37(0010 0101)125(0111 1101)
x&(x-1)10000010 01000111 1100

从上应该不难看出,其实x & (x-1)的 步骤就是把最低位的1消掉的同时不影响高位的1存在。所以如果一直重复该步骤,可以实现将一个一个的低位1消除掉,直到没有1存在为止。

image.png

// 原理在上已经解释的很清楚,实现过程也比较简单,这里就省略了
class Solution {
    public int[] countBits(int n) {

        int[] res = new int[n+1];

        for(int i=0;i<=n;i++){

            res[i] = calOneCnt(i);
        }

        return res;
    }

    public static int calOneCnt(int x){
        
        int count = 0;
        while(x>0){
            x&=(x-1);
            count++;
        }
        return count;
    }

}
第三版:【动态规划解法】
n1234567
二进制0000 00010000 00100000 00110000 01000000 01010000 01100000 0111
个数(1)1121223

可以发现如下规律:设f(n)表示二进制表示下1的个数

①如果n2的i次方,【注意,这里n不是2的倍数,而2的i次方,i属于正整数】那么n的二进制中只含一个1

满足:``f(n) = 1`

②如果一个数n,满足如下条件: 2^(i) < n < 2^(i+1)【为了方便,我们只看左边的界限,先忽略右面的界限,并将2^(i)假设为z】,则上面的假设条件切换为:

假设数z是2的i次方,数n满足 n>zn小于2*z

实践便可得知 ==》f(n) = f(z) + f(n-z)

二进制的加法,高位和低位的二进制相加,高位1 的个数 + 低位1的个数就是最后的总个数,因为f(z)只有高位存在1,其余位都是0,f(n-z)只有低位存在1,其他位也是0,所以二者加起来就是f(n)

举个例子:n = 10,z = 8,f(10) = 1010个数为2,f(8)=1000个数为1,f(10-8)=0010个数为1。这种相加并不会影响到1的增多和减少。

y = n-z【必定满足0<y<n】 ,结论就是:f(n) = f(n-z) + f(z) 因为f(z) = 1,所以可以表示为:

📢📢📢📢

f(n) = f(n-z) + 1 【z是小于n的最大2次方整数】

好像我们找到了状态转移方程 😁😁😁 ==》 【动态规划解法】

⚠️⚠️⚠️⚠️

发现规律了就可以写代码来解决问题了。

image.png

class Solution {
    public int[] countBits(int n) {
        

        int[] res = new int[n+1];
        
        //01)n=0的时候,二进制中1的个数为0,也是为了下面循环避免越界问题
        //02)lastZ就是当前循环中小于i的最大2次方数
        res[0] = 0;
        int lastZ = 0; 

        //03)循环逻辑
        for(int i=1;i<=n;i++){
            
            //04)执行一次 Brian Kernighan  算法,如果是2的i次方,那么去掉1个0,x返回就变成0了
            // 如果返回0,那么n=i的时候,二进制中1的个数就是1
            // 否则的话,f(n) = f(n-z) + 1
            if(processOneTimeRemove(i)==0){
                res[i] = 1;
                lastZ = i;
            }else{
                res[i] += (res[i-lastZ]+1);
            }
        }

        return res;
    }


    //执行一次 Brian Kernighan  算法,如果是2的i次方,那么去掉1个0,x返回就变成0了
    public static int processOneTimeRemove(int x){
        
        return x&(x-1);
    }

}
第四版:【奇偶特性–动态规划】

1、设n满足n%2=0【大白话:n是偶数】,那么可得,n的二进制最低位=0【这个是真的,不信的可以自行测试,不可能出现偶数的最低位=1😂,因为偶数一定是2的倍数,那么表现在二进制中,就是2 + 2 +。。。。2==> 2^i + 2^(i-1) + 2^(i-2)......+ 2^1

前提:设f(n)表示二进制表示下1的个数

所以可以得出:

f(n) = f(n>>1)>> 表示n除以2】

2、当n是奇数的时候,就反过来可得n的二进制最低位=1.

所以也可以同样得出:

f(n) = f(n>>1) + 1>> 表示n除以2】

3、将规律整合一下,可以得出最后的规律:

f(n) = f(n>>1) + n%2==0?0:1

image.png

class Solution {
    public int[] countBits(int n) {
        

        int[] res = new int[n+1];

        for(int i=0;i<=n;i++){

            res[i] = (res[i>>1] + (i%2==0?0:1));

        }
        
        return res;

    }

}

总结:

这道题实现方法实在是太多了,【暴力法、不为人知的算法、记录高位法、动态规划法、奇偶二进制特性法等等】在没有写题解之前,确实思路有限,但是经过这么一次整理,我相信,不管是写作的我还是阅读的你,对于二进制必定已经加深很多了。

大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值