当前位置: 首页 > news >正文

Leetcode 11 java

338. 比特位计数

提示

给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

示例 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 <= 105

进阶:

  • 很容易就能实现时间复杂度为 O(n log n) 的解决方案,你可以在线性时间复杂度 O(n) 内用一趟扫描解决此问题吗?
  • 你能不使用任何内置函数解决此问题吗?(如,C++ 中的 __builtin_popcount )

1 我的想法:

可以用两个int类型xor异或运算的底层逻辑就是在比较二进制每一位,直接int x ,x和0做异或运算,然后一个i作为循环计数就可以了。我不清楚这个时间复杂度怎么样,还有进阶的函数怎么写?

2 对您想法的评价

您提到使用异或运算来计算二进制中 1 的个数,这个思路需要进一步细化。

异或运算(XOR)的特性是 "相同为 0,不同为 1",但直接用 x 与 0 进行异或运算(x ^ 0)的结果还是 x 本身,并不能直接统计 1 的个数。

【解释一下,我没明白,我以为返回的就是1的个数,一位一位的比较?】

您可能对异或运算的结果有一些误解,我们来详细解释一下为什么 x ^ 0 不能直接得到 1 的个数:

异或运算(XOR)的实际结果

异或运算的规则是:相同位为 0,不同位为 1,但它的运算结果是一个完整的二进制数,而不是 1 的个数。

举例来说:

  • 当 x = 5(二进制 101)时,x ^ 0 的结果是 101 ^ 000 = 101(仍然是 5)
  • 当 x = 6(二进制 110)时,x ^ 0 的结果是 110 ^ 000 = 110(仍然是 6)

可以看到,x ^ 0 的结果还是 x 本身,它只是把 x 的二进制原样保留了下来,并没有统计 1 的个数。

为什么会有这样的误解?

您提到 "一位一位的比较",这个思路是对的,但异或运算的输出并不是 "比较后统计的数量",而是 "比较后得到的新二进制数"。

如果想通过位运算统计 1 的个数,需要额外的步骤:

  1. 用 x & (x-1) 消除最右边的 1(每次操作减少一个 1)
  2. 记录消除操作的次数(次数就是 1 的个数)

例如统计 5(101)的 1 的个数:

plaintext

5 & 4 = 101 & 100 = 100(剩1个1)
4 & 3 = 100 & 011 = 000(剩0个1)
共执行2次操作,所以1的个数是2

总结

  • x ^ 0 的结果是 x 本身,不会直接得到 1 的个数
  • 异或运算的核心是 "位的比较",而不是 "位的计数"
  • 统计 1 的个数需要结合循环和 x & (x-1) 这样的位操作技巧

如果您想基于异或运算实现统计,可以设计这样的逻辑:不断用 x 与自身右移后的结果异或,逐步消除高位 1,但这种方法本质上还是需要循环计数,和 x & (x-1) 方法复杂度类似。

如果要基于异或运算来实现,可以考虑使用 "x & (x-1)" 的技巧,这个操作能消除二进制中最右边的 1。通过循环执行这个操作直到 x 变为 0,循环的次数就是 1 的个数。这种方法的时间复杂度是 O (k),其中 k 是二进制中 1 的个数,整体时间复杂度为 O (n log n),因为每个数最多有 log n 个 1。

3 Java 实现分析

下面提供两种实现方案,分别对应基础要求和进阶要求:

1. 基础方案(O (n log n) 时间复杂度)【Brian Kernighan 算法】

这种方法使用 "x & (x-1)" 的技巧来统计每个数中 1 的个数:

public class CountBitsBasic {public int[] countBits(int n) {int[] ans = new int[n + 1];for (int i = 0; i <= n; i++) {ans[i] = countOnes(i);}return ans;}// 统计一个数的二进制表示中1的个数private int countOnes(int x) {int count = 0;while (x > 0) {// 消除最右边的1x &= x - 1;count++;}return count;}public static void main(String[] args) {CountBitsBasic solution = new CountBitsBasic();System.out.println(java.util.Arrays.toString(solution.countBits(5))); // 输出 [0,1,1,2,1,2]}
}
2. 进阶方案(O (n) 时间复杂度)

这种方法利用动态规划和位运算的特性,实现线性时间复杂度:

public class CountBitsAdvanced {// 主方法:计算0到n每个数的二进制中1的个数,返回结果数组public int[] countBits(int n) {// 创建结果数组,长度为n+1(因为要包含0到n共n+个数)int[] ans = new int[n + 1];// 初始化:0的二进制表示是0,没有1,所以个数为0ans[0] = 0; // 从1开始遍历到n,计算每个数的1的个数for (int i = 1; i <= n; i++) {// 核心逻辑:利用动态规划和位运算推导当前数的1的个数// 1. i >> 1 等价于 i除以2(整数除法),获取i右移一位后的数//    例如:i=5(101),右移一位是2(10);i=6(110),右移一位是3(11)// 2. i & 1 用于判断i的奇偶性://    - 若i是奇数,二进制最后一位是1,i&1=1//    - 若i是偶数,二进制最后一位是0,i&1=0// 3. 状态转移规律://    - 偶数i:二进制最后一位是0,1的个数和i/2相同(ans[i] = ans[i>>1])//    - 奇数i:二进制最后一位是1,1的个数比i/2多1(ans[i] = ans[i>>1] + 1)ans[i] = ans[i >> 1] + (i & 1);}// 返回结果数组return ans;}// 主函数:测试代码public static void main(String[] args) {// 创建当前类的实例CountBitsAdvanced solution = new CountBitsAdvanced();// 调用countBits方法计算n=5时的结果,并打印数组// 预期输出:[0,1,1,2,1,2],对应0到5每个数的1的个数System.out.println(java.util.Arrays.toString(solution.countBits(5))); }
}

进阶方案解析

进阶方案的核心思路是利用数字之间的二进制关系:

  1. 对于偶数 i(二进制最后一位是 0):i 的二进制表示中 1 的个数与 i/2 相同

    • 例如:6 (110) 和 3 (11) 都有 2 个 1
  2. 对于奇数 i(二进制最后一位是 1):i 的二进制表示中 1 的个数比 i/2 多 1

    • 例如:5 (101) 有 2 个 1,而 2 (10) 有 1 个 1

通过位运算可以高效实现这一判断:

  • i >> 1 相当于 i/2(整数除法)
  • i & 1 用于判断 i 是否为奇数(结果为 1 则是奇数,0 则是偶数)

这种方法不需要调用任何内置函数,并且只需一次遍历即可完成计算,时间复杂度为 O (n),空间复杂度为 O (n)(用于存储结果)。

4 什么是动态规划?

动态规划(Dynamic Programming,简称 DP)是一种通过将复杂问题分解为重叠子问题,并存储子问题的解(避免重复计算) 来高效解决问题的方法。

它的核心思想是 “以空间换时间”【是这样的】,适用于具有重叠子问题最优子结构特性的问题。

动态规划的核心特性

要使用动态规划解决问题,需满足两个关键条件:

  1. 重叠子问题
    问题的求解过程中,会反复遇到相同的子问题。例如计算斐波那契数列时,fib(5) = fib(4) + fib(3),而 fib(4) = fib(3) + fib(2),其中 fib(3) 被重复计算。

  2. 最优子结构
    问题的最优解包含子问题的最优解。例如求 “从起点到终点的最短路径”,若路径 A→B→C 是最优解,则 A→B 和 B→C 也分别是对应子问题的最优解。

动态规划的基本步骤

  1. 定义状态
    用一个数组(或变量)dp[i] 表示 “问题在第 i 种情况下的解”。例如:

    • 斐波那契问题中,dp[i] 表示第 i 个斐波那契数;
    • 本题(统计二进制中 1 的个数)中,dp[i] 表示 i 的二进制中 1 的个数。
  2. 推导状态转移方程
    找到 dp[i] 与 dp[i-1] 或其他子问题的关系。例如:

    • 斐波那契数列:dp[i] = dp[i-1] + dp[i-2]
    • 本题中,若 i 是偶数(二进制末尾为 0),则 dp[i] = dp[i/2](右移一位后 1 的个数不变);若 i 是奇数(二进制末尾为 1),则 dp[i] = dp[i-1] + 1(比前一个数多一个 1)。
  3. 初始化边界条件
    确定最小子问题的解。例如:

    • 斐波那契数列:dp[0] = 0dp[1] = 1
    • 本题:dp[0] = 0(0 的二进制没有 1)。
  4. 计算最终结果
    按照状态转移方程,从边界条件逐步推导到目标问题的解。

动态规划 vs 暴力递归

  • 暴力递归会重复计算重叠子问题,时间复杂度极高(如斐波那契递归的时间复杂度为 O(2^n))。
  • 动态规划通过存储子问题的解(如用数组 dp 记录),将时间复杂度优化为 O(n) 或 O(n^2) 等,但需要额外的空间存储 dp 数组(空间复杂度通常为 O(n))。

举例:用动态规划解决本题(统计二进制中 1 的个数)

以 n=5 为例,步骤如下:

  1. 定义 dp[i] 为 i 的二进制中 1 的个数。
  2. 状态转移方程:
    • 若 i 是偶数(i % 2 == 0):dp[i] = dp[i >> 1](右移一位等价于除以 2,1 的个数不变);
    • 若 i 是奇数(i % 2 == 1):dp[i] = dp[i - 1] + 1(比前一个数多一个末尾的 1)。
  3. 初始化:dp[0] = 0
  4. 计算:
    • dp[1] = dp[0] + 1 = 1(1 是奇数);
    • dp[2] = dp[1] = 1(2 是偶数,2>>1=1);
    • dp[3] = dp[2] + 1 = 2(3 是奇数);
    • dp[4] = dp[2] = 1(4 是偶数,4>>1=2);
    • dp[5] = dp[4] + 1 = 2(5 是奇数)。

最终结果为 [0,1,1,2,1,2],与示例一致,且时间复杂度为 O(n)(只需遍历一次),空间复杂度为 O(n)(存储 dp 数组)。

总结

动态规划的核心是用数组记录子问题的解,避免重复计算,通过状态转移方程从已知推未知。它特别适合解决具有重叠子问题和最优子结构的问题,能大幅优化时间复杂度。本题的进阶解法(线性时间 O(n))正是利用了动态规划的思想。

【以空间换时间到底怎么理解,类似于多线程并行吗(比喻而已,我知道thread不能这么定义)】

“以空间换时间” 和 “多线程并行” 是完全不同的概念,用一个生活场景的比喻或许能更清晰地理解:

假设你需要每天早上快速找到一双干净的袜子出门:

  • 不额外占用空间的做法:每次穿完袜子随手扔在抽屉里,第二天早上需要在一堆乱袜子里翻找配对,每次都要花 10 分钟(时间成本高,但抽屉里的袜子是乱堆的,没额外占用空间)。
  • 以空间换时间的做法:你买了几个袜子收纳盒,每次穿完袜子按颜色分类放进盒子里,第二天早上直接从对应盒子里拿,只要 1 分钟就能找到(时间成本低了,但多花了买收纳盒的钱,占用了额外的空间)。

核心区别:

  1. 以空间换时间

    • 本质:通过额外存储中间结果(比如动态规划中的dp数组、缓存技术中的缓存空间),避免重复计算或重复操作,从而减少时间消耗。
    • 特点:是单线程内的优化思路,不涉及并行处理,只是用更多的内存 / 存储空间换取更快的执行速度。
    • 例子:
      • 计算斐波那契数列时,用数组存储已计算的fib(i),避免递归中的重复计算(原本O(2^n)的时间复杂度优化为O(n),但多了O(n)的空间);
      • 数据库的索引机制,通过额外存储索引文件(空间),让查询时不用全表扫描(时间)。
  2. 多线程并行

    • 本质:将一个大任务拆分成多个子任务,让多个线程同时执行(比如一个线程算fib(5),另一个线程同时算fib(4)),从而缩短总时间。
    • 特点:是利用多核 CPU 的并行能力,不依赖额外空间,核心是 “同时做多个事”。
    • 局限性:如果任务之间有依赖(比如fib(5)依赖fib(4)的结果),就无法并行,此时多线程也无法加速。

总结:

  • “以空间换时间” 是用存储换效率,属于 “空间资源→时间优化”;
  • “多线程并行” 是用并行能力换效率,属于 “CPU 资源→时间优化”。

两者都是优化手段,但原理和适用场景完全不同。

动态规划中的 “以空间换时间”,就是通过存储子问题的解(占用空间),避免重复计算(节省时间),和多线程无关。

http://www.dtcms.com/a/311588.html

相关文章:

  • Agentic RAG:自主检索增强生成的范式演进与技术突破
  • ADB 查看 CPU 信息、查看内存信息、查看硬盘信息
  • 计算学习理论(PAC学习、有限假设空间、VC维、Rademacher复杂度、稳定性)
  • PHP 与 MySQL 详解实战入门(2)
  • Linux中使用Qwen模型:Qwen Code CLI工具
  • stm32F407 实现有感BLDC 六步换相 cubemx配置及源代码(二)
  • JavaScript将String转为base64 笔记250802
  • 人工智能篇之计算机视觉
  • golang——viper库学习记录
  • 牛客 - 旋转数组的最小数字
  • 题单【模拟与高精度】
  • 先学Python还是c++?
  • 工具自动生成Makefile
  • 机器学习——K 折交叉验证(K-Fold Cross Validation),实战案例:寻找逻辑回归最佳惩罚因子C
  • 深入理解C++中的vector容器
  • VS2019安装HoloLens 没有设备选项
  • 大模型(五)MOSS-TTSD学习
  • 二叉树的层次遍历 II
  • 算法: 字符串part02: 151.翻转字符串里的单词 + 右旋字符串 + KMP算法28. 实现 strStr()
  • Redis数据库存储键值对的底层原理
  • 信创应用服务器TongWeb安装教程、前后端分离应用部署全流程
  • Web API安全防护全攻略:防刷、防爬与防泄漏实战方案
  • Dispersive Loss:为生成模型引入表示学习 | 如何分析kaiming新提出的dispersive loss,对扩散模型和aigc会带来什么影响?
  • 二、无摩擦刚体捉取——抗力旋量捉取
  • uniapp 数组的用法
  • 【c#窗体荔枝计算乘法,两数相乘】2022-10-6
  • Python Pandas.from_dummies函数解析与实战教程
  • 【语音技术】什么是动态实体
  • 【解决错误】IDEA启动SpringBoot项目 出现:Command line is too long
  • 5734 孤星