# 2020q3 Homework (quiz2) contributed by <`JimmyLiu530`> ###### tags: `進階電腦系統理論與實作` [題目網址](https://hackmd.io/@sysprog/2020-quiz2#%E6%B8%AC%E9%A9%97-1) :::info 此作業範圍: [數值系統](https://hackmd.io/@sysprog/c-numerics) 及 [bitwise](https://hackmd.io/@sysprog/c-bitwise) 操作 ::: ## 測驗 1: ASCII 字元判斷 欲將以下用來判斷指定的記憶體範圍內是否全是有效的 `ASCII 字元`的函式 ```c= #include <stddef.h> bool is_ascii(const char str[], size_t size) { if (size == 0) return false; for (int i = 0; i < size; i++) if (str[i] & 0x80) /* i.e. (unsigned) str[i] >= 128 */ return false; return true; } ``` 在64位元的架構下,改寫成**一次比對`64位元`的資料**來提高效率。如下: ```c= #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <string.h> bool is_ascii(const char str[], size_t size) { if (size == 0) return false; int i = 0; while ((i + 8) <= size) { uint64_t payload; memcpy(&payload, str + i, 8); if (payload & MMM) return false; i += 8; } while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } ``` ### 程式說明 到目前為止,ASCII共定義128個字元(從第0~127號),若用2進位來表示 00000000 ~ 01111111 就是 ASCII 可表示的字元範圍。我們可以觀察出這些有效字元轉成2進位後,MSB 都會是`0`,所以用`& 10000000(0x80)`去對每個字元做檢測,若結果 >0,表示無效位元。 因為我們想要一次比對 `64` 位元(8 bytes),因此原本用來檢測的 `0x80` 要擴展成 `0x8080808080808080`,所以上述程式中,MMM應填入 `0x8080808080808080`。 最後第17~21行用來檢測最後那些`不足 8 bytes` 的部分。 ### 延伸問題 1. `memcpy` 這邊之所以用 `memcpy` 是為了讓一次比對的這 8 bytes 對齊,也就是放在同一個 word 中,好讓 cpu 一次讀取完成。 :::warning NOTE: 當資料來源和欲寫入的目的記憶體區塊有部分重疊的話,應改用 [`memmove`](https://www.tutorialspoint.com/c_standard_library/c_function_memmove.htm) ::: 2. 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」 ```c= #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <ctype.h> #include <string.h> #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) bool contain_letters(const char str[], size_t size) { if (size == 0) return false; size_t n = size/8; for (size_t i = 0; i < n; i++, str+=8){ uint64_t *chunk = (uint64_t *) str; if((*chunk & PACKED_BYTE(0x80)) == 0){ /* is valid ASCII */ uint64_t A = *chunk + PACKED_BYTE(128 - 'A'); uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' -1); uint64_t uppercase = (A ^ Z) & PACKED_BYTE(0x80); uint64_t a = *chunk + PACKED_BYTE(128 - 'a'); uint64_t z = *chunk + PACKED_BYTE(128 - 'z' -1); uint64_t lowercase = (a ^ z) & PACKED_BYTE(0x80); if ((uppercase | lowercase) & PACKED_BYTE(0x80)) return true; } } n = size % 8; for(size_t i = 0; i < n; i++){ if((str[i] >= 65 && str[i] <= 90) || (str[i] >= 97 && str[i] <= 122) ) return true; } return false; } ``` - 利用測驗5的`PACKED_BYTE` 及 只有大寫字母的 `(A ^ Z) & PACKED_BYTE(0x80)` 這項會有 0x80 的特性去做改寫。 - 新增對小寫字母的判斷 (21~23行) - 若(uppercase | lowercase)其中有包含0x80,則為真 3. 承 (2),考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對 ```c= // TODO ``` ----------------------------------------------------------- ## 測驗2: 16進位字元轉數值 開發解析器 (parser) 時,常會將十六進位表示的字串 (hexadecimal string) 轉為數值,例如 `'0xF'` (大寫 F 字元) 和 `'0xf'` (小寫 f 字元) 都該轉換為 15。考慮以下不需要分支 (branchless) 的實作: ```c= uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` 已知 `in` 一定隸屬於 `'0'`, `'1'`, `'2'`, …, `'9'` 及 `'a'`, `'b'`, `'c'`, …, `'f'` (小寫) 及 `'A'`, `'B'`, `'C'`, …, `'F'` (大寫) 這些字元。預期 `hexchar2val('F')` 和 `hexchar2val('f')` 回傳 `15`,且 `hexchar2val('0')` 回傳 `0`,其他輸入都有對應的數值。 以下摘自 ASCII 表格 * `'0'`, `'1'`, `'2'`, … , `'9'` 對應到 `0x30`, `0x31`, `0x32`, … ,`0x39` * `'a'`, `'b'`, `'c'`, …, `'f'` (小寫) 對應到 `0x61`, `0x62`, `0x63`, …, `0x66` * `'A'`, `'B'`, `'C'`, …, `'F'` 對應到 `0x41`, `0x42`, `0x43`, …, `0x46` ### 程式說明 首先我們可以從變數的命名看出一些端倪,`in & MASK` 的結果為 `letter`,代表這邊想區分出 `數字` 跟 `字母`。再觀察我們會發現數字的 ASCII 都是`0x3X`, a~f小寫字母都是`0x6X`, A~F大寫字母都是`0x4X`,換成2進位表示比較: | in | hex | binary | | ---: | --- | ---- | | 數字 | 0x3X |0**0**11 _ _ _ _ | | 大寫字母 | 0x4X | 0**1**00 _ _ _ _ | | 小寫字母 | 0x6X | 0**1**10 _ _ _ _ | 發現數字跟字母的第4跟第6個 bit 剛好會相反,但為了要表現出 "如果是字母,letter 才會有值",所以用第6個 bit 來區分而不是第4個,因此 `MASK` 取 `01000000` = `0x40`。 接下來假設 `in` 是數字,則 letter = `0x0`,不管 `AAA` 跟 `BBB` 是什麼,shift = `0x0`,最後`(in + shift) & 0xf` 都會得到 `in` 對應的數字,也就是說我們無法從 數字 來推敲出 `AAA` 跟 `BBB`,所以得從 字母 下手。 假設 `in` = 'A' = 0x41 = 0100 _0001_ 轉成數值會是 10 = 0000 _1010_,因為最後一行 ( in + shift ) & 0xf 的 `& 0xf`,所以我們只要看**最低位的4個 bit** 就好。又 0001 (in) + _ _ _ _ = 1010 (輸出數值),很明顯 _ _ _ _會是 1001,所以 `shift` = 0000 **1001**。又因為 `in` 是字母,所以 `letter` = 0100 0000,`shift` 可透過 (letter >> `3`) | (letter >> `6`) 得到,因此 `AAA = 3`, `BBB = 6`。 > 為什麼只透過 `in = 'A'` 的推論就可以直接斷定地說 `AAA = 3`, `BBB = 6`? 難道 `in` 換成 B, C, D, b, c, d也會是一樣的結果嗎? > > 是的! 因為`'A'` 與 `'a'` 最低位的4個 bit 皆為 0001,且若把 `shift` 視為 `in` 跟 `其對應的數值` 最低位的4個 bit 的差值,就會發現 `shift` 是不變的! 舉例來說如果 `in`從 A 換成 C, `in` 的值增加2 (從0x41到0x43),但其實其對應的值也會增加2 (從10到12),所以他們的差值是不變的。 | var | hex | binary | decimal | ---: | --- | ---- | --- | | in(='A') | 0x41 |0100 **0001** | 65 | | (in+shift) & 0xf | 0x0A | 0000 **1010** | 10 | | shift | 0x09 | 0000 ==1001== | 9 | | letter | 0x40 | 0100 0000 | 64 | ### 延伸問題 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值 ```c= #include <assert.h> uint64_t hexstr2val(const char str[], size_t size) { if(size == 0) return 0; assert(size <= 18); assert(str[0] == '0' && (str[1] == 'x' || str[1] == 'X')); uint64_t output = 0; for (size_t i = 2; i < size; i++){ uint8_t tmp = hexchar2val(str[i]); output = (output << 4) | tmp; } return output; } ``` - 只允許字串長度最多`18 bytes`(含0x)且開頭只能是 `0X` 或 `0x` - 針對字串中的字元去呼叫 `hexchar2val` - 將結果先存入一個 64 bits 的變數,等整個字串轉換完成再輸出 ----------------------------------------------------------- ## 測驗3: 快速mod運算 除法運算的成本往往高於加法和乘法,於是改進的策略被引入,其中一種策略是將除法改為乘法運算。 ```c= const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(XXX) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint64_t quotient = ((__uint128_t) M * n) >> 64; return n - quotient * D; } ``` 其中 `__uint128_t` 是 GCC extension,用以表示 128 位元的無號數值。 預期 quickmod(5) 會得到 2, quickmod(55) 得到 1, quickmod(555) 得到 0 (555 是 3 的倍數)。 ### 程式說明 依據題目給的提示 $\dfrac{n}{D} = n*\dfrac{\dfrac{2^N}{D}}{2^N}$,以及程式第6行可看出 `N = 64` 並且 M = $\dfrac{2^N}{D}$= $\dfrac{2^{64}}{3}$,但因為 $\dfrac{2^{64}}{3}$ 無法用整數表達,因此我們要先估算,並將差值補償回去。選項中最接近 $\dfrac{2^{64}}{3}$ 的就是 $\dfrac{2^{64}-1}{3}+1$,所以分子那項就是`0xFFFFFFFFFFFFFFFF`,因此 `XXX = 0xFFFFFFFFFFFFFFFF`。 接下來我們要驗證其結果的正確性。欲驗證 $\dfrac{n}{D}$ = $\dfrac{M*n}{2^{64}}$,其中 M = $\dfrac{2^{64}-1}{D}+1$。 右式 = $\dfrac{M*n}{2^{64}}$ = $\dfrac{{(\dfrac{2^{64}-1}{D}+1})*n}{2^{64}}$ = $\dfrac{{(2^{64}-1+D})*n}{D*2^{64}}$ = $\dfrac{2^{64}*n-n+D*n}{D*2^{64}}$ = $\dfrac{n}{D}+\dfrac{n(D-1)}{D*2^{64}}$,又因為 n <= $2^{32}-1$ < $2^{64}$,所以$\dfrac{n(D-1)}{D*2^{64}}$ < 1,因此最後結果存入 `quotient` 時,小於 1 的部分會被忽略,就會得到`右式 = 左式`的結果。因此,當我們能得到正確的商數,就能保證`餘數(=n - quotient * D)`正確。 ### 延伸問題 比較透過處理器的整數除法指令及本技巧的效能差距 // TODO ----------------------------------------------------------- ## 測驗4: 是否被指定的除數整除 延伸測驗 3,我們想判斷某個數值能否被指定的除數所整除,在 D 和 M 都沿用的狀況下,程式碼如下: ```c= bool divisible(uint32_t n) { return n * M <= YYY; } ``` ### 程式說明 > 參考`tsengsam` 大大的作法,不僅清楚也易懂! 根據第三題得知 M = $\dfrac{2^{64}-1}{3}+1$,若寫成16進位型式,`M = 0x5555555555555556`。 已知 D = 3,因此我們可以將所有正整數分成三類 - `3k`, `3k+1`, `3k+2`,其中 k 屬於正整數。 當 `n = 3k`, $n*M$ = $3k*M$ = $3k*0x555...56$ = $3k*(0x555...5+1)$ = $k(0xfff...f+3)$ = $2k$ ==(重點!!因為 `0xfff...f` 再加3會 overflow 因此變成 2)== 當 `n = 3k+1`, $n*M$ = $(3k+1)*M$ = $3k*0x555...56 + M$ = $2k+M$ 當 `n = 3k+2`, $n*M$ = $(3k+2)*M$ = $3k*0x555...56 + 2M$ = $2k+2M$ 整除跟不可整除就差在有沒有 M ,所以當`n * M <= M-1`時,n 一定能被 D=3 整除。 :::info Note: 有沒有辦法證明到 對於所有的 D 都適用? > 令 D = k,則 $M = \dfrac{2^{64}-1}{k}+1$,並且可將所有正整數分成 k 類: km, km+1, km+2,...,km+(k-1) > > 當 n = km, $n*M$ = $km*\dfrac{2^{64}-1}{k}+1$ = $m*2^{64}-m+km$,因為 $2^{64}$在64位元的無號整數中會是`0x0`,所以$m*2^{64}-m+km$ = $(k-1)*m$ > 當 n = km+1, $n*M$ = $(km+1)*M$ = $km*M + M$ = $(k-1)m+M$ > 當 n = km+2, $n*M$ = $(km+2)*M$ = $km*M + 2M$ = $(k-1)m+2M$ ... > 當 n = km+(k-1), $n*M$ = $(km+(k-1))*M$ = $km*M + (k-1)M$ = $(k-1)m + (k-1)M$ 因此,由上述證明可見不管 D 帶多少都會成立 ::: ----------------------------------------------------------- ## 測驗5: 字串英文字母全改為小寫 考慮 `strlower` 函式的作用是將指定的字串 (或部分字串) 的英文字母全改為小寫,其 in-place 的實作如下: ```c= #include <ctype.h> #include <stddef.h> /* in-place implementation for converting all characters into lowercase. */ void strlower(char *s, size_t n) { for (size_t j = 0; j < n; j++) { if (s[j] >= 'A' && s[j] <= 'Z') s[j] ^= 1 << 5; else if ((unsigned) s[j] >= '\x7f') /* extended ASCII */ s[j] = tolower(s[j]); } } ``` 在 64 位元處理器架構 (以下針對 `x86_64`, little endian),我們可引入向量化 (vectorized) 的手段,避免一次比對一個字元,而是直接操作一整個 word (對 `x86_64` 來說,就是 64 bits,或 8 個位元組)。沿用上述的 `strlower` 函式,我們用這樣的思路實作向量化的 `strlower`,程式碼列表如下: ```c= #include <ctype.h> #include <stddef.h> #include <stdint.h> #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) /* vectorized implementation for in-place strlower */ void strlower_vector(char *s, size_t n) { size_t k = n / 8; for (size_t i = 0; i < k; i++, s += 8) { uint64_t *chunk = (uint64_t *) s; if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */ uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2); uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3); uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2; *chunk ^= mask; } else strlower(s, 8); } k = n % 8; if (k) strlower(s, k); } ``` 對應的測試程式碼如下: ```c= #include <stdio.h> #include <string.h> int main() { /* quote from https://www.gutenberg.org/files/74/74-h/74-h.htm */ char str[] = "This eBook is for the use of anyone anywhere at no cost and with \ almost no restrictions whatsoever. You may copy it, give it away or \ re-use it under the terms of the Project Gutenberg License included \ with this eBook or online at www.gutenberg.net"; int n = strlen(str); strlower_vector(str, n); puts(str); } ``` 參考執行輸出: > this ebook is for the use of anyone anywhere at no cost and with almost no restrictions whatsoever. you may copy it, give it away or re-use it under the terms of the project gutenberg license included with this ebook or online at `www.gutenberg.net` ### 程式說明 `PACKED_BYTE(b)` 的作用是將傳入的 b, (e.g. 0x80), 擴展成64位元的無號整數 (e.g. 0x8080...80) 程式第13行用來判斷是否為 ASCII 字元,這我們在測驗1已經做過了,所以知道 PACKED_BYTE(VV1) = 0x8080808080808080 => (((uint64_t)(VV1) & (0xff)) * 0x0101010101010101u) = 0x8080808080808080 =>`VV1 = 0x80` 接下來,觀察 `strlower` 的第8行發現相對應的大小寫字母只差 00100000(0x20),因此我們可以推論 `mask = 0x2020...20`,又 `mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2;`,所以 (A ^ Z) & PACKED_BYTE(VV4) = 0x8080...80 => `VV4 = 0x80`。此外,根據`mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2;`我們也得知 (A ^ Z) = 1 _ _ _ _ _ _ _ 1 _ _ _ _ _ _ _ ... 最後,為了比較好理解,賦予`PACKED_BYTE(128 - 'A' + VV2)` 跟 `PACKED_BYTE(128 - 'Z' + VV3)` 意義。至於為什麼要這麼看,我們後面再解釋。 > 將 `PACKED_BYTE(128 - 'A' + VV2)` 看成 使最小的大寫字母 'A' 與其相加 >= 0x80 的最小數; > 將 `PACKED_BYTE(128 - 'Z' + VV3)` 看成 使最大的大寫字母 'Z' 與其相加 < 0x80 的最大數。 > 因此,`'A'` 在 ASCII 的值為0x41,最小數取 0x3f,因為 0x80 - 0x41 = 0x3f,所以PACKED_BYTE (128 - 'A' + VV2) = 0x3f3f...3f,整理最後可以得到`VV2 = 0`,同理,`VV3 = -1`。 為甚麼會這麼看呢? 根據上述第三段的最後一行,(A ^ Z) = 1 _ _ _ _ _ _ _ 1 _ _ _ _ _ _ _ ... 代表 A 跟 Z 只能有一個長 1 _ _ _ _ _ _ _ 1 _ _ _ _ _ _...,而且一定是 A,因為 A > Z。所以當最小的大寫字母 'A' 與 PACKED_BYTE(128 - 'A') 相加都能達到 0x80了,那麼'B', 'C',...,'Z'一定也可以。 相反的,Z 每兩個 byte 的 MSB 就一定要是0,所以當最大的大寫字母 'Z' 與 PACKED_BYTE(128 - 'Z' -1) 相加都不超過 0x80了,那麼'B', 'C',...,'Z'一定也不超過。 ### 延伸問題 將前述測試程式 main 函式的 `char str[]` 更換為 `char *str`,會發生什麼事?請參閱 C 語言規格書,說明 string literal 的行為 參考 [字串(字元陣列 char str[])與字元指標(char* str)的關係](https://hackmd.io/@karasu/string-and-pointer-in-c) ```c= char str[] = "Hello World"; ``` 與 ```c= char *str = "Hello World"; ``` 皆宣告了 str 字串,在 C-style string 的函數皆可使用,但背後的意義卻是不相同。 前者是個 char array, 含12 bytes(含\0),"Hello World" 對於 str 來說是 initializer,將字元一個一個複製進 str 陣列 而後者是個指向 char 的 pointer,由於 "Hello World" 本身就是一個 string literal (在C99標準[6.4.5] 提到),所以 str 指向這個 string literal 值得一看的例子: 1. 只有char *s可以使用 *s++寫法 ```c= #inlcude <stdio.h> int main(void) { char s[] = "Hello World"; for (int i = 0; i < 11; i++){ printf("%c", *s++); } } ``` 會出現 error ```c= error: lvalue required as increment operand ``` 這是因為雖然 s = &s[0],但 s 是"常數",恆等於 &s[0]。而 char *s 為指標指向 s[0],但卻是變數,可以任意改變,因此可用 *s++來更改 pointer 的值 2. Segment fault ```c= #include <stdio.h> int main(void){ char* str = NULL; gets(str); printf("%s\n", str); return 0; } ``` 會出現 error ```c= error: Segment fault (core dumped) ``` 應改成 `char str[] = NULL` ,或是在第4、5行中加入 `str = (char *)malloc(N*sizeof(char));` 才對 ----------------------------------------------------------- ## 測驗6: LeetCode [137.Single number II](https://leetcode.com/problems/single-number-ii/) > Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one. Note: Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory? Example 1: Input: [2,2,3,2] Output: 3 Example 2: Input: [0,1,0,1,0,1,99] Output: 99 考慮以下程式碼為 LeetCode 137. Single Number II 的題解: ```c= int singleNumber(int *nums, int numsSize) { int lower = 0, higher = 0; for (int i = 0; i < numsSize; i++) { lower ^= nums[i]; lower &= KKK; higher ^= nums[i]; higher &= JJJ; } return lower; } ``` ### 程式說明 我們一樣賦予 `lower` 跟 `higher` 意義來幫助理解,`lower` 想像成所有之前出現一次的數的集合;`higher`想像成所有之前出現兩次的數的集合,而且這兩個集合交集為空集合。 一開始,先將兩個集合清空。接著,一個一個去檢查 `nums` 陣列的數字: 1. if (`num[i]` 已經在 lower),則移除 `num[i]`,對應到程式 line 5 > 若此數已在 lower,代表之前已出現過一次,包含這次已第二次,因此接下來它不該出現在 lower 中 => 移除 else if (`num[i]` 不在 higher),則將 `num[i]` 加入至 lower,對應到程式 line 6 > 那如果不在 lower,有兩種可能:(1)這次是**第一次**出現 及(2)這次是**第三次**出現 >既然該數不在上一輪的 higher 中,代表這次是第一次出現,所以要將該數加入 lower 因此,`KKK = ~higher` 2. if (`num[i]` 已經在 higher),則移除 `num[i]`,對應到程式line 7 > 若此數已在 higher,代表此數已出現過兩次,包含這次已第三次,因此接下來它不該出現在 higher 中 => 移除 else if (`num[i]` 不在 lower),則將 `num[i]` 加入至 higher,對應到程式 line 8 > 那如果不在 higher,有兩種可能:(1)這次是**第二次**出現 及(2)這次是**第一次**出現 > 既然該數不在這一輪的 lower 中,代表這次是第二次出現,所以要將該數加入 higher 因此,`JJJ = ~lower` 3. 最後,回傳 lower 集合剩下的數字。 :::info 為甚麼 XOR 可以辦到像是模擬出一個集合的操作? 因為 XOR 的運算特性: 自反: `X ^ Y ^ Y = X` 交換律: `X ^ Y = Y ^ X` 結合律: `(X ^ Y) ^ Z = X ^ (Y ^ Z)` 因此,只要有 XOR 兩次(不用連續)相同的變數時,該變數就會消失,即 X ^ Y ^ Z ^ Y = X ^ Z > 我們 down 到 `bit` 的觀點來看會更加清楚: 若該位元出現過一次 1 則會放在 lower;出現兩次會在 higher,三次則都沒有 | | 初始| 3 | 2 | 3 | 3 | |--|--|---|---|---|---| | binary | ----- | 0011 | 0010 | 0011 | 0011 | | lower |0000| 0011 | 0001 | 0000 | **0010** | | higher |0000| 0000 | 0010 | 0001 | 0000 | > 先拿數字跟 lower 一個 bit 一個 bit 比對,若某字元已經出現過一次(即 lower 中該字元為 1),則這次為第二次,所以 lower 中該 bit 應 clear (即 XOR 操作)。若某字元在 lower 中為 0 ,則需檢查該字元在 higher 中是否為 1,若無,則 lower 中該字元 set(即 & ~higher操作)。 ::: ### 延伸問題 1. 請撰寫不同上述的程式碼,並確保在 LeetCode 137. Single Number II 執行結果正確且不超時 ```c= int singleNumber(int* nums, int numsSize){ int one = 0, two = 0, three = 0; for (int i = 0; i < numsSize; ++i){ two |= one & nums[i]; one ^= nums[i]; three = one & two; one &= ~three; two &= ~three; } return one; } ``` ![](https://i.imgur.com/ahxdHmA.png) 這也是 down 到位元的層級去看,one, two, three 分別代表第 i 位元出現過1, 2, 3次的 mask。 假設現在出現一個數字 nums[i],更新 one 的方法就是 `one ^= nums[i]`,而更新 one 的方法就是用上一個狀態的 one 與 nums[i] 做 and 再跟原本的 two 做 or,所以 two 要比 one 更早更新。至於 three 要如何更新就由 `one & two` 決定,目的在於將已經出現三次的位元 clear。最後 one 剩下的值就是只出現一次的。 2. 延伸題意,將重複 3 次改為改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼 重複5次 => 需要 $\lceil log_25 \rceil$ = $3$ 個 32-bit integers 作為 counter 重複n次 => 需要 $\lceil log_2n \rceil$ 個 32-bit integers 作為 counter ```c= /* 重複5次 */ int singleNumber(int* nums, int numsSize){ int one = 0, two = 0, three = 0, four = 0; for (int i = 0; i < numsSize; ++i){ three ^= one & two & nums[i]; two ^= one & nums[i]; one ^= num[i]; four = one & two; one &= ~four; two &= ~four; two &= ~four; } return one; } ``` 透過比較此程式碼與上面延伸問題1的程式碼,可以清楚的推廣到重複7次、11次、13次...至於像是重複4次,就可以利用重複2次的程式碼實作,重複6次,能用重複3次的程式碼實作,以此類推。[詳細解說](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers)