剑指 Offer 56 - I. 数组中数字出现的次数

解题思路:

题目要求时间复杂度 O(N),空间复杂度 O(1),因此首先排除 暴力法 哈希表统计法

   


简化问题: 一个整型数组 nums 里除 一个 数字之外,其他数字都出现了两次。

设整型数组 nums中出现一次的数字为 x,出现两次的数字为 a, a, b, b, ...,即:nums=[a,a,b,b,...,x]

异或运算有个重要的性质,两个相同数字异或为 0 ,即对于任意整数 a有 a⊕a=0。因此,若将 nums中所有数字执行异或运算,留下的结果则为出现一次的数字 x,即:

  a⊕a⊕b⊕b⊕...⊕x

=  0⊕0⊕...⊕x

=  x

public int[] singleNumber(int[] nums) {
    int x = 0;
    for(int num : nums)  // 1. 遍历 nums 执行异或运算
        x ^= num;
    return x;            // 2. 返回出现一次的数字 x
}

设 nums=[3,3,4,4,1],以上计算流程如下图所示


本题难点:

       数组 nums有 两个 只出现一次的数字,因此无法通过异或直接得到这两个数字。设两个只出现一次的数字为 x , y ,由于 x≠y,则 x和 y二进制至少有一位不同(即分别为 0 和 1 ),根据此位可以将 nums 拆分为分别包含 x和 y 的两个子数组。两子数组都满足 「除一个数字之外,其他数字都出现了两次」分别对两子数组遍历执行异或操作,即可得到两个只出现一次的数字 x, y 。


算法流程:

   1. 遍历 nums执行异或:

    设整型数组 nums=[a,a,b,b,...,x,y],对 nums中所有数字执行异或,得到的结果为 x⊕y ,即:

                           a⊕a⊕b⊕b⊕...⊕x⊕y=  0⊕0⊕...⊕x⊕y=  x⊕y

2. 循环左移计算 m:

    根据异或运算定义,若整数 x⊕y某二进制位为 1 ,则 x和 y 的此二进制位一定不同换言之,找到 x⊕y某为 1 的二进制位,即可将数组 nums拆分为上述的两个子数组。根据与运算特点,可知对于任意整数 a 有:
          若 a&0001=1 ,则a的第一位为 1 ;
          若 a&0010=1,则a的第二位为 1 ;
          以此类推……

    因此,初始化一个辅助变量 m=1,通过与运算从右向左循环判断,可获取整数 x⊕y首位 1 ,记录于 m中,代码如下:

while(z & m == 0) // m 循环左移一位,直到 z & m != 0
    m <<= 1

举例:

 

3.拆分 nums为两个子数组:


 4.分别遍历两个子数组执行异或:

    通过遍历判断 nums中各数字和 m做与运算的结果,可将数组拆分为两个子数组,并分别对两个子数组遍历求异或,则可得到两个只出现一次的数字,代码如下:

for(int num: nums) {
    if((num & m) != 0) x ^= num;  // 若 num & m != 0 , 划分至子数组 1 ,执行遍历异或
    else y ^= num;                // 若 num & m == 0 , 划分至子数组 2 ,执行遍历异或
}
return new int[] {x, y};          // 遍历异或完毕,返回只出现一次的数字 x 和 y

5.返回值:

    返回只出现一次的数字 x, y 即可。

    设 nums=[3,3,4,4,1,6]以上计算流程如下图所示。

复杂度分析:

    时间复杂度 O(N): 线性遍历 nums使用 O(N) 时间,遍历 x⊕y二进制位使用 O(1) 时间。
    空间复杂度 O(1): 辅助变量 a, b, x, y 使用常数大小额外空间。


代码:

class Solution {
    public int[] singleNumbers(int[] nums) {
        int x=0,y=0,n=0,m=1;
	for(int num:nums){//1.异或遍历, a⊕a⊕b⊕b⊕...⊕x⊕y=  0⊕0⊕...⊕x⊕y=  x⊕y
	    n^=num;
	}
	while((n&m)==0){//2.循环左移,直到n&m!=0,计算m
	    m<<=1;//初始化一个辅助变量m=1,通过与运算从右向左循环判断,可获取整数x⊕y首位1 ,记录于m中
	}//结束结果如:(1⊕6)&1=1,m=1,跳出循环
	for (int num : nums) {//3.遍历nums分组
	    if((num&m)!=0)//4.当num&m!=0,划分至子数组1,执行遍历异或
		x^=num;
	else              //4.当num&m==0,划分至子数组2,执行遍历异或
		y^=num;
	}
	return new int[]{x,y};//遍历异或完毕,返回只出现一次的数字x和y
    }
}

 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值