[LeetCode]Single Number, Single Number II & Single Number III

Single Number

问题描述:

  Given an array of integers, every element appears twice except for one. Find that single one.

  Note:Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

题目大意:

  给你一个int型数组,在这个数组中只有一个元素只出现过一次,其他元素都恰好出现两次。找出只出现了一次的这个元素。但是这里要求时间复杂度为O(n),空间复杂度要求为O(1)(只允许开辟有限个内存空间)。

分析:

  从二进制的角度来看这个问题,这是一个计数问题。我们首先考虑一个比特的情况:如果给你两个1和一个0这个时候答案应该是0,因为1出现了两次,而0只出现了一次。看到这里,我们会发现,出现次数就相当于对某一个比特位进行计数,而计数的次数与重复的次数有关系。此时我们只需要设计一个计数器来对这一位进行计数,只是这个计数器是每记到二就自动清零。那么这个计数器就是一个模二的加法器,巧合的是模二加法器就是异或运算。

  由于每一个比特都是独立的,所以对于一个int数的32个比特而言每一个比特都可以这么做。而多余的那个int的每一个比特由于这个数只出现一次,因此计算到最后最后剩下的那些比特位为1的恰好组成了这个数。于是我们的算法就是遍历数组对每一个元素进行异或运算(按位的模二加法运算)。

C++代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ret=0;
        for(int num :nums)
            ret=ret^num;
        return ret;
    }
};

Single Number II

问题描述:

  Given an array of integers, every element appears three times except for one, which appears exactly once. Find that single one.

  Note:Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

题目大意:

  给你一个int型数组,在这个数组中只有一个元素只出现过一次,其他元素都恰好出现三次。找出只出现了一次的这个元素。但是这里要求时间复杂度为O(n),空间复杂度要求为O(1)。

分析:

  这一题的思路与Single Number一样,同样我们考虑一个比特位的情况,这里需要对这个比特进行计数到3时归0,换句话说这里需要一个模3加法器,但是并没有现成的模三加法器,因此我们需要构造一个能够位运算的模三加法器。那么计数器要经历0、1、2这三个状态,但是一个比特位的位计数器只有两个状态,保存不了三个状态。因此我们需要扩展一个比特,即用两个比特位来保存位计数器的三个状态。这里我们假设b为低位、a为高位。用ab这两个比特位作为这一位c的位计数器,每当这一位c来1时位计数器就进行状态转换否则维持原状态,而最后的结果会保存在计数器的低位b里。

  我们可以得到ab的状态变化表(这里的a1b1表示ab的下一个状态,当c为0时不会变化因此就状态不发生变化):

ab c a1 b1
00 1 0 1
00 0 0 0
01 1 1 0
01 0 0 1
10 1 0 0
10 0 1 0

   上面的表其实也就是一个真值表。此时,我们将这个状态变化表用逻辑表达式进行表示(数字电路课程里面用真值表可写出逻辑表达式):

       b1=a'b'c+a'bc

       a1=a'bc+ab'c'

  这里需要注意:a1和b1分别是a和b的下一个状态因此需要区对待。

         逻辑电路中“+”对应C++里面的异或运算;

         逻辑电路中乘法对应C++里面的按位与;

         逻辑电路中非对应C++里面的按位取反;

  由于int的每一个比特都是独立的,因此可以扩展到int类型的位运算。

C++代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int a = 0, b = 0;
        int a1= 0, b1 = 0;
        for (int c : nums)
        {
            b1 = ~a&~b&c ^ ~a&b&~c;
            a1 = ~a&b&c ^ a&~b&~c;
            b=b1;
            a=a1;
        }
        return b;
    }
};

  到这里我们会觉得上面的代码还可以简化,首先a1本身就多余,而b1变量却不能直接去掉,因为这里b1的引入是为了在计算a1时让a1还能使用上一个状态的b因此不能使用"b=~a&~b&c+~a&b&~c;"这行代码来让b保存下一个状态的b。如果我们不使用b1,我们只需要在写出真值表时不使用b而使用b1作为输入即可,那么我们根据真值表重新写逻辑表达式:

       b1=a'b'c+a'bc'=a'(b+c)

       a1=a'b1'c+ab1'c'=b1'(a+c)

  于是代码变成这样:

C++代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int a = 0, b = 0;
        for (int c : nums)
        {
            b = ~a&(b^c);
            a = ~b&(a^c);
        }
        return b;
    }
};

  怎么样,这个时候是不是觉得代码简短又对称?

小结1

  看了前面两道题,我们需要对这一类型的题进行一下总结。

  1.这里我们要对每一位都进行模n的计数器与一个int类型的重复次数有关系。一个整数实际就是一个32位的数,如果这个数出现了n次那么它的每一个比特都会出现n次,因此我们需要统计n次。拿Single Number这一题而言,如果数字是2,2,3。它们的二进制是010,010,011。可以看出第一个比特(从右往左数起)出现了1次1,第二个比特出现了3次1进行模二计数可得最终结果是011,即答案是3。

  2.从Single Number和Single Number II 我们能够总结出:"给你一个xx型数组,在这个数组中只有一个元素只出现过一次,其他元素都恰好出现N次。找出只出现了一次的这个元素"。我们都可以通过设计一个模N位计数器来解题。即根据状态转换规律写出真值表,再由真值表写出逻辑表达式,最后得到最终的代码。不过,这里我们需要注意一下,当N为奇数时是需要这么做的,当N为偶数时你可以直接用Single Number的代码就可以解题,因为重复偶数次其实就是偶数次的异或运算。

  3.为什么要通过真值表来写出逻辑表达式最后得到代码?首先这是二进制运算,二进制运算最好的解释就是用逻辑表达式,很多时候逻辑表达式并不是一眼就能看出来。而真值表却能立马写出来。另外,学过数字电路的同学都能够立马由真值表写出逻辑表达式。因此一步很难完成的工作就可以通过两步很简单的工作来轻松完成。计算机的CPU本身就是一个巨大的数字电路,它因此能完成很多基本运算,CPU的硬件电路中包含了能很轻松的完成加法、模二加法等运算单元,但是却没有直接的模N加法器(即上文所说的模N位计数器),因此我们可以由代码来设计这样的加法器。

Single Number III

问题描述:

  Given an array of numbers nums, in which exactly two elements appear only once and all the other elements appear exactly twice. Find the two elements that appear only once.

  For example:

    Given nums = [1, 2, 1, 3, 2, 5], return [3, 5].

  Note:

    The order of the result is not important. So in the above example, [5, 3] is also correct.
    Your algorithm should run in linear runtime complexity. Could you implement it using only constant space complexity?
题目大意:

  给你一个数字组成的数组,里面只有两个数字只出现了一次,其他数字均出现两次,找出这两个只出现了一次的数字。举例:给你的数字数组是[1,2,1,3,2,5 ],那么应当返回[3,5]。

  注意:你输出的两个数字的顺序并不会影响答案正确性,但这里要求算法的时间复杂度是O(n),空间复杂度是O(1)。

分析:

  这一题与上面的题不一样,我们以题目给的例子进行说明。如果我们用Single Number的方式运算得到的应该是3和5异或的结果,即011XOR101=110结果是6,我们如何找到3,5和6之间的区别呢?这里我们还是要从二进制角度查找区别。6是3和5异或得结果,说明6中为1的位是3和5的不同之处。因此可以从6的二进制中位中是1的那一位来区分3和5(比如6的二进制数的从右往左数的第三位是1)。而原数组中出现两次的那些数在第三位只可能有两种情况:是1或是0。

  于是我们可以根据第三位是0还是1来将原数组分成两组来讨论。如果第三位是0则包含3和其它恰好出现两次且第三位是0的数;如果第三位是1则包含5和其他恰好出现两次且第三位是1的数。然后再用Single Number的算法跑一下每一组,即可将每一组的数字给找出来。

  举例而言:原数组是[2,2,3,4,4,5],3 xor 5=6。6的二进制是110,选取第三位来对原数组分组:

       2的二进制是 010 第三位是0

       3的二进制是 011 第三位是0

       4的二进制是 100 第三位是1

       5的二进制是 101 第三位是1

  于是分成两组,第一组是[2,2,3],第二组是[4,4,5],再用Single Number的算法可得第一组是3,第二组是5。

C++代码:

class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int diff=0;  
        for(int num :nums)  
            diff^=num;  
        diff&=-diff;
        vector<int> ret={0,0};
        for(int num : nums)
        {
            if(0==(num&diff))
                ret[0]^=num;
            else
                ret[1]^=num;
        }
        return ret;
    }
};

 

posted @ 2019-04-18 16:58  J坚持C  阅读(436)  评论(0编辑  收藏  举报