剑指offer刷题-自用— 数组中只出现一次的数字
Posted 王六六的IT日常
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剑指offer刷题-自用— 数组中只出现一次的数字相关的知识,希望对你有一定的参考价值。
1.剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组
n
u
m
s
nums
nums 里除两个数字之外,其他数字都出现了两次。
请写程序找出这两个只出现一次的数字。
要求时间复杂度是
O
(
n
)
O(n)
O(n),空间复杂度是
O
(
1
)
O(1)
O(1)。
题目要求时间复杂度
O
(
n
)
O(n)
O(n),空间复杂度
O
(
1
)
O(1)
O(1),因此首先排除 暴力法
和 哈希表统计法
。因为map空间复杂度为
O
(
n
)
O(n)
O(n)。
异或运算有个重要的性质,两个相同数字异或为 0 0 0。
因此,若将 nums 中所有数字执行异或运算,留下的结果则为出现一次的数字 x 。
异或运算满足交换律,即运算结果与 nums 的元素顺序无关:
public int[] singleNumber(int[] nums)
int x = 0;
for(int num : nums) // 1. 遍历 nums 执行异或运算
x ^= num;
return x; // 2. 返回出现一次的数字 x
数组 nums有 两个 只出现一次的数字,因此无法通过异或直接得到这两个数字。
解法一:
1.如果从头到尾依次异或数组中的每个数字,那么最终的结果刚好是那两个不同的数字异或的结果res
,因为异或运算的性质,成对出现两次的数字全部在异或中抵消了。
2.由于结果中的两个数字肯定不一样,那么res
肯定不为0,在这个结果数字的二进制表示中至少有一位为1。
3.在res
中找到第一个为1的位的位置,记为第k
位。由于这一位是两个不同的数字异或的结果,则说明两个数字的其中一个第k
位为1,另一个第k
位为0。使用与&
操作,1 & 1 = 1, 0 & 1 = 0
;
4.以第k
位是不是1为标准,把原数组中的数字分成两个子数组,第一个子数组中每个数字的第k位都是1,而第二个子数组中每个数字的第k位都是0。
5.出现了两次的数字肯定在同一个子数组中。只出现了一次的两个数字分别在两个子数组中。
6.再一次用到异或的性质,将第一个子数组中的所有元素进行异或,最后剩下的就是那个只出现一次的数字a
,而另一个只出现一次的数字用res^a
就行了(因为res=a ^ b
,b= res ^ a = a ^ b ^ a=b
)。
总结上述过程:
1. 异或得到 res
2. 取res第k位为1的数
3. 将数分为两个集合,第k位为1的集合和第k位不是1的集合
4. 其中那两个不同的数字分别在这两个集合,且相同的元素是在同一个集合里面
5. 于是将其转化成了求重复数字中的单个数值的问题
代码:
class Solution
public int[] singleNumbers(int[] nums)
//出现两次的数字异或后都互相抵消了,剩下的就是那两个不同数字,异或的结果为res
//定义res的初始值为数组第一个元素
int res = nums[0];
for(int i = 1;i < nums.length;i++)
res ^= nums[i];
//在结果数字res中找到第一个为1的位的位置,记为第k位
//1 & 1 = 1, 0 & 1 = 0;
int k = 0;
while((res >> k & 1) != 1 )
k++;
//并不需要将数据分为两个集合存起来,所以不违反O(1)空间复杂度的要求
int a = 0;
//遍历数组,将数组分为两个集合,一个第k位全是0,一个第k位全是1
//出现两次的数字在一个集合中,出现一次的两个数字分别在两个集合中
for(int i = 0;i < nums.length;i++)
//第k位为1的集合
if(((nums[i] >> k) & 1) == 1)
//集合中执行异或操作找出其中一个只出现一次的数字a
a ^= nums[i];
//res= a^b , b = res^a
return new int[]a, res^a;
解法二:
jyd-题解
class Solution
public int[] singleNumbers(int[] nums)
//因为相同的数字异或为0,任何数字与0异或结果是其本身。
//所以遍历异或整个数组最后得到的结果就是两个只出现一次的数字异或的结果:即 z = x ^ y
int z = 0;
for(int i : nums) z ^= i;
//我们根据异或的性质可以知道:z中至少有一位是1,否则x与y就是相等的。
//我们通过一个辅助变量m来保存z中哪一位为1.(可能有多个位都为1,我们找到最低位的1即可)。
//举个例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位为1.
//我们将m初始化为1,如果(z & m)的结果等于0说明z的最低为是0
//我们每次将m左移一位然后跟z做与操作,直到结果不为0.
//此时m应该等于1000,同z一样,第四位为1.
int m = 1;
while((z & m) == 0) m <<= 1;
//我们遍历数组,将每个数跟m进行与操作,结果为0的作为一组,结果不为0的作为一组
//例如对于数组:[1,2,10,4,1,4,3,3],我们把每个数字跟1000做与操作,可以分为下面两组:
//nums1存放结果为0的: [1, 2, 4, 1, 4, 3, 3]
//nums2存放结果不为0的: [10] (碰巧nums2中只有一个10,如果原数组中的数字再大一些就不会这样了)
//此时我们发现问题已经退化为数组中有一个数字只出现了一次
//分别对nums1和nums2遍历异或就能得到我们预期的x和y
int x = 0, y = 0;
for(int i : nums)
//这里我们是通过if...else将nums分为了两组,一边遍历一遍异或。
//跟我们创建俩数组nums1和nums2原理是一样的。
if((i & m) == 0) x ^= i;
else y ^= i;
return new int[]x, y;
2.剑指 Offer 56 - II. 数组中唯一只出现一次的数字(困难)
给你一个整数数组 nums ,除某个元素仅出现一次外,其余每个元素都恰出现三次 。请你找出并返回那个只出现了一次的元素。
Krahets大佬的题解:
考虑数字的二进制形式,对于出现三次的数字,各 二进制位 出现的次数都是 3的倍数。因此,统计所有数字的各二进制位中1的出现次数,并对 3 求余,结果则为只出现一次的数字。
使用 与运算 ,可获取二进制数字 num的最右一位
配合 无符号右移操作 ,可获取 num所有位的值
建立一个长度为 32 的数组 counts
,通过以上方法可记录所有数字的各二进制位的 1的出现次数。
int[] counts = new int[32];
for(int i = 0; i < nums.length; i++)
for(int j = 0; j < 32; j++)
counts[j] += nums[i] & 1; // 更新第 j 位
nums[i] >>>= 1; // 第 j 位 --> 第 j + 1 位
将 counts
各元素对 3求余,则结果为 “只出现一次的数字” 的各二进制位。
for(int i = 0; i < 32; i++)
counts[i] %= 3; // 得到 只出现一次的数字 的第 (31 - i) 位
利用 左移操作 和 或运算 ,可将 counts数组中各二进位的值恢复到数字 res上。
for(int i = 0; i < counts.length; i++)
res <<= 1; // 左移 1 位
res |= counts[31 - i]; // 恢复第 i 位的值到 res
最终返回 res即可。
实际上,只需要修改求余数值 m ,即可实现解决 除了一个数字以外,其余数字都出现 m 次 的通用问题。
class Solution
public int singleNumber(int[] nums)
int[] counts = new int[32];
for(int num : nums)
for(int j = 0; j < 32; j++)
counts[j] += num & 1;
num >>>= 1;
int res = 0, m = 3;
for(int i = 0; i < 32; i++)
res <<= 1;
res |= counts[31 - i] % m;
return res;
基本一个类型的都找出来做一遍:
以上是关于剑指offer刷题-自用— 数组中只出现一次的数字的主要内容,如果未能解决你的问题,请参考以下文章