前言
今天又做了一道经典的题目[看了一道经典的题解],忍不住记录一波
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一下