算法解析——单身狗问题

2024-06-01 21:44
文章标签 算法 问题 解析 单身

本文主要是介绍算法解析——单身狗问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

欢迎来到博主的专栏:算法解析
博主ID代码小豪

文章目录

    • 什么是单身狗问题
    • leetcode_136——只出现一次的数字I
      • 使用位运算解决单身狗问题。
    • leetcode_137——只出现一次的数字II
      • 统计二进制数解决单身狗问题
      • leetcode_260 只出现一次数字III
      • 分区域按位异或解决问题。
    • 总结

什么是单身狗问题

最近也是度过了5.20和儿童节这两个单身狗受难日,由于这两天学生都出去谈恋爱了,才让我有机会坐在图书馆里沉浸式刷题(也不知是喜是悲)。

在机缘巧合下,我在牛客网和leetcode上都刷到了类似的问题:如何在非空数组当中,找到只出现一次的数字。牛客网对这道题型的起名也很有意思,叫做:单身狗问题,我想这也很符合我的现状(笑)。

leetcode_136——只出现一次的数字I

题目如下:

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

这是leetcode上给出的示例

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

输入:nums = [4,1,2,1,2]
输出:4

解题思路一:暴力检索法
我们遍历整个数组的元素,每遍历一个元素时,检查数组中的其余元素是否和该元素相等,若相等,就返回该位置的元素。反之检查下一个元素

在这里插入图片描述
(博主不会制作动图,放弃了,后面用静图)。

这种方法的时间复杂度为O(N^2),空间复杂度为O(1),但是题目中要求时间复杂度是线性的,显然O(N^2)的算法不符合要求。

方法2:数组映射法。
以示例2为例。先遍历一遍数组,找到差值最大的两个元素。示例2中最小值为1,最大值为4,因此建立一个4个元素的数组。

在这里插入图片描述
映射数组中的[0]代表1的个数,[1]代表2的个数,[2]代表3的个数,[3]代表4的个数,然后遍历数组,统计出现数字的个数,映射数组和数组的关系如下,假如当前数组元素num,数组中的最小值为min,映射数组为map。

那么map[num-min]表示的就是数组中num的个数

在这里插入图片描述
通过遍历映射数组,可以发现[3]只有一个数字,那么单身狗数就是[3]+数组的最小值1,即4。数字4是数组中的单身狗。

这种方法的时间复杂度为O(N),空间复杂度为O(N)。符合要求。

但是有没有更好的方法呢?有,上两种算法的难度不高,下面要讲的算法才是重中之中,如果你也是第一次遇到这种题,相信下面的解法会让惊叹。

使用位运算解决单身狗问题。

上面的两种算法的逻辑实际上都是人类的逻辑,也就是通过寻找数字之间的关系找到单身狗,但这些方法都不够高效。

真正高效的方法是利用计算机的机器逻辑,也就是计算机的思考方式,我们站在计算机的角度考虑问题,这些数字其实不是1,2,3,4。而是一个个二进制数字。那么示例1中的数组在计算机眼中应该是这样的。
在这里插入图片描述
人类对数字的运算方式是加减乘除,而机器也有对数字的运算方式,分别是按位于,按位或,按位取反、以及按位异或。它们的规则如下:

按位与:符号是&,相同位(bit)上的数字都为1,运算结果为1,反之为0(记作:同一为1,其余为0)。
按位或:符号是|,有1为1,反之为0
按位取反:符号是~,1变为0,0变为1
按位异或:符号是^,相同位(bit)上的数字相同位0,不同为1.记作(相同为0,不同为1)。

解决问题的重点就在于这个按位异或的计算了,我们思考以下两个问题。
(1)一个二进制数与0按位异或的结果是什么?
(2)两个相同的二进制数按位异或的结果是什么?

假如当前有一个二进制数01010101,与0按位异或,其结果为不变,解题如下:
01010101
00000000
——————
01010101

解释:位数相同为0,不同为1,因此任意二进制数与0按位异或时,位上是0的数变为0,位上是1的数任为1。所以任意一个二进制数与0按位异或的结果不变。

假如现在有两个相同的二进制数按位异或,其结果为0。解题如下:
01010101
01010101
——————
00000000
解释:两个相同的二进制数的每个二进制位都相同,按位异或的计算方式是相同为0,不同为1,因此计算结果为0。

从上面两个结论可以推导出下面的结论:
a⊕b⊕b=a⊕0=a。

这个结论可以干什么?没错,这个结论就是解决这个单身狗问题最快的方式,大家仔细想想,单身狗数的数组是怎么样的?除了1个数字出现1次外,其余的数字均出现两次。那么这就好办了,我们让整个数组的数字都进行按位异或计算,那么得出结果是不是就是单身狗数?
在这里插入图片描述
计算结果为:

在这里插入图片描述
计算结果为:
在这里插入图片描述
于是我们得出了这个问题的解决思路:将数组中的全部元素进行异或运算,结果即为数组中只出现一次的数字。

class Solution {
public:int singleNumber(vector<int>& nums) {int ret=0;for(const auto&e:nums){ret^=e;}return ret;}
};

leetcode_137——只出现一次的数字II

这是单身狗问题的plus版,我们还是先看题目:

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

示例 1:

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

示例 2:

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

这个单身狗问题出现两次的其余数组变成了三次,那么大家思考以下?我们还能不能继续用异或大法?不能。因为只有两个相同的数才会异或的结果为0。但是解决问题的思路还是要放在二进制数上。

统计二进制数解决单身狗问题

这个方法适用所有单个单身狗数的变种问题,也就是除单身狗数外,无论是均有两个、三个、还是四个,都能使用这种方法。

我们先来思考一个问题,如何找到单身狗数的本质是什么?其实就是在众多的数据中找到特定的二进制数。

  1. 我们假设单身狗数的第一个二进制位是1。其余的数字的第一个二进制位是0,那么我们可以轻易的得出这么一个结论:整个数组第一个二进制位是1的元素有一个
  2. 如果存在第二个数字的二进制位也是1呢?由于第二个数字会在数组中出现3次,那么整个数组中第一个二进制位的元素有4个。
  3. 如果单身狗数的第一个二进制位是1,其余的n个数字的第一个二进制位是1。那么整个数组的第一个二进制位的元素有3n+1个。

有没有发现这么一个规律?如果单身狗数的某一位是1,那么整个数组中,和单身狗数一样该位是1的数字会有3n个。算上单身狗数会有3n+1个。于是我们可以得出下面的结论

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

那么解题思路就来了,我们统计所有数组中第i位是1的数字个数,让这个结果%3,就可以得出单身狗在第i位是1还是0.

如何统计所有数组中第i位是1的数字个数的方法如下:
假设x的二进制数如下:
00100100 10010011

我们要知道x的第5位是0还是1,我们让1右移(<<)4位。然后让x与1<<4的结果按位与(&),就能知道第5位的结果是1还是0了。
1<<4的结果如下:
00000000 00010000

计算结果如下:
00100100 10010011
00000000 00010000
——————————
00000000 00010000

代码如下:

class Solution {
public:int singleNumber(vector<int>& nums) {int ret = 0;for (int i = 1; i <= 32; i++){int n = 0;//统计次数for (int x : nums){if ((x & (1 << (i-1))) == 1 <<(i-1)){n++;}}if (n % 3 == 1)//次数%3=1则说明第i位为1ret |= 1 << (i-1);}return ret;}
};

leetcode_260 只出现一次数字III

老规矩,先看题目

给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序
返回答案。

示例 1:

输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。

示例 2:

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

这次的单身狗问题属于promax版,因为单身狗数从1个变为了2个。但是好在其余元素只出现两次,这就说明我们又可以用异或大法解决问题了(统计大法只能用于单个单身狗数)。

分区域按位异或解决问题。

前面已经推导出了一个结论:
a⊕b⊕b=a⊕0=a

由这个公式我们还能推导出下一个结论:
a⊕b⊕b⊕c=a⊕0⊕c=a⊕c

从上式可以得出,如果一个数组存在两个单身狗数,对这个数组的所有元素进行异或计算的结果等于两个单身狗数的异或结果。那么我们该怎么从这个异或的结果分离出两个单身狗数呢?我们先来看看两个数进行异或的结果具有什么性质:

假如a为01010100,c为11001010,a⊕c的结果为
01010100
11001010
——————
10011110

异或结果的具有这么一个意义:如果异或结果上的某一个位是1,就说明a或b中只有一个数,在这个位是1。如果a在这个位是1,那么b在这个位则是0。反之亦然。

那么解题思路就是,找到异或结果当中任意一位的1,然后将整个数组分为两组,一组是在这位是1的数字,另一组是在这位为0的数字,我们神奇的发现,这两个单身狗数竟然被分为了两组。

我们拿示例1为例,整个数组的所有元素异或的结果等于单身狗数3和5的异或结果。
3的二进制数是0000 0011.
5的二进制数是0000 0101
3和5的异或结果为:
0000 0011
0000 0101
——————
0000 0110

我们取异或结果的任意一位1,例如取第二位。我们根据第二位是否为1将数组分为两部分
在这里插入图片描述

可以发现一个神奇的现象,那就是数组中的两个单身狗数被分到不同的组当中,我们分别对这两个组进行异或操作,就能得到两个单身狗数。

那么剩下的问题就只剩一个了,那就是如何找到异或结果当中哪一位是1,实际上处理方法也很简单,我们可以让异或结果的每个位都与1进行按位与,就可以知道第几位是1了。

但是这个方法还是有点麻烦,我们有更好的方法。假设异或结果是x,那么x&-x的结果会只留下一个1。很神奇吧,原理在于:正值的补码和原码相同,负值的补码则是正值的反码+1。那么x与x的反码相&的结果为0,如果让x的反码再加上1,就会让按位与的结果只留下一个1

这么说好像有点抽象了,我们试试让x=1.
1的补码为0000 0001。
-1的补码为1的反码+1。1的反码是1111 1110.反码加1为1111 1111.
0000 0001
1111 1111
——————
0000 0001

大家可以多试试几个数,总之结果都是一样。

代码如下:

class Solution {
public:vector<int> singleNumber(vector<int>& nums) {int eor = 0;//eor是所有元素的异或结果for (int num : nums){eor ^= num;}int flag = eor & (-eor);//找到一位1.int single1 = 0;//单身狗数1int single2 = 0;//单身狗数2for (int num : nums){if ((num & flag) == flag){single1 ^= num;}else{single2 ^= num;}}return { single1,single2 };}
};

这个代码的逻辑没有问题,但是通过不了leetcode的测试,因为存在一个示例是存在问题的。

[1,1,0,-2147483648]。
该数组所有元素的异或结果为:-2147483648。那么这个结果存在什么问题呢?还不记不得我们要进行一个操作,那就是让异或的结果与其负值进行按位于运算。那么-(-2147483648)的值为2147483648,而int类型可以存储的最大值为2147483647.超出了范围,所以编译不通过,解决问题是可以将xor的int类型改为unsigned int。但是不太优雅。更像是走了歪门邪道通过的测试。

最主要的问题还是eor的值是一个特殊值,那么我们就加上一个判断,如果xor等于int类型的最小值,就不将其转换成正数。这才是更优的解决方案。

代码如下:

class Solution {
public:vector<int> singleNumber(vector<int>& nums) {int eor = 0;//eor是所有元素的异或结果for (int num : nums){eor ^= num;}int flag =eor==INT_MIN?eor:eor & (-eor);//找到一位1.int single1 = 0;//单身狗数1int single2 = 0;//单身狗数2for (int num : nums){if ((num & flag) == flag){single1 ^= num;}else{single2 ^= num;}}return { single1,single2 };}
};

总结

实际上博主并不仅仅是在思考单身狗问题的算法,而是想要抛出一个思想,那就是如果在刷OJ题的时候,人的逻辑难以解决某个问题,那么能不能换个角度,在机器逻辑上寻找突破口呢?当然了博主在这方面还没有太多经验。还要多多努力。

这篇关于算法解析——单身狗问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1022197

相关文章

全面解析HTML5中Checkbox标签

《全面解析HTML5中Checkbox标签》Checkbox是HTML5中非常重要的表单元素之一,通过合理使用其属性和样式自定义方法,可以为用户提供丰富多样的交互体验,这篇文章给大家介绍HTML5中C... 在html5中,Checkbox(复选框)是一种常用的表单元素,允许用户在一组选项中选择多个项目。本

Python包管理工具核心指令uvx举例详细解析

《Python包管理工具核心指令uvx举例详细解析》:本文主要介绍Python包管理工具核心指令uvx的相关资料,uvx是uv工具链中用于临时运行Python命令行工具的高效执行器,依托Rust实... 目录一、uvx 的定位与核心功能二、uvx 的典型应用场景三、uvx 与传统工具对比四、uvx 的技术实

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.

MySQL 设置AUTO_INCREMENT 无效的问题解决

《MySQL设置AUTO_INCREMENT无效的问题解决》本文主要介绍了MySQL设置AUTO_INCREMENT无效的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参... 目录快速设置mysql的auto_increment参数一、修改 AUTO_INCREMENT 的值。

关于跨域无效的问题及解决(java后端方案)

《关于跨域无效的问题及解决(java后端方案)》:本文主要介绍关于跨域无效的问题及解决(java后端方案),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录通用后端跨域方法1、@CrossOrigin 注解2、springboot2.0 实现WebMvcConfig

Redis过期删除机制与内存淘汰策略的解析指南

《Redis过期删除机制与内存淘汰策略的解析指南》在使用Redis构建缓存系统时,很多开发者只设置了EXPIRE但却忽略了背后Redis的过期删除机制与内存淘汰策略,下面小编就来和大家详细介绍一下... 目录1、简述2、Redis http://www.chinasem.cn的过期删除策略(Key Expir

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Java死锁问题解决方案及示例详解

《Java死锁问题解决方案及示例详解》死锁是指两个或多个线程因争夺资源而相互等待,导致所有线程都无法继续执行的一种状态,本文给大家详细介绍了Java死锁问题解决方案详解及实践样例,需要的朋友可以参考下... 目录1、简述死锁的四个必要条件:2、死锁示例代码3、如何检测死锁?3.1 使用 jstack3.2

解决JSONField、JsonProperty不生效的问题

《解决JSONField、JsonProperty不生效的问题》:本文主要介绍解决JSONField、JsonProperty不生效的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录jsONField、JsonProperty不生效javascript问题排查总结JSONField