分門別類刷演算法,堅持,進步!
大家好,我是刷題困難戶老三,這一節我們來刷幾道很有意思的求次數問題
,它們都有同一類非常巧妙的解法。
這種解法是什麼呢?往下看吧!
基礎知識
在開始之前,我們最好先了解一些位運算的基礎知識。
原反補碼
先簡單說一下,原碼、反碼、補碼。
一個數在計算機中的二進位制表示形式, 叫做這個數的機器數。機器數是帶符號的,在計算機用一個數的最高位存放符號, 正數為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
[3]. https://blog.csdn.net/White_Idiot/article/details/70178127
[4].https://blog.csdn.net/qq_30374549/article/details/89520849
[6]. https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/computercode.html