算法解析——单身狗问题

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

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监