K:劍指offer-56 題解 誰說數字電路的知識不能用到算法中?從次數統計到數字電路公式推導,一文包你全懂

前言:

本題解整理了一位大佬在leetcode中的代碼的方法,該博文致力於讓全部人都可以可以看懂該方法。爲此,本題解將從統計數字出現次數的解題方式開始講起,再推導出逐位統計的解題方式,指望以按部就班的方式得出最終代碼的思想。java

相關知識關鍵字:

二進制、位運算、真值表、邏輯表達式、狀態機數組

題解:

對於數組nums,其只有一個數字出現了一次,其他數字均出現了三次,一種直觀的想法是直接採用一個map統計各個字符出現的次數,最後再遍歷map中的各個鍵值對,直到找到只出現了一次的數字。其代碼以下code

public int singleNumber(int[] nums) {
        //統計各個數字出現的次數,鍵爲數字,值爲出現的次數
        Map<Integer,Integer> map =new HashMap<Integer,Integer>();
        for(int i:nums){
            if(!map.containsKey(i)){
                map.put(i,1);
                continue;
            }
            map.put(i,map.get(i)+1);
        }
        //遍歷map中的鍵值對,查看值出現次數爲1的鍵,即爲答案
        int result = 0;
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            if(entry.getValue()==1){
                result = entry.getKey();
                break;
            }
        }
        return result;
    }

對於該解題方法,其空間複雜度爲O(n),時間複雜度爲O(n),這顯然不會是該題的最優解。blog

在得出逐位運算的解題方式以前,咱們須要研究下該數組中的數字用二進制的方式進行表示的特色。leetcode

以題幹給出的示例1爲例,nums=[3,4,3,3],將數組中各個數字採用二進制的方式寫出,
3 = (0011)2
4 = (0100)2
3 = (0011)2
3 = (0011)2get

經過對數組中各個數的二進制表示形式逐位進行觀察,咱們能夠發現,當數組中只出現一次的那個數字(用k表示)在二進制的對應位爲0時,該對應位爲1在數組各個數字中出現的總次數應當爲3^n ,當k的對應位爲1時,該對應位爲1在數組各個數字中出現的總次數應當爲 3^n + 1,爲此,咱們能夠統計數字中的各個位中1出現的次數,當爲3^n 次時,只出現一次的數字的對應位應當爲0,當爲3^n + 1次時,只出現一次的數字的對應位應當爲1。由此,咱們能夠獲得以下代碼:it

public int singleNumber(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        int result = 0;
        for(int i = 0;i<32;i++){
            //統計該位1的出現次數狀況
            int count = 0;
            int index = 1<<i;
            for(int j:nums){
                //該位與操做後的結果不爲0,則表示該位爲1的狀況出現了
                if((index&j)!=0){
                    count++;
                }
            }
            //該位上出現1的次數mod3後爲1,表示出現一次的數字該位爲1
            if(count%3==1){
                result|=index;
            }
        }
        return result;
    }

對於該解題方法,其時間複雜度爲O(n),空間複雜度爲O(1)。在某種程度上,這是最優解了。可是,該題解仍有改進的空間(其時間複雜度的常係數爲32)。class

有了對數組中數字的各二進制位進行逐一統計分析出現次數的相關基礎後,咱們即可以推導出那個擊敗100%的答案的解法了。回顧上面的解題方法的分析部分,其須要咱們對數字的二進制位逐位進行統計,對於int數據類型,咱們須要遍歷32次數組(int佔4字節),以便統計出各個二進制位出現的次數。那咱們有沒有辦法只遍歷一次數組便得出答案呢?固然有,咱們能夠一次分析32bit的int的各個位在數組的各個數字中出現的次數。在分析上面的代碼咱們能夠發現,實際上,咱們只須要記錄對應位出現的次數爲0、一、2次的狀況,當對應位出現次數爲3的時候,咱們即可以將該位出現的次數置爲0,從新開始進行計數。因爲int型中的各個二進制位出現的次數爲3進制的,爲此咱們須要兩個位來記錄各個位出現的次數,由此咱們須要引入兩個變量a,b來統計對應位出現的次數。由ab兩個變量組合起來來記錄各個二進制位出現爲1的狀況。變量a表示高位的狀況,變量b表示低位的狀況,而在遍歷數組運算完成以後,遍歷b的值即是答案。基礎

變量ab組合的各個二進制位組合的形式有以下三種,考慮進新引入的變量c的各二進制位的狀況,咱們能夠獲得以下真值表:變量

真值表

由以上真值表,咱們即可得出變量a,b的邏輯表達式,其表示以下
a = a’(!b’)(!c)+(!a’)b’c
b = (!a’)b’(!c)+(!a’)(!b’)c = (!a’)[b’(!c)+(!b’)c] = (!a’)[b’^c]

由此,咱們能夠獲得以下代碼

public int singleNumber(int[] nums) {
        //a對應位爲1表示出現2次的記錄,b對應位表示出現1次或0次的記錄,ab共同組成該位出現的次數
        int a = 0,b =0;
        for(int i:nums){
            int temp = a;
            a = (~a&b&i)|(a&~b&~i);
            b = ~temp&(b^i);
        }
        return b;
    }

實際上,咱們還能對a的邏輯表達式進行簡化,先獲得b的邏輯表達式,以後用b代替b’做爲輸入,由此能夠簡化a爲
a = (!a’)(!b)c+a’(!b)(!c) = (!b)[(!a’)c+a’(!c)] = (!b)[a’^c]

由此,咱們能夠獲得以下代碼

public int singleNumber(int[] nums) {
        //a爲對應位的1出現2次的記錄,b爲對應位出現1次的記錄,ab共同組成該位出現的次數
        int a = 0,b =0;
        for(int i:nums){
            b = ~a&(b^i);
            a = ~b&(a^i);
        }
        return b;
    }

至此,咱們獲得了最終的代碼。


這個是本人的公衆號,致力於寫出絕大部分人都能讀懂的技術文章。歡迎相互交流,咱們博採衆長,共同進步。

公衆號二維碼.jpg

相關文章
相關標籤/搜索