137. 只出现一次的数字 II

1.题目介绍

2.题解

2.1 哈希表

思路

同本系列题I,不过多赘述

代码

class Solution {
public:
    int singleNumber(std::vector<int>& nums) {
        std::unordered_map<int,int> map;
        for (int num:nums){
            map[num]++;
        }
        for (auto pair:map){
            if (pair.second == 1) return pair.first;
        }
        return 0;
    }
};

结果展示

空间代价不是很理想

2.2 依次确定每一个二进制位

思路(分治算法)

这里的思路要类比上一题中的异或运算(排除所有出现偶次的数,使用了异或运算的性质)
这里重复出现三次,是一个三的倍数,如何利用好这个性质?
1.首先思考整体求和除三或者对其求和用3求余,发现了一个问题,对总和用3求余的时候,确实能求出0,1,2,但是这里的数可能是n3 + (0,1,2),无法确定这个n?
2.我们就在想能不能消除这个n,最好把这个n拆成n次一位一位的运算就好了,那么每一位我都能确定是0,1,2(或者更少的数),之后统一再变回来。
3.我们再看看这个表达式:n
3 + (0,1,2),是不是感觉很像一种类似三进制的公式;
4.换句话说,如果这里的进制位数小于3,比如像二进制,就可以直接将数组中所有元素每一位求和后对3求余,得到的结果定是在(0,1)之间取一个,是一个确定的值(即单独出现的这个数的该位),之后再通过二进制的数确定该数即可
5.可是我并不知道单独出现的这个数的位数?有限定:-2^31 <= nums[i] <= 2^31 - 1,数组中的元素都在 int(即 32 位整数)范围内

代码

class Solution {
public:
    int singleNumber(std::vector<int>& nums) {
        int result = 0, temp = 0, pos = 0;
        for (int i = 0; i < 32; i++)
        {
            for (int num : nums)
            {
                temp += (num >> i) & 1;
            }
            result += (temp % 3) << i;
            temp = 0;
        }
        return result;
    }
};

结果展示

这里遍历每个二进制位使得时间代价乘上了一个logC,能否进一步优化?

2.3 数字电路设计

思路

我们在计算机组成原理中,或许学过一课:简单的加法器(半加器和全加器),

那我们是否能模仿这里面的思路使用(整体位运算)代替(第i位相加综合%3的for循环操作)呢?
答案是可以的,这也就是我们为何在该系列I中使用异或运算是最优解的原因,那里均为两个一组,正好也满足二进制的进位制。但本题是三个一组,就需要进行一定的修改了。
思考一下我们这里如何解决这个三次一进位的问题呢?
我们在I中只使用了temp一个中间变量,temp每一位(i)的值正是(0,1)中取一个,足够,那我们现在能否使用两个中间变量来表示(0,1,2)呢?可以的
请看下图



我们来分析一下对于ai,bi的公式怎么来的吧

1.对于\(a_i\)

\[a_i = a_i' b_i x_i + a_i b_i' x_i' \]

1.当 \(a_i = 0\)\(a_i',\) 只有当 \(b_ix_i\)时(同时为1),\(a_i进位为1,否则保持原值为0\)
2.当 \(a_i = 1\) 即 $a_i, $ 只有当 \(b_i'x_i'\)*(同时为0) , \(a_i进位为0,否则保持原值1\)

2.对于\(b_i\)

\[bᵢ = aᵢ'bᵢ'xᵢ + aᵢ'bᵢxᵢ' = aᵢ'(bᵢ ⊕ xᵢ) \]

\[b_i=a_i^{\prime} b_i^{\prime} x_i+a_i^{\prime} b_i x_i^{\prime}=a_i^{\prime}\left(b_i \oplus x_i\right) \]

3.总结

当我们遍历完数组中的所有元素后,\((a_ib_i)\) 要么是 (00),表示答案的第 \(i\) 位是 0; 要么是(01),表示答案的第 \(i\) 位是 1。因此我们只需要返回 \(b\) 作为答案即可。

\[\begin{cases} a = (\sim a\,\&\,b\,\&\,x) | (a\,\&\,\sim b\,\& \sim x)\\ b = \sim a \& (b ^{\wedge}x)\\ \end{cases} \]

代码

1.pair

表达式 \(pair{(\sim a \& b \& num) | (a \& \sim b \& \sim num), \sim a \& (b ^{\wedge} num)}\) 会计算两个整数值,然后创建一个匿名 pair 对象并将这两个值传递给 pair 的构造函数

2.tie

tie相当于是一个解包pair的作用,将pair中的两个值分别赋值给a,b
相当于\(a = (\sim a \& b \& num) | (a \& \sim b \& \sim num), b = \sim a \& (b ^{\wedge} num)\)

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int a = 0, b = 0;
        for (int num: nums) {
            tie(a, b) = pair{(~a & b & num) | (a & ~b & ~num), ~a & (b ^ num)};
        }
        return b;
    }
};

2.4 数字电路设计优化

我们发现方法三中计算 \(b\) 的规则较为简单,而 \(a\) 的规则较为麻烦,因此可以将「同时计算」改为「分别计算」,即先计算出 \(b\),再拿新的 \(b\) 值计算 \(a\)

根据真值表可以设计出电路:\(a_i=a_i^{\prime}b_i^{\prime}x_i+a_ib_i^{\prime}x_i^{\prime}=b_i^{\prime}(a_i\oplus x_i)\)
这样就与 \(b_i\) 的电路逻辑非常类似了。最终的转换规则即为:

\[\begin{cases} \mathbf{b}=\sim\mathbf{a}\&(\mathbf{b}^{\wedge}{\mathbf{x}})\\ \mathbf{a}=\sim\mathbf{b}\&(\mathbf{a}^{\wedge}{\mathbf{x}}) \end{cases} \]

代码

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int a = 0, b = 0;
        for (int num: nums) {
            b = ~a & (b ^ num);
            a = ~b & (a ^ num);
        }
        return b;
    }
};

作者:力扣官方题解
链接:https://leetcode.cn/problems/single-number-ii/solutions/746993/zhi-chu-xian-yi-ci-de-shu-zi-ii-by-leetc-23t6/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2023-10-15 22:33  DawnTraveler  阅读(5)  评论(0编辑  收藏  举报