PS:《剑指offer》是很多同学找工作都会参考的一本面试指南,同时也是一本算法指南(为什么它这么受欢迎,主要应该是其提供了一个循序渐进的优化解法,这点我觉得十分友好)。现在很多互联网的算法面试题基本上可以在这里找到影子,为了以后方便参考与回顾,现将书中例题用Java实现(第二版),欢迎各位同学一起交流进步。
GitHub: https://github.com/Uplpw/SwordOffer。
剑指offer完整题目链接: https://blog.csdn.net/qq_41866626/article/details/120415258
1 题目描述
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
leetcode链接: 数组中只出现一次的两个数字(以下代码已测试,提交通过)
2 测试用例
一般是考虑功能用例,特殊(边缘)用例或者是反例,无效测试用例这三种情况。甚至可以从测试用例寻找一些规律解决问题,同时也可以让我们的程序更加完整鲁棒。
(1)功能用例:数组正常,包含1个只出现一次的数字。
(2)边缘用例:单个元素数组。
(3)无效用例:数组为空。
3 思路
分析:
如果不要求空间复杂度,可以利用哈希表解决问题(时间复杂度O(n),空间复杂度O(n))。但是要求不能使用额外空间,只能从其他角度分析问题。
注意:自身与自身异或是0,0与任何数字异或还是该数字,某个数字与其相反数进行与操作可得最低为为1的值(如4 (0100)& -4 =4)
利用异或操作的特点,可以有以下解决问题过程:
(1)首先,这道题目可以看成数组中只出现一次的一个数字的延伸。如果所有数字都出现两次,只有一个数字是出现1次,那么可以通过把所有所有进行异或运算解决。因为x^x = 0。
(2)但如果有两个数字出现一次,依旧把所有数字异或,最终的结果就是那两个出现一次的数字a,b异或的结果。下面是重点:
-
因为a,b不想等,因此结果肯定不为0,那么结果的二进制表示至少有一位为1,找到那个1的位置p,然后我们就可以根据第p位是否为1将所有的数字分成两堆,这样我们就把所有数字分成两部分,且每部分都是只包含一个只出现一次的数字、其他数字出现两次。完成问题的转化,从而解决该问题。
实例分析(以2,4,3,6,3,2,5,5为例):
相关数字的二进制表示为: 2 = 0010 3 = 0011 4 = 0100 5 = 0101 6 = 0110 步骤1 全体异或:2^4^3^6^3^2^5^5 = 4^6 = 0010 步骤2 确定位置:对于0010,从右数的第二位为1,因此可以根据倒数第2位是否为1进行分组 步骤3 进行分组:分成[2,3,6,3,2]和[4,5,5]两组 步骤4 分组异或:2^3^6^3^2 = 6,4^5^5 = 4,因此结果为4,6。
4 代码
算法实现:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class NumberAppearOnce {
// 1. hashmap
public static int[] singleNumbers(int[] nums) {
if (nums == null || nums.length == 0) {
return new int[0];
}
int[] array = new int[2];
Map<Integer, Integer> map = new HashMap<>();
int length = nums.length;
for (int i = 0; i < length; i++) {
if (map.containsKey(nums[i])) {
map.put(nums[i], -1);
} else {
map.put(nums[i], 1);
}
}
Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator();
int index = 0;
while (it.hasNext()) {
Map.Entry<Integer, Integer> entry = it.next();
if (entry.getValue() == 1) {
array[index++] = entry.getKey();
}
if (index >= 2) {
break;
}
}
return array;
}
// 2. 利用异或
public static int[] singleNumbers2(int[] nums) {
if (nums == null || nums.length == 0) {
return new int[0];
}
int temp = nums[0];
int length = nums.length;
for (int i = 1; i < length; i++) {
temp = temp ^ nums[i];
}
int[] result = new int[]{0, 0};
//注意:自身与相反数与操作可以得到最低位1、其他位为0代表的数字,所以再与其他数字与操作,同位不为1结果为0(快捷方法)
int index = temp & -temp;
for (int i = 0; i < length; i++) {
// 同位不是1
if ((index & nums[i]) == 0){
result[0] = result[0] ^ nums[i];
} else{
result[1] = result[1] ^ nums[i];
}
}
return result;
}
public static void main(String[] args) {
int[] data = new int[]{1, 2, 5, 2};
int[] result1 = singleNumbers(data);
int[] result2 = singleNumbers2(data);
for (int i = 0; i < result1.length; i++) {
System.out.print(result1[i] + "\t");
}
System.out.println();
for (int i = 0; i < result2.length; i++) {
System.out.print(result2[i] + "\t");
}
System.out.println();
}
}
参考
在解决本书例题时,参考了一些大佬的题解,比如leetcode上的官方、K神,以及其他的博客,在之后的每个例题详解后都会给出参考的思路或者代码链接,同学们都可以点进去看看!
本例题参考:
本文如有什么不足或不对的地方,欢迎大家批评指正,最后希望能和大家一起交流进步、拿到心仪的 offer !!!