leetcode:137. 只出现一次的数字 II

题目来源

题目描述

在这里插入图片描述

题目解析

哈希表

我们可以使用哈希映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对,键表示一个元素,值表示其出现的次数。

在统计完成后,我们遍历哈希映射即可找出只出现一次的元素。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        unordered_map<int, int> freq;
        for (int num: nums) {
            ++freq[num];
        }
        int ans = 0;
        for (auto [num, occ]: freq) {
            if (occ == 1) {
                ans = num;
                break;
            }
        }
        return ans;
    }
};

求和

注意,防止溢出

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        long sum_total = 0, sum_single = 0;
        std::unordered_set<int> set;
        for(auto num : nums){
            sum_total += num;
            if(set.count(num)){
                continue;
            }
            set.insert(num);
            sum_single += num;
        }
        
        return (sum_single * 3 - sum_total) / 2;
    }
};

思路

如下图,考虑数字的二进制形式,对于出现三次的数字,各个二进制位出现的次数都是3的倍数。因此,统计所有数字的各二进制位中1的出现次数,并对3求余,结果则为只出现一次的数字
在这里插入图片描述
在具体的操作实现上,问题中给出数组中的数据在int范围之内,那么我们就可以在实现上可以对int的32个位每个位进行依次判断该位1的个数求余3后是否为1,如果为1说明结果该位二进制为1可以将结果加上去。最终得到的值即为答案。

在这里插入图片描述

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ans = 0;
        for (int i = 0; i < 32; ++i) {
            int sum = 0;
            for(auto num : nums){
                if((num >> i & 1) == 1){
                    ++sum;
                }
            }
            if(sum % 3 != 0){
                ans |= (1 << i);
            }
        }
        return ans;
    }
};

在这里插入图片描述

统计

有限状态自动机

分析:DFA

如果我们考虑「除了某个元素只出现一次以外,其余每个元素均出现两次」的情况,那么可以使用「异或」运算。

利用相同数异或为 0 的性质,可以帮助我们很好实现状态切换:
在这里插入图片描述

本题是考虑「除了某个元素只出现一次以外,其余每个元素均出现三次」的情况,那么对应了「出现 0 次」、「出现 1 次」和「出现 2 次」三种状态,意味着至少需要两位进行记录,且状态转换关系为:

在这里插入图片描述

这是一道经典的 DFA 入门题

DFA,全称 Deterministic Finite Automaton 即确定有穷自动机:从一个状态通过一系列的事件转换到另一个状态,即 state -> event -> state。

  • 确定:状态以及引起状态转换的事件都是可确定的,不存在“意外”。
  • 有穷:状态以及事件的数量都是可穷举的。

计算机操作系统中的进程状态与切换可以作为 DFA 算法的一种近似理解。如下图所示,其中椭圆表示状态,状态之间的连线表示事件,进程的状态以及事件都是可确定的,且都可以穷举。

class Solution {
    public int singleNumber(int[] nums) {
        int one = 0, two = 0;
        for(int x : nums){
            one = one ^ x & ~two;
            two = two ^ x & ~one;
        }
        return one;
    }
}

具体推导过程如下

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

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

在这里插入图片描述
如下图所示,由于二进制只能表示 0, 1 ,因此需要使用两个二进制位来表示 3个状态。设此两位分别为 two , one,则状态转换变为:
在这里插入图片描述
接下来,需要通过 状态转换表 导出 状态转换的计算公式 。首先回忆一下位运算特点,对于任意二进制位 xx ,有:

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

计算 oneone 方法:

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,即:
    在这里插入图片描述

返回值:

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值