有限状态机

我们再来看只出现一次的数字 II这道题,除了前面讲的解决方式以外,我们还可以使用有限状态机。如果想要了解有限状态机,最好能有数字电路设计的基础。我们这样思考一下,假设有一种运算符 *,它具有以下功能。

1 * 1 * 1 = 0
1 * 0 = 1
0 * 1 = 1
1 * 1 = x

这个 x 是多少暂且不管,我们只知道这个运算符 *(我们自己发明的)具有交换律,并且每 3 个相同的数字通过这个运算符运算之后结果为 0 ,那么这道题就很容易解了,只需要把数组中的所有数字都通过这个运算符运算一遍即可,但问题的关键是怎么来设计这个运算符呢?

我们看到上面有 3 种状态分别是 0,1,x ,如果是两种状态可以使用 01 表示,只需要 1 位数,如果是 3 种状态至少需要 2 位数,注意这里是至少,你也可以使用 3 位, 4 位,5…… 都是可以的,关键是要找好他们之间的转化关系。这里我们就以 2 位来分析下,要注意 0 和任何数字运算的结果都不变。按照题意的要求,如果某个数出现 4 次,就让他的结果变成出现 1 次,也就是说周期是 3 ,每个数都会有下面几种状态。

出现 1 次
出现 2 次
出现 3 次

看到这里其实大家已经想到了,这不就是传说中的 3 进制吗?在二进制中一个位置要么是 1 要么是 0 ,只能表示两种状态,如果要表示 3 种状态我们可以使用两位数字来表示。

00表示出现 1 次
01表示出现 2 次
10表示出现 3 次

如果输入的是0(状态始终不变)

0 出现的次数 XY(输入之前的状态) 输入数字 XY(输入之后的状态)
1 00 0 00
2 01 0 01
3 10 0 10

如果输入的是1(始终跳到下一个状态)。

1 出现的次数 XY(输入之前的状态) 输入数字 XY(输入之后的状态)
1 00 1 01
2 01 1 10
3 10 1 00

输入和输出的关系知道了,下面我们来找一下他们的推导公式,先看一下输入之后状态 x1 的有哪些,一个是 0 出现次数为 3 的那行,一个是 1 出现次数为 2 的那行,所以我们可以得出。

X = (X & ~Y & ~num) | (~X & Y & num)
这里num是输入的数字

如果看不懂的可以先看下卡诺图化简法,这是数字电路设计中的知识(如果大学没学过的,这里可以先跳过)。其实很简单,如果是 1 就用原来的数字表示,如果是 0 就在前面添加非运算符 ~ ,关于数电的知识这里就不在过多展开。同理我们还可以推导出 Y

Y = (~X & Y & ~num) | (~X & ~Y & num)

通过上面表格我们可以看到如果 0 出现一次的话结果是 00 ,如果 1 出现一次的话结果是 01 ,所以无论输入的是 0 还是 1 ,如果只出现 1 次的话,结果就会保存在 Y 中,最后我们只需要返回 Y 的值即可。当然这里 XY 还是可以化简的,如果对卡诺图化简法不是很了解的话,我们不用化简,直接运行也是没问题的。

public int singleElement(int[] nums) {
    int x = 0;
    int y = 0;
    for (int num : nums) {
        // 为了防止变量x被覆盖,这里先把计算的结果保存起来,最后在赋值给x。
        int tmp = x & ~y & ~num | ~x & y & num;
        y = ~x & y & ~num | ~x & ~y & num;
        x = tmp;
    }
    return y;
}

上面我们说过,无论是使用 2 位数, 3 位数, 4 位数 …… 都是可以的,只要能存储下 3 种状态即可,下面我们就用一个 3 位数来计算下, 3 位数可以表示 8 种状态,可以随便选。

001 表示 1 次
010 表示 2 次
100 表示 3 次

如果输入的是0(状态始终不变)。

0 出现的次数 XYZ(输入之前的状态) 输入数字 XYZ(输入之后的状态)
1 001 0 001
2 010 0 010
3 100 0 100

如果输入的是1(始终跳到下一个状态)。

1 出现的次数 XYZ(输入之前的状态) 输入数字 XYZ(输入之后的状态)
1 001 1 010
2 010 1 100
3 100 1 001

根据上面的状态我们可以推出转换关系如下。

X = X & ~Y & ~Z & ~num | ~X & Y & ~Z & num
Y = ~X & Y & ~Z & ~num | ~X & ~Y & Z & num
Z = ~X & ~Y & Z & ~num | X & ~Y & ~Z & num

上面我们并没有化简,通过上面的表看到无论是 0 还是 1 ,如果只出现 1 次他们都会保存在 Y 中,所以最后只需要返回 Y 即可。

public int singleElement(int[] nums) {
    int x = 0;
    int y = 0;
    // 因为我们定义的001表示的是1次,所以初始化的时候让z的二进制位全部
    // 变为1,-1的二进制表示为11111111 11111111 11111111 11111111
    int z = -1;
    for (int num : nums) {
        int tmpX = x & ~y & ~z & ~num | ~x & y & ~z & num;
        int tmpY = ~x & y & ~z & ~num | ~x & ~y & z & num;
        z = ~x & ~y & z & ~num | x & ~y & ~z & num;
        x = tmpX;
        y = tmpY;
    }
    return y;
}

int 范围内能表示 3 种状态的可以挑选上亿个都不止,那么这样写下去答案就比较多了。

信奥赛编程(刷题请进)>>>

经过两年的打磨,我的新作《算法秘籍》已经出版,有需要的可以点击购买。也可以点击 内容介绍 查看详情。