深入理解计算机系统——位运算、树状数组

目录

一、位运算

[1. 相反数](#1. 相反数)

[2. LowBit:提取最低位的 1](#2. LowBit:提取最低位的 1)

[3. Letter:判断十六进制数含不含字母](#3. Letter:判断十六进制数含不含字母)

二、树状数组

[1. 树状数组是什么?](#1. 树状数组是什么?)

[2. 工作原理](#2. 工作原理)

[2.1 单点更新 add(i, delta)](#2.1 单点更新 add(i, delta))

[2.2 前缀和查询 sum(i)](#2.2 前缀和查询 sum(i))

[2.3 区间和](#2.3 区间和)


一、位运算

1. 相反数

对于一个有符号整数 x,其相反数可以通过 "取反加一" 得到:

cpp 复制代码
-x = ~x + 1
  • ~x:按位取反(包括符号位)

  • +1:加一操作

以 4 位为例
x = 5 (0101)~x = 1010~x+1 = 1011 → 即 -5 的补码。

2. LowBit:提取最低位的 1

LowBit(x) 返回一个整数,其二进制表示中只有 x 的最低位 1 被保留,其余位均为 0。

cpp 复制代码
unsigned LowBit(unsigned x) {
    return x & (-x);
}

数学推导

  • x 的二进制形式为 ...100...0(末尾有 k 个 0,即最低位 1 在第 k+1 位)。

  • ~x 将该 1 变为 0,后面的 0 全变为 1,前面的位取反。

  • ~x + 1 使得后面的 1 进位,恰好将原来那个 1 的位置恢复为 1,而更低位置 0。

  • 因此 x & (~x + 1) = 2^k

  • 而上述已经证明过 ~x+1 <=> -x。

3. Letter:判断十六进制数含不含字母

设一个十六进制数字的 4 位为x3x2x1x0(x3 是最高位,权值 8)。

字母范围 1010 ~ 1111 的二进制特点:

  • x3 = 1 (最高位必须为 1,因为 10~15 最高位都是 1)

  • (x2, x1) 不能同时为 0 ------ 因为如果 x2=0, x1=0 且 x3=1,则可能得到 1000 (8),1001(9),这是数字,不是字母。

所以逻辑表达式为:

cpp 复制代码
is_letter = b3 AND (b2 OR b1)

我们需要对 32 位整数中的每 4 位,也就是8个十六进制数 同时应用上述逻辑。

使用掩码分别取出每个十六进制数的 x3, x2, x1

  • x3(bit3)对应掩码 0x888888881000 1000 ... 1000

  • x2(bit2)对应掩码 0x444444440100 0100 ... 0100

  • x1(bit1)对应掩码 0x222222220010 0010 ... 0010

cpp 复制代码
	unsigned x1 = x & 0x22222222;   // 取出每 4 位中的第 2 位(从右数,位权 2)
	unsigned x2 = x & 0x44444444;   // 取出每 4 位中的第 3 位(位权 4)
	unsigned x3 = x & 0x88888888;   // 取出每 4 位中的第 4 位(位权 8)

为了进行逻辑运算,还需要将x3、x2、x1对应位上的数字都提取出来。

  • x3 右移 3 位,使每个半字节的 b3 移到 bit0

  • x2 右移 2 位,使 b2 移到 bit0

  • x1 右移 1 位,使 b1 移到 bit0

最终进行逻辑运算:

cpp 复制代码
	unsigned a = (x3 >> 3) & ((x2 >> 2) | (x1 >> 1));

a即是32位中每个十六进制数是否为字母的编码,1代表为字母,0代表为数字。

二、树状数组

1. 树状数组是什么?

树状数组是一个精巧的二进制分治结构,通过 lowbit 实现了快速跳跃。

其核心是维护一系列不同大小的块,使得任意前缀和都能由 O(log n) 个块拼成。

  • 单点更新add(index, delta) O(log n)

  • 前缀和查询sum(index) O(log n)

  • 通过前缀和差得到区间和sum(r) - sum(l-1)

2. 工作原理

假设原数组为 a[1..n]

树状数组 c[1..n] 的定义是:

c[i] = sum( a[i - lowbit(i) + 1] , ... , a[i] )

即从 i 向前数 lowbit(i) 个元素的和。

2.1 单点更新 add(i, delta)

要将 a[i] 增加 delta,需要更新所有 包含 a[i] 的 c[j]

这些 j 恰好是:i, i+lowbit(i), i+lowbit(i)+lowbit(i+lowbit(i)), ... ≤ n

原因

c[j] 覆盖的区间是 (j-lowbit(j), j]

j 包含 i,则 i > j-lowbit(j),即 j-i<lowbit(j)

  • 第一步:j₁ = i + lowbit(i)lowbit(i) < lowbit(j₁)

  • 第二步:j₂ = j₁ + lowbit(j₁)lowbit(i)+lowbit(j₁) < lowbit(j₂)

  • 依此类推,每一步累积的步长总和都小于下一步的 lowbit,从而保证 i 始终在新节点的左边界右侧。

cpp 复制代码
	void Add(int index,int val) {
		while (index<TArr.size()) {
			TArr[index] += val;
			index += LowBit(index);
		}
	}
    void update(int index, int val) {
		Add(index+1, val-Arr[index]);
		Arr[index] = val;
    }

2.2 前缀和查询 sum(i)

A[1] + ... + A[i]

C 的定义,C[i] 已经包含了从 i-lowbit(i)+1i 的和。

因此我们累加 C[i],然后令 i -= lowbit(i),继续累加,直到 i=0

cpp 复制代码
int sum(int i) {
    int res = 0;
    while (i > 0) {
        res += C[i];
        i -= lowbit(i);
    }
    return res;
}

2.3 区间和

cpp 复制代码
int range_sum(int l, int r) {
    return sum(r) - sum(l-1);
}
相关推荐
中屹指纹浏览器2 小时前
2026浏览器指纹检测技术演进与多账号反检测实战策略
经验分享·笔记
锅挤2 小时前
数据结构复习(第一章):绪论
数据结构·算法
skywalker_112 小时前
力扣hot100-5(盛最多水的容器),6(三数之和)
算法·leetcode·职场和发展
汀、人工智能2 小时前
[特殊字符] 第95课:冗余连接
数据结构·算法·链表·数据库架构··冗余连接
生信研究猿2 小时前
leetcode 226.翻转二叉树
算法·leetcode·职场和发展
一只小白0002 小时前
反转单链表模板
数据结构·算法
橘颂TA2 小时前
【笔试】算法的暴力美学——牛客 WY22 :Fibonacci数列
算法
独小乐2 小时前
012.整体框架适配SDRAM|千篇笔记实现嵌入式全栈/裸机篇
c语言·汇编·笔记·单片机·嵌入式硬件·arm·gnu
GHL2842710903 小时前
Qwen-Agent 内置RAG学习
学习·ai