leetcode -- 二進位制

長安不亂發表於2021-06-05

leetcode -- 二進位制

在學習程式語言的運算子時,大部分語言都會有與,或等二進位制運算子,我在初期學習這些運算子的時候,並沒有重點留意這些運算子,並且在後續的業務程式碼中也沒有頻繁的使用過,直到後來的一些演算法題目和原始碼中經常遇到它們的身影,這些二進位制運算子相比普通的運算子具有更快的效率,比如hashMap的原始碼就是將%替換成了&。我們在日常的很多需求都是可以轉化為二進位制運算子來完成的,這篇文章整理我在刷leetcode的時候遇到的一些二進位制題目,對於很多問題,其實我們是沒有意識來用這些二進位制運算子。本文中的程式碼都在我的github

公共子串

查詢倆個二進位制數中的公共部分,可以直接使用位與操作&,因為其中最重要的是確認哪些位置上是1,但是如果查詢範圍內數的公共子串呢?每個數進行位與操作就會超時,因此必須要考慮使用特殊的思想。

leetcode, 第201題,Bitwise AND of Numbers Range

Given two integers left and right that represent the range [left, right], return the bitwise AND of all numbers in this range, inclusive.

Example 1:

Input: left = 5, right = 7
Output: 4

Example 2:

Input: left = 0, right = 0
Output: 0

Example 3:

Input: left = 1, right = 2147483647
Output: 0

Constraints:

  • 0 <= left <= right <= 231 - 1

此題目是計算一個區間內所有數的位與結果,這道題咋看很簡單,既然要範圍內的所有數字都進行位與操作,那麼直接使用迭代位與。

// 直接所有數字進行位與
public int rangeBitwiseAnd(int left, int right) {
    if(left == right) return left;
    int sum = left;

    for(int index = left + 1; index <= right; ++index) {
        sum &= index;
    }

    return sum;
}

但是這種寫法是超時的,如果範圍是1到2147483647,那麼程式是無法通過的。

暴力解法看來不可取,只能從資料中找規律了,我們來看位與這種運算

可以從圖中看出,最後的結果是所有數字都是1的部分,也就是所有數的公共子串,問題就變成了找所有數的公共子串,那麼如何找到所有數的公共子串呢?我們思考一下,一個數大於另一個數在二進位制上的表現是什麼,比如10>9,那麼10的二進位制數,從右數的話,第二位是1,大於了9的第二位0,同時9和10的後面的數字都是0000 10XX,也就是說一個數大於另一個數,也就是在某位上的值突然變化了,而我們所求的區間是連續的,也就是這裡面所有的數都是按照順序變化的,既然是按照順序變化的,我們只要找出順序中變化最大的那個就可以了。

什麼是變化最大的?也就是最小數與最大數之間的變化,我們只要找到這倆個數的公共子串即可,上面我們說一個數大於另一個數,二進位制表現是某位上的值突然從0變成1,那麼問題在於如何找到這個變化的位數,我們可以看9和10,如果我們能把右邊的01和10切掉就好了,切掉的話就是公共子串了,切掉可以表現為移位操作,也就是不斷的往後移位,一直到公共部分,到公共部分具體變現為什麼呢?也就是倆個數相等。

從上面的分析我們知道了解題思路,移位操作,將最大和最小的倆個值進行移位,一直到倆個數相等,再將這個數後面全變成0,還是移動,只不過方向是往左移位。

// 找出公共子串,移位操作
public int rangeBitwiseAnd_1(int left, int right) {
    int shift = 0;

    while(left < right) {
        left >>>= 1;
        right >>>= 1;
        ++shift;
    }

    return left << shift;
}

但是上面這個解法的用時只能擊敗leetcode中24%的人,記憶體只能擊敗41%的人,那幫人整天就以壓榨效能為樂。如何才能再次提升呢?

上面的演算法其實說白了就是將特定位後面的1全部變成0,清除右邊的所有1,Brian Kernighan演算法就是用來清除二進位制當中的1,具體操作就是將n和n-1進行位與運算。

我們需要迭代計算n與n-1,直到m和n相等即可。有的時候我會想,直接將最小值和最大值進行位與操作呢?我們可以看5,6,7

如果將5和7進行位與,發現是錯誤的,畢竟右邊第一位二者都是相同的1,因此相鄰數做位與操作

// Brian Kernighan演算法結合
public int rangeBitwiseAnd_2(int left, int right) {

    while(left < right) {
        // 這種操作會增加記憶體操作
        //right &= (right - 1);
        right = right & (right - 1);
    }

    return right;
}

&=和=&,我以前一直以為二者只是寫法不同,沒有區別,雖然二者的執行時間相同,但是二者的記憶體消耗卻不同,=&操作的記憶體消耗只有37.5MB,&=卻有37.9MB的記憶體消耗。

反轉操作

反轉在多個問題中都有出現,如何反轉二進位制數呢?可以提取移位,也可以分而治之操作。

leetcode, 第190題,Reverse Bits

Reverse bits of a given 32 bits unsigned integer.

Note:

  • Note that in some languages such as Java, there is no unsigned integer type. In this case, both input and output will be given as a signed integer type. They should not affect your implementation, as the integer's internal binary representation is the same, whether it is signed or unsigned.
  • In Java, the compiler represents the signed integers using 2's complement notation. Therefore, in Example 2 above, the input represents the signed integer -3 and the output represents the signed integer -1073741825.

Follow up:

If this function is called many times, how would you optimize it?

Example 1:

Input: n = 00000010100101000001111010011100
Output:    964176192 (00111001011110000010100101000000)
Explanation: The input binary string 00000010100101000001111010011100 represents the unsigned integer 43261596, so return 964176192 which its binary representation is 00111001011110000010100101000000.

Example 2:

Input: n = 11111111111111111111111111111101
Output:   3221225471 (10111111111111111111111111111111)
Explanation: The input binary string 11111111111111111111111111111101 represents the unsigned integer 4294967293, so return 3221225471 which its binary representation is 10111111111111111111111111111111.

Constraints:

  • The input must be a binary string of length 32

此題是將二進位制進行反轉,這個反轉問題在什麼地方都存在,整數反轉、連結串列反轉,字串反轉等等,而且迴文字有時也是反轉問題。最容易想到的就是一個一個的進行顛倒位置就可以,主要是二進位制的顛倒如何來做?

我們可以使用1與這個數進行位與運算,這個位與操作提取出反轉數的最低位,之後將最低位進行移位,移位之後將之加到之前的值上即可。

// 逐位顛倒
public int reverseBits(int n) {
    int result = 0;
    for(int index = 0; index < 32; ++index) {
        result |= (n & 1) << (31 - index);
        // 這裡的位或與相加差不多
        // result += (n & 1) << (31 - index);
        n >>= 1;
    }
    return result;
}

其實上面的實現還有一種寫法,我們可以看出要是最後一位是0的話,那麼其實就不用相加,這個時候使用位異或或者位或將之前的計算結果儲存下來就可以了。

// 反轉
public int reverseBits_1(int n) {
    int result = 0;
    for(int index = 0; index < 32; ++index) {
        result ^= ((n >> index) & 1) == 1 ? (1 << (31-index)):0;
    }
    return result;
}

反轉是將其中的每一位進行顛倒,那麼能不能擴大想一下,將其中的每兩位顛倒,將其中的每四位顛倒,將其中的每八位顛倒,將其中的每十六位顛倒,那麼就簡單了,我們按照這種分而治之的思路來看

分別對之進行16位,8位,4位,2位,1位的顛倒處理,16位顛倒如何實現呢?將這個數分別右移16位,之後左移16位,二者位或合併即可,那麼8位顛倒呢?如果直接右移8位和直接左移8位,那麼中間還存在一段1001 1101沒有被移出去,直接位或合併的話,就會產生干擾,最好的辦法是右移8位的時候,將倆邊靠左邊的八位數提取出去,之後再進行右移8位。

那麼如何才能提取呢,參照上面倆種演算法中將數與1進行&提取出第一個數,我們也可以這樣做,在8位顛倒的時候,右移操作需要使用1111 1111 0000 0000 1111 1111 0000 0000 進行&提取出左邊八位數,在左移操作需要使用0000 0000 1111 1111 0000 0000 1111 1111進行&提取右邊八位數,4位、2位、1位都是如此操作。

// 分而治之合併,這是因為它是固定的32位
public int reverseBits_2(int n) {
    n = (n >>> 16) | (n << 16);
    n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff)  << 8);
    n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);
    n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333)<< 2);
    n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);
    return n;
}

這種分而治之的思路非常的巧妙,個人感覺直接寫不出這樣的程式碼來,在java語言中,也有反轉的實現,那麼其中的實現原理是否可以參照一二,從原始碼中來看,它也是分而治之,不過它進行了很多優化,它是從1位,2位,4位,最後將8位中的四部分分別進行顛倒,那麼就少了一步16位顛倒了。

// 和上面的一樣,不過寫法優化了,這也是java中的Integer.reverse(int i)的原始碼
public int reverseBits_3(int n) {
    n = ((n & 0x55555555) << 1) | ((n >>> 1) & 0x55555555);
    n = ((n & 0x33333333) << 2) | ((n >>> 2) & 0x33333333);
    n = ((n & 0x0f0f0f0f) << 4) | ((n >>> 4) & 0x0f0f0f0f);
    n = (n << 24) | ((n & 0xff00) << 8) | ((n >>> 8) & 0xff00) | (n >>> 24);
    return n;
}

統計問題

leetcode,第338題,Counting Bits

Given an integer n, return an array ans of length n + 1 such that for each i (0 <= i <= n), ans[i] is the number of 1's in the binary representation of i.

Example 1:

Input: n = 2
Output: [0,1,1]
Explanation:
0 --> 0
1 --> 1
2 --> 10

Example 2:

Input: n = 5
Output: [0,1,1,2,1,2]
Explanation:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101

Constraints:

  • 0 <= n <= 105

Follow up:

  • It is very easy to come up with a solution with a runtime of O(n log n). Can you do it in linear time O(n) and possibly in a single pass?
  • Can you do it without using any built-in function (i.e., like __builtin_popcount in C++)?

此問題主要是討論範圍0 - n,每個二進位制數中1的個數,聽到這個範圍內每個數的統計問題,我最先想到的是動態規劃,更有可能的方向是前後數是有關係的。關鍵是如何找出前後數之間的關係呢?數都會有進位的操作,在沒進位之前,只需要在前面的基礎上+1即可,但是進位之後就需要重新開始,那麼這個只需要+1的範圍數是哪些呢?這些範圍有多大?我們可以將十進位制數先轉化為二進位制數

從上面我們發現2就是0前面加上了1,3就是1前面加上了1,從圖中也可以看出4,5,6,7都是0,1,2,3前面加了一個1而已,也就是說範圍就是2的冪次方,這樣的話,我們就需要倆個變數b和start,一個是2的冪次方,一個就是用來遍歷的。

// 最高有效位
public int[] countBits_1(int n) {
    int[] results = new int[n+1];

    if(n == 0) return results;
    // 範圍內的數
    int start = 0;
    // 最大範圍
    int b = 1;

    while(b <= n) {

        // 對範圍內的數進行遍歷,每次+1即可
        while(start < b && start + b <= n) {
            results[start + b] = results[start] + 1;
            ++start;
        }

        start = 0;
        // 範圍都是2的冪次方,直接移位
        b <<= 1;

    }

    return results;
}

此方法時間和空間複雜度都很低,這是需要其中需要大量的遍歷迴圈,並且還多出了幾個變數。

那麼我們能不能將這個最高有效位的寫法改進一下,變成二進位制的用法,我們知道每次只需要找到對應的數+1即可,但是有個轉折就是2,4,8這些數是進位的數,我們可以看到2和1如果位與就是0,4和3位與也是0,8和7位與也是0,這是因為它們進位了,因此要判斷是否在轉折點,只需要判斷x&(x-1)是否為0,只要為0,那麼就得到進位了,最高有效位已經發生了改變,後面的數就需要根據這個最高有效位進行計算。

// 最高有效位
public int[] countBits_2(int n) {
    int[] results = new int[n+1];

    int max_bit = 0;
    for(int index = 1; index <= n; ++index) {
        // 判斷最高有效位是否發生了改變
        if((index & (index - 1)) == 0) {
            max_bit = index;
        }
        results[index] = results[index - max_bit] + 1;
    }

    return results;
}

此寫法的時間和空間效率都很高。

我們再來看這些二進位制數,2右移一位就是1,3右移一位是1的二進位制+1,4右移一位就是2,5右移一位就是2,偶數的右移是不減少1的個數的,但是奇數的右移是減少了1的個數。這樣也是前後數之後的關係了。

// 最低有效位
public int[] countBits_3(int n) {
    int[] results = new int[n+1];
    if(n == 0) return results;

    for(int index = 1; index <= n; ++index) {
        // index & 1檢視是否是有效位
        results[index] = results[index >> 1] + (index & 1);
    }

    return results;
}

Brian Kernighan演算法原理:對於任意整數,其中x&(x-1),就是將x的二進位制數中最後一個1變成0的操作。

根據這個Brian Kernighan,就可以用來數清楚1的個數,根據這個思想來看的話有倆種方法:

  1. 不斷的迴圈操作直到這個數變成0。
  2. Brian kernighan演算法讓前後數之間有了關係,這是因為x&(x-1)這個數肯定小於x,在x之前就已經計算出來了,並且它與x只相差一個1,因此x = x & (x - 1) + 1。

我們直接寫第二種方法,這種方法直接設定每一個位運算的結果推導,也被稱為最低設定位

// 最低設定位
public int[] countBits(int n) {
    int[] results = new int[n+1];

    if(n == 0) return results;
    for(int index = 1; index <= n; ++index) {
        results[index] = 1 + results[index&(index - 1)];
    }

    return results;
}

此題可以根據數的規律來找到二進位制中1的個數,最高有效位的倆種方法和最低有效位的一種方法,還可以利用Brian Kernighan演算法原理來做的。

leetcode,第191題,Number of 1 Bits

Share

Write a function that takes an unsigned integer and returns the number of '1' bits it has (also known as the Hamming weight).

Note:

  • Note that in some languages, such as Java, there is no unsigned integer type. In this case, the input will be given as a signed integer type. It should not affect your implementation, as the integer's internal binary representation is the same, whether it is signed or unsigned.
  • In Java, the compiler represents the signed integers using 2's complement notation. Therefore, in Example 3, the input represents the signed integer. -3.

Example 1:

Input: n = 00000000000000000000000000001011
Output: 3
Explanation: The input binary string 00000000000000000000000000001011 has a total of three '1' bits.

Example 2:

Input: n = 00000000000000000000000010000000
Output: 1
Explanation: The input binary string 00000000000000000000000010000000 has a total of one '1' bit.

Example 3:

Input: n = 11111111111111111111111111111101
Output: 31
Explanation: The input binary string 11111111111111111111111111111101 has a total of thirty one '1' bits.

Constraints:

  • The input must be a binary string of length 32.

Follow up: If this function is called many times, how would you optimize it?

此題也是統計一個二進位制中1的個數,當然程式的入口直接輸入32位的字串而是int型別的變數。

首先想到的思路就是迴圈移位判斷,一位一位的判斷是否為1,如果為1,那麼+1,如果不為1,那麼直接移位判斷下一步。當然這裡要注意的是移位是無符號右移還是有符號右移,不過這裡的位數是確定的32位,因此無論是否有符號,只要迴圈32次即可

public int hammingWeight(int n) {
    if(n == 0 || n == 1) return n;
    int num = 0;
    // 迴圈32位判斷
    for(int index = 0; index < 32; ++index) {
        // 判斷是否為1
        if((n & 1) == 1) {
            ++num;
        }
        // 移位到下一位進行判斷
        n >>= 1;
    }

    return num;
}

在上一題中有Brian Kernighan演算法思想,這個就是將1變成0的操作,我們只要一直做這個操作,看要做多少次才會讓數變成0,那麼這個次數就是1的個數。

public int hammingWeight_1(int n) {
    if(n == 0 || n == 1) return n;
    int num = 0;
    // 判斷是否已經為0
    while(n != 0) {
        ++num;
        n = n & (n - 1);
    }

    return num;
}

查詢問題

leetcode,第136題,Single Number

Given a non-empty array of integers nums, every element appears twice except for one. Find that single one.

You must implement a solution with a linear runtime complexity and use only constant extra space.

Example 1:

Input: nums = [2,2,1]
Output: 1

Example 2:

Input: nums = [4,1,2,1,2]
Output: 4

Example 3:

Input: nums = [1]
Output: 1

Constraints:

  • 1 <= nums.length <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104
  • Each element in the array appears twice except for one element which appears only once.

此題說明一個陣列中所有的數都是出現過倆次的,只有一個數會出現一次,找出這個出現一次的數。

題目說明陣列中的所有數都會出現倆次,但只有一個數會出現一次,那麼如果我們將陣列進行排序,那些所有的數都是有次序的,這樣的話只要檢視前後數是否相同即可。

public int singleNumber(int[] nums) {
    int len = nums.length;
    if(len == 1) return nums[0];
    int one_num = Integer.MAX_VALUE;
    Arrays.sort(nums);

    // 因為所有的數都是倆個,只有一個數是一個,因此長度必定為奇數
    for(int index = 0; index < len - 1; index += 2) {
        if(nums[index] != nums[index + 1]) {
            one_num = nums[index];
            break;
        }
    }

    // 如果上述的迴圈無法找出,那麼單數就在陣列的最後一個
    if(one_num == Integer.MAX_VALUE) one_num = nums[len - 1];

    return one_num;
}

上述的方法思想很簡單,但是時間和空間都很高,這其中包含了排序,我們需要換一種思路來看待這個問題。

題目中說明所有的數都是倆個,如果有一個運算能像消消樂那樣,倆個倆個的消除就好了,全部消除之後剩下的那個就是我們所查詢的單數,在二進位制運算中可以使用異或運算子,異或運算就可以將相同的倆個數變成0,如果倆個數不同,那麼先將倆個數結合起來,這個在HashMap的hash運算中前16與後16位結合一樣,之後碰到雙數的時候還可以消去結合的那個數。

public int singleNumber(int[] nums) {
    if(nums.length == 1) return nums[0];

    for(int index = 1; index < nums.length; ++index) {
        // 使用異或運算子
        nums[0] ^= nums[index];
    }

    return nums[0];
}

leetcode,第137題,Single Number II

Given an integer array nums where every element appears three times except for one, which appears exactly once. Find the single element and return it.

You must implement a solution with a linear runtime complexity and use only constant extra space.

Example 1:

Input: nums = [2,2,3,2]
Output: 3

Example 2:

Input: nums = [0,1,0,1,0,1,99]
Output: 99

Constraints:

  • 1 <= nums.length <= 3 * 104
  • -231 <= nums[i] <= 231 - 1
  • Each element in nums appears exactly three times except for one element which appears once.

此題是上一題的擴充,此題是所有的數都會出現三次,只有一個數出現一次,一個小小的改動,讓上一題中所有的思想都無法使用,需要重新來思考此題目。當然最簡單的可以使用雜湊表來一個一個的查詢。

如果不用雜湊表來解決這個問題的話,這個問題就很難思考出來了。既然我們無法從十進位制數下手,那麼看看這些數的二進位制

我們從圖中可以看出如果我們將每一位相加之後對3取餘就可以得到出現過一次的數了,我們依次確定每一個二進位制位,因為對於出現了三次的數來說,對應的二進位制上肯定是三個0或者三個1,無論是哪種情況,它們都是3的倍數,我們使用3取餘正好就去除了出現過三次的數。

當然在實現的過程中,還需要考慮如何對每個二進位制數進行相加取餘,之後放入到對應的二進位制位上,可以使用移位運算。

public int singleNumber(int[] nums) {
    int res = 0; 

    // 這裡的32,是因為int型別的二進位制位有32位,
    for(int index = 0; index < 32; ++index) {
        int sum = 0;
        for(int num:nums) {
            sum += (num >> index) & 1;
        }
        // 對3取餘還需要進行移位操作
        res |= (sum % 3) << index;
    }

    return res;
}

上述這種思路的效率都不高,雖然它的思路很難想到。

如果我們能用一個變數記錄下出現過一次one的數就好了,當然在迴圈陣列的時候,也會碰到出現倆次two、三次three的數,我們使用三個數來表示表示陣列中數出現的情況,當然如何使用三個int型別的數來統計查詢出現過一次的數才是最難,需要考慮其中的操作到底該如何設計?two是與one相關的,只有出現過一次之後再出現一次才能被記錄到two中,而three是one和two同時組合才能得到的,當然其中最重要的就是one的值。

  • one,值與one進行異或,這裡的異或是為了考慮three的值,
  • two,one與值進行位與,之後進行位或。位與判斷是否倆個數相同,位或是將結果賦值給two,
  • three,one與two進行位與,因為數出現倆次的時候,one是0,數出現一次的時候two是0,因此這樣可以得到three值。

當一個數出現了三次了,one和two中的值如何進行糾正?就是如何將one和two中關於此數的影響進行去除,可以將one和two都位與three相反數。

對於上述思路的第一個問題

陣列中數的順序是否影響結果,比如1,1,1,3的時候,是否會按照我們預想中的執行,糾正的效果如何呢?從下圖我們看出基本還是能夠完成基本的功能的,雖然到達3的時候,three變成了0,並沒有記錄好1,但是隻要one記錄完成即可。

那麼如何順序變化為1,1,3,1的時候呢?

我們發現雖然中間過程3的時候並沒有如我們預想那般完美,這時候因為one和two都有值,one是3,two是1,three自然也就有值了,但是在值變成1的時候,one還是能夠得到的。

public int singleNumber_1(int[] nums) {
    int one = 0, two = 0, three = 0; 

    for(int num:nums) {
        // 首先更新的是第二個數
        two |= one & num;
        one ^= num;
        three = one & two;

        // 糾正
        one &= ~three;
        two &= ~three;
    }

    return one;
}

其實我們思考一下,這些解法都是通過記錄狀態、狀態轉換來實現,上面的方法使用one、two、three三個變數來記錄,狀態轉換則是通過各種特殊的位運算實現。one、two、three記錄狀態過於平白,在二進位制中,倆位就可以實現四種變化了,00,01,10,11。而此題其實只是需要三種變化,正如所有數都有倆個,當數達到倆個的時候就會轉化為最初狀態了,這裡是數達到三個的時候就轉化為最初狀態。想法是可以做的,但是這個狀態轉換如何實現呢?這是每一個思路上的大問題,因為狀態轉換的運算設計必須是精巧的並且滿足全部要求,這裡的second first來表示倆位,first的更新通過first與值進行異或之後與second的反運算進行位與,second的運算也是類似的,這樣在更新的時候就考慮倆位了。

  • 第一次遇到,first先更新為1,使用異或運算,second此時為0,因此將second的反運算與之進行位與,first^num不管是什麼都會通過的。而second更新的時候,first的反運算會讓結果保持在0。second first = 0X
  • 第二次遇到,firstnum成為了0,因此first也就是0了,此時secondnum則是完全通過,那麼second first = X0
  • 第三次遇到,first^num與second就相同了,second的反運算與之位與將first變成了0,之後second與num相同,那麼二者異或就是0,那麼second也會被更新為0,那麼second first = 00

當然上面的只是一種理想的表述,因為如果中間出現了其他數就會進行狀態疊加,此時就會發生變化,比如2,2,3,2

前面的倆個2,2都沒有問題,按照上面的思路來實現的,之後遇到3之後就變成了01,這個其實更像是一種狀態的疊加,之後再遇到2,就可以消去這種2的狀態。

public int singleNumber_2(int[] nums) {
    int first = 0, second = 0; 
   
    for(int num:nums) {
        // 狀態進行了轉化
        first = ~second & (first ^num);
        second = ~first & (second ^num);
    }

    return first;
}

程式碼非常的簡單,但是這種思路確實無法想到,如何使用位運算設計出狀態轉換操作也是本題一個非常大的難點。

leetcode,第260題,Single Number III

Given an integer array 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. You can return the answer in any order.

You must write an algorithm that runs in linear runtime complexity and uses only constant extra space.

Example 1:

Input: nums = [1,2,1,3,2,5]
Output: [3,5]
Explanation:  [5, 3] is also a valid answer.

Example 2:

Input: nums = [-1,0]
Output: [-1,0]

Example 3:

Input: nums = [0,1]
Output: [1,0]

Constraints:

  • 2 <= nums.length <= 3 * 104
  • -231 <= nums[i] <= 231 - 1
  • Each integer in nums will appear twice, only two integers will appear once.

題目說明陣列中只有倆個數是隻出現過一次,其他數都出現了倆次,找出出現過一次的數。與前面一樣都是明確了數量,不同於前面的就是它需要找出倆個數。但是前面的思路還是可以用的,我們可以將之先排序,這樣整個陣列就變得有規律了,之後進行迴圈前後對比,如果相同則比較下一組,如果不同,那麼將之儲存起來,之後比較下一個數。整體的思路基本不變,就是一些細節是需要改動一下變得符合題意的。

public int[] singleNumber(int[] nums) {
    if(nums.length == 1 || (nums.length == 2 && nums[0] != nums[1])) return nums;
    // 進行排序
    Arrays.sort(nums);

    int[] result = new int[2];
    int index = 0;
    int result_index = 0;
    // 迴圈比較
    for(; index < nums.length - 1;) {
        if(nums[index] == nums[index + 1]) {
            index += 2;
        }else {
            result[result_index] = nums[index];
            ++index;
            ++result_index;
        }
    }
    if(index == nums.length - 1) result[result_index] = nums[index];
    return result;
}

現在思考一下,陣列全部進行異或是否能解決這個問題呢?當然不能,之前那個只有一個,因此可以,但是這裡是倆個,如果全部異或,那麼異或的結果就是倆個數的不同位。知道了倆個數不同位能做什麼呢?似乎啥都做不了,啊哈哈。

以前我們做題的時候總會先把大問題轉化為小問題,把陌生的問題轉化為熟悉的問題,這裡似乎也可以,是不是可以將倆個轉化為一個呢?我們可以將這個大的陣列進行分組,分組的要求就是

  • 倆個只出現一次的數應該在不同的組中,
  • 相同的數被分到相同的組中。

之前我們說全部異或的結果就是知道了倆個數不同位,當此位為1的時候,就代表這倆個數在這個對應位上是不同的,那麼這個位就可以用來分組了,因為如果倆個數相同,那麼這倆個數肯定也會被分到相同的組中,這一下就滿足了上面倆個條件。那麼整個演算法過程可以分成三步

  1. 先將陣列中所有的數進行異或,得到異或結果,
  2. 之後找出位為1的位置,這個位置也就是不同之處,
  3. 利用不同之處將陣列進行分組,並且通過異或得到不同的數
public int[] singleNumber_1(int[] nums) {
    if(nums.length == 1 || (nums.length == 2 && nums[0] != nums[1])) return nums;

    int[] result = new int[2];
    int sum_all = 0;
    int differ_pos = 1;
    // 先將陣列中所有的數進行異或
    for(int num:nums) {
        sum_all ^= num;
    }
    // 之後找出sum_all為1的位置,這個位置也就是不同之處
    while((sum_all & differ_pos) == 0) {
        differ_pos = differ_pos << 1;
    }
    // 將陣列進行分組,並且通過異或得到不同的數
    for(int num:nums) {
        if((num & differ_pos) == 0) {
            result[0] ^= num;
        }else {
            result[1] ^= num;
        }
    }

    return result;
}

這是一個絕妙的思路,它的時間空間效率都很好。

進位制轉化

在很多場景都需要進位制轉化和格式轉化,其中最頻繁出現的肯定就是十進位制轉化為二進位制,這個在計算機組成原理就可以提及到,第一個想到的方法肯定就是除基取餘法,就是將數除以2,之後記下餘數,再用商除以2,一直這樣除下去,記錄所有的餘數,直到商為0,之後把之前記錄下來的餘數倒著排放在一起,就是轉化後的二進位制數,

// 十進位制轉化為二進位制,除基取餘法
public void getResult(int num){
    StringBuilder sb = new StringBuilder();
    while(num > 0) {
        // 每次都要%2
        sb.insert(0, num % 2);
        num /= 2;
    }

    System.out.println(sb.toString());
}

當然我們知道java中有很多操作二進位制的運算子,其中我們就可以使用移位運算子,每次移位,之後輸出0或者1,這裡的只要使用位與1即可,此方法非常的方便的。

public void getResult_1(int num){

    for(int index = 31; index >= 0; --index) {
        System.out.print((num >> index) & 1);
    }

}

總結

二進位制運算子的使用在很多時候都有妙用,這些用法可以讓我們的程式碼看起來更加的成熟,不至於很小白,啊哈哈。異或的公平用於hashMap中,也可用於消除相同元素中,hashMap的擴容使用了移位,移位更加的方便,也可用於遍歷整個數的情況,位與可用於判斷相同數中,找出公共點,當然也可用於判斷奇偶或者1這些需求。一個需求可以實現的很巧妙,也可以實現的很樸素,這還是取決於程式設計師的功力。

相關文章