数据结构和算法三十一

剑指 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]
限制:
       2 <= nums.length <= 10000
方法一:分组异或
思路:
让我们先来考虑一个比较简单的问题:

如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

答案很简单:全员进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 0,不同结果为 1。那么在计算过程中,成对出现的数字的所有位会两两抵消为 0,最终得到的结果就是那个出现了一次的数字。

那么这一方法如何扩展到找出两个出现一次的数字呢?

如果我们可以把所有数字分成两组,使得:
      1、两个只出现一次的数字在不同的组中;
      2、相同的数字会被分到相同的组中。

那么对两个组分别进行异或操作,即可得到答案的两个数字。这是解决这个问题的关键。

那么如何实现这样的分组呢?

记这两个只出现了一次的数字为a和b,那么所有数字异或的结果就等于a和b异或的结果,我们记为x。如果我们把x写成二进制的形式xkxk−1⋯x2x1x0,其中xi∈{0,1},我们考虑一下xi=0 和xi=1的含义是什么?它意味着如果我们把a和b写成二进制的形式,ai和bi的关系—xi=1表示ai和bi不等,xi=0 表示ai和bi相等。假如我们任选一个不为0的xi,按照第i位给原来的序列分组,如果该位为0就分到第一组,否则就分到第二组,这样就能满足以上两个条件,为什么呢?

首先,两个相同的数字的对应位都是相同的,所以一个被分到了某一组,另一个必然被分到这一组,所以满足了条件 2。

这个方法在xi=1 的时候a和b不被分在同一组,因为xi=1 表示ai和bi不等,根据这个方法的定义[如果该位为0 就分到第一组,否则就分到第二组]可以知道它们被分进了两组,所以满足了条件1。

在实际操作的过程中,我们拿到序列的异或和x之后,对于这个[位]是可以任取的,只要它满足xi=1。但是为了方便,这里的代码选取的是[不为0的最低位],当然你也可以选择其他不为0的位置。
至此,答案已经呼之欲出了。
算法:
先对所有数字进行一次异或,得到两个出现一次的数字的异或值。
在异或结果中找到任意为 1 的位。
根据这一位对所有的数字进行分组。
在每个组内进行异或操作,得到两个数字。

class Method{
    public int[] singleNumbers(int[] nums) {
        int ret = 0;
        for (int n : nums) {
            ret ^= n;
        }
        int div = 1;
        while ((div & ret) == 0) {
            div <<= 1;
        }
        int a = 0, b = 0;
        for (int n : nums) {
            if ((div & n) != 0) {
                a ^= n;
            } else {
                b ^= n;
            }
        }
        return new int[]{a, b};
    }
}

复杂度分析:

  • 时间复杂度:O(n),我们只需要遍历数组两次。
  • 空间复杂度:O(1),只需要常数的空间存放若干变量。

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

题目:在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:
       输入:nums = [3,4,3,3]
       输出:4
示例 2:
       输入:nums = [9,1,7,9,7,9,7]
       输出:1
限制:
       1 <= nums.length <= 10000
       1 <= nums[i] < 2^31
解题思路
如下图所示,考虑数字的二进制形式,对于出现三次的数字,各 二进制位 出现的次数都是 3 的倍数。
因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字。
在这里插入图片描述

方法一:有限状态自动机
各二进制位的 位运算规则相同 ,因此只需考虑一位即可。如下图所示,对于所有数字中的某二进制位1的个数,存在 3 种状态,即对 3 余数为 0, 1, 2。

  • 若输入二进制位 1 ,则状态按照以下顺序转换;

  • 若输入二进制位 0 ,则状态不变。

            0→1→2→0→⋯
    在这里插入图片描述
    如下图所示,由于二进制只能表示0, 1,因此需要使用两个二进制位来表示3个状态。设此两位分别为 two, one,则状态转换变为:
    ​              00→01→10→00→⋯
    在这里插入图片描述

接下来,需要通过 状态转换表 导出 状态转换的计算公式 。首先回忆一下位运算特点,对于任意二进制位 x,有:

  • 异或运算:x ^ 0 = xx ^ 1 = ~x
  • 与运算:x & 0 = 0x & 1 = x

计算one方法:
设当前状态为 two one,此时输入二进制位 n 。如下图所示,通过对状态表的情况拆分,可推出 one 的计算方法为:

if two == 0:
  if n == 0:
    one = one
  if n == 1:
    one = ~one
if two == 1:
    one = 0

引入 异或运算 ,可将以上拆分简化为:

if two == 0:
    one = one ^ n
if two == 1:
    one = 0

引入 与运算 ,可继续简化为:

one = one ^ n & ~two

在这里插入图片描述

计算 two方法
由于是先计算 one,因此应在新 one的基础上计算 two。
如下图所示,修改为新 one后,得到了新的状态图。观察发现,可以使用同样的方法计算 two,即:

two = two ^ n & ~one

在这里插入图片描述
返回值:

以上是对数字的二进制中 “一位” 的分析,而 int 类型的其他 31 位具有相同的运算规则,因此可将以上公式直接套用在 32 位数上。

遍历完所有数字后,各二进制位都处于状态 00 和状态 01 (取决于 “只出现一次的数字” 的各二进制位是1还是0),而此两状态是由 one 来记录的(此两状态下 twos恒为0),因此返回ones即可。

复杂度分析:

  • 时间复杂度 O(N): 其中 N 位数组 nums 的长度;遍历数组占用 O(N) ,每轮中的常数个位运算操作占用 O(32×3×2)=O(1) 。
  • 空间复杂度 O(1): 变量 ones, twos使用常数大小的额外空间。
class Method1{
    public int singleNumber(int[] nums) {
        int ones = 0, twos = 0;
        for(int num : nums){
            ones = ones ^ num & ~twos;
            twos = twos ^ num & ~ones;
        }
        return ones;
    }
}

方法二:遍历统计

此方法相对容易理解,但效率较低,总体推荐方法一。

使用 与运算 ,可获取二进制数字 num 的最右一位 n1:n1 = num & i
配合 无符号右移操作 ,可获取 num 所有位的值(即n1~ n32):num = num >>> 1

建立一个长度为 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 上(循环区间是i∈[0,31] )。

for(int i = 0; i < counts.length; i++) {
    res <<= 1; // 左移 1 位
    res |= counts[31 - i]; // 恢复第 i 位的值到 res
}

最终返回 res 即可。

复杂度分析:

  • 时间复杂度 O(N): 其中N位数组nums的长度;遍历数组占用O(N) ,每轮中的常数个位运算操作占用 O(1) 。
  • 空间复杂度 O(1): 数组 counts 长度恒为 32 ,占用常数大小的额外空间。

代码:

实际上,只需要修改求余数值 m ,即可实现解决 除了一个数字以外,其余数字都出现 m 次 的通用问题。

class Method2{
    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;
    }
}

总结:

      时光如流水,逝者如斯夫!平时在做事的时候都感叹时间为什么过的如此之慢!可当闲下来的时候却发现时间却怎么也不够用!时间又过的太快!此生苦短,对酒当歌,人生几何?
      想了很久未来的路!都说三十而立!三十岁之前都应该以事业为主!如果没有立业何来立家之说?成家立业应该倒过来,先立业再成家!钱钟书先生曾在《围城》一书中说到:“围城外的人想进去,围城内的人想出来”!他用简短的话解释了“婚姻”二字!的确,说句不好听的!谁不想结婚?可现实却迫使我们必须得很优秀了之后才有资格去谈婚论嫁!不是我们不想,而是真的被现实打压怕了!于是便出现了各种各样的骚操作!
比如:先上车,后补票的说法!当然,希望是真爱哈~~~!如果不是那就真的太悲催了!还不如单身一人自由自在多好!它不香吗?重点是一个人没有羁绊!无忧无虑,无拘无束!
“婚姻”说简单也简单!就是柴米油盐酱醋茶,真心相爱不负他!“婚姻”说难也很难!就是五味杂陈辛酸泪,海王绿茶真爱废!
先立业吧!立不了业什么也不是!只能活在底层世界当中!
      最后,愿我们都能在各行各业中能够取得不同的成就,不负亲人、朋友、老师、长辈和国家的期望!能够用自身的所学知识为国家贡献出自己的一份力量!一起加油!
                                                                                                                       2021年5月17日夜

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值