位运算的常见操作符:
按位与 &
按位或 |
按位异或 ^
取反 ~
左移,右侧补0 <<
右移,左侧补符号位 >>
右移,左侧补零 >>>
位运算的面试题大部分靠平时积累,新题在面试场上较难想出解题思路。
布隆过滤器的原理和使用:
网页黑名单系统
垃圾邮件过滤系统
爬虫的网址判断重复系统
容忍一定程度的失误率
对空间要求比较严格(用哈希表或者数据库对数据进行搜索占用空间太大,不符合!!!)
以上情况很可能是考察布隆过滤器的内容!!
布隆过滤器可精确的代表一个集合;
可精确判断某一元素是否在此集合中;
精确程度由用户的具体设计决定;
做到百分之百的精确即正确是不可能的!!!(比如:允许有万分之一的失误率)
布隆过滤器的优势在于,利用很少的空间可以做到精确率较高!!!
布隆过滤器的原理:
k个哈希函数(优秀且彼此相互独立,输出域都大于等于m)
bitarray数组(bit类型的数组,长度为m,每个位置只占一个bit,只有0和1两种状态)
同一个输入对象经过k个哈希函数计算出的结果相互独立!!对算出来的每个结果都对m取余,取余的结果对bitarray相应的位置上置1!
如果某个位置上已经是1了,那么就不需要做什么。
当所有的输入对象都进行了上述过程之后,bitarray中的大部分应该都已经置1了!!这样一个布隆过滤器就生成了!!
检查一个输入是否加入过布隆过滤器:
将这个输入经过上述k个哈希函数进行计算,计算结果对m取余,然后在bitarray中查看相应位置是否都为1,如果有一个不为1,那么这个输入一定没有加入到过布隆过滤器!
但是,如果所有的都是1,那么这个输入就在这个集合中,但是这种情况是有可能发生误判的。
布隆过滤器的bitarray的大小的确定:
如果布隆过滤器的大小相对于样本的数量来说过小的话,那么失误的概率会变大。因为样本数量特别多,
所以有可能大部分的bitarray位置都置1了,所以很容易将一个没有加入过布隆过滤器的输入判定为加入过!!!
因此,布隆过滤器的大小m,是由样本的数量n以及我们想要的失误率p决定的。
设布隆过滤器大小m,样本数量为n,失误率为p,哈希函数数量为k。
那么
m=-(n*ln p)/pow(ln 2, 2),向上取整,之所以向上取整是因为这样可以比预期的失误率更低一些。
k=ln 2*(m/n)
需要注意的是单个样本的大小不影响布隆过滤器的大小,只影响了哈希函数的实现细节。
同时,哈希函数的计算结果应该保证大于m,因为计算出的结果要对m取余。
真正的失误率的计算公式应该是:
pow(1-pow(e,(-n*k/m)),k)
总结生成布隆过滤器的过程:
1. 注意到题目允许有一定程度的失误率。
2. 根据样本个数n,和允许的失误率p, 结合一下公式:
m=-(n*ln p)/pow(ln 2, 2)求出m。(向上取整)
3. 根据已经求得的m,以及一下公式:
k=ln 2*(m/n)=0.7*(m/n);(向上取整)
求得哈希函数个数k。
4. 当然,由于求m的时候可能向上取整了,所以失误率可能不是p,因此可以用下面的公式重新验证
一下p:
pow(1-pow(e,(-n*k/m)),k)
案例一:
如何不用任何额外变量交换两个整数的值?
a=a^b;
b=a^b;
a=a^b;
对于上面的原理的证明如下:
第一步:a=a^b b=b
第二步: b=a^b=a^b^b=a a=a^b
第三步:a=a^b=a^b^a=b b=a
所以此时a和b正好交换了。
异或运算的性质:
异或的自反性:a^b^b=a^0=a;
异或的结合律:(a^b)^c=a^(b^c);
异或的交换律:a^b=b^a;
异或运算简单的理解就是不进位的加法,例如:
1+1=0;0+0=0;1+0=1;
特殊性质:x^x=0; x^0=x;x^x^x=x;
实现代码如下:
class Swap {
public:
vector<int> getSwap(vector<int> num) {
// write code here
num[0]=num[0]^num[1];
num[1]=num[0]^num[1];
num[0]=num[0]^num[1];
return num;
}
};
案例二:给定两个32位整数a和b,返回a和b中较大的。但是不能用任何比较判断!
方法一:得到a-b的符号,根据该符号决定返回a或者b。
下面的程序中,如果n为整数,sign函数返回1;n为复数,sign函数返回0。
public static int flip(int n){
return n^1;
}
public static int sign(int n){
return flip((n>>31)&1);
}
public static int getMax1(int a,int b){
int c=a-b;
int scA=sign(c);
int scB=flip(scA);
return a*scA+b*scB;//如果c为正数,那么scA就是1;scB就是0!!那么返回的就是a
}
但是上面的方法可能会有问题,当a-b溢出时,会发生错误!!
方法二:根据提前判断两个数符号是否相同来避免溢出的错误判断。符号相同的两个数相减,
结果一定不会溢出;符号不同的数相减,才有可能溢出。因此当符号不同的时候不用a-b的值来判断,而是根据他们的符号来判断:
public static int getMax2(int a,int b){
int c=a-b;
int as=sign(a);//a的符号,as=1表示a为非负数,as=0表示a为负数
int bs=sign(b);//b的符号
int cs=sign(c);//a-b的符号
int difab=as^bs;//表示a和b是否符号不相同,不相同为1,相同为0
int sameab=flip(difab);//表示a和b是否符号相同,相同为1不同为0
int returnA=difab*as+sameab*cs;
int returnB=flip(returnA);
return a*returnA+b*returnB;
}
class Compare {
public:
int getMax(int a, int b) {
// write code here
int c=a-b;
bool flag=samesign(a,b);
//如果flag为true表示a和b符号相同,否则他们就是异号的
//符号不同的数相减的结果有可能溢出
if(flag){
return c>0?a:b;
}else{
if((a>>(31))==1)
return b;
else
return a;
}
}
bool samesign(int a,int b){
int c=a>>(31);
int d=b>>(31);
if(c==d)
return true;
else
return false;
}
};
对于上面的实现来说,因为防止a和b相减结果溢出,所以只有在符号相同的时候才用c来比较;否则比较的是它们的符号。
案例三:给定给一个整形数组arr,其中只有一个数出现了奇数次,其他的数都出现了偶数次,请打印这个数。要求时间复杂度为O(N),额外空间复杂度为O(1).
因为
n^0=n
n^n=0
异或运算满足交换律;
异或运算满足结合律;
所以按照原始的arr数出现的顺序异或的结果,与将arr中出现偶数次的同一个数先异或,再和出现奇数次的数
进行异或的结果是相同的。
所以令E=0,然后将E和arr中的所有数依次异或,最后的结果就是出现奇数次的那个数。
class OddAppearance {
public:
int findOdd(vector<int> A, int n) {
// write code here
int eo=0;
for(int i=0;i<n;i++){
eo=eo^A[i];
}
return eo;
}
};
案例四:给定给一个整型数组arr,其中有两个数出现了奇数次,其他的数都出现了偶数次,打印这两个数。
要求时间复杂度为O(N),额外空间复杂度为O(1)。
1. 整数n与0的异或结果为n。
2, 整数n与自己异或结果为0。
eo=0,arr中a和b出现了奇数次,剩下的数都出现了偶数次。
eo与arr中所有的数异或完成后,eo=a^b,因为a和b为不同的数,所以eo不为0,。
eo中必定有一位为1,假设第k为为1的话,那么说明a和b的第k位一定不一样。
设置新整数eo'=0.
eo'与arr中第k为为1的那些整数进行异或,异或完成后,eo'为a或b中的一个,
另一个数等于eo'^eo。
class OddAppearance {
public:
vector<int> findOdds(vector<int> arr, int n) {
// write code here
vector<int> result;
int eo=0;
for(int i=0;i<n;i++){
eo=eo^arr[i];
}
int temp=eo;
//下面应该找到eo中为1的某一位
int count=0;
while(eo!=0){
if(eo%2==1)
break;
eo=eo>>(1);
count=count+1;
}//也就是说从右边数第count+1位是1,count表示需要移动的次数
//下面就要找到arr中从右边数第count+1位是1的数
int eo1=0;
for(int j=0;j<n;j++){
if((arr[j]>>(count))%2==1)
eo1=eo1^arr[j];
}
//eo1代表的是其中一个奇数
eo=temp^eo1;
//eo代表的是另一个奇数
if(eo>eo1){
result.push_back(eo1);
result.push_back(eo);
}else{
result.push_back(eo);
result.push_back(eo1);
}
return result;
}
};
对于上面的实现方式来说,找为1的位比较麻烦,为了简化算法,可以参考下面的实现方式:
public class OddAppearance { public int[] findOdds(int[] arr, int n) { int eO = 0, eOhasOne = 0; for (int curNum : arr) { eO ^= curNum; } int rightOne = eO & (~eO + 1); for (int cur : arr) { if ((cur & rightOne) != 0) { eOhasOne ^= cur; } } int small = Math.min(eOhasOne, (eO ^ eOhasOne)); int big = Math.max(eOhasOne, (eO ^ eOhasOne)); return new int[] { small, big }; } }上面的方式中,
eO & (~eO + 1) 左边的表达式求的就是eo的从最右边开始的第一个为1的位m为1,其他位都是0的数。这样的话,别的数和这个数与操作之后,凡是不为0的,那么它们从右边数第m位一定都是1!!!
案例五:请设置一种加密过程,完成对明文text的加密和解密工作。
异或运算可完成简单的加密与解密过程。
明文text,用户给定的密码pw,假设密文为cipher,
cipher=text^pw
text=cipher^pw=(text^pw)^pw=text^(pw^pw)=text。
如果text长度大于pw,循环使用pw与text进行按位异或。