LeetCode 137. 只出现一次的数字 II【哈希表;位运算;数字逻辑;DFA】中等

news2024/11/24 11:59:24

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。

示例 1:

输入:nums = [2,2,3,2]
输出:3

示例 2:

输入:nums = [0,1,0,1,0,1,99]
输出:99

提示:

  • 1 <= nums.length <= 3 * 10^4
  • -2^31 <= nums[i] <= 2^31 - 1
  • nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次

可使用哈希映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对,键表示一个元素,值表示其出现的次数。在统计完成后,我们遍历哈希映射即可找出只出现一次的元素。这里不对代码进行说明。

解法1 依次确定每一个二进制位

为了方便叙述,我们称「只出现了一次的元素」为「答案」。

由于数组中的元素都在 i n t int int(即 32 32 32 位整数)范围内,因此可以依次计算答案的每一个二进制位是 0 0 0 还是 1 1 1

具体地,考虑答案的第 i i i 个二进制位( i i i 0 0 0 开始编号),它可能为 0 0 0 1 1 1。对于数组中非答案的元素,每一个元素都出现了 3 3 3 次,对应着第 i i i 个二进制位的 3 3 3 0 0 0 3 3 3 1 1 1,无论是哪一种情况,它们的和都是 3 3 3 的倍数(即和为 0 0 0 3 3 3。因此:

答案的 i i i 个二进制位,就是数组中所有元素的第 i i i 个二进制位之和除以 3 3 3 的余数

这样一来,对于数组中的每一个元素 x x x ,我们使用位运算 (x   >>   i)   &   1 \texttt{(x >> i) \& 1} (x >> i) & 1 得到 x x x 的第 i i i 个二进制位,并将它们相加再对 3 3 3 取余,得到的结果一定为 0 0 0 1 1 1,即为答案的第 i i i 个二进制位

细节:需要注意的是,如果使用的语言对「有符号整数类型」和「无符号整数类型」没有区分,那么可能会得到错误的答案。这是因为「有符号整数类型」(即 int \texttt{int} int 类型)的第 31 31 31 个二进制位(即最高位)是补码意义下的符号位,对应着 − 2 31 -2^{31} 231 ,而「无符号整数类型」由于没有符号,第 31 31 31 个二进制位对应着 2 31 2^{31} 231 。因此在某些语言(例如 Python)中需要对最高位进行特殊判断。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ans = 0;
        for (int i = 0; i < 32; ++i) {
            int total = 0;
            for (int num : nums) 
                total += ((num >> i) & 1); // 所有数的第i个二进制位之和
            if (total % 3) ans |= (1 << i); // 设置答案的第i个二进制位
        }
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n log ⁡ C ) O(n \log C) O(nlogC) ,其中 n n n 是数组的长度, C C C 是元素的数据范围,在本题中 log ⁡ C = log ⁡ 2 32 = 32 \log C=\log 2^{32} = 32 logC=log232=32 ,也就是我们需要遍历第 0 ∼ 31 0\sim31 031 个二进制位。
  • 空间复杂度: O ( 1 ) O(1) O(1)

实际上本方法还可继续优化。我们设置三个数 o n e , t w o , t h r e e one, two, three one,two,three 分别表示所有元素的所有二进制位中出现一次、二次、三次的位。最后的 o n e one one 就是答案。过程如下,设 n u m num num 为当前元素:

  • 使用 o n e   &   n u m one\ \& \ num one & num 表示「 n u m num num o n e one one 中同时出现的二进制位」表示的值,这些二进制位目前出现了两次,因此 t w o   ∣ =   ( o n e   &   n u m ) two\ |=\ (one\ \&\ num) two = (one & num)
  • 使用 o n e   x o r   n u m one\ xor\ num one xor num ,这表示设置那些出现了一次的二进制位,出现了两次的二进制位会被置零。
  • 再令 o n e   &   t w o one\ \&\ two one & two 表示「 o n e one one t w o two two 中同时出现的二进制位」表示的值,这些二进制位目前出现了三次,令 t h r e e = ( o n e   &   t w o ) three = (one\ \&\ two) three=(one & two)
  • 相应的位出现了三次,则(和对 3 3 3 取余一样)将该位重置为 0 0 0 ——令 t w o   & =    t h r e e ,   o n e   & =    t h r e e two\ \&=\ ~three,\ one\ \&=\ ~three two &=  three, one &=  three

最后返回 o n e one one 作为答案,它记录了那些只出现一次的二进制位,在本题中就等于「答案」的值。

class Solution {
public: 
    int singleNumber(vector<int>& nums) {
        int one = 0, two = 0, three;
        for (int num : nums) {
            // two的相应的位等于1,表示该位出现2次
            two |= (one & num);
            // one的相应的位等于1,表示该位出现1次
            one ^= num;
            // three的相应的位等于1,表示该位出现3次
            three = (one & two);
            // 如果相应的位出现3次,则该位重置为0
            two &= ~three;
            one &= ~three;
        }
        return one;
    }
};

解法2 数字电路设计

方法2以及后续进行优化的方法3需要有一定的数字电路设计的基础。需要对以下知识有一定的了解:

  • 简单的门电路(例如与门、异或门等)
  • 给定数字电路输入和输出(真值表),使用门电路设计出一种满足要求的数字电路结构

门电路表示:我们将会用到四种门电路,使用的符号如下:

  • 非门:我们用 A ′ A' A 表示输入为 A A A 的非门的输出;
  • 与门:我们用 A B AB AB 表示输入为 A A A B B B 的与门的输出。由于「与运算」具有结合律,因此如果同时用了多个与门(例如将 A A A B B B 进行与运算后,再和 C C C 进行与运算),我们可以将多个输入写在一起(例如 A B C ABC ABC );
  • 或门:我们用 A + B A+B A+B 表示输入为 A A A B B B 的或门的输出。同样地,多个或门可以写在一起(例如 A + B + C A+B+C A+B+C );
  • 异或门:我们用 A ⊕ B A\oplus B AB 表示输入为 A A A B B B 的异或门的输出。同样的,多个异或门可以写在一起(例如 A ⊕ B ⊕ C A\oplus B\oplus C ABC )。

在方法二中,我们是依次处理每一个二进制位的,那么时间复杂度中就引入了 O ( log ⁡ C ) O(\log C) O(logC) 这一项。既然我们在对两个整数进行普通的二元运算时,都是将它们看成整体进行处理的,那么我们是否能以普通的二元运算为基础,同时处理所有的二进制位

答案是可以的。我们可以使用一个「黑盒」存储当前遍历过的所有整数。「黑盒」的第 i i i 位为 { 0 , 1 , 2 } \{ 0, 1, 2 \} {0,1,2} 三者之一,表示当前遍历过的所有整数的第 i i i 位之和除以 3 3 3 的余数。但由于二进制表示中只有 0 0 0 1 1 1 而没有 2 2 2 ,因此我们可以考虑在「黑盒」中使用两个整数来进行存储,即:

黑盒中存储了两个整数 a a a b b b ,且会有三种情况:

  • a a a 的第 i i i 位为 0 0 0 b b b 的第 i i i 位为 0 0 0,表示 0 0 0
  • a a a 的第 i i i 位为 0 0 0 b b b 的第 i i i 位为 1 1 1,表示 1 1 1
  • a a a 的第 i i i 位为 1 1 1 b b b 的第 i i i 位为 0 0 0 ,表示 2 2 2

为了方便叙述,我们用 ( 00 ) (00) (00) 表示 a a a 的第 i i i 位为 0 0 0 b b b 的第 i i i 位为 0 0 0,其余的情况类似。

当我们遍历到一个新的整数 x x x 时,对于 x x x 的第 i i i x i x_i xi ,如果 x i = 0 x_i=0 xi=0 ,那么 a a a b b b 的第 i i i 位不变;如果 x i = 1 x_i=1 xi=1 ,那么 a a a b b b 的第 i i i 位按照 ( 00 ) → ( 01 ) → ( 10 ) → ( 00 ) (00)\to(01)\to(10)\to(00) (00)(01)(10)(00) 这一循环进行变化。因此我们可以得出下面的真值表:
270

当我们考虑输出为 a i a_i ai 时,根据真值表可以设计出电路:
a i = a i ′ b i x i + a i b i ′ x i ′ a_i = a_i'b_ix_i + a_ib_i'x_i' ai=aibixi+aibixi
当我们考虑输出为 b i b_i bi 时,根据真值表可以设计出电路:
b i = a i ′ b i ′ x i + a i ′ b i x i ′ = a i ′ ( b i ⊕ x i ) b_i = a_i'b_i'x_i + a_i'b_ix_i' = a_i'(b_i \oplus x_i) bi=aibixi+aibixi=ai(bixi)
将上面的电路逻辑运算转换为等价的整数位运算,最终的转换规则即为:

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

细节:由于电路中的 a i a_i ai​ 和 b i b_i bi 是「同时」得出结果的(同时根据旧有的 a , b a,b a,b 得到新的 a , b a,b a,b ),因此我们在计算 a a a b b b 时,需要使用临时变量暂存它们之前的值,再使用转换规则进行计算。

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;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 是数组的长度。
  • 空间复杂度: O ( 1 ) O(1) O(1)

解法3 数字电路设计优化

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

对于原始的真值表:
250
我们将第一列的 b i b_i bi​ 替换新的 b i b_i bi 即可得到:
250
根据真值表可以设计出电路:
a i = a i ′ b i ′ x i + a i b i ′ x i ′ = b i ′ ( a i ⊕ x i ) a_i = a_i'b_i'x_i + a_ib_i'x_i' = b_i'(a_i \oplus x_i) ai=aibixi+aibixi=bi(aixi)
这样就与 b i b_i bi​ 的电路逻辑非常类似了。最终的转换规则即为:

需要注意先计算 b b b,再计算 a a a

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;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) ,其中 n n n 是数组的长度。
  • 空间复杂度: O ( 1 ) O(1) O(1)

解法4 DFA(余数状态转换)

如方法1所述,对于所有数字中的某二进制位 1 1 1 的个数,存在 3 3 3 种状态,即对 3 3 3 余数为 0 , 1 , 2 0, 1, 2 0,1,2

  • 若输入二进制位 1 1 1 ,则状态按照以下顺序转换;
  • 若输入二进制位 0 0 0 ,则状态不变
    0 → 1 → 2 → 0 → ⋯ 0 \rightarrow 1 \rightarrow 2 \rightarrow 0 \rightarrow \cdots 0120

    如下图所示,同样由于二进制只能表示 0 , 1 0, 1 0,1 ,因此需要使用两个二进制位来表示 3 3 3 个状态。设此两位分别为 t w o , o n e two , one two,one ,则状态转换变为:
    00 → 01 → 10 → 00 → ⋯ 00 \rightarrow 01 \rightarrow 10 \rightarrow 00 \rightarrow \cdots 00011000

    接下来通过状态转换表导出状态转换的计算公式
  • 计算 o n e one one 的方法:设当前状态为 t w o   o n e two\ one two one ,此时输入二进制位 n n n 。如下图所示:
  • 计算 t w o two two 的方法:由于是先计算 o n e one one ,因此应在新 o n e one one 的基础上计算 t w o two two 。如下图所示,修改为新 o n e one one 后,得到了新的状态图。观察发现,可用同样的方法计算 t w o two two

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1100581.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux基础】Linux 发展史

转载于 https://blog.csdn.net/weixin_45031801/article/details/133551510 文章目录 1、前言2、Linux 发展史2.1 UNIX 发展史2.1.1 C 语言对 UNIX 的影响 2.2 Linux 的诞生2.2.1 概述2.2.2 开源的优势2.3.3 系统结构内核层Shell层应用层 3、Linux 应用领域3.1 服务器领域3.2 …

Python算法练习 10.16

leetcode 437 路径总和III 给定一个二叉树的根节点 root &#xff0c;和一个整数 targetSum &#xff0c;求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始&#xff0c;也不需要在叶子节点结束&#xff0c;但是路径方向必须是向下的&#xff…

SpringBoot集成Dubbo

1. 构建SpringBoot环境 1.1 创建一个duubo-spring-boot-demo项目 duubo-spring-boot-demo&#xff1a;是父工程&#xff0c;方便依赖管理duubo-spring-boot-demo-consumer&#xff1a;是服务消费方&#xff0c;继承父工程duubo-spring-boot-demo-provider&#xff1a;是服务提…

数据结构 2 第二章 线性结构 代码实现

头文件 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdlib.h>//malloc函数头文件 #include <time.h> #include <string.h> #include <limits.h> #include <ctype.h> #include <math.h> # include <stdio.h> 一、单链表 1.单链…

Segment Anything(论文解析)

Segment Anything 摘要1.介绍2 SAM任务SAM模型 摘要 我们介绍了“Segment Anything” (SA) 项目&#xff1a;这是一个新的任务、模型和数据集对于图像分割。使用我们高效的模型进行数据收集&#xff0c;我们构建了迄今为止最大的分割数据集&#xff08;远远超过其他数据集&…

C语言进行实验:通过程序实现线算图取值【支持VC++ 6.0编辑器环境运行】

背景&#xff1a; 一、实验目的和要求 1、能描述数据基本类型及其常量的表示方法&#xff1b; 2、会对变量进行定义及初始化&#xff1b; 3、能使用运算符与表达式对变量赋值&#xff1b; 4、会描述C语句的概念及种类、C语言常用的输入/出方式&#xff1b; 5、会设计顺序…

typora主题切换与推荐主题

在这篇博文中&#xff0c;我将向你展示如何给typora更换主题&#xff0c;并推荐几款出色的主题。通过这些主题的使用&#xff0c;你可以为你的typora编辑器增添一抹别样的风采&#xff0c;让你的写作体验更加美好、舒适。 typora替换主题的步骤非常简单&#xff0c;只需按照以…

科技资讯|苹果Vision Pro可通过手势ID检测不同用户

近日&#xff0c;美国专利局公布了苹果公司的一项专利申请&#xff0c;该专利申请涉及基于手部特征验证用户身份的技术。苹果指出&#xff0c;可能是多个家庭成员都想使用 Apple Vision Pro&#xff0c;系统必须识别不同的手势以控制 visionOS。在另一个示例中&#xff0c;苹果…

无频闪护眼灯哪个好?五款无频闪护眼台灯推荐

青少年的近视率持续升高&#xff0c;保护眼睛非常重要。台灯是用眼环境的必备品&#xff0c;而市面上款式多样不知如何购买。这期就来聊聊护眼台灯的选购问题&#xff01; 都说成人的世界不容易&#xff0c;社交网络上时常有人吐槽996工作制&#xff0c;但要知道的是现在的学生…

(vue)el-descriptions 描述列表无效

(vue)el-descriptions 描述列表无效 原因&#xff1a;element 的版本不够 解决&#xff1a;运行下面两个命令 npm uninstall element-ui //卸载之前安装的版本 npm i element-ui -S //重新安装解决参考&#xff1a;https://blog.csdn.net/weixin_59769148/article/details/1…

vector+算法sort与list+sort的效率比较,容易写错的地方原因探析

我写的代码&#xff1a; #include <iostream> using namespace std; #include <vector> #include <list> #include <algorithm> int main() {const int N 10000000;vector<int> v;list<int> l;for (int i 0; i < N; i){v.push_back(…

SpringCloud Gateway网关梳理

前言 在中大型系统中&#xff0c;Gateway网关发挥着关键的作用&#xff0c;Gateway可以运用在许多应用场景&#xff0c;如接口限流、日志监控、授权认证等等。下面对过往的Gateway知识做一个归纳与总结。 一、路由 路由是Gateway网关中的基础组件&#xff0c;它由一个org.sp…

C#开发的OpenRA游戏之金钱系统(2)

C#开发的OpenRA游戏之金钱系统(2) 在我们玩这个游戏时,就会发现每当创建一个新精炼工厂,就会带有一台采矿车,这是怎么样实现的呢? 这个就需要查看建筑物下面这个字段: FreeActor: Actor: HARV SpawnOffset: 1,2 Facing: 256 FreeActor就是标记为免费的物品,在这里

xdma axi-stream

xdma 回环 vivado 里有官方示例 fpga&#xff1a;pcie rx – axi-stream master – axi-stream slave – pcie tx 流程&#xff1a;电脑启动读取&#xff0c;然后电脑再在超时时间内写入。或者电脑启动写入&#xff0c;然后电脑再在超时时间内读出。只读取或只写入会报超时&am…

小程序开发平台源码系统+活动在线报名小程序功能 带完整的搭建教程

今天来给大家分享一下小程序开发平台源码系统的活动在线报名小程序功能。活动在线报名小程序是一种方便快捷的活动报名方式&#xff0c;可以通过小程序进行宣传和报名&#xff0c;让参与者可以方便快捷地进行报名和参加活动。小程序有完整的搭建教程&#xff0c;以下是部分功能…

【14】基础知识:React - redux

一、 redux理解 1、学习文档 英文文档&#xff1a;https://redux.js.org/ 中文文档&#xff1a;http://www.redux.org.cn/ Github: https://github.com/reactjs/redux 2、redux是什么 redux 是一个专门用于做状态管理的 JS 库(不是 react 插件库)。 它可以用在 react&am…

记一次MySQL5初始化被kill的问题排查 | 京东云技术团队

写在前面 由于测试环境JED申请比较繁琐&#xff0c;所以Eone提供了单机版Mysql供用户使用&#xff0c;近期Eone搭建Mysql5的时候发现莫名被kill了&#xff0c;容器规格是4C8G&#xff0c;磁盘30G 这不科学&#xff0c;之前都是可以的&#xff0c;镜像没变&#xff0c;配置没变…

分享大数据培训班班型

泰迪大数据培训班有VIP就业保障班&#xff0c;项目实战班&#xff0c;技能进阶班&#xff0c;技能提升班。不同班型对应不同学习服务。 一、VIP 就业保障班 二、大数据分析/开发项目实战班 三、技能进阶班 四、技能提升班

RabbitMQ 安装和使用Demo

同步是阻塞 MQ&#xff1a;消息队列&#xff0c;基础数据结构中"先进先出"的数据结构。用来解决应用解耦&#xff0c;异步消息&#xff0c;流量消峰等问题。 RabbitMQ、RocketMQ、Kafka RocketMQ 是阿里的 应用层开发使用RabbitMQ 大数据开发Kafka MQ是不是微服务都…

【MATLAB源码-第49期】基于蚁群算法(ACO)算法的栅格路径规划,输出最佳路径图和算法收敛曲线图。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 蚁群算法是一种模拟自然界蚂蚁觅食行为的启发式优化算法。在蚁群系统中&#xff0c;通过模拟蚂蚁之间通过信息素沟通的方式来寻找最短路径。 在栅格路径规划中&#xff0c;蚁群算法的基本步骤如下&#xff1a; 1. 初始化: …