目录
3.3.有40亿个无序且不重复的无符号的整数,如何快速判断这个数是否在那40亿个数当中
这一部分本来是C语言的内容,当学习位图时,发现这一部分的只是有点欠缺,所以恶补了一下,望周知!!!
1.进制与原反补码
我们先用几个例子来进行学习:
- 123(10) = 1*10^2 + 2*10^1 + 3*10^0
- 111(2) = 7 (10)
这就是进制
我们知道在C语言中,一个整型的大小是4个字节,一个字节是8个比特位,一个bit对应0或者1
所以对于数字7 :
// 7
// 00000000 00000000 00000111 -原码
// 00000000 00000000 00000111 -反码
// 00000000 00000000 00000111 -补码
对于正整数来说,我们通过10进制来找到二进制,写成32位这时就是原码,而正整数的原反补码是一样的。
(注意32位的整型,第一位的0代表正数,1代表负数,这个位置叫做符号位)
那么数字-7呢?
// -7
// 10000000 00000000 00000111 -原码 因为是负数第一位符号位为1
// 11111111 11111111 11111000 -反码 反码通过原码符号位不变,其他位按位取反
// 11111111 11111111 11111001 -补码 补码等于反码加1
这里我们发现跟正整数有较大的区别
整数在内存中以二进制的补码形式存储 ,操作也是通过二进制的补码形式,最终转换为10进制
2.位运算
基本规则:
接下来我们按顺序来研究这几个位运算操作!!!
2.1.按位与
当 位运算 对应位置 均为1时 才是1。
// 按位与
0 & 0 = 0;
1 & 0 = 0;
0 & 1 = 0;
1 & 1 = 1;
0111 & 0101 = 0101
2.2.按位或
只要 对应位为1,那么都是1
// 按位或
0 | 0 = 0;
1 | 0 = 1;
0 | 1 = 1;
1 | 1 = 1;
0110 | 0101 = 0111
2.3.异或
相同为0,不同为1
// 异或
0 ^ 0 = 0;
1 ^ 0 = 1;
0 ^ 1 = 1;
1 ^ 1 = 0;
0110 ^ 0101 = 0011
2.4.取反
位置为0变成1,位置为1变成0
// 取反
~0011 = 1100
~1100 = 0011
2.5.移位
左移(<<):将一个二进制数的所有位向左移动指定的位数。左移操作会在右侧添加0,并丢弃左侧超出指定位数的位。左移操作可以看作是将一个数乘以2的指定次幂。
右移(>>):将一个二进制数的所有位向右移动指定的位数。右移操作会在左侧添加0或者符号位,并丢弃右侧超出指定位数的位。右移操作可以看作是将一个数除以2的指定次幂。
例如,对于二进制数1010,进行左移和右移操作:
- 左移2位(1010 << 2):结果为101000,相当于将1010乘以2的2次幂(4),即40。
- 右移1位(1010 >> 1):结果为0101,相当于将1010除以2的1次幂(2),即5。
- 左移n位,原二进制对应的10进制数乘以2^n次方
- 右移n位,原二进制数对应10进制除以2^n次方
注意这个操作的二进制数是原数据的“补码”!!!
// 例如
// int a = 7;
// int b = a << 1;
// 00000000 00000000 00000111 7的补码
// 00000000 00000000 00001110 左移后的7的补码
// 00000000 00000000 00001110 左移后的原码 大小为14(10)
当我们对int a = -7进行左移时,我们就要进行原反补码的转换了……
另外,右移操作符分为两种情况
- 算术移位:丢弃右边位,左边空缺位填原本的符号位
- 逻辑移位:丢弃右边位,左边空缺填补0
// 将7左移
// 00000000 00000000 00000111
// 0 00000000 00000000 0000111 0 左移 左边删去一位 右边补0
将7右移
// 00000000 00000000 00000111
// 0 0000000 00000000 00000011 1 右移 左边补0或1 右边删去一位
注意:
- 左移的本质为由低地址向高地址,而不是单纯的向左移动。
- 右移是从高地址往低地址
这里因为计算机分为大端机和小端机原因,他们左移的逻辑不同。
3.部分面试题
3.1.不创建新的变量,实现两个变量的交换
void swap(int &a, int &b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
这里的原理:
- 对于任意的整数num,恒存在 num 异或 num = 0
- 任何num恒存在:num 异或 0 等于num
总结一下就是异或就有交换律和结合率
接下来我们用实际数据来显示:
int a = 5; // 0101
int b = 3; // 0011
a = a ^ b; // a = 0101 ^ 0011 = 0110
b = a ^ b; // b = 0110 ^ 0011 = 0101 = 3
a = a ^ b; // a = 0110 ^ 0101 = 0011 = 5
// 我们用10进制
// a = 5 ^ 3;
// b = 5 ^ 3 ^ 3 = 5
// a = 5 ^ 3 ^ 5 = 3
3.2.求一个整数存储在内存中二进制中1的个数
int countFunc(int a)
{
int count = 0;
int array[32] = { 0 };
// 核心逻辑
for (int i = 31; i > 0; i--)
{
if ((a & 1) == 1)
{
count++;
array[i] = 1;
}
a = (a >> 1);
}
// 打印这个二进制数
for (int i = 0; i < 32; i++)
{
if (i % 8 == 0 && i > 0)
cout << " ";
cout << array[i];
}
cout << endl;
return count;
}
原理:
- 如果一个数num的二进制最后一位为1,那么 num&1 = 1,否则为0
- 我们判断一个数有多少个1,就是不断的挪动这个数二进制的每一个位置,不断&1
总结一下我们就只需要不断的判断与1是否为1,是的话就计数,每次循环结束前,右移一位
实际数据:
// a = 5
// 00000000 00000000 00000101
// 1
// 00000000 00000000 00000001
// a & 1 = 1
// 00000000 00000000 00000001
// a = (a>>1)
// 00000000 00000000 00000010
// a & 1 = 0
// 00000000 00000000 00000000
3.3.有40亿个无序且不重复的无符号的整数,如何快速判断这个数是否在那40亿个数当中
题目解析:40亿个数据,我们如果存储在普通的数据结构中,遍历的话时间复杂度就是O(logN)到O(N),这个时间消耗是非常大的,而且需要开辟大量的空间,并且这些数据也是无序的,寻常的方法就需要先排序后遍历!
方法一:二分查找,我们通过二分查找的时间复杂度是O(logN),也就是2^N = 40亿,很容易解出来N=32次,也就是我们只用32次二分查找就能找到这个数字,这时空间消耗也是特别大的,计算如图:
这种方法内存消耗过大,因而不太合适
方法二:通过比特位映射,我们知道一个bit表示0或者1,我们可以通过40亿个比特位映射40亿个数据,如果这个数据存在,那我们就在40亿个比特位中对应的那个位置计为1,反之计为0. 接着我们通过位运算符来进行操作实现判断!!!我们再来计算一下空间消耗:
因此第二种方案是可行的!!!那么接下来我们对第二种方法进行实现:
原理:
当我们抽象出这个模型时大概就知道怎么操作了!
- 因为0到2^32次方比特位,对应0到2^32次方个数,当我们需要查找一个数num时我们如何查找?首先每个整型表示着32个比特位,对于0来说他在1*32中,对于1来说他来2*32中,那么对于num来说他在num/32个位置
- 当我们查找到num在第几个整型时,因为一个数据映射在对应整型位置的32个比特位中的一个上,那么在第几个我们就可以通过 num%32来找到
- 我们通过1,2就能找到任意一个数的映射位置了,因为1个比特位表示着0或1,我们将这个位置设为1表示存在,设为0表示不在。需要只把这一个2^32个比特流的第num/32个整型的第num%32个位置设为1.(只改一个位置)
将原理实现一下:
namespace zhong
{
// N为非类型模版参数
template<size_t N>
class bitset
{
public:
// N为比特位个数
bitset()
{
// 开辟空间大小,所有整型均设为0
_bits.resize(N / 32 + 1, 0);
}
// 把对应比特位的数据置为1
void set(size_t x)
{
// 找到数组中的第几个整型
size_t i = x / 32;
// 找到这个整型下的第几个比特位
size_t j = x % 32;
// 将1左移j位置,到达映射位,通过 或 可以把这一位变为1其他位不变
// 因为任何数 或1 都是 1, 1左移j位其他位置全为0,不会对其他位影响
_bits[i] |= (1 << j);
}
// 删除这个值
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
// 这里我们通过取反,使得第j位为0,其他位为1
// 接着与一下,把对应映射的第j位变成0,其他位置因为是1,与 后均为本身
_bits[i] &= ~(1 << j);
}
// 查找这个值在不在
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
// 将1左移到对应位置与一下就好了
if (_bits[i] & (1 << j))
return true;
else
return false;
}
private:
vector<int> _bits;
};
}
void test()
{
zhong::bitset<100> bs;
bs.set(66);
bs.set(88);
cout << bs.test(66) << endl;
cout << bs.test(88) << endl;
bs.reset(88);
cout << endl;
cout << bs.test(66) << endl;
cout << bs.test(88) << endl;
}
我们在STL库中有位图这一个容器,底层实现原理如上所述。
我们在C++学习进阶:哈希思想的进一步体现-CSDN博客 中会对具体实现进行讲解。
这道题的代码:
void find_mass_data()
{
// 文件操作将40亿个数据写入array中
// 这里仅做模拟
int array[] = {111,33,21,45};
// 开辟42亿个比特位的位图
bitset<0xffffffff> bs;
for (auto e : array)
{
// 通过set实现映射
bs.set(e);
}
int x = 9999999999;
// 实现O(1)复杂度查找
bool ifExit = bs.test(x);
}
注意这一块代码也只是模拟实现而已,具体思路就是这样,具体的解答应该根据具体场景……