- 面试题
- 技巧
- 位运算(以前用过)与异或运算(魔性)
定义与性质
- 异或运算:相同为0,不同为1
- 同或运算:相同以1,不同为0.
(能长时间记住的概率接近0%)
- 异或运算就记成**无进位相加**!(相加不进位)
- 性质:
- 0^N=N
- N^N=0
- A^B=B^A===>交换律
- (A^B)^C=A^(B^C)===>结合律
- 结合c和d,同样一批数,不管选择什么样的顺序,最终结果应该是一样的
- 无进位相加在多个数的情况下也适用,并且多个数无进位相加改变顺序不改变结果(偶数个1是0,奇数个1是1)
- 最根本的性质为无进位相加这一个性质
- 用途、作用、题目:
- 如何不用额外变量交换两个数?
- 一个数组中有-一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数
- 怎么把一个int类型的数,提取出最右侧的1来
- 一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
- 一个数组中有一种数出现K次,其他数都出现了M次,(M>1,K<M)找到出现了K次的数,。要求:额外空间复杂度O(1),时间复杂度O(N)。
如何不用额外变量交换两个数(额外变量+求和交换+异或交换)
- 综合运用异或的性质
- 主要使用N^N=0和0^N=N
- 异或运算的效率高于加减法的效率
- 位运算效率最高!!!
- 证明:
- ❗注意点:只有在a和b指的是**两块不同的内存区域**时,这种交换的方法才会生效;假如在一个内存位置进行交换,会将这个内存位置刷为0;一直是同一个变量在与自己做异或运算并赋值给自己(即第一步将自己变为0,后两部都为0^0=0)
- 有时候不要装逼,要注意使用条件,老老实实写交换;要上游可以保证不是对同一内存进行交换才可以这么用,不能保证最好不要这么用;拿捏不住尺寸,最好还是老老实实写原来的交换
- 在java中这么写其实没什么好处,但面试官可能会问这种恶心的,自己当了面试官不要这么问🤔
- 底层程序员、写操作系统的程序员、用汇编语言的程序员会经常用到这种操作!!!
- 底层一般全是位运算,上层程序员很少用(同事看不懂就离谱)(编译器优化?),注重性能的地方一般这么用
- 脚本语言或者一些优化过的语言有可能位运算和加减运算一样快也是有可能的!
a = a ^ b; //int tmp = a;
b = a ^ b; //a = b;
a = a ^ b; //b = tmp;
一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数
- 哈希表词频统计,每种词记录出现的频数===>装逼失败,优化空间复杂度===>用一个变量找到
- 异或运算处理:初始化一变量eor=0,一次把所有值都与eor异或起来,最终的eor就是我们要求的结果(即奇数次的数字)
- 使用了异或运算的交换律和结合律:
- 偶数个同一数据异或起来结果为0
- 奇数个同一数据异或起来结果为该数据本身
- 不是装逼,这种方法确实好,只要一个变量即可(额外空间复杂度降为O(1)===>常数空间)
怎么把一个int类型的数,提取出最右侧的1来(常规操作)
- 这是一个常规操作,虽然现在看起来蛋疼,但是这种方式是大量使用到的
- 题意:将该数写成二进制的形式,只保留住最后一位1,其余为都变为0,这就是想要的数
- 解答:结果=a&(-a)=a&(~a+1)
- 原理1(补码角度):从补码的角度考虑,对a取反加1就是求a的相反数的补码(取反包括了符号位,不包括符号位就是a的补码===>正数的补码是本身,负数的补码……);又因为计算机中整数以补码的形式存在,并且取反包括了符号位,所以能够在相与的过程中将前面的位都置为0,而最后一位1因为加了1所以仍旧保持1===>负数补码的便捷求法:符号位保持为1,保持最后一位1不变,将除最后一位1所在位之前的所有位都取反
- 原理2(直观角度):取反的时候将最后一位1的左边的每一位都取反了,最后一位1会被搞成0,最后一位1右侧所有的0都会变为1===>相当于最后一位1左侧都取相反状态,并且把最后一位1打散了(拆散:最后一位1会被搞成0,最后一位1右侧所有的0都会变为1,用0进行分隔);对打散的状态再加个1还原回来(拧回来,合回来);最后一“与”就得到了结果(最后一位1)
- 在机器中整数是按补码形式存储的,正数的补码是他本身,负数的补码是将除符号位外的部分取反再整体加1
一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
- 两种数出现了奇数次,因为是两种数,所以这两种数一定不一样
- 将所有的数与到eor上去,结果为eor=a^b;
- 因为a和b不相等,所以eor≠0;===>找eor中找为1的位置(哪个都行)===>刚才求得了最右的1(提取出一个数最右侧的1)
- 为1的位置说明a的该位置和b的该位置不一样
- 将整个数组分为如下两类(对于arr中所有的数):
- 上述4中位置上为1的数;
- 上述4中位置上为0的数;
- 设a的位置为1,b的位置为0;还是a的位置为0,b的位置为1;这两种都是一样的,反正这两种必然分开
- 这两类中包括那些出现了偶数次的数
- 再设一变量eor’,eor’=上述5中两类中某一类中的元素异或起来的值===>这样就得到了两个出现奇数次的数的其中一个(把其中一个给拽出来了)
- 最后再将eor^eor’,结果就为另一出现奇数次的数
- 这样两个数都被得到了
- 通过异或之后的某一位为1的位将数组分类,其中两个出现奇数次的数分别位于这两类中,并且其余的数也被分配到这两类中去了且都为偶数个,不会对结果产生影响===>最根本的目的就是为了将出现奇数次的两个数分离开来(分离为1不为1的目的就在于此,不是为了避免同一位上不同造成的影响—->这没有影响)
- 正负数都一样===>因为用补码表示,可以统一计算!
- eor’中的’在java中不合法
- 假设a为小的那个,b为大的那个该怎么比===>直接用大于号和小于号?
- 举例:a=6,b=10
package class01;
public class Code07_EvenTimesOddTimes {
// arr中,只有一种数,出现奇数次
public static void printOddTimesNum1(int[] arr) {
int eor = 0;
for (int i = 0; i < arr.length; i++) {
eor ^= arr[i];
}
System.out.println(eor);
}
// arr中,有两种数,出现奇数次
public static void printOddTimesNum2(int[] arr) {
int eor = 0;
for (int i = 0; i < arr.length; i++) {
eor ^= arr[i];
}
// eor = a ^ b ===> a和b是两种数
// eor != 0
// eor必然有一个位置上是1
// 0110010000
// 0000010000
int rightOne = eor & (~eor + 1); // 提取出最右的1
int onlyOne = 0; // eor'
for (int i = 0 ; i < arr.length;i++) {
// arr[1] = 111100011110000
// rightOne= 000000000010000
if ((arr[i] & rightOne) != 0) {
onlyOne ^= arr[i];
}
}
System.out.println(onlyOne + " " + (eor ^ onlyOne));
}
// 该方法用于统计N的二进制中1出现的次数(个数)
public static int bit1counts(int N) {
int count = 0;
// 011011010000
// 000000010000 1
// 011011000000
//
while(N != 0) {
int rightOne = N & ((~N) + 1);
count++;
N ^= rightOne;
// N -= rightOne
}
return count;
}
public static void main(String[] args) {
int a = 5;
int b = 7;
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println(a);
System.out.println(b);
int[] arr1 = { 3, 3, 2, 3, 1, 1, 1, 3, 1, 1, 1 };
printOddTimesNum1(arr1);
int[] arr2 = { 4, 3, 4, 2, 2, 2, 4, 1, 1, 1, 3, 3, 1, 1, 1, 4, 2, 2 };
printOddTimesNum2(arr2);
}
}
一个数组中有一种数出现K次,其他数都出现了M次,(M>1,K<M)找到出现了K次的数。要求:额外空间复杂度O(1),时间复杂度O(N)。
- 额外空间复杂度O(1)的意思是说用有限几个变量就能做出来,而不是只能用一个变量
- 任何一个数都能转换为数组形式的二进制状态===>用长度为32的数组额外空间复杂度依旧为O(1)
- 负数没影响=>因为我们是用状态信息来搞的
- 思路:
- 定义一个长度为32的数组(存放所有数中各位置为1出现的次数)===>int类型,假如词频统计过多也可以使用long类型
- 将每一个数(int类型)都转换为数组形式的二进制状态并将其加到a中定义的数组中去
- 对所定义的数组进行遍历
- 假如某一位可以整除M,则出现K次的那个数该位置上为0
- 假如某一位不可以整除M,则出现K次的那个数该位置上为1
- 因为有M>1与K<M的限制,所以不存在K是M的倍数的关系;并且一个数出现了K次,多个数出现了M次,所以一共有多个数的个数*M+K个数
- 代码中虽然有一个双层循环,但是内层循环是固定为32次的,只有外层循环需要根据数组大小而产生变化,所以最高阶依然是n,去掉常数后依旧为O(n)
- 先开始通过&分离每一位,最后通过|设置进去(相加也行,因为每一位只操作一次并且都是加1,不会出现进位)
- 用对数器能够最快提高自己的能力===>用对数器做测试,有oj用oj
package class01 ;
public class Code08_ _KM {
// 使用对数器验证代码是否正确
// 对数器的代码想写多烂就写多烂,只要能保证对就行,不对也可以调
// 先写经典解,用hash表法进行解题:统计词频
public static int test(int口 arr, int k, int m) {
HashMap<Integer, Integer> map = new HashMap<>O);
for (int num : arr) {
if (map.containsKey(num)) {
map.put( num, map. get(num) + 1);
} else {
map.put(num, 1);
}
}
for (int num : map.keySet()) { // 获取键的集合
if (map.get(num) == k) { // 看键所对应的值是否为K
return num;
}
}
return -1;
}
//请保证arr中,只有一种数出现了K次,其他数都出现了M次
public static int onlyKTimes(int[] arr, int k, int m) {
int[] t = new int[32];
// t[0] 0位置的1出现了几个
// t[i] i位置的1出现了几个
for (int num : arr) {
for(inti=0;i<=31;i++){
t[i]+=(num>>i)&1;
}
}
int ans = 0;
for(int i=0; i<32; i++){
if (t[i] % m != 0){ //在第i位上,有1
ans|=(1 << i);
}
return ans;
}
// 随机生成数组
public static int[] randomArray(int maxKinds, int range, int k, int m) {
int ktimeNum = randomNumber(range);
// 2===>至少两种数
int numKinds = (int)(Math.random() * maxKinds) + 2;
//k * 1 + (numKinds - 1) * m===>总个数(数组长度)
int[] arr = new int[k + (numKinds - 1) * m];
int index = 0;
for( ;index < k;index++) {
arr[index] = ktimeNum;
numKinds--;
}
numKinds--;
HashSet<Integer> set = new HashSet<>() ;
set.add(ktimeNum);|
while(numKinds != 0) {
int curNum = 0;
do {
curNum = randomNumber(range);
}while(set.contains(curNum));
set.add(curNum);
numKinds-- ;
for(inti=0; i<m; i++){
arr[index++] = curNum;
}
}// arr填好了
// 打乱数组(太有顺序了)
for (int i = 0; i < arr.length; i++) {
// i位置的数,我想随机和j位置的数做交换
int j = (int)(Math.random() * arr.length);// 0 ~ N-1
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
return arr;
}
// 题目升级===>出现了k次就返回出现了k次的数,没有出现k次就返回-1
public static int onlyKTimesPlus(int[] arr, int k, int m) {
int[] t = new int[32];
// t[0] 0位置的1出现了几个
// t[i] i位置的1出现了几个
for (int num : arr) {
for(inti=0;i<=31;i++){
t[i]+=(num>>i) & 1;
}
}
int ans = 0;
for(int i=0; i<32; i++){
// 题目升级===>出现了k次就返回出现了k次的数,没有出现k次就返回-1
if(t[i] % m == 0) {
continue;
}
if(t[i] % m == k) {
ans |= (1 << i);
}else {
return -1;
}
/**
错误写法
if (t[i] % m != 0 && t[i] % m == k) {
ans |= (1 << i);
} else {
return -1;
}
*/
}
return ans;
}
// 随机生成数组
public static int[] randomArrayPlus(int maxKinds, int range, int k, int m) {
int ktimeNum = randomNumber(range);
//真命天子出现的次数
// 以0.5(50%)的概率决定是k次,还是一个随机次的但是一定比m小的次数
int times = Math.random() < 0.5 ? k : ((int)(Math.random() * (m - 1)) + 1);
// 2===>至少两种数
int numKinds = (int)(Math.random() * maxKinds) + 2;
//k * 1 + (numKinds - 1) * m===>总个数(数组长度)
// 参数k没用了
int[] arr = new int[times + (numKinds - 1) * m];
int index = 0;
for( ;index < k;index++) {
arr[index] = ktimeNum;
numKinds--;
}
numKinds--;
HashSet<Integer> set = new HashSet<>() ;
set.add(ktimeNum);|
while(numKinds != 0) {
int curNum = 0;
do {
curNum = randomNumber(range);
}while(set.contains(curNum));
set.add(curNum);
numKinds-- ;
for(inti=0; i<m; i++){
arr[index++] = curNum;
}
}// arr填好了
// 打乱数组(太有顺序了)
for (int i = 0; i < arr.length; i++) {
// i位置的数,我想随机和j位置的数做交换
int j = (int)(Math.random() * arr.length);// 0 ~ N-1
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
return arr;
}
// [-range, +range]
public static int randomNumber(int range) {
return ((int)(Math.random() * range) + 1) - ((int)(Math.random() * range) + 1);
}
public static void main(String[] args) {
//int[] arr={4,3,1,3,3,1,1,4};
//int k=2;
//int m=3;
//System.out.println(onlyKTimes(arr, 2, 3));
// int kinds = 100;
int kinds = 4;
Lnt range = 200;
int testTime = 100000;
int max = 9;
System.out.print1n("测试开始");
for (int i = 0; i < testTime; i++) {
int a = (int)(Math.random() * max) + 1; //a 1~ 9
int b = (int)(Math.random() * max) + 1;//b1~9
// 让随机生成的两个数其中小的为k,大的为m
int k = Math.min(a, b);
int m = Math.max(a, b);
//k<m
if (k == m) {
m++; // 保证m比k大
}
int[] arr = randomArray(kinds, range, k, m);
int ans1 = test(arr, k, m);
int ans2 = onlyKTimes(arr, k, m);
if(ans1 != ans2) {
System.out.println("出错了! ");
}
}
System.out.print1n("测试结束");
}
}
- 用哈希表的方法时复杂度是O(n),而用定义的长度为32的数组时复杂度为O(1)
- 通过对数器发现,上述代码中的onlyKTimesPlus方法在特殊情况时,仍旧有问题,边界情况仍需仔细考虑:
- 因为t二进制形式的数组初始化为全0,在遇到出现k次的数恰好是0时,就会出现问题;会造成0与其他出现m次的数据进行相同的处理(即continue忽略掉),会不断地触发continue===>因此会漏掉出现k次的刚好为0这种情况。
- 初始化为-1也不行,因为都是为1,这样或的时候就没有意义了;倒是可以考虑与0与一下,将能被m整除的位置上的和0与一下(随机应变===>实现同一种功能,代码有很多种写法)
- 有可能有的情况下就是写不出完美的代码的,就是需要打补丁才行!(虽然不好看,但是依然完成了任务)
- 解决方案:最后ans=0时检查一下,是不是0出现了k次(有更好的方式,单独遍历一回也没事)
}
// 题目升级===>出现了k次就返回出现了k次的数,没有出现k次就返回-1
public static int onlyKTimesPlus(int[] arr, int k, int m) {
int[] t = new int[32];
// t[0] 0位置的1出现了几个
// t[i] i位置的1出现了几个
for (int num : arr) {
for(inti=0;i<=31;i++){
t[i]+=(num>>i) & 1;
}
}
int ans = 0;
for(int i=0; i<32; i++){
// 题目升级===>出现了k次就返回出现了k次的数,没有出现k次就返回-1
if(t[i] % m == 0) {
continue;
}
if(t[i] % m == k) {
ans |= (1 << i);
}else {
return -1;
}
}
if(ans == 0){
int count = 0;
for (int num : arr) {
if(num == 0){
count++ ;
}
}
if (count != k)
return -1;
}
return ans;
}