Java.Integer.bitCount(int)源码解析

本文详细介绍了如何使用位操作和分治策略优化计算整数中1的个数的算法,从最初的常规循环方法逐步演进到高效的bitCount(int)源码实现。通过分析二进制规律,展示了如何利用位运算减少计算复杂度,从O(N)降低到O(logN),并探讨了代码优化过程中的思考和技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

如何求解一个二进制中1的个数?有常规的O(N)法,还有基于分治的O(logN),即Java的bitCount(int)方法。
对于bitCount(int)源码,是已经优化过的代码,已经看不到原始的分治逻辑,显的很难。但从分治原理到优化,思路非常简单,感受分治的魅力,感受挖掘规律进行优化的魅力。
所有的困难都是由简单知识点结合内部逻辑联系组合而成!

一、由易到难,头脑热身

如何求二进制中1的个数?常规的方法就是对每位二进制进行判定累计。

 	public int hammingWeight(int n) {
        int cnt = 0;
        for(int i = 0;i < 32;i++){
            if((1 << i & n) != 0) ++cnt;
        }
        return cnt;
    }

二、简单优化,一题多解

如果二进制计算基础,即常见位运算,那基本知道如何快速将最后一个1消掉。n - 1会导致二进制最后一个1被借用,其后的0全部变为1,如8 = 0x1000,8 - 1 = 7 = 0x0111; 那么n & (n - 1)就能把最后一个1消掉,如0x1000 & 0x0111 = 0x0000;

	public int hammingWeight(int n) {
        int cnt = 0;
        while(n != 0){
            ++cnt;

            n = n & (n - 1);
        }
        return cnt;
    }

这样就能减少判定次数,而且没有if判定。

三、分治优化

15 = 1111,如何分治计算1的个数,直接统计1的个数即可,即不断做加法即可。
在这里插入图片描述

提取关键问题,如何让前一位和后一位做加法呐
直接将二进制无符号右移一位,前后两位不就对齐了吗?再用0101…来将左边多余的二进制抹除,再进行最终的加法运算。
0101 = 5,所以需要用5来抹除多余的1.

n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);

统计了1位,接下来统计2位,再统计4位,继续统计8位 / 16位,都是同样的道理,直接通过无符号右移不同的位数进行加法统计即可。

public int hammingWeight(int n) {
		// 用0101来抹除多余的1 + 右移1位对齐。
        n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);
        // 用0011来抹除多余的1 + 右移2位对齐。
        n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
        // 用00001111来抹除多余的1 + 右移4位对齐。
        n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);
        // 用0000000011111111来抹除多余的1 + 右移8位对齐。
        n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);
        // 用00000000000000001111111111111111来抹除多余的1 + 右移16位对齐。
        n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
        
        return n;
    }

四、bitCount(int)源码优化

上面就是bitCount的分治原理,再深入挖掘二进制的规律,挖掘计算中的个性,来做一个优化。

  1. 用0101来抹除多余的1 + 右移1位对齐。
    n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);
    对于两位二进制来讲,0x11 - 0x01 = 0x10 = 2,表示有2个1,0x10 - 0x01 = 0x01 = 1表示有1位二进制,就是这么巧!
    0x11 >>> 1 = 0x01;0x10 >>> 1 = 0x01;
    对于第2为为0的情况,自然不用管,毕竟0x01 - 0x00 = 1;0x00 - 0x00 = 0;
    所以可以用减法,少一次与运算,n = n - ((n >>> 1) & 0x55555555)

  2. 用0011来抹除多余的1 + 右移2位对齐。
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
    无法优化,没有二进制规律,而且最多4个1需要3位二进制表示。

  3. 用00001111来抹除多余的1 + 右移4位对齐。
    n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);
    这里需要统计的是1byte中1的个数,而1的个数最多有8个,4位二进制完全够用了,所以可以先做加法运行,再对多余的0进行抹除,来减少一次运算。即n = (n + (n >>> 4)) & 0x0f0f0f0f;

第4/5点同理,但是从第4点开始,就有8位的空间来统计二进制数,而int只有32位,只需6个bit可以完成统计,所以可进一步优化!
先不管多余的二进制(未对齐的错误运算),最后统一把其抹除,只用6bit即可,所以用0x111111 = 0x3f来抹除多余的二进制。

疑问:为什么4位时不行?而8/16位可以呐?还是回归到6bit足够表示32位二进制个数了,4bit不行,下次运算时,紧挨着的2bit被运算,而且还抹不掉这个未对齐的错误运算!

bitCount源码,即最终优化过的代码,

/**
     * Returns the number of one-bits in the two's complement binary
     * representation of the specified {@code int} value.  This function is
     * sometimes referred to as the <i>population count</i>.
     *
     * @param i the value whose bits are to be counted
     * @return the number of one-bits in the two's complement binary
     *     representation of the specified {@code int} value.
     * @since 1.5
     */
    @HotSpotIntrinsicCandidate
    public static int bitCount(int i) {
        // HD, Figure 5-2
        i = i - ((i >>> 1) & 0x55555555);
        i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
        i = (i + (i >>> 4)) & 0x0f0f0f0f;
        i = i + (i >>> 8);
        i = i + (i >>> 16);
        return i & 0x3f;
    }

总结

1)分治统计,从O(N)降到O(logN)。
2)从易到难,一步步挖掘内在规律和个性,一步步优化,完成经典之作。
3)所有困难都是由简单知识点和它们之间的内在逻辑联系构成!

参考文献

[1] LeetCode 位1的个数
[2] bitCount 源码解析
[3] JDK 12

### HAL_TIM_PeriodElapsedCallback 函数功能与用法 #### 1. 功能描述 `HAL_TIM_PeriodElapsedCallback` 是 STM32 HAL 库中的回调函数,用于处理定时器周期结束事件。当定时器的计数值达到设定的最大值并触发更新事件时,该回调函数会被调用[^1]。 此函数的主要作用是在中断服务程序中被自动调用,允许用户在不修改底层驱动的情况下实现自定义逻辑。它通常用来响应特定的时间间隔到达后的动作,例如刷新数据、切换状态或其他实时任务调度[^2]。 --- #### 2. 定义形式 以下是 `HAL_TIM_PeriodElapsedCallback` 的典型定义: ```c void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { // 用户可以在此处编写自己的代码来处理定时器周期溢出事件 } ``` - **参数说明** - `TIM_HandleTypeDef *htim`: 这是一个指向定时器句柄结构体的指针,包含了配置和运行状态的信息。通过这个句柄,可以在回调函数内部访问当前定时器的相关属性或重新设置其行为。 --- #### 3. 使用方法 为了使能这一回调机制,需完成以下几个步骤: 1. 初始化定时器:利用 `HAL_TIM_Base_Init` 或其他初始化接口完成硬件资源分配以及基础参数配置(如预分频系数、计数器周期等)。 2. 启动带中断模式的定时器:调用 `HAL_TIM_Base_Start_IT(htim)` 来开启定时器及其关联的中断请求。这一步会启用相应的中断线,并注册默认的中断服务例程(ISR)[^1]。 3. 实现回调函数:根据实际需求重写 `HAL_TIM_PeriodElapsedCallback` 方法的内容。每当发生一次完整的计数循环后,即进入下一轮计数前,都会跳转到此处执行指定的操作[^3]。 4. 清除标志位/中断挂起比特 (可选): 如果需要手动管理某些特殊类型的干扰信号,则可能还需要借助宏指令如 __HAL_TIM_CLEAR_IT() 对应位置零操作。 --- #### 示例代码片段 下面展示了一个简单的应用案例——每秒钟点亮 LED 一次: ```c #include "stm32f4xx_hal.h" // 假设已正确设置了 GPIO 和 TIM 句柄 htim2 uint8_t led_state = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ if(htim->Instance == TIM2){ // 判断是否来自 TIM2 中断 if(led_state == 0){ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 打开LED led_state = 1; } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 关闭LED led_state = 0; } } } int main(void){ /* MCU Initialization */ // 配置GPIO PA5作为输出端口 // 设置 TIM2 参数 TIM_HandleTypeDef timHandle; timHandle.Instance = TIM2; timHandle.Init.Prescaler = 8399; // 设定预分频值使得频率接近1KHz timHandle.Init.CounterMode = TIM_COUNTERMODE_UP; timHandle.Init.Period = 9999; // 计数至最大值约等于一秒 timHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; if(HAL_TIM_Base_Init(&timHandle) != HAL_OK){ Error_Handler(); } // 开启 IT 模式的定时器 HAL_TIM_Base_Start_IT(&timHandle); while(1); } ``` 上述例子展示了如何结合外部设备控制形成规律性的脉冲序列。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值