剑指-56 找出不重复的数组元素


前言

今天又做了一道经典的题目[看了一道经典的题解],忍不住记录一波


Get点:
(1)为了反正整形溢出,求二分时使用 (high + (low - high) ) >>> 1;
(2)求从右到左第一个不为0的数位 int t = 1; whie(t != 0){ if ((t & n) == 1) return t; t <<= 1;}
(3)异或运算可以求得数组中出现基数次的数的异或
(4)与运算符必须要打括号才能作为条件

一、题目描述

剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]


第一时间的想法是哈希表或者set,但是要求空间复杂度1,顿时一筹莫展。。。
不多逼逼,直接看题解

二、异或性质

对于相同的数,以后后会得到0。异或具有交换律、结合律、分配率,反正完全当加法看就行。
另外有一条:n ^ 0 === n

对于一个数组,若只有一个单独的数【奇数个】,其他都是两个【偶数】,通过一趟异或可以最后得到那个单独的数。

但是这题要求两个唯一的数【x和y】,就不能使用这样的方法了。
做法是将数组划分为两个数组,一个数组只包含x,一个包含y,在采用这样的方式即可。

难点在于求这样数组的方式。看到的很巧妙的方式有两种

三、O(n)解法:与出不同

第一趟异或所有的数,得到x ^ y
利于与运算得到x ^ y的第一个不为0的数位,并用整数i表示该数位【即1位1的形式,如10000(b) == 16 表示第五位为1】
利用i区分该数组: 因为假设这个数位为digit,则x和y的这一位出现了不同,因此可以按照这一位是不是1将数组划分成两个子数组
最后,返回这两个子数组异或出来的数即可。

  public int[] singleNumbers(int[] nums) {
        int sum = 0;
        for (int num : nums) sum ^= num;

        int i = 1;
        while ((i & sum) == 0) i <<= 1;

        int x = 0, y = 0;
        for (int num : nums) {
            if ((num & i)== 0) x ^= num;
            else y ^= num;
        }
        return new int[]{x, y};
    }

这个解法最后一个循环有一个极骚气的操作,省去了多遍历一趟的任务。

四、O(nlogn)解法:二分法

虽然时间复杂度不符合,但是觉得挺有意思的。

由于0与任何数相异或都不为0,因此0的个数不确定,需要单独统计。【若0为1个,直接异或,就能得到剩下那个;若0为2个,进行下面的算法】

每次取出中间值,对左右两边进行第二节的算法方式:
(1)若左边 > 0, 右边 < 0,找到符合要求的数组划分;直接返回所求的划分值。
(2)若左边 > 0, 右边 = 0, 不符合要求,且x,y都在左边,high = mid - 1;
(3)若右边 > 0, 左边 = 0, x, y都在右边,high = mid + 1

class Solution {
    public int[] singleNumbers(int[] nums) {
        int sum = 0, min = Integer.MAX_VALUE, max = Integer.MIN_VALUE, zeroCount = 0;
        for (int num: nums) {
            if (num == 0) {
                zeroCount += 1;
            }
            min = Math.min(min, num);
            max = Math.max(max, num);
            sum ^= num;
        }
        // 需要特判一下某个数是0的情况。
        if (zeroCount == 1) {
            return new int[]{sum, 0};
        }
        int lo = min, hi = max;
        while (lo <= hi) {
            // 根据 lo 的正负性来判断二分位置怎么写,防止越界。
            int mid = (lo < 0 && hi > 0)? (lo + hi) >> 1: lo + (hi - lo) / 2;
            int loSum = 0, hiSum = 0;
            for (int num: nums) {
                if (num <= mid) {
                    loSum ^= num;
                } else {
                    hiSum ^= num;
                }
            }
            if (loSum != 0 && hiSum != 0) {
                // 两个都不为0,说明 p 和 q 分别落到2个数组里了。
                return new int[] {loSum, hiSum};
            }
            if (loSum == 0) {
                // 说明 p 和 q 都比 mid 大,所以比 mid 小的数的异或和变为0了。
                lo = mid + 1;
            } else {
                // 说明 p 和 q 都不超过 mid
                hi = mid - 1;
            }
        }
        // 其实如果输入是符合要求的,程序不会执行到这里,为了防止compile error加一下
        return null;
    }
}


五、“只有1个数字只出现1次,剩下出现n次”地通用解法

使用一个32为的数组,存储数组中每个数字的对应数位的相加和,最后对每个位数余n,将这个32数组组成二进制返回。

原理:【以n == 3相同为例】
在这里插入图片描述
任何一个数位,若出现1次,则相加总是为%n == 1.
若出现3次,%3 == 0
那如果将所有的这n个数相加,这多余的1位就是那个唯一的提供的,所有数位都一样。

第二个循环中,有一个很骚包的操作:
利用该位的余数快速恢复。

public int singleNumber(int[] nums) {
        int[] bits = new int[32];

        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            for (int j = 31; j >= 0; j--) {
                bits[j] += num & 1;
                num >>>= 1;
                if (num == 0) break;
            }
        }


       int result = 0;
       for (int i = 0; i < 32; i++) {
           result <<= 1;
           result |= bits[i] % 3;
       }
        return result;
    }

不会的话,也可以用比较简单的方法,使用二进制转十进制的方法,要用到求次方的方法,使用以前学过的移位的求次方方法【时复O(logn)】

 public int singleNumber(int[] nums) {
        int[] bits = new int[32];

        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            for (int j = 31; j >= 0; j--) {
                bits[j] += num & 1;
                num >>>= 1;
                if (num == 0) break;
            }
        }


       int result = 0;
       for (int i = 0; i < 32; i++) {
           result  += pow(i) *( bits[31 - i] % 3);
       }
        return result;
    }

    int pow (int n) {
        int m = 2;
        int res = 1;
        while (n != 0) {
            if ((n & 1) != 0) res *= m;
            m *= m;
            n >>>= 1;
        }
        return res;
    }

将3改为m,就是m情况下的通用解法。
或方法比较难记,还是要理解的好。

还有一种方法,用的是用的是有穷状态机,先 mark一下

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值