来自leetcode的单号II

时间:2014-01-23 00:35:10

标签: java algorithm bit-manipulation puzzle

关于leetcode的单号II的问题是:

给定一个整数数组,除了一个元素外,每个元素都会出现三次。找一个单一的。 注意: 您的算法应具有线性运行时复杂性。你可以在不使用额外内存的情况下实现吗?

实际上,我已经从网站上找到了解决方案,解决方法是:

public int singleNumber(int[] A) {
    int one = 0, two = 0;
    for (int i = 0; i < A.length; i++) {
        int one_ = (one ^ A[i]) & ~two;
        int two_ = A[i] & one | ~A[i] & two;
        one = one_;
        two = two_;
    }
    return one;
}

但是,我不知道为什么这段代码可以工作,实际上我不知道第一次看到这个问题时想到这个问题的方法呢?任何帮助。 THX!

7 个答案:

答案 0 :(得分:6)

这个想法是将数字重新解释为GF(3)上的向量。原始数字的每个位都成为向量的一个组成部分。重要的部分是对于GF(3)向量空间中的每个向量v,求和v + v + v得到0.因此,所有向量的和将保留唯一向量并取消所有其他向量。然后,结果再次被解释为一个数字,它是所需的单个数字。

GF(3)向量的每个分量可以具有值0,1,2,其中加法执行mod 3.“1”捕获低位,“2”捕获结果的高位。因此,尽管该算法看起来很复杂,但它所做的只是“无需进位的数字加法模3”。

答案 1 :(得分:5)

因此,我遇到了一些编码问题,并在相当长的一段时间内坚持了下来,在Google上进行了大量研究之后,通过不同的帖子和门户网站,我已经理解了这个问题。我会尽力解释它。

问题有3个解决方案:

  1. 使用HashMap :但是,我们知道这会增加O(N)空间的复杂性,我们不希望这样。但是需要一点理解,方法是迭代数组,获取数字的计数并将其维护在map中。然后迭代地图,计数为1的位置就是您的答案。
  2. 使用按位运算符:此方法的逻辑是考虑以位为单位的数字,然后将每个位置的所有位相加。因此,相加后,您将看到总和是3的倍数或3 + 1的倍数(因为其他数字仅发生一次)。此后,如果对这个总和进行取模,将得到结果。您将通过该示例更好地理解。

    示例:数组-[5、5、5、6]
    5位代表:101
    6位表示:110

    [101,101,101,110](值的二进制表示)
    添加到特定位置后,我们将具有
    0th-> 3,1th-> 1,2nd-> 4
    如果你乘以3,它将变成
    0th-> 0,1th-> 1,2nd-> 1
    用十进制表示就是我们的答案6.
    现在我们需要编写相同的代码。我已经用注释解释了代码。
public class SingleNumberII {
    /*
    * Because the max integer value will go upto 32 bits
    * */
    private static final int INT_SIZE = 32;

    public int singleNumber(final int[] A) {

        int result = 0;
        for(int bitIterator = 0; bitIterator < INT_SIZE; bitIterator++) {
            int sum = 0, mask = (1 << bitIterator);
            /*
            * Mask: 
            * 1 << any number means -> it will add that number of 0 in right of 1
            * 1 << 2 -> 100
            * Why we need mask? So when we do addition we will only count 1's,
            * this mask will help us do that
            * */
            for(int arrIterator = 0; arrIterator < A.length; arrIterator++) {
                /*
                * The next line is to do the sum.
                * So 1 & 1 -> 0
                * 1 & 0 -> 0
                * The if statement will add only when there is 1 present at the position
                * */
                if((A[arrIterator] & mask) != 0) {
                    sum++;
                }
            }

            /*
            * So if there were 3 1's and 1 0's
            * the result will become 0
            * */
            if(sum % 3 == 1) {
                result |= mask;
            }
        }

        /*So if we dry run our code with the above example
        * bitIterator = 0; result = 0; mask = 1;
        * after for loop the sum at position 0 will became 3. The if 
        * condition will be true for 5 as - (101 & 001 -> 001) and false for 6
        * (110 & 001 -> 000)
        * result -> 0 | 1 -> 0
        * 
        * bitIterator = 1; result = 0; mask = 10;
        * after for loop the sum at position 1 will became 1. The if
        * condition will be true for 6 as - (110 & 010 -> 010) and false for 5
        * (101 & 010 -> 000)
        * result -> 00 | 10 -> 10
        * 
        * bitIterator = 2; result = 10; mask = 100;
        * after for loop the sum at position 2 will became 4. The if
        * condition will be true for 6 as - (110 & 100 -> 100) and true for 5
        * (101 & 100 -> 100)
        * result -> 10 | 100 -> 110 (answer)
        * */
        return result;
    }
}

正如我们看到的那样,这不是最佳解决方案,因为我们不需要对其进行32次以上的迭代,而且也没有那么概括。这就是访问我们的下一个方法的原因。

  1. 使用按位运算符(优化和通用)
    因此,对于这种方法,我将尝试解释该方法,然后编写代码,然后进行概括。 为了类比,我们将2个标记(一个,两个)视为它们的集合。 因此,我们第一次出现该数字时,仅当它不存在于两个中时,才将它添加到一个中。我们将对两个进行相同的操作,这意味着如果该数字第二次出现,我们会将其从1中删除,然后将其添加到两个(仅当一个中不存在该数字时),而第三次出现时该数字将被删除从第二组开始,将不再存在于任何一组中。 您可能有一个问题,为什么在第4点中解释了2个(或变量)原因。
public int singleNumberOptimized(int[] A) {
    int one = 0, two = 0;
    /*
    * Two sets to maintain the count the number has appeared
    * one -> 1 time
    * two -> 2 time
    * three -> not in any set
    * */
    for(int arrIterator = 0; arrIterator < A.length; arrIterator++){
        /*
        * IF one has a number already remove it, and it does not have that number
        * appeared previously and it is not there in 2 then add it in one.
        * */
        one = (one ^ A[arrIterator]) & ~two;
        /*
         * IF two has a number already remove it, and it does not have that number
         * appeared previously and it is not there in 1 then add it in two.
         * */
        two = (two ^ A[arrIterator]) & ~one;
    }

    /*
    * Dry run
    * First Appearance : one will have two will not
    * Second Appearance : one will remove and two will add
    * Third Appearance: one will not able to add as it is there in two
    * and two will remove because it was there.
    *
    * So one will have only which has occurred once and two will not have anything
    * */
    return one;
}
  1. 如何更一般地解决这类问题?
    您需要创建的集合数取决于k的值(其他整数的出现)。 m> = log(K)。 (要对数组中的1进行计数,以使每当计数的1达到某个值(例如k)时,计数便会返回零并重新开始。要跟踪到目前为止我们遇到了多少个1,我们需要一个计数器。假设计数器有m位。)m是我们需要的集合数。
    对于其他所有内容,我们都使用相同的逻辑。但是等待我该返回什么,逻辑是对所有集合进行“或”运算,最终将对单个数字与自身以及一些0(对这个单个数字进行求和)进行“或”运算。 为了更好地理解此特定部分,请阅读这篇帖子here
    我已尽力向您解释解决方案。希望你喜欢它。
    #HappyCoding

答案 2 :(得分:4)

乍一看,代码似乎很棘手,很难理解。 但是,如果以布尔代数形式考虑问题,一切都会变得很清楚。

我们需要做的是存储每一位的1's数。由于32位中的每一个都遵循相同的规则,因此我们只需要考虑1位。我们知道一个数字最多出现3次,因此我们需要 2位进行存储。现在我们有4个状态,00、01、10和11,但是我们只需要3个状态。

在您的解决方案中,选择01(代表1)和10(代表2)。令one代表第一位,two代表第二位。然后,我们需要为one_two_设置规则,以便它们按我们希望的那样工作。完整的循环是00-> 10-> 01-> 00(0-> 1-> 2-> 3/0)。

最好制作 Karnaugh地图,也就是 K地图。对于卡诺地图Ref.

三态计数器


之后的A[i]twoonetwo_one_的值

0 0 0 | 0 0
0 0 1 | 0 1
0 1 0 | 1 0
0 1 1 | X X
1 0 0 | 0 1
1 0 1 | 1 0
1 1 0 | 0 0
1 1 1 | X X

在这里 X 表示我们不在乎这种情况,或者只关心最终值2和1,无论它们的输出是1还是什么也可以考虑,结果将是相同的,并且第四和第八种情况不存在,因为两种情况不能同时为一种。

如果您正在考虑我如何提出上表。我将解释其中之一。考虑第七种情况,当A [i]为1时,两个为1,即已经存在重复两次的A [i]。最后有3个A [i]。由于其中有3个,因此two_one_都应重置为0。

考虑使用one_
对于两种情况(即第二种情况和第五种情况),其值为 1 。采取 1 与考虑在K-map中的最小项相同。

one_ = (~A[i] & ~two & one) | (A[i] & ~two & ~one)

如果~two很普遍,那么

(~A[i] & one) | (A[i] & ~one) will be same as (A[i]^one)

然后

one_ = (one ^ A[i]) & ~two

考虑使用two_
对于两种情况(即第三种情况和第六种情况),其值为 1 。采取 1 与考虑在K-map中的最小项相同。

two_ = (~A[i] & two & ~one) | (A[i] & ~two & one)
对计算出的 two _ 进行

位操作将解决该问题。但是,正如您提到的

two_ = (A[i] & one) | (~A[i] & two)

考虑到无关 X ,对于上述所有情况,考虑到 X < / strong>不会影响我们的解决方案。

考虑使用K-map并考虑使用X
对于两种情况(即第三和第六种情况),其值为 1 ;对于两种情况(即第四和第八种情况),其值为 X 。现在,考虑最小期限

two_

现在,以上面的表达式中的公共(A&one)和(〜A&Two),您将剩下(1 |〜2)和(1 |〜one)1。因此,我们'将被留下

two_ = (~A[i] & two & ~one) | (A[i] & ~two & one) |  (~A[i] & two & one) | (A[i] & two & one)

For more insights

答案 3 :(得分:3)

有三种状态:0,1,2

所以不能使用单个位,必须使用高/低位来表示它们:00,01,10

这是逻辑:

高/低00 01 10

x = 0 00 01 10

x = 1 01 10 00

高是高和低的函数。

如果低== 1则高= x,否则高=高&amp; 〜X

我们有

高=低&amp; x |高&amp; 〜X

这等于你:“int two_ = A [i]&amp; one | ~A [i]&amp; two;”

同样,我们作为高和低的函数都很低:

如果高== 1则低= ~x,否则低=低XOR x

答案 4 :(得分:0)

我有一个更直接的解决方案:

int singleNumber(int A[], int n) {
    int one = 0, two = 0, three = ~0;

    for(int i = 0; i < n; ++i) {
        int cur = A[i];
        int one_next = (one & (~cur)) | (cur & three);
        int two_next = (two & (~cur)) | (cur & one);
        int three_next = (three & (~cur)) | (cur & two);
        one = one_next;
        two = two_next;
        three = three_next;
    }

    return one;
}

答案 5 :(得分:0)

首先是我的头脑,它更大但更容易理解。只需实现3的附加模式。

*

class Solution {
    public:
        int sum3[34], bit[33];
        int singleNumber(int A[], int n) {
            int ans(0);
            for(int i=0;i<33;i++){
                bit[i + 1] = 1<<i;
            }
            int aj;
            for(int i=0;i<n;i++){
                    for(int j=1;j<33;j++){
                        aj = abs(A[i]);
                    if(bit[j] & aj) sum3[j]++;
                }
            }
            for(int i=0;i<33;i++){
                sum3[i] %= 3;
                if(sum3[i] == 1) ans += bit[i];
            }
            int positve(0);
            for(int i=0;i<n;i++){
                if(A[i] == ans){
                    positve++;
                }
            }
            if(positve%3 == 1)
            return ans;
            else return -ans;
        }
    };

*

答案 6 :(得分:0)

这是另一种解决方案。

   public class Solution {  
        public int singleNumber(int[] nums) {  
           int p = 0;  
           int q = 0;  
           for(int i = 0; i<nums.length; i++){  
              p = q & (p ^ nums[i]);  
              q = p | (q ^ nums[i]);  
           }  
           return q;  
        }  
    }  

来自this blog post的分析。