🔥专栏:【算法工作坊】算法实战揭秘
✏️一.基础位运算 知识补充
位运算是计算机科学中的一个基本概念,它直接对整数在内存中的二进制表示进行操作。位运算通常比算术运算要快,因为它们可以在硬件级别执行,并且不需要调用高级语言中的函数。下面是一些常见的位运算符及其用途:
-
按位与 (&):当两个相应的二进制位都是1时,结果为1;否则为0。
- 例如:
0101 & 0111 = 0101
- 例如:
-
按位或 (|):当两个相应的二进制位中至少有一个是1时,结果为1;否则为0。
- 例如:
0101 | 0111 = 0111
- 例如:
-
按位异或 (^):当两个相应的二进制位不同时,结果为1;相同则为0。
- 例如:
0101 ^ 0111 = 0010
- 例如:
-
按位取反 (~):将每个二进制位取反,1变成0,0变成1。
- 例如:
~0101
在大多数编程语言中会考虑整数的长度(如32位),结果会是所有位取反的结果。
- 例如:
-
左移 (<<):将一个二进制数的所有位向左移动指定的位置数,右边空出的位置用0填充。
- 例如:
0010 << 1 = 0100
,相当于乘以2的幂。
- 例如:
-
右移 (>>):将一个二进制数的所有位向右移动指定的位置数,左边空出的位置通常用符号位填充(对于无符号整数和正数用0填充)。
- 例如:
0100 >> 2 = 0001
,相当于除以2的幂。
- 例如:
public class basicKnowledge {
//在进行位运算时,注意添加小括号
//1.基础位运算
/**
* >>
* <<
* ~ 取反
* & 有0为0
* | 有1为1
* ^ 相同为0,不同为1/无进位相加
*/
//2.给定一个数n,确定它的二进制表示中第x位是0还是1
/**
* (n>>x) & 1
* 0->0
* 1->1
*/
//3.将一个数n的二进制表示的第x位修改成1
/**
* n |=(1<<x)
*/
//4.将一个数n的二进制表示的第x位修改成0
/**
* n &= (~(1<<x))
*/
//5.提取一个数n二进制中最右端的1 (low bit)
/**
* n & -n
* -n 本质是将最右侧的1的左边区域全部变为相反
*/
//6.干掉一个数n二进制表示最右侧的1
/**
* n & (n-1)
* n-1的本质是将最右侧的1的右边区域(包含这个1),全部变成相反
*/
//异或运算的运算律
/**
* 1.a^0=a
* 2.a^a=0
* 3.a^b^c=a^(b^c)
*/
}
位运算的应用包括但不限于:
- 数据压缩
- 加密算法
- 散列函数
- 算法优化,比如快速地设置、清除或测试特定位的状态
理解位运算有助于编写更高效、更紧凑的代码,在某些情况下可以用来替代复杂的逻辑运算。
✒️二.位图思想
位图思想是一种利用二进制位来表示数据状态的数据结构技术。位图通常用于管理和跟踪大量离散的状态信息,尤其在内存有限的情况下,它可以有效地减少空间占用。位图的基本思想是使用一个或多个二进制位来表示一个状态或标识符,从而使得存储和检索变得极为高效。
基本概念
- 位:位图中的最小单位,通常是一个二进制位(0 或 1)。
- 状态:每个位代表的状态或标识符,例如可用/不可用、已读/未读等。
- 索引:每个位对应一个索引值,索引值通常是从0开始的整数。
如何工作
- 分配位图:首先,你需要确定位图的大小。这取决于你要跟踪的状态的数量以及每个状态所需的位数。
- 设置和查询位:使用位操作(如按位或、按位与、按位异或等)来设置或查询位图中的位。
- 管理状态:通过改变位图中的位来更新状态信息。
优点
- 节省空间:相比其他数据结构(如数组、哈希表),位图可以极大地减少存储空间的需求,因为每个状态只需要一位来表示。
- 快速访问:位操作通常是非常高效的,因此位图提供了快速的状态查询和修改能力。
- 易于实现:位图的概念简单,实现起来也比较直观。
典型应用场景
- 权限控制:在用户权限管理中,可以使用位图来表示不同级别的权限。
- 缓存管理:在缓存系统中,位图可用于追踪缓存项的有效性或状态。
- 网络协议:在网络通信协议中,位图可以用来表示包的状态或选项。
- 数据库索引:在数据库设计中,位图索引可以用来加速查询操作,特别是在布尔类型字段上。
- 图像处理:虽然这里的“位图”通常指的是像素点阵图像,但概念上有相似之处,因为每个像素也可以被视为一个状态的集合。
🖋️三.面试题 01.01.判断字符是否唯一
题目链接:面试题 01.01.判断字符是否唯一
代码
public boolean isUnique(String astr) {
//利用鸽巢思想优化代码
if(astr.length()>26){
return false;
}
int bitMap=0;
for(int i=0;i<astr.length();i++){
int offset=astr.charAt(i)-'a';
if(((bitMap>>offset)&1)==1){
//重复
return false;
}
//将该位改为1,记录为已存在
bitMap|=(1<<offset);
}
return true;
}
算法原理
此处运用到了位图思想,如知识点二,其实位图的思想和哈希表和相似
本质就是用每一个比特位的0和1作为标识符
鸽巢原理
鸽巢原理指出,如果有更多的物品(信鸽)需要放入较少数量的容器(鸽巢)中,则至少有一个容器将包含多于一个的物品。在这个算法中,如果字符串 astr
的长度超过26(即小写字母的数量),那么根据鸽巢原理,至少会有一个字符重复出现。
遍历字符串
遍历字符串中的每个字符,通过计算字符与 'a'
的偏移量来确定在位图中的位置。这是因为 'a'
到 'z'
的ASCII码是连续的,因此 'a'
的偏移量为0,'b'
为1,以此类推。
检查字符是否重复
通过位运算来检查当前字符是否已经出现过。具体做法是将 bitMap
右移 offset
位后,与1进行按位与操作。如果结果为1,说明该位已经被设置过了,即该字符已经出现过一次,此时直接返回 false
。
标记字符已出现
如果没有重复字符,就将 bitMap
中对应的位设置为1,表示该字符已经出现过。
优点
- 空间效率:只需使用一个整数(32位)即可存储26个字母的状态,大大减少了空间开销。
- 时间效率:对于每个字符的操作都只需要常数时间,因此总的时间复杂度为 O(n),其中 n 是字符串的长度。
总结
这个算法利用了位运算的高效性和鸽巢原理来实现一个简洁而高效的字符串唯一性检测功能。这种方法非常适合于处理字符集相对较小的情况,例如只包含小写字母的字符串。
🖌️四. 268.丢失的数字
题目链接:268.丢失的数字
代码
public int missingNumber(int[] nums) {
int ret=0;
for(int i=0;i<nums.length;i++){
ret^=nums[i];
ret^=i;
}
return ret^nums.length;
}
//高斯求和
public int missingNumber1(int[] nums) {
int ret = nums.length * (nums.length + 1) / 2;
for (int i = 0; i <nums.length; i++) {
ret -= nums[i] ;
}
return ret;
}
算法原理
走两遍数组运用了异或的性质,任何数与自己异或等于0:
a ^ a = 0 ,
-
异或性质:异或操作具有以下性质:
- 任何数与0异或等于它本身:
a ^ 0 = a
。 - 任何数与自己异或等于0:
a ^ a = 0
。 - 异或操作满足交换律和结合律:
a ^ b = b ^ a
,(a ^ b) ^ c = a ^ (b ^ c)
。
- 任何数与0异或等于它本身:
-
遍历过程中的异或操作:在遍历过程中,
ret
中累积了所有数组元素和索引的异或结果。- 对于数组中的每个元素
nums[i]
,将其与当前的ret
进行异或。 - 对于每个索引
i
,也将其与当前的ret
进行异或。
- 对于数组中的每个元素
-
最终操作:最后一步是将
ret
与数组的长度nums.length
进行异或。- 数组长度
nums.length
实际上是应该存在的最大索引n
。
- 数组长度
🖍️五.371.两整数之和
题目链接:371.两整数之和
代码
public int getSum(int a, int b) {
while(b!=0){
int x=a,y=b;
//^ 无进位加和
a=x^y;
//求进位
b=(x&y)<<1;
}
return a;
}
算法原理
这里是基础位运算的一个复合使用,^可以理解为二进制的 无进位相加 而 (x&y)<<1 可以理解为 求这两个数相加的进位(只有进位),通过循环,直到b为0,即可
初始化循环条件:
当 b
不为0时,继续循环。这意味着只要还有进位,就需要继续计算。
无进位加法:
int x = a, y = b;
a = x ^ y;
- 使用按位异或 (
^
) 来计算a
和b
的无进位加和。异或操作能够得出两个数相加的结果,但忽略进位的影响。例如,1 ^ 1 = 0
,1 ^ 0 = 1
,0 ^ 0 = 0
。
计算进位:
b = (x & y) << 1;
- 使用按位与 (
&
) 来找出哪些位会产生进位,然后将这些位左移一位,模拟进位的效果。例如,1 & 1 = 1
,1 & 0 = 0
,0 & 0 = 0
。将结果左移一位相当于将进位加到了更高的一位上。
循环直至无进位:
- 继续上述过程,直到
b
为0,即不再有进位为止。此时a
的值就是最终的结果。
📝六. 137.只出现一次的数字II
题目链接:137.只出现一次的数字II
代码
public int singleNumber(int[] nums) {
int ret = 0;
for (int i = 0; i < 32; i++) {
int sum = 0;
for (int x : nums) {
if (((x >> i) & 1) == 1) {
sum += 1;
}
}
/**
* (3n+1)%3=1
* 3n%3=0
*/
sum %= 3;
if (sum == 1) {
ret |= (1 << i);
}
}
return ret;
}
算法原理
题目中是3次,其实可以类比到n次
在32位的位图中,将所有数组中的数字加起来在%3会等于1,通过这种形式就会消除这些重复3次的数字,剩下的二进制序列实际上就是单独的数的二进制序列
统计每位上1的个数:
int sum = 0;
for (int x : nums) {
if (((x >> i) & 1) == 1) {
sum += 1;
}
}
- 对于当前位
i
,遍历数组nums
中的每个元素x
。 - 使用位右移操作
x >> i
将当前位移到最低位,然后与1进行按位与操作& 1
来检查该位是否为1。 - 如果该位为1,则增加计数器
sum
。
处理计数结果:
sum %= 3;
- 计算
sum
对3取模的结果。由于每个数字除了唯一出现一次的数字之外,其他数字都出现了三次,因此sum
对3取模的结果为1,则说明该位为1。
设置结果位:
if (sum == 1) {
ret |= (1 << i);
}
- 如果
sum
对3取模的结果为1,则将结果ret
中对应的位设置为1。
📱七. 面试题 17.19. 消失的两个数字
题目链接:面试题 17.19. 消失的两个数字
代码
public int[] missingTwo(int[] nums) {
//1.先把所有的数异或在一起
int tmp=0;
for(int x:nums){
tmp^=x;
}
for(int i=1;i<=nums.length+2;i++){
tmp^=i;
}
//2.找出a,b两个数比特位不同的哪一位
int offset=0;
while(true){
if(((tmp>>offset)&1)==1){
break;
}else{
offset++;
}
}
//3.将所有数按照 offset 的不同分为两类
int[] ret=new int[2];
for(int x:nums){
if(((x>>offset)&1)==1){
ret[0]^=x;
}else{
ret[1]^=x;
}
}
for(int i=1;i<=nums.length+2;i++){
if(((i>>offset)&1)==1){
ret[0]^=i;
}else{
ret[1]^=i;
}
}
return ret;
}
算法原理
先遍历两遍,去除已存在的数,剩下的两个数a^b,将所有数分为两份,找到这个数第一个为1的部分也就是,第一个两个数不同的地方,分为两组数,一组是在此处比特位为1的,另一组为在此处比特位为2的,通过这种方式倒退出两个数字
初始化异或结果:
int tmp = 0;
for (int x : nums) {
tmp ^= x;
}
for (int i = 1; i <= nums.length + 2; i++) {
tmp ^= i;
}
- 首先将数组
nums
中的所有元素进行异或操作,得到一个中间结果tmp
。 - 然后将
[1, n+2]
范围内的所有整数也进行异或操作,并将结果与之前的中间结果tmp
进行异或。 - 最终得到的
tmp
是两个缺失数字的异或结果。
找出不同的比特位:
int offset = 0;
while (true) {
if (((tmp >> offset) & 1) == 1) {
break;
} else {
offset++;
}
}
- 寻找
tmp
中第一个为1的比特位的位置offset
。这是因为两个缺失数字的异或结果tmp
中至少有一位是1,这一位在两个缺失数字中是不同的。
分类异或:
int[] ret = new int[2];
for (int x : nums) {
if (((x >> offset) & 1) == 1) {
ret[0] ^= x;
} else {
ret[1] ^= x;
}
}
for (int i = 1; i <= nums.length + 2; i++) {
if (((i >> offset) & 1) == 1) {
ret[0] ^= i;
} else {
ret[1] ^= i;
}
}
- 将所有数字(包括数组
nums
中的数字和[1, n+2]
范围内的数字)按照它们在offset
位上的值分成两组。 - 对于每一组,分别进行异或操作。由于每一组中的数字在
offset
位上是不同的,最终每一组的异或结果就是其中一个缺失的数字。
为什么能找出两个缺失的数字?
-
异或性质:异或操作具有以下性质:
- 任何数与0异或等于它本身:
a ^ 0 = a
。 - 任何数与自己异或等于0:
a ^ a = 0
。 - 异或操作满足交换律和结合律:
a ^ b = b ^ a
,(a ^ b) ^ c = a ^ (b ^ c)
。
- 任何数与0异或等于它本身:
-
最终异或结果:经过第一阶段的异或操作后,
tmp
存储了两个缺失数字的异或结果。这意味着tmp
中至少有一位是1,这一位在两个缺失数字中是不同的。 -
分类异或:通过找出
tmp
中第一个为1的比特位offset
,可以将所有数字分为两组。由于两个缺失数字在这一个比特位上是不同的,因此每组中的异或结果分别对应一个缺失的数字。