算法解析——单身狗问题

欢迎来到博主的专栏:算法解析
博主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.mzph.cn/web/20424.shtml

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

相关文章

C语言:如何写文档注释、内嵌注释、行块注释?

技术答疑流程 扫描二维码&#xff0c;添加个人微信&#xff1b;支付一半费用&#xff0c;获取答案&#xff1b;如果满意&#xff0c;则支付另一半费用&#xff1b; 知识点费用&#xff1a;10元 项目费用&#xff1a;如果有项目任务外包需求&#xff0c;可以微信私聊

【scikit-learn009】异常检测系列:单类支持向量机(OC-SVM)实战总结(看这篇就够了,已更新)

1.一直以来想写下机器学习训练AI算法的系列文章,作为较火的机器学习框架,也是日常项目开发中常用的一款工具,最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下scikit-learn框架OCSVM模型相关知识体系。 3.欢迎批评指正,欢迎互三,跪谢一键三连! 4.欢迎…

Vue3实战笔记(57)—一键换肤:在Vuetify中打造个性化主题切换体验

文章目录 前言一键换肤总结 前言 在当今追求极致用户体验的时代&#xff0c;为应用程序提供个性化的主题切换功能已经成为提升用户满意度和留存率的关键因素之一。Vuetify&#xff0c;作为基于Vue.js的流行前端框架&#xff0c;以其丰富的组件库和高度可定制性&#xff0c;为开…

day05-多任务-正则-装饰器

一、多任务 1-进程和线程 进程是操作系统分配资源的最小单元 线程执行程序的的最小单元 线程依赖进程&#xff0c;可以获取进程的资源 一个程序执行 先要创建进程分配资源&#xff0c;然后使用线程执行任务 默认情况下一个进程中有一个线程 2-多任务介绍 运行多个进程或线程执…

民国漫画杂志《时代漫画》第38期.PDF

时代漫画38.PDF: https://url03.ctfile.com/f/1779803-1248636380-dd7daa?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

CATIA进阶操作——创成式曲面设计入门(1)线架设计,三维点、直线、平面、曲线

目录 引出三维空间点生成三维直线三维平面三维曲线总结异形弹簧新建几何体草图编辑&#xff0c;画一条样条线进行扫掠&#xff0c;圆心和半径画出曲面上的螺旋线再次选择扫掠&#xff0c;圆心和半径 其他自定义信号和槽1.自定义信号2.自定义槽3.建立连接4.进行触发 自定义信号重…

【YOLOv5/v7改进系列】引入ODConv——即插即用的卷积块

一、导言 提出了一种称为全维度动态卷积(ODConv)的新颖设计&#xff0c;旨在克服当前动态卷积方法的局限性并提升卷积神经网络(CNN)的性能。以下是该论文提出的全维度动态卷积设计的优点和存在的缺点分析&#xff1a; 优点&#xff1a; 增强特征学习能力&#xff1a; ODConv通…

【UML用户指南】-04-从代码到UML的关键抽象

1、关键抽象 声明了一个名为paint的操作&#xff0c;它的实现调用名为drawString的另一个操作&#xff0c;drawString操作负责在指定的位置上打印“Hello,World!”。在通常的面向对象的方式下&#xff0c;drawString是一个名称为g的参数上的一个操作&#xff0c;g的类型是类Gr…

VMWare下安装Linux虚拟机(图文)

大家好&#xff0c;在当今科技发展迅速的时代&#xff0c;虚拟化技术在企业和个人用户中变得越来越普遍。VMware作为一款领先的虚拟化软件&#xff0c;为用户提供了在单一物理计算机上运行多个操作系统的能力&#xff0c;为开发、测试和运维等任务提供了便利。在这篇文章中&…

打开C语言常用的内存函数大门(三) —— memset()函数(内含讲解用法和模拟实现)

文章目录 1. 前言2. memset函数2.1 memset函数原型2.2 memset函数参数的介绍2.3 memset函数的使用演示 3. memset函数的模拟实现4. 总结 1. 前言 哈喽&#xff0c;我们又见面了。通过前面两个内存函数(memcpy、memmove函数)讲解的锤炼后&#xff0c;对如何解析一个自己从来没有…

【TB作品】msp430f5529单片机墨水屏,口袋板,tmp421温度,温控风扇

文章目录 一、扬声器模块介绍二、驱动介绍三、程序介绍四、全部代码下载 msp430f5529d单片机墨水屏&#xff0c;口袋板&#xff0c;tmp421温度&#xff0c;温控风扇 基本要求&#xff1a;高于20度开转&#xff0c;温度越高转速越快&#xff0c;高于40度风扇停转&#xff0c;温…

知识计算概述

文章目录 知识计算研究现状技术发展趋势 知识计算 随着知识图谱技术及应用的不断发展&#xff0c;图谱质量和知识完备性成为影响知识图谱应用的两大重要难题&#xff0c;以图谱质量提升、潜在关系挖掘与补全、知识统计与知识推理作为主要研究内容的知识计算成为知识图谱应用的重…

C语言 | Leetcode C语言题解之第119题杨辉三角II

题目&#xff1a; 题解&#xff1a; int* getRow(int rowIndex, int* returnSize) {*returnSize rowIndex 1;int* row malloc(sizeof(int) * (*returnSize));row[0] 1;for (int i 1; i < rowIndex; i) {row[i] 1LL * row[i - 1] * (rowIndex - i 1) / i;}return row…

Qt第三方库QicsTable简单实例(1)

闲来无事&#xff0c;无意间看到一个Qics表格操作第三方库&#xff0c;自己写了一个特别简单的实例&#xff0c;效果如图所示&#xff1a; 操作界面的数据还是特别快的&#xff0c;因为使用了模型

【Spring Cloud】分布式配置动态刷新

目录 问题解决方案1.使用Spring Boot Actuator监控接口【不推荐】流程图使用Spring Boot Actuator的步骤 2.Spring Cloud Bus第一种方案问题Spring Cloud Bus流程图Spring Cloud Bus实现客户端刷新的步骤开发准备实现1. 在config-server中添加依赖2.在config-server中添加配置a…

你喜欢什么样的狗?喜欢内狗还是外狗?论看门狗的重要性:极低功耗微处理器复位电路CN803/CN809/CN810

看门狗&#xff1a;缩写WTD,英文WATCH DOG. 你的程序会死机吗&#xff1f; 陷入死循环或程序指针不知跑哪里去了&#xff0c;看门狗的作用是把程序拉回来&#xff0c;重新开始跑。 有了看门狗&#xff0c;程序就不会死机&#xff0c;所以很重要。 比如一个温度控制器&#xf…

OBproxy基础运维

简介 obproxy 属于OceanBase的代理&#xff0c;生产环境中 OceanBase 数据库的数据 会以 多副本的形式 存放在各个 OBServer 节点上&#xff0c;obproxy 接收用户发出的 SQL 请求&#xff0c;并将 SQL 请求转发至最佳目标 OBServer 节点&#xff0c;最后将执行结果返回给用户&…

算法题解记录27+++随机链表的复制(百日筑基)

一、题目描述&#xff1a; 题目难度&#xff1a;中等 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每…

小柴带你学AutoSar系列一、基础知识篇(4)编译

小柴带你学AutoSar总目录https://blog.csdn.net/qianshang52013/article/details/138140235?spm1001.2014.3001.5501 Flechazohttps://www.zhihu.com/people/jiu_sheng 编译真的很重要&#xff01;了解一下机器是如何工作的吧。当然啦&#xff01;通过学习这篇文章还可以学习…

unityBIM

Revit模型到Unity勉强能用 1、Revit直接导出FBX&#xff0c;然后拖到unity里面 2、通过Navisworks导出FBX&#xff0c;拖到unity里面。 我什么都还没做&#xff0c;只建立了一个空的URP效果&#xff0c;把FBX拖进去&#xff0c;挂了一个相机控制器&#xff0c;效果勉强看得过…