当前位置: 首页 > article >正文

只出现一次的数字 II

优质博文:IT-BLOG-CN

题目

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

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

示例 1:
输入:nums = [2,2,3,2]
输出:3

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

1 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
nums中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次

代码

方法一:哈希表

思路与算法

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

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

class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> freq = new HashMap<Integer, Integer>();
        for (int num : nums) {
            freq.put(num, freq.getOrDefault(num, 0) + 1);
        }
        int ans = 0;
        for (Map.Entry<Integer, Integer> entry : freq.entrySet()) {
            int num = entry.getKey(), occ = entry.getValue();
            if (occ == 1) {
                ans = num;
                break;
            }
        }
        return ans;
    }
}

时间复杂度: O(n),其中 n 是数组的长度。
空间复杂度: O(n)。哈希映射中包含最多 ⌊n/3⌋+1 个元素,即需要的空间为 O(n)。

方法二:依次确定每一个二进制位

思路与算法

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

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

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

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

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

细节

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

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

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

方法三:数字电路设计

说明

方法三以及后续进行优化的方法四需要读者有一定的数字电路设计的基础。读者需要对以下知识:

简单的门电路(例如与门、异或门等)

给定数字电路输入和输出(真值表),使用门电路设计出一种满足要求的数字电路结构

有一定的了解。

门电路表示

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

非门:我们用 A表示输入为 A 的非门的输出;

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

或门:我们用 A+B 表示输入为 A 和 B 的或门的输出。同样地,多个或门可以写在一起(例如 A+B+C);

异或门:我们用 A⊕B 表示输入为 A 和 B 的异或门的输出。同样的,多个异或门可以写在一起(例如 A⊕B⊕C)。

思路与算法

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

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

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

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

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

(aibi)xi新的 (aibi)
00000
00101
01001
01110
100
10100

当我们考虑输出为 ai时:

(aibi )xi新的 ai
0000
0010
0100
0111
1001
1010
根据真值表可以设计出电路:ai =aibixi+aibixi 当我们考虑输出为 bi时:
(ai bi)xi新的 bi
0000
0011
0101
0110
1000
1010

根据真值表可以设计出电路:bi=aibi xi+aib i x i =a i (bi ⊕xi) 将上面的电路逻辑运算转换为等价的整数位运算,最终的转换规则即为:

{ a = (~a & b & x) | (a & ~b & ~x)
{ b = ~a & (b ˆ x)

其中 ~, &, |, ˆ 分别表示按位非、与、或、异或运算。

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

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

class Solution {
    public int singleNumber(int[] nums) {
        int a = 0, b = 0;
        for (int num : nums) {
            int aNext = (~a & b & num) | (a & ~b & ~num), bNext = ~a & (b ^ num);
            a = aNext;
            b = bNext;
        }
        return b;
    }
}

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

方法四:数字电路设计优化
思路与算法

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

对于原始的真值表:

(aibi)xi新的 (ai bi)
00000
00101
01001
01110
10010
10100
我们将第一列的 bi替换新的 bi即可得到:
(ai, 新的 bi)xi新的 ai
0000
0110
0100
0011
1001
1010
根据真值表可以设计出电路:

ai=ai bi xi +ai bi xi =bi (ai ⊕ xi)
这样就与 bi的电路逻辑非常类似了。最终的转换规则即为:

{ b = ~a & (b ˆ x)
{ a = ~b & (a ˆ x)

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

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

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


http://www.kler.cn/news/319805.html

相关文章:

  • Redis:事务
  • Hive 的窗口函数 详解
  • 代码随想录算法训练营| 454.四数相加II 、 383. 赎金信 、 15. 三数之和 、 18. 四数之和
  • 有威胁的武器武装检测系统源码分享
  • WebGL渲染与创建2D内容
  • 树——数据结构
  • 移动端如何实现智能语音交互
  • 【LGR-200-Div.4】洛谷入门赛 #27 A - H题解,包含(C++, Go语言)
  • Mybatis中sql数组为空判断
  • SpringBoot 整合docker,执行容器服务
  • Qt系统相关——事件
  • JavaScript --模版字符串用反引号
  • Qt (19)【Qt 线程安全 | 互斥锁QMutex QMutexLocker | 条件变量 | 信号量】
  • python学习笔记(3)——控制语句
  • Java获取Object中Value的方法
  • 数据结构-3.链表
  • 无人机在隧道中如何实现在无卫星信号下的自主导航
  • 将ipad作为数位板使用教程/出现延迟拖拽怎么办?
  • 在jupyter notebook中取消代理服务器的解决方案
  • 编程遇到问题了?一个命令让 AI 解决你的困惑!
  • Android——内部/外部存储
  • HTTP常见状态码 HTTP的逐步发展(通俗易懂版)
  • 利士策分享,赚钱有道,底线思维不可抛
  • 网络安全-shire写任务计划、反弹shell、写私钥、反序列化
  • 机器学习算法与Python实战 | 三万字详解!GPT-5:你需要知道的一切(上)建议收藏!
  • 性能监控之Python实战SkyWalking链路追踪
  • [java][git]git学习
  • Linux动态库防止逆向编译参数
  • keil的debug功能
  • IT行业的未来:技术变革与创新的持续推动