算法解析——单身狗问题

欢迎来到博主的专栏:算法解析
博主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位为1
                ret |= 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;//单身狗数1
        int single2 = 0;//单身狗数2

        for (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;//单身狗数1
        int single2 = 0;//单身狗数2

        for (int num : nums)
        {
            if ((num & flag) == flag)
            {
                single1 ^= num;
            }
            else
            {
                single2 ^= num;
            }
        }

        return { single1,single2 };
    }
};

总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

iperf3带宽压测工具使用

iperf3带宽压测工具使用 安装下载地址&#xff1a;[下载入口](https://iperf.fr/iperf-download.php)测试结果&#xff1a;时长测试&#xff08;压测使用&#xff09;:并行测试反向测试UDP 带宽测试 iPerf3 是用于主动测试 IP 网络上最大可用带宽的工具 安装 下载地址&#x…

SAP 生产订单批量报工(代码分享)

最近公司一直在对成本这块的业务进行梳理,影响比较大的就是生产这块的报工,经常会要求要批量的冲销报工,然后在继续报工,来调整生产订单的实际工时,前面的博客中已经给大家分享了批量冲销生产订单的代码, 下面给大家分享一下生产订单批量报工的代码 首先流程制造和离散制…

如何解决研发数据传输层面安全可控、可追溯的共性需求?

研发数据在企业内部跨网文件交换&#xff0c;是相对较为普遍而频繁的文件流转需求&#xff0c;基于国家法律法规要求及自身安全管理需要&#xff0c;许多企业进行内部网络隔离。不同企业隔离方案各不相同&#xff0c;比如银行内部将网络隔离为生产网、办公网、DMZ区&#xff0c…

如何用ChatGPT上热门:完整使用教程与写作技巧

1. ChatGPT概述修订 ChatGPT是一款基于深度神经网络的语言生成技术&#xff0c;能够协助用户创造出各类高品质的文字材料&#xff0c;适宜广泛的应用场景&#xff0c;如编撰文章、文学创作及社交媒体内容生成。 2. 利用ChatGPT生成热门内容的基本步骤 为了有效利用ChatGPT创作…

代码随想录算法训练营第三十五 | ● 860.柠檬水找零 ● 406.根据身高重建队列 ● 452. 用最少数量的箭引爆气球

860.柠檬水找零 讲解链接&#xff1a;https://programmercarl.com/0860.%E6%9F%A0%E6%AA%AC%E6%B0%B4%E6%89%BE%E9%9B%B6.html 本题只有5元10元20元&#xff0c;只需要考虑收到5、10、20这三种情况&#xff1b; 收到5元&#xff0c;五块的个数&#xff1b; 收到10&#xff0c;找…

Appium安装及配置(Windows环境)

在做app相关自动化测试&#xff0c;需要使用appium来做中转操作&#xff0c;下面来介绍一下appium的环境安装配置 appium官方文档&#xff1a;欢迎 - Appium Documentation 一、下载appium 下载地址&#xff1a;https://github.com/appium/appium-desktop/releases?page3 通…

Java多线程--volatile关键字

并发编程的三大特性 可见性有序性原子性 可见性 为什么会有可见性问题&#xff1f; 多核CPU 为了提升CPU效率&#xff0c;设计了L1&#xff0c;L2&#xff0c;L3三级缓存&#xff0c;如图。 如果俩个核几乎同时操作同一块内存&#xff0c;cpu1修改完&#xff0c;当下是对c…

TDR原理的介绍

目录 简介 简单定义 TDR测试原理 简介 时域和频域就像孪生兄弟一样&#xff0c;经常在测试测量领域同时出现&#xff0c;可谓是工程师们分析问题和解决问题的两大法宝。所以&#xff0c;在某些测试场景中&#xff0c;如果有时域信息的护法&#xff0c;咱们就能从时频域两个维…

嵌入式Linux shell编程实例

1. 输入两个数&#xff0c;实现两个数的相加 &#xff08;1&#xff09;具体实现代码如下 1 #!/bin/bash2 read a3 read b4 sum$(($a$b))5 echo "$sum"&#xff08;2&#xff09;编辑完内容后按Esc键再输入:wq保存&#xff0c;回车退出&#xff0c;执行结果如下图&a…

uniapp创建支付密码实现(初始密码,第二次密码)

示例&#xff1a; 插件地址&#xff1a;自定义数字/身份证/密码输入框&#xff0c;键盘密码框可分离使 - DCloud 插件市场 1.下载插件并导入HBuilderX&#xff0c;找到文件夹&#xff0c;copy number-keyboard.vue一份为number-keyboard2.vue&#xff08;number-keyboard.vue是…

[数据集][目标检测]脑溢血检测数据集VOC+YOLO格式767张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;767 标注数量(xml文件个数)&#xff1a;767 标注数量(txt文件个数)&#xff1a;767 标注类别…

java 远程调试

1.远程启动时 jdk1.8-32\jre\bin\java.exe -Dfile.encodingUTF-8 -Djava.library.pathlib -agentlib:jdwptransportdt_socket,servery,suspendn,address5005 -jar local-com.yuetai.service-0.0.1-SNAPSHOT.jar --spring.config.locationapplication.yml 2.本地调试项目连接远…

关于前端代码移动端的适配方案

为什么需要适配&#xff1f; 由于PC端和移动端的分辨率不同&#xff0c;前端展示的页面在两端设备如果原模原样的搬运则会导致PC端或移动端看到的画面相对于其设备的分辨率及其的不合理。 最为常见的是PC端正常浏览的网页没有做移动端适配&#xff0c;由于移动端分辨率普遍低于…

Base64码转换

title: Base64码转换 date: 2024-06-01 20:30:28 tags: vue3 后端图片前端显示乱码 现象 后端传来一个图片&#xff0c;前端能够接收&#xff0c;但是console.log()后发现图片变成了乱码&#xff0c;但是检查后台又发现能够正常的收到了这张图片。 处理方法 笔者有尝试将图…

虚拟现实环境下的远程教育和智能评估系统(三)

本周继续进行开发工具的选择与学习&#xff0c;基本了解了以下技术栈的部署应用&#xff1b; 一、Seata&#xff1a; Seata&#xff08;Simple Extensible Autonomous Transaction Architecture&#xff09;是一款开源的分布式事务解决方案&#xff0c;旨在提供高性能和简单易…

WordPress plugin MStore API SQL注入漏洞复现(CVE-2023-3077)

0x01 产品简介 WordPress和WordPress plugin都是WordPress基金会的产品。WordPress是一套使用PHP语言开发的博客平台。该平台支持在PHP和MySQL的服务器上架设个人博客网站。WordPress plugin是一个应用插件。 0x02 漏洞概述 WordPress plugin MStore API 3.9.8 版本之前存在S…

信息学奥赛初赛天天练-18-挑战程序阅读-最长公共子序列、字符串与数组越界的巧妙应用

PDF文档公众号回复关键字:20240601 1 2023 CSP-J 阅读程序2 阅读程序&#xff08;程序输入不超过数组成字符串定义的范围&#xff1a;判断题正确填√&#xff0c;错误填&#xff1b;除特殊说明外&#xff0c;判断题1.5分&#xff0c;选择题3分&#xff0c;共计40分&#xff…

【30天精通Prometheus:一站式监控实战指南】第12天:windows_exporter从入门到实战:安装、配置详解与生产环境搭建指南,超详细

亲爱的读者们&#x1f44b;   欢迎加入【30天精通Prometheus】专栏&#xff01;&#x1f4da; 在这里&#xff0c;我们将探索Prometheus的强大功能&#xff0c;并将其应用于实际监控中。这个专栏都将为你提供宝贵的实战经验。&#x1f680;   Prometheus是云原生和DevOps的…

PVE虚拟机 安装 OpenWrt

1、创建虚拟机 2、操作系统 3、磁盘&#xff0c;先删除 4、网络 5、其它默认 6、在 local 分区上传镜像 7、登录PVE虚拟机 # 切换到镜像目录 cd /var/lib/vz/template/iso/# 把镜像导入磁盘 qm importdisk 102 openwrt-buddha-version-v7_2022_-x86-64-generic-squashfs-uefi…

从零开始学习Slam-旋转矩阵旋转向量四元组(二)

本文参考&#xff1a;计算机视觉life 仅作笔记用 书接上回&#xff0c;上回不清不楚的介绍了旋转矩阵&旋转向量和四元组 现在回顾一下重点&#xff1a; 本着绕谁谁不变的变则 假设绕z轴旋转θ&#xff0c;旋转矩阵为&#xff1a; 再回顾一下旋转向量的表示以及这个基本记不…