分門別類刷演算法,堅持,進步!

刷題路線參考: https://github.com/chefyuan/algorithm-base

大家好,我是刷題困難戶老三,這一節我們來刷幾道很有意思的求次數問題,它們都有同一類非常巧妙的解法。

這種解法是什麼呢?往下看吧!

基礎知識

在開始之前,我們最好先了解一些位運算的基礎知識。

原反補碼

先簡單說一下,原碼、反碼、補碼。

一個數在計算機中的二進位制表示形式, 叫做這個數的機器數。機器數是帶符號的,在計算機用一個數的最高位存放符號, 正數為0, 負數為1.

比如,十進位制中的數 +3 ,假如計算機字長為8位,轉換成二進位制就是00000011。如果是 -3 ,就是 10000011 。

  • 原碼

原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值. 比如如果是8位二進位制:

[+1]原 = 0000 0001

[-1]原 = 1000 0001

  • 反碼

正數的反碼是其本身

負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反。

[+1] = [00000001]原 = [00000001]反

[-1] = [10000001]原 = [11111110]反

  • 補碼

正數的補碼就是其本身

負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)

[+1] = [00000001]原 = [00000001]反 = [00000001]補

[-1] = [10000001]原 = [11111110]反 = [11111111]補

補碼是人腦認識裡不太直觀的一種表示方式,之所以發明補碼,是為了讓機器以一種一致的方式來處理加法運算。

更多知識建議閱讀《j計算機組成原理》。

與或非異或運算

在處理整型數值時,位運算子可以直接對組成整型數值的各個位進行操作。這些位運算子在位模式下工作。位運算子包括:&|~^

  • 與(&)

對應位都為1,結果為1,否則結果為0

int a=129;
int b=128;
System.out.println("a與b的結果:"+(a&b));
# 輸出
a與b的結果:128

計算過程如下:

10000001 &
10000000 =
10000000
  • 或(|)

對應位只要有一個為1,結果是1,否則就為0

int a=129;
int b=128;
System.out.println("a或b的結果:"+(a|b));
# 輸出
a或b的結果是:129

計算過程如下:

10000001 |
10000000 =
10000001
  • 非(~)

位為0,結果是1;位為1,結果是0

 int a = 8;
System.out.println("非a的結果:"+(~a));
# 輸出
非a的結果:-9

計算過程如下

        //8轉換為二進位制
1000
// 補符號位
01000
// 取反
10111 (補碼)
// 轉原始碼除符號位取反+1
11001
  • 異或(^)

對應位相同,結果是0,否則結果是1

1111 ^
0010 =
1101

移位運算

移位運算見名知意,是數字二進位的移動,我們這裡只討論int型的移位運算。

  • << 左移運算子

數值的補碼全部左移若干位,符號位和高位丟棄,低位補 0。

  • >> 右移運算子

數值的補碼全部右移若干位,符號位不變。

假如int是8位二進位制,兩個例子如下:

10的補碼為0000 1010,左移一位變成20(0001 0100),右移一位變成5(0000 0101)

5的補碼為0000 0101,左移一位變成10(0000 1010),右移一位變成2(0000 0010)

求次數問題

LeetCode136. 只出現一次的數字

題目:136. 只出現一次的數字 (https://leetcode-cn.com/problems/single-number/)

難度:簡單

描述:

給定一個非空整數陣列,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。

說明:

你的演算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎?

思路:

雜湊法

用雜湊表儲存每一個元素出現的次數,最後找到出現一次的元素。

程式碼如下:

    public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
//儲存元素出現的次數
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
//遍歷獲取出現次數為1的情況
for (int k : map.keySet()) {
if (map.get(k) == 1) {
return k;
}
}
return -1;
}

時間複雜度:O(n)

空間複雜度:O(n)

位運算

題中要求空間複雜度O(1),雜湊法明顯是不合要求的。

這裡有一個全新的方法:位運算

異或運算有如下特點:

  • 一個數和 0 做異或運算等於本身:a⊕0 = a
  • 一個數和其本身做 異或 運算等於 0:a⊕a = 0
  • 異或 運算滿足交換律和結合律:a⊕b⊕a = (a⊕a)⊕b = 0⊕b = b

可以重複分利用異或運算的特性,異或陣列所有元素,最後留下的那個就是隻出現一次的元素。

    public int singleNumber(int[] nums) {
int ans = 0;
for (int i = 0; i < nums.length; i++) {
//異或運算
ans ^= nums[i];
}
return ans;
}

時間複雜度:O(n)

空間複雜度:O(1)

LeetCode137. 只出現一次的數字 II

題目:137. 只出現一次的數字 II (https://leetcode-cn.com/problems/single-number-ii/)

難度:中等

描述:

給你一個整數陣列 nums ,除某個元素僅出現 一次 外,其餘每個元素都恰出現 三次 。請你找出並返回那個只出現了一次的元素。

這道題和 劍指 Offer 56 - II. 陣列中數字出現的次數 II 是一樣的。

思路:

雜湊法

第一反應還是雜湊法,不用多說了,直接上程式碼:

    public int singleNumber(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
for (int k : map.keySet()) {
if (map.get(k) == 1) {
return k;
}
}
return -1;
}

時間複雜度:O(n)

空間複雜度:O(n)

位運算

好了,又到了我們的主角出場。

將我們的數的二進位制位每一位相加,然後對每一位的和與3取餘:

這個原理是什麼呢?

如果其他數都出現 3 次,只有目標數出現 1 次,那麼每一位的 1 的個數無非有這兩種情況,

  • 為 3 的倍數(全為出現三次的數)
  • 3 的倍數 +1(包含出現一次的數)

這個 3 的倍數 +1 的情況也就是我們的目標數的那一位。

程式碼如下:

    public int singleNumber(int[] nums) {
int res = 0;
for (int i = 0; i < 32; i++) {
int count = 0;
for (int num : nums) {
//檢查第i位是否為1
if ((num >> i & 1) == 1) {
count++;
}
}
if (count % 3 != 0) {
//將第i位設為1
res = res | 1 << i;
}
}
return res;
}

時間複雜度:O(n)

空間複雜度:O(1)

LeetCode260. 只出現一次的數字 III

題目:260. 只出現一次的數字 III (https://leetcode-cn.com/problems/single-number-iii/)

難度:中等

描述:

給定一個整數陣列 nums,其中恰好有兩個元素只出現一次,其餘所有元素均出現兩次。 找出只出現一次的那兩個元素。你可以按 任意順序 返回答案。

這道題和 劍指 Offer 56 - I. 陣列中數字出現的次數 是一模一樣的。

思路:

這次不是一個重複的元素了,是兩個。還是先上我們樸素的雜湊法。

雜湊法

程式碼如下:

    public int[] singleNumber(int[] nums) {
int[] res = new int[2];
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
int index = 0;
for (int k : map.keySet()) {
if (map.get(k) == 1) {
res[index] = k;
index++;
}
}
return res;
}

時間複雜度:O(n)

空間複雜度:O(n)

位運算[5]

我們在 LeetCode136. 只出現一次的數字 裡只用了一個異或就找出了那個出現一次的數字。

這道題怎麼辦呢?

要是我們能把它分成兩組就好了。

怎麼分呢?

大家都知道異或運算對應位相同,結果是0,否則結果是1

我們可以根據兩個數某一位是否是0和1來把陣列分為兩組。

例如陣列: [12,13,14,17,14,12]

異或的結果是:13^17。

分組位找到了。

那麼怎麼藉助分組位進行分組呢?

13、17的異或值,可以僅保留異或值的分組位,其餘位變為 0,例如 11100變成00100。

為什麼要這麼做呢?在第二題提到,我們可以根據 a & 1 來判斷 a 的最後一位為 0 還是為 1,所以我們將 11100變成00100之後,然後陣列內的元素 x & 001 即可對 x 進行分組 。

那麼我們如何才能僅保留分組位,其餘位變為 0 呢?

可以利用 x & (-x) 來保留最右邊的 1。

程式碼如下:

    public int[] singleNumber(int[] nums) {
int bitMask = 0;
//把陣列中的所有元素全部異或一遍
for (int num : nums) {
bitMask ^= num;
}
//保留最右邊的1
bitMask &= -bitMask;
int[] res = {0, 0};
for (int num : nums) {
//將陣列分成兩部分,每部分分別異或
if ((num & bitMask) == 0) {
res[0] ^= num;
} else {
res[1] ^= num;
}
}
return res;
}

總結

三道求次數問題就這麼做完了。

求次數問題的樸素做法是Hash法,使用Hash儲存元素出現次數。

但是Hash法空間複雜度是O(n),如果要求O(1)的空間複雜度就不行了。

這時候就要靈活利用位運算的方法,位運算的關鍵在於充分了解位運算的相關應用。

簡單的事情重複做,重複的事情認真做,認真的事情有創造性地做。

點贊關注不迷路,咱們下期見!


博主演算法練習生一枚,刷題路線和思路主要參考如下!

參考:

[1]. https://github.com/chefyuan/algorithm-base

[2]. https://leetcode-cn.com/problems/single-number-ii/solution/ti-yi-lei-jie-wei-yun-suan-yi-wen-dai-ni-50dc/

[3]. https://blog.csdn.net/White_Idiot/article/details/70178127

[4].https://blog.csdn.net/qq_30374549/article/details/89520849

[5].https://leetcode-cn.com/problems/single-number-iii/solution/javawei-yun-suan-jie-jue-ji-bai-liao-999-dp5b/

[6]. https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/computercode.html