剑指offer(第二版)——数组中只出现一次的两个数字

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神,以及其他的博客,在之后的每个例题详解后都会给出参考的思路或者代码链接,同学们都可以点进去看看!

本例题参考:

  1. https://www.jianshu.com/p/1a9997924cc6

本文如有什么不足或不对的地方,欢迎大家批评指正,最后希望能和大家一起交流进步、拿到心仪的 offer !!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值