『每日算法 · 基础知识篇』备战面试,坚持算法 第二话——异或运算!


前言

大多数人认为异或运算只是数学上的概念,在程序编写方面只不过是两个符号而已。

在关于算法上面就会懒得去练习异或,看见相关题目课程也会选择性的跳过,其实这只是因为你并不了解异或真正的作用,当然你也就不会知道它真正的用法。

接下面由本篇文章,带你了解异或以及它的用法!

一、认识异或运算

异或运算:相同为0,不同为1,
同或运算:相同为1,不同为0,

这个不好进行记忆,我们一般记作:异或运算是无进位相加,所以我们也称异或运算为半加运算

异或运算的运算性质

任何数与0异或结果为任何数,如下:
0 ^ N = N

任何数与它本身异或结果为0,如下:
N ^ N = 0

异或运算满足交换律,如下:
c =a ^ b =b ^ a

异或运算满足结合律,如下:
c =(a ^ b) ^ c = a ^ (b ^ c)

二、异或运算相关练习

光说不练假把式,异或运算更重要的还是在实际算法中更够应用到,接下来的这几个题目在面试中也是常考重点!

题目一

如何不用额外变量交换两个数

我们一般交换两个变量都是会申请一个额外空间来当作中转站进行临时存储,但是我们使用到位运算之后就可以不需要申请额外空间来进行两数交换。

代码如下:

public static void swap (int[] arr, int i, int j) {
    
    
	arr[i]  = arr[i] ^ arr[j];
	arr[j]  = arr[i] ^ arr[j];
	arr[i]  = arr[i] ^ arr[j];
}

不理解的话可以使用m,n分别带入arr[i],arr[j],如下:

初始条件:arr[i]=m,arr[j]=n,接下来走一遍swap函数的流程:

  1. 将初始条件带入arr[i] = arr[i] ^ arr[j],得:
    arr[i] = m ^ n

  2. 将上一步结果带入arr[j] = arr[i] ^ arr[j],得:
    arr[j] = m ^ n ^ n = m ^ 0 = m

  3. 将上一步结果带入arr[i] = arr[i] ^ arr[j],得:
    arr[i] = m ^ n ^ m = m ^ m ^ n = 0 ^ n = n

最后结果arr[i]=n,arr[j]=m,完成交换!

题目二

一个数组中有一种数出现了奇数词,其他数都出现了偶数次,怎么找到并打印这种数

这个题我们可以使用异或运算的运算性质:任何数与它本身异或结果为0,任何数与0异或结果为任何数。

又因为它可以使用交换律,我们可以调换出现偶数次的数字位置,让他们先相互抵消,最后异或结果就是出现奇数次的那个数了,如下:

变换前:2 ^ 3 ^ 3 ^ 5 ^ 2 ^ 5 ^ 3 ^ 6 ^ 3
变换后:2 ^ 2 ^ 3 ^ 3 ^ 3 ^ 3 ^ 5 ^ 5 ^ 6

代码如下:

public static void printOddTimesNum1(int[] arr) {
    
    
	int eor = 0;
	for (int i = 0; i < arr.length; i++) {
    
    
		eor ^= arr[i];
	}
	System.out.println(eor);
}

题目三

怎么把一个int类型的数,提取出最右侧的1来

这个题目看似没用但其实非常有用,不管有没有用,关键是你得会,这个题也为接下来的题打基础的。

这里抽出最右侧的1,也是数字对应的二进制序列的最右侧的1,所以我们依然使用位运算的得到最右侧的1。

先说一下题意:
原来的二进制序列为:0000 1010 0101 0000
我们要得到的结果为:0000 0000 0001 0000

思路就是:先给原来的二进制取反,然后加 1 得到一个新的二进制序列,然后让这个新的二进制序列与原来的二进制序列按位与即可,演示如下:

原来的序列:0000 1010 0101 0000
取反的序列:1111 0101 1010 1111
取反加1的序列:1111 0101 1011 0000
按位与之后的序列:0000 0000 0001 0000

可见最后结果是我们预想的那个结果,接下来使用代码实现这个思路,代码如下:

int rightOne = N & ((~N) + 1);

这里取反加1其实就是变成这个数的相反数,所以代码也可以变成:

int rightOne = N & (-N);

题目四

一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数

这个题我们使用题目二的思路,可以得到这两个出现奇数次的异或结果,到这一步我们只需要把这两个数分离出来即可

怎么分离现在就成为了关键之处当然我们继续使用位运算,在题目三之中我们能提取出来一个数的最右侧的 1,而两个数异或之时只有对应位的数不同才会出现异或之后对应位的结果为1

所以这两个出现奇数次的异或结果的最右侧的 1 的对应位置上这两个数肯定其中一个是1,另外一个不是1

我们在知道这个条件之后,对数组中所有的数按条件进行异或,这样就可以分离出来这两个数了。

代码如下:

public static void printOddTimesNum2(int[] arr) {
    
    
    int eor = 0;
    for (int i = 0; i < arr.length; i++) {
    
    
        eor ^= arr[i];
    }
    // eor是a和b是两种数的异或结果
    // 提取出eor最右侧的1
    int rightOne = eor & (-eor); 
    //其中一个结果
    int onlyOne = 0; 
    //第二次按条件进行异或,只有符合条件的数字才会进行异或
    for (int i = 0; i < arr.length; i++) {
    
    
        // arr[1]  =  111100011110000
        // rightOne=  000000000010000
        if ((arr[i] & rightOne) != 0) {
    
    
            onlyOne ^= arr[i];
        }
    }
    //eor^onlyOne 可以得到另外一个结果
    System.out.println(onlyOne + " " + (eor ^ onlyOne));
}

题目五

一个数组中有一种数出现K次,其他数都出现了M次(M>1,K<M),找到出现K次的数,要求额外空间复杂度O(1),时间复杂度O(N)

这个题与之前的题有所不同,这个题思路是,我们使用标记数组,来统计所有数字二进制序列的每个个位置的 1 的个数,然后对每个位置的 1 与M进行求余,如果结果不等于0,就说明出现k次的那个数的二进制序列在这个位置是1。

接下来给32个位置都进行判断我们就可以知道出现K次的数字的二进制序列了。

代码如下:

public static HashMap<Integer, Integer> map = new HashMap<>();

public static int onlyKTimes(int[] arr, int k, int m) {
    
    
	if (map.size() == 0) {
    
    
		//初始化map
		mapCreater(map);
	}
	//标记数组,标记对应位置的1出现了几个
	int[] t = new int[32];
	for (int num : arr) {
    
    
		while (num != 0) {
    
    
			int rightOne = num & (-num);
			t[map.get(rightOne)]++;
			num ^= rightOne;
		}
	}
	int ans = 0;
	// 如果这个出现了K次的数,就是0
	// 那么下面代码中的 : ans |= (1 << i);
	// 就不会发生
	// 那么ans就会一直维持0,最后返回0,也是对的!
	for (int i = 0; i < 32; i++) {
    
    
		if (t[i] % m != 0) {
    
    
			ans |= (1 << i);
		}
	}
	return ans;
}

public static void mapCreater(HashMap<Integer, Integer> map) {
    
    
	int value = 1;
	for (int i = 0; i < 32; i++) {
    
    
		map.put(value, i);
		value <<= 1;
	}
}

总结

这些题目都是在面试中经常出现的相关题目,大家务必进行仔细揣摩,力求掌握!


本篇文章参考:

1.左神体系学习班课程

猜你喜欢

转载自blog.csdn.net/apple_51673523/article/details/126784279