数组中数字出现的次数 —— 有限状态自动机法解析

文章概述:

刷 leetcode 时看到K神的题解,觉得有限状态自动机法十分优秀,但是理解起来并不容易。于是花了些时间学习研究,并在此用笔记叙述对这种方法的理解,方便以后回顾。

接下来会对题目做简单的介绍,对题解感兴趣可以点击以下链接阅读:

面试题56 - II. 数组中数字出现的次数 II(位运算 + 有限状态自动机,清晰图解)


题目及代码:

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。

请找出那个只出现一次的数字。如:

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

class Solution {
    public int singleNumber(int[] nums) {
        int ones = 0, twos = 0;
        for(int num : nums){
            ones = ones ^ num & ~twos;
            twos = twos ^ num & ~ones;
        }
        return ones;
    }
}
作者:jyd
链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/solution/mian-shi-ti-56-ii-shu-zu-zhong-shu-zi-chu-xian-d-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

很精妙吧,只用了十行代码就解决了问题,并且时间复杂度为 O(N),空间复杂度为 O(1),反正我看完后感到极度舒适。接下来将叙述理解代码的思路:

1. 大致思路

做题之前,我们首先要在脑海里形成一个大致的思路,然后围绕着它不断去填充细节,这样才不容易跑偏以及思路混乱。题目给出的数组中,有一个数字只出现一次,其余数字都出现三次。

我们在思考时候不妨把这个只出现一次的数字先拿出来,那么显然剩下的数字加起来正好能被3整除且余数为0。由此可以得到一个简单的结论:如果把数组的所有数字加起来除以3,得到的余数就正好是我们想要的那个只出现一次的数字了。

2. 一般解法

有了大致思路后,若是单纯的解题其实已经很简单了,比如用哈希表记录每个数字出现的次数然后查找次数为1的值,但是哈希表建表耗时较久,遍历统计既要统计每个数字出现的次数又要去查找,因此效率上是远不如有限状态自动机法的。

而为什么有限状态自动机法会更优秀呢?因为该方法没有老老实实地去统计每个数字的次数,粗暴地去理解,它利用位运算及状态转换,能让三个相同的数在运算后变为0,那么所有数运算后剩下的就是只出现一次的那个数,就不用再费劲地去统计和查询了,接下来会进一步地解释说明。

3. 算法的理解

我们用位运算做这道题,考虑数字的二进制形式,int型为32位数,为了理解更加简单,那就先当作只有1位来思考(反正实际位运算时,32位是一起做同样的运算的)。

核心还是只有一个数字会出现一次而其他数字会出现三次,那么结合有限状态自动机我们的解题思路做出稍许改进,我们可以设计一个算法与数字的二进制形式的每一位运算,用一个变量根据数字的二进制形式中位上 “1” 出现的次数记录当前的状态:初始状态是0,如果输入0状态则不变,如果输入1,一个则变为1,第二个则变为2,第三个就回归到0,如此形成了循环。

能发现这种做法的好处了吗?想象一下我们在玩消消乐,数组中每个数的位进行这种运算,三个相同的数运算完成后,不管这三个数位上是0还是1,他们的结果都必定是0,如此数组中出现三此的数带来的影响就被消除了,最后他们的结果,也就是0再与这个只出现一次的数进行运算,运算结果恰恰就是这个数(这个数是0,运算结果为0;这个数是1,运算结果为1),我们要的东西直接就得到了。

来吧,假设现在的数组是{n1, n1, n1, n2},且数组里每个数的二进制都只有一位,本题最终算法所设计出来的运算姑且称为 # 运算,初始状态当然是0了。现在我们将数组中的每个数作为算法的输入:

0 # n1 # n1 # n1 =》0

0 # n2 =》n2

那么时间复杂度和空间复杂度也能推测出来了。程序只需要把数组中的n个数逐个扫描一遍作为算法的输入,故时间复杂度为O(N);只需要用几个变量记录当前的状态,故空间复杂度为O(1)。

顺带一提,这种算法和数组中数字出现的先后次序是无关的,毕竟我们在做的,本质上就是从数字的每个位拿到0或1去进行某种运算,自然先拿到谁位上的数据作为输入就无所谓了,你只需要确保把所有的位都输入到这种运算中就行。

4. 获得公式

接下来便借用K神的图而不对公式推导做详细说明了,毕竟文章的初衷是为了理解思路中的难点。有疑问可以直接去看题解,里面有非常详细的说明。公式的推导运用了数字电子技术中的知识点,可以去了解一下逻辑代数、函数化简和卡诺图等相关知识。

当然,要理解为何用two和one来记录状态还需要想明白一件事。其实我们也可以用一个int数来记录状态,直接0、1、2三个状态就好了。但是这里,一个整型数有32位,我们希望用位运算一次把他们都解决,而不是整32个int型变量来存储状态。所以最后的结果是体现在位上的,而位只有0、1两种选择,面对三种状态显然我们就只能用两个位来存储状态了。

状态转换图

one公式推导

two公式推导

5. 返回值

现在理解上的最后一个难关就是,为何返回值是“one”了。

现在我们已经知道,三种状态是 00 =》01 =》10,我们还知道,出现三次的数运算完成后结果是00。那么接下来再与只出现一次的这个数(位)做最后的运算:它是0,结果为00;它是1,结果为01.

发现了么,最终的结果要么是00,要么是01,two恒为0,one恒为出现一次的数所代表的值。所以,现在可以想起来每个数有32位了。他们的运算结果ones,它的32个位的每个位,正好就是出现一次的数对应位的值,自然,ones就是只出现一次的数了。

总结

写到这里文章就差不多结束了,虽然因此在这道题上花的时间有点多,但是收获却是不少的:掌握了一种优秀的算法,较好地明白了有限状态循环机,位运算更熟练了。甚至还能举一反三:一个数组,除一个数字只出现一次之外,其他数字都出现了n次,解法还是大同小异吧,无非是多设置一些变量记录状态:one、two、three······

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值