java数据结构与算法刷题-----LeetCode191:位1的个数(重点,位运算分治典型题)

java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846

在这里插入图片描述

  1. 思路分析

位运算分治算法

  1. 代码
    在这里插入图片描述
public class Solution {
    // you need to treat n as an unsigned value
        /** 位运算分治法,O(1) O(1) 
     * 位运算分治
     * 就是两个两个算,然后四个四个算这样,为了方便理解,用十进制9876543210举例
     * 首先两个两个分治,98,76,54,....  可以通过 & M1 = 0x55555555; // 01010101010101010101010101010101 来进行
     * 然后4个,4个分治,9876,5432,...         & M2 = 0x33333333; // 00110011001100110011001100110011
     * 然后8个,8个,   98765432,....         & M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
     * 然后16个,16个,                       & M8 = 0x00ff00ff; // 00000000111111110000000011111111
     * 然后32个 依次类推                       &M16= 0x0000ffff; // 00000000000000001111111111111111
     * 我们是int类型,一共32位,
     *
     * 现在假设两个两个分治,10,11,01,00,如何知道有几个1呢?
     *  10 我们可以看到有1个1。 取出1和0,然后相加,变成01,也就是1,就是分治结果
     *  11 有2个1,取出1和1,相加 变成10,也就是2,就是分治结果
     *  举个例子========================================================================
     * 例如整数987654321的二进制是0011 1010 1101 1110 0110 1000 1011 0001,其中1出现的次数为17
     * 两两分组,计算每组高1位和低1位中的1的个数,相加来计算
     * 0011 1010 1101 1110 0110 1000 1011 0001
     *     ——   00  11  10  10 11   01 11  10   01 10  10   00  10  11 00   01
     *     ——  0+0 1+1 1+0 1+0 1+1 0+1 1+1 1+0 0+1 1+0 1+0 0+0 1+0 1+1 0+0 0+1
     *     ——   00  10  01 01 10    01 10  01   01 01  01  00   01  10  00 01
     *          0个 2个 1个 1个   可见成功统计出 0011 1010 有4个1,两个两个计数,结果0010 0101 => 00表示0个1,10表示2个1,01表示1个1,01表示1个1
     * 四四分组,计算每组高2位和低2位中的1的个数,相加来计算
     * 0010 0101 1001 1001 0101 0100 0110 0001
     *     ——  0010    0101 1001 1001   0101 0100  0110   0001
     *     ——  00+10 01+01 10+01 10+01 01+01 01+00 01+10 00+01
     *     ——  0010  0010  0011  0011  0010  0001  0011  0001
     *         2个   2个       统计出0010 0101 有4个1,结果变为 0010 0010 => 0010 表示2个1,0010 表示2个1
     * 八八分组,计算每组高4位和低4位中的1的个数,相加来计算
     * 0010  0010  0011  0011  0010  0001  0011  0001
     *     ——  00100010  00110011  00100001  00110001
     *     ——  0010+0010 0011+0011 0010+0001 0011+0001
     *     ——  00000100  00000110  00000011  00000100
     *          4个          统计出0010 0010 有4个1 结果变为 0000 0100 表示4个1
     * 十六十六分组,计算每组高8位和低8位中的1的个数,相加来计算
     * 00000100  00000110  00000011  00000100
     *     ——  0000010000000110   0000001100000100
     *     ——  00000100+00000110   00000011+00000100
     *     ——  0000000000001010  0000000000000111
     * 三十二三十二分组,计算每组高16位和低16位中的1的个数,相加最后得到的二进制结果转换成十进制数就是该数中1的个数
     * 0000000000001010  0000000000000111
     *     ——  0000000000001010  0000000000000111
     *     ——  0000000000001010+0000000000000111
     *     ——  0000000000000000 0000000000010001
     *     ——  17
     */
    /**
     * 如何让分治进行运算呢? 也就是 1011中两个两个分,10,11怎么运算
     * 比如10 ,我们要让1和0运算 就让他们错位即可,比如>>>1 ,但是这样会有问题
     * i = 10 ,i >>> 1 = 01
     * i + (i>>>1)  = 10 + 01 ,这样就参与了运算
     * 但是11 , 我们要1和1运算
     * i = 11, i>>1 = 01
     * 11 + 01 这样是错误的
     * 因此我们需要通过 & M1 = 0x55555555; // 01010101010101010101010101010101 来进行
     * i1 = 1011 & M1 = 0001
     * i2 >>> 1 = 0101 & M1 = 0101
     * 此时 i1 & i2 = 0001 + 0101 = 0110 ================= 01 表示1个1,10 表示2个1 。一共3个1.
     * 
     */

    /** 因此可以将代码写成这样,但是有很多多余操作可以优化
     * i = 987654321;
     * i = (i & M1) + ((i>>1)&M1); //1个运算1个,也就是两个两个分治,比如01 变成 0和1运算
     * i = (i & M2) + ((i>>2)&M2); //两个运算两个,也就是四个四个分治,比如0101 变成 01 和 01 运算
     * i = (i & M4) + ((i>>4)&M4); //四个运算四个
     * i = (i & M8) + ((i>>8)&M8); //八个运算八个
     * i = (i & M16) + ((i>>16)&M16); //16个运算16个
     * return i;
     */
    private static final int M1 = 0x55555555; // 01010101010101010101010101010101
    private static final int M2 = 0x33333333; // 00110011001100110011001100110011
    private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
    private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111
    private static final int M16= 0x0000ffff; // 00000000000000001111111111111111
    public int hammingWeight1(int n) {
        n = (n & M1) + ((n>>>1)&M1); //1个运算1个,也就是两个两个分治,比如01 变成 0和1运算
        n = (n & M2) + ((n>>>2)&M2); //两个运算两个,也就是四个四个分治,比如0101 变成 01 和 01 运算
        n = (n & M4) + ((n>>>4)&M4); //四个运算四个
        n = (n & M8) + ((n>>>8)&M8); //八个运算八个
        n = (n & M16) + ((n>>>16)&M16); //16个运算16个
        return n;
    }
    /**
        接下来是Integer.bitCount()JavaApi的源码中对上面代码的优化
        首先,1个1个比较,两个两个分治,比如1011
            (1011 >>> 1) & M1  = 0101
            1011 - 0101 = 0110 , 01表示1个1,10表示2个1
        我们发现 直接用i - 右移的结果就是得数,因此
        n = (n & M1) + ((n>>>1)&M1);
        可以优化为
        n = n - ((n>>>1)&M1); 少了一次 & 运算

        =========================================
        两个两个比较时,没有什么好的优化方式,因此(n & M2) + ((n>>>2)&M2);不变
        =========================================
        4个4个比较时,和1个1个比较一样,可以直接优化掉一个&运算。>>> 4 然后相加 ,然后&M4
        0010  0010  0011  0011  0010  0001  0011  0001 直接>>>4
        0000  0010  0010  0011  0011  0010  0001  0011 然后相加
        0010  0100  0101  0110  0101  0011  0100  0100 然后 & M4
        0000  1111  0000  1111  0000  1111  0000  1111
        0000  0100  0000  0110  0000  0011  0000  0100 结果正确
        因此(n & M4) + ((n>>>4)&M4)
        可以优化为
        (n + (n>>>4)) & M4
        =========================================
        八个八个比较,其实此时就已经算出结果了,可以进行汇总了,所以我们需要让答案全部集中到一起
        我们选择全部集中在最后6位中,因此直接位移>>>4相加,这样答案整体后向移动了
        0000  0100  0000  0110  0000  0011  0000  0100 >>> 8
        0000  0000  0000  0100  0000  0110  0000  0011 相加
        0000  0100  0000  1010  0000  1001  0000  0111
        没用了 没用了  前8位已经没用了
        因此,(n & M8) + ((n>>>8)&M8)可以优化为n + (n >>> 8)
        ==========================================
        然后同理,这次一次移动16个,进行分治结果整合
        0000  0100  0000  1010  0000  1001  0000  0111 >>> 16
        0000  0000  0000  0000  0000  0100  0000  1010 相加
        0000  0100  0000  1010  0000  1101  0001  0001
        没用   没用  没用  没用  没用   没用  
        因此 (n & M16) + ((n>>>16)&M16)可以优化为 n + (n >>> 16)
        =========================================
        最终,取最后6位即可,为什么取6位?如果取5位就是00011111 = 31,int 类型有32位,如果取5位不够当做结果
        00111111 = 1 + 2 + 4 + 8 + 16 + 32  可以保存所有结果
        因此通过n & 0x3f 即可取出结果(0x3f = 00000000 00000000 00000000 00111111 )
     */
     public int hammingWeight(int n) {
        n = n - ((n>>>1)&M1); //1个运算1个,也就是两个两个分治,比如01 变成 0和1运算
        n = (n & M2) + ((n>>>2)&M2); //两个运算两个,也就是四个四个分治,比如0101 变成 01 和 01 运算
        n = (n + (n>>>4)) & M4; //四个运算四个
        n = n + (n >>> 8); //八个运算八个
        n = n + (n >>> 16); //16个运算16个
        return n & 0x3f;//0x3f = 00000000 00000000 00000000 00111111 也就是只保留后6位
    }
}

刷题一定要坚持,总结套路,不单单要把题做出来,要举一反三,也要参考别人的思路,学习别人解题的优点,找出你觉得可以优化的点。

  1. 单链表解题思路:双指针、快慢指针、反转链表、预先指针
  1. 双指针:对于单链表而言,可以方便的让我们遍历结点,并做一些额外的事
  2. 快慢指针:常用于找链表中点,找循环链表的循环点,一般快指针每次移动两个结点,慢指针每次移动一个结点。
  3. 反转链表:通常有些题,将链表反转后会更好做,一般选用三指针迭代法,递归的空间复杂度有点高
  4. 预先指针:常用于找结点,比如找倒数第3个结点,那么定义两个指针,第一个指针先移动3个结点,然后两个指针一起遍历,当第一个指针遍历完成,第二个指针指向的结点就是要找的结点
  1. 数组解题思路:双指针、三指针,下标标记
  1. 双指针:多用于减少时间复杂度,快速遍历数组
  2. 三指针:多用于二分查找,分为中间指针,左和右指针
  3. 下标标记:常用于在数组范围内找东西,而不想使用额外的空间的情况,比如找数组长度为n,元素取值范围为[1,n]的数组中没有出现的数字,遍历每个元素,然后将对应下标位置的元素变为负数或者超出[1,n]范围的正数,最后没有发生变化的元素,就是缺少的值。
  4. 差分数组:对差分数组求前缀和即可得到原数组
  1. 用差值,作为下标,节省空间找东西。比如1900年到2000年,就可以定义100大小的数组,每个数组元素下标的查找为1900。
  2. 前缀和数组,对于数组 [1,2,2,4],其差分数组为 [1,1,0,2],差分数组的第 ii个数即为原数组的第 i-1 个元素和第 i个元素的差值,也就是说我们对差分数组求前缀和即可得到原数组
  1. 前缀和:假设有一个数组arr[1,2,3,4]。然后创建一个前缀和数组sum,记录从开头到每个元素区间的和。第一个元素是0。第二个元素,保存第一个和sum[1] = sum[0]+arr[0],第二个元素,保存第二个和sum[2] = sum[1]+arr[1]
  2. 位运算,异或。不使用额外空间找东西可用
  1. 任何数异或0 都为本身。a^0 = a
  2. 自己异或自己 = 0。a^a = 0
  3. 满足交换律:aba = baa = b(aa) = b^0 = b
  1. 栈解题思路:倒着入栈,双栈
  1. 倒着入栈:适用于出栈时想让输出是正序的情况。比如字符串’abc’,如果倒着入栈,那么栈中元素是(c,b,a)。栈是先进后出,此时出栈,结果为abc。
  2. 双栈:适用于实现队列的先入先出效果。一个栈负责输入,另一个栈负责输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值