三、位图
0、位图概念
所谓位图,就是用每一个比特位来存放某种状态(0或1),是一种哈希思想的应用,适用于海量数据,整数,数据无重复的场景。通常是用来判断某个数据存不存在的。(注意比特位访问的顺序,下图是使用byte[]数组来充当位图,它的第一位是第一个byte的第一个比特位也就是byte[0]最右边的比特位)
2、位图求交、并、差集
3、位图模拟实现
import java.util.Arrays;
public class MyBitSet {
public byte[] elem;
//usedSize记录当前位图当中 存在了多少个有效的数据
public int usedSize;
public MyBitSet() {
this.elem = new byte[1];
}
/**
* @param n 多少位
* n = 12
* 有可能会多给一个字节,但是无所谓。
*/
public MyBitSet(int n) {
this.elem = new byte[n/8+1];
}
/**
* 设置某一位为 1
* @param val
*/
public void set(int val) {
if(val < 0) {
throw new IndexOutOfBoundsException();
}
int arrayIndex = val / 8 ;
//扩容
if(arrayIndex > elem.length-1) {
elem = Arrays.copyOf(elem,arrayIndex+1);
}
int bitIndex = val % 8;
// 将elem[arrayIndex]的第bitIndex个比特位设置位1。
elem[arrayIndex] |= (1 << bitIndex);
usedSize++;
}
/**
* 判断当前位 是不是1
* @param val
*/
public boolean get(int val) {
if(val < 0) {
throw new IndexOutOfBoundsException();
}
int arrayIndex = val / 8 ;
int bitIndex = val % 8;
if((elem[arrayIndex] & (1 << bitIndex)) != 0) {
return true;
}
return false;
}
/**
* 将对应位置 置为 0
* @param val
*/
public void reSet(int val) {
if(val < 0) {
throw new IndexOutOfBoundsException();
}
int arrayIndex = val / 8 ;
int bitIndex = val % 8;
elem[arrayIndex] &= ~(1 << bitIndex);
usedSize--;
}
//当前位图中记录的元素的个数
public int getUsedSize() {
return usedSize;
}
}
位图的应用:
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
补充、常见位运算总结
a,基础运算符
常见位运算符:
右移:>>
左移:<<
取反 : ~
与 :& 有0就为0
或 : | 有1就为1
异或:^ 相同为0,相异为1/无进位相加
b,给定一个数n,确定他的二进制表示第x位是0还是1
最低位在最右边
n:0110101000 我想要知道第x位的是0还是1,只需要让这一位和1与一下就可以了,
0110101000
(n>>x)&1 这个结果就是n的第x位是0还是1,并且不会改变n的大小。
c,将一个数n的二进制表示第x位修改为1
n:0110101000
0000001000 将这两个数或一下就可以得到修改后的值,所以我们需要构造出一个第x为1,其他位为0的数字,也就是(1<<x)那么最终的公式是:
n=n|(1<<x) 或者写成这样也可以 n|=(1<<x)
可以对a=0进行改造得到我们想要的值
d,将一个数n的二进制表示第x位修改为0
n:0110101000
1111110111 将这两个数&一下就可以了,所以我们需要构造出来一个第x位为0,其他位为1的二进制数,也就是~(1<<x) 那么最终运算的公式是:
n&=(~(1<<x)) 或者写成这样也可以 n=n&(~(1<<x))
e,位图思想
本质就是哈希表,我们常见的哈希表是数组形式比如int[10]={0,1,2,3,4,5,6,7,8,9},根据下标可以在O(1)时间复杂度下找到arr[i],位图的思想就说:把一个数n的二进制表示整体看做一个哈希表,每一位0或者1可以表示两个状态。boolean数组或许就是这样。(未完待续…)
f,提取一个二进制数最右侧的1
这个怎么提取确实难想,直接上答案吧:
n&(-n) 这样一个数字就是成功提取了二进制数n的最右侧1,
n = 0110101000 ,对进行取反加1得到 -n:1001011000
n = 0110101000
-n = 1001011000
ret = n&(-n) = 0000001000 这个就是最终的提取结果,n不会被改变,这个结果需要一个变量来接收.
g, 将一个二进制数n最右侧的1改为0
这个算法按理讲应该可以想出来,我要改变最低为的1,就代表比这一位高的都不能变,比这一位低的都是0,那么n-1就会是一个高位和n一致,第位从1全变成1,要改变的这一位从1变成了0,那么n&(-n)就是最终结果;
n=0110101000
n-1=0110100111
n&(n-1)=0101010000 ok,这就成功把最右侧1改为了0
h,运算符的优先级
自己写的时候能加括号就加括号
在编程中,运算符的优先级决定了表达式中运算的执行顺序。下面是一些常见运算符的优先级:
- 括号 (): 最高优先级,表达式会首先被求值。
- 一元运算符: 如
!
、-
、++
、--
等。 - 算术运算符:
*
、/
、%
、+
、-
。乘、除、取模的优先级高于加、减。 - 位运算符:
~
、&
、|
、^
、<<
、>>
。 - 关系运算符:
<
、>
、<=
、>=
、==
、!=
。 - 逻辑运算符:
&&
、||
。 - 赋值运算符:
=
、+=
、-=
、*=
、/=
、%=
等。
当表达式中含有多个运算符时,优先级高的运算符会先被求值。如果优先级相同,则从左到右依次求值。
我们可以使用括号来改变表达式的求值顺序。括号内的表达式会首先被求值。
没咋用过逗号运算符
例如:
5 + 3 * 2 // 结果是 11, 因为 * 的优先级高于 +
(5 + 3) * 2 // 结果是 16, 因为括号内的表达式先被求值
位运算符是一类特殊的运算符,它们直接对数字的二进制位进行操作。位运算符的优先级如下:
- 位非 (~): 最高优先级,对一个数的二进制位进行取反操作。
- 位与 (&): 对两个数的对应位进行"与"操作。
- 位或 (|): 对两个数的对应位进行"或"操作。
- 位异或 (^): 对两个数的对应位进行"异或"操作。
- 左移 (<<): 将一个数的二进制位向左移动指定的位数。
- 右移 (>>): 将一个数的二进制位向右移动指定的位数。
位运算符的优先级高于算术运算符,但低于一元运算符。
例如:
a = 5 & 3 // 结果为 1,因为 5 的二进制是 101,3 的二进制是 011,按位与得到 001,即 1
b = 5 | 3 // 结果为 7,因为 5 的二进制是 101,3 的二进制是 011,按位或得到 111,即 7
c = 5 ^ 3 // 结果为 6,因为 5 的二进制是 101,3 的二进制是 011,按位异或得到 110,即 6
d = 5 << 1 // 结果为 10,因为 5 的二进制是 101,左移 1 位得到 1010,即 10
e = 5 >> 1 // 结果为 2,因为 5 的二进制是 101,右移 1 位得到 10,即 2
理解位运算符的优先级和使用方法对于一些底层编程和优化很有帮助。
i,异或(^)运算的规律
1,a^0=a
2,a^a=0
3,abc=a(bc) 异或运算满足交换律,一大堆数异或在一起,计算结果和异或顺序无关。
前两个还是比较好理解的,最后一个特性也是与生俱来的,因为异或没有进位相加
PS:
7,8两小节可以帮助我们完成力扣这三道题目(191,338,461)
第9可以帮助我们完成力扣这两道题目(136,260)