# 2020q3 Homework2 (quiz2) ## [題目](https://hackmd.io/@sysprog/2020-quiz2) ## 1. is_ascii ### 目的: 改善效率 ```c #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <string.h> #include <stdio.h> #define MMM 0x8080808080808080 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; } int main(int argc, char **argv) { int i; for(i = 1; i < argc; i++) printf("%s: %s\n", argv[i], is_ascii(argv[i], strlen(argv[i])) ? "True" : "False"); return 0; } ``` 執行結果: ![](https://i.imgur.com/6h3zv9L.png) 美國資訊交換標準代碼: **A**merican **S**tandard **C**ode for **I**nformation **I**nterchange 定義了128個字元,它主要用於顯示現代英語。 原版的is_ascii()是逐個char做比對,一個char只有8bit很慢。 所以 `uint64_t payload;` 這行的用意是,指定一個64bit的空間作暫存。 再來透過==memcpy==把字串讀進這個空間。 再來就是整個程式的核心 `payload & MMM` 判斷字串是否在ASCII的範圍裡,最直觀的做法就是,每個byte要小於128(即0~127)。 在二進位的世界裡: $$127_{10} = 0111\ 1111_2$$ 也就是最左邊的位元(**Most Significant Bit**)必須是零,因此只要這的bit是1,這個字元必定不符合ASCII的基本規範。 最簡單的判斷方法就是直接用==AND==來過濾。 ```XXXX XXXX``` ==AND== ```1000 0000``` 得到 ```X000 0000``` 再用if判斷有無,馬上就會知道條件是否符合。 而二進位```1000 0000```用十六進位表達就會是 ==0x80== (8bit) 又payload有64bit那麼大,(64bit) / (8bit) = (8組0x80) 故這題答案是8組0x80,即 **0x8080808080808080** ### 延伸問題 memcpy :::success 延伸問題: 為何用到 `memcpy`? ::: memcpy語法: `memcpy(target_address, source_address, size_in_bytes)` 其功能是把指定區域的內容複寫到目標空間。 ```graphviz digraph structs { node[shape=record] source [label="source|<0>0|<1>1|<2>0|<3>...|<4>0|<5>1|<6>0"]; target [label="target|<0>0|<1>1|<2>0|<3>...|<4>0|<5>1|<6>0"]; source:0 -> target:0; source:1 -> target:1; source:2 -> target:2; source:3 -> target:3; source:4 -> target:4; source:5 -> target:5; source:6 -> target:6; } ``` 雖會因架構而異,但 CPU 一般不會一次只處理 1 byte 的資料。 ==Block size==指每次執行的所抓取的資料塊大小,memcpy有多個版本,==byte-by-byte==, ==word-by-word==...所以編譯器會選一個適合尺寸,在資料對齊後,讓CPU可以更有效率去複寫,減少cycle數。 若是不用memcpy而改用strcpy或是其他函數,則難以預料其後果,很可能會變慢或有預想外的行為。 比如說用**強轉**,在**ARM**上有可能會造成[Unaligned Data Access](https://www.keil.com/support/man/docs/armcc/armcc_chr1359124229461.htm) `uint64_t *payload = (uint64_t *) str` **效能差異**: 我丟了一個NGS實驗的DNA序列檔,避開讀檔時間,針對個別函數去計時,觀察到6倍以上的速度差距。 ```c int main(int argc, char **argv) { char * buffer = 0; unsigned long long length; FILE *f; if (argv[1]) f = fopen(argv[1], "rb"); else return 1; if(f) { fseek (f, 0, SEEK_END); length = ftell (f); fseek (f, 0, SEEK_SET); buffer = malloc (length); if(buffer) fread (buffer, 1, length, f); fclose(f); } else return 1; double time_spent = 0.0; clock_t begin = clock(); bool b = ori_is_ascii(buffer, length); clock_t end = clock(); time_spent = (double)(end - begin) / CLOCKS_PER_SEC; printf("Time elpased for original is_ascii is %f seconds\n", time_spent); begin = clock(); b = opt_is_ascii(buffer, length); end = clock(); time_spent = (double)(end - begin) / CLOCKS_PER_SEC; printf("Time elpased for optimized is_ascii is %f seconds\n", time_spent); return 0; } ``` ``` $ ./run ~/Analysis/25ctrl24_L4_1.fq Time elpased for original is_ascii is 3.680090 seconds Time elpased for optimized is_ascii is 0.590715 seconds ``` --- ## 2. hexchar2val ### 目的: 將十六進位的字元轉換成值 ```c #include <stdint.h> #include <stdio.h> #define MASK 0x40 #define AAA 3 #define BBB 6 uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } int main(int argc, char **argv) { int i; for(i = 1; i < argc; i++) printf("%s = %d\n", argv[i], hexchar2val(*(argv[i]))); return 0; } ``` 執行結果: ![](https://i.imgur.com/P9Sjrds.png) 首先我們先觀察一下字元的編碼 [0-9],[A-F],[a-f] 數字組```0x30```, ```0x31```, ```0x32```, … ```0x39``` 字母大寫 ```0x41```, ```0x42```, … ```0x46``` 字母小寫 ```0x61```, ```0x62```, … ```0x66``` 取字元'0'改用二進位表達,即```0x30``` ```0011 0000``` 取字元'A'改用二進位表達,即```0x41``` ```0100 0001``` 取字元'0'改用二進位表達,即```0x61``` ```0110 0001``` 我們可以看到數字的值剛好等於十六進位的個位數。 而字母的值剛好等於十六進位的個位數再```+9```。 同時,在這裡可以發現,用```0100 0000```當MASK可以很簡單地把數字跟字母分開。 MASK完後,後續的shift只要選好適當的AAA, BBB值,就可以使shift變數保持恆為0。 ```in + 0``` 還是```in```,```in```再跟```0xf```==AND==一次就只剩後面4個bit,這4個bit就是十六進位的個位數值。 到這裡,數字的轉換解決了。 字母的部分,只要想辦法讓```in```可以```+9```,就可以得到它的值。 在這裡,```letter```的值就是```0100 0000```。 再來就是湊數字了,想辦法讓shift變數恆等於9: 往右位移 3, 6次,分別得到```0000 1000```跟```0000 0001``` 這兩個再==OR==起來就是9了,即```0000 1001``` 這時```(in + shift) & 0xf```就會是我們要的東西了。 ### 延伸問題 Hexspeak :::success 延伸問題: Hexspeak 擴充`hexchar2val`,並輸出對應的十進位數值 ::: ```c #include <stdint.h> #include <stdio.h> #include <string.h> uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & 0x40; const uint8_t shift = (letter >> 3) | (letter >> 6); return (in + shift) & 0xf; } int hexspeak(const char *in) { const char *str; str = in + 2; int r = 0; while (*str) { r = r << 4; // r * 16 r += hexchar2val(*str); str++; } return r; } int main(int argc, char const *argv[]) { for (size_t i = 1; i < argc; i++) { printf("%s = %d\n", argv[i], hexspeak(argv[i])); } return 0; } ``` 輸入的字串格式上都以"0x"做開頭,因此可以直接跳過前兩個字元(`str = in + 2`),剩下的部分再加總起來,因為是16進位,所以每個位數要作加法前都要乘以16,可以用位移簡化。 執行結果: ![](https://i.imgur.com/BK0VtQi.png) --- ## 3. quickmod ### 目的: 當除數已知,快速求餘數 ```c #include <stdint.h> #include <stdio.h> #include <stdlib.h> const uint32_t D = 3; #if defined (__aarch64__) | defined(__amd64__) | defined(__x86_64__) #define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint64_t quotient = ((__uint128_t) M * n) >> 64; // {[(2^N - 1) / D] + 1} * [ n / (2^N)] return n - quotient * D; } #else #define M ((uint32_t)(UINT32_C(0xFFFFFFFF) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint32_t quotient = ((__uint64_t) M * n) >> 32; // {[(2^N - 1) / D] + 1} * [ n / (2^N)] return n - quotient * D; } #endif int main(int argc, char **argv) { uint32_t i = quickmod(5); printf("%d\n", i); i = quickmod(55); printf("%d\n", i); i = quickmod(555); printf("%d\n", i); return 0; } ``` 計算$n/d$有個特別的方法,由於分子分母同乘任意值,其值不變。所以只要把會用到的最大值事先除以已知的分母$d$ (denominator),之後再乘回去變數$n$,這樣做的理由是,對電腦而言,除法計算所需要的操作遠比乘法來的複雜,用乘法取代除法,就可以節省運算所需的時間。 在電腦上,$N$位元的組合可以表達 $(2^N)$ 個數字。 故 $$\dfrac{n}{d}=\dfrac{n(2^N)}{d(2^N)}=n\times(\dfrac{2^N}{d})\div(2^N)$$ 從上式可知,除法可拆成三個部分 1. 變數 $n$ 2. $\dfrac{2^N}{d}$ --> $\dfrac{2^N-1}{d}+1$ 3. $2^N$ (1)由函數的參數提供;(3)除以二的N次方可用位移N次簡化;剩下的就是(2),又$d$已知,只要將計算所得作為常數儲存,即可迅速帶入公式求解。 但因為受限於電腦編碼的特性,數字組合包括零,所以最大值其實是 $(2^N-1)$ ,我們只能用 $2^N - 1$ 作為近似值計算。 $2^N - 1$ 就是全部填滿,即0xFFF.....直到填滿空間。而為了補足使用近似值所造成的誤差,可再將計算結果加一。 以 $(\dfrac{2^N-1}{d}+1)$ 取代 $\dfrac{2^N}{d}$ 舉個簡單的例子: 設$N=4$ (即只用4bit簡化計算),求$n/3$;在此假設下,最大值為$2^4-1=15_{10}$,可表示成$1111_2$。當$d$已知(d=3),可得$n/d=[n\times(\frac{15}{3}+1)] >> 4$。設$n=7$,帶入算式得$42_2>>4$ 位移後得 ==0010 ~~0011~~==,即商為$2$。欲計算餘數,則 $n-dq=7 - 3 \cdot 2 = 1$ 以相同原理可推廣到更高的位元 (8-bit,16-bit,32-bit,64-bit...) :::info **程式的改良:** 受限於硬體限制,有些機器沒有 ==__uint128_t== 可用,所以我稍微改寫了一下原始碼,讓它可以在編譯時,選擇比較適當的大小作運算。 多數編譯器可利用以下巨集簡單分辨是否為64位元處理器 #if defined (__aarch64__) | defined(__amd64__) | defined(__x86_64__) #define code for 64-bit #else #define code for 32-bit #endif ::: 參考資料: [Faster remainders when the divisor is a constant: beating compilers and libdivide](https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/?fbclid=IwAR2yz_T_siwv77DCOuFo8tZoPrSpxDaQ1zbh6OCbBat_Qs3CgbHN3Y_NPYY) --- ## 4. divisible ### 目的: 當除數已知,判斷是否整除 ```c #include <stdint.h> #include <stdio.h> #include <stdlib.h> const uint32_t D = 3; #if defined (__aarch64__) | defined(__amd64__) | defined(__x86_64__) #define M ((uint64_t)(UINT64_C(0xFFFFFFFFFFFFFFFF) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint64_t quotient = ((__uint128_t) M * n) >> 64; // {[(2^N - 1) / D] + 1} * [ n / (2^N)] return n - quotient * D; } #else #define M ((uint32_t)(UINT32_C(0xFFFFFFFF) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint32_t quotient = ((__uint64_t) M * n) >> 32; // {[(2^N - 1) / D] + 1} * [ n / (2^N)] return n - quotient * D; } #endif typedef unsigned char bool; #define YYY (M - 1) bool divisible(uint32_t n) { return n * M <= YYY; } int main(int argc, char **argv) { uint32_t i = 5; printf("MOD = %d, %s\n", quickmod(i), divisible(i) ? "divisible" : "undivisible"); i = 55; printf("MOD = %d, %s\n", quickmod(i), divisible(i) ? "divisible" : "undivisible"); i = 555; printf("MOD = %d, %s\n", quickmod(i), divisible(i) ? "divisible" : "undivisible"); return 0; } ``` 除法部分,原理與上題quickmod相同。 回顧一下除法原理: 被除數 $\div$ 除數 = 商 + 餘(小數點以後的部分) $$\dfrac{n}{d}=q + r\space;\space q \in Z\space;\space 0 \leq r < 1$$ 假設我們的機器是64位元,當兩個64位元數字相乘會得到128位元的數字。參考到上一題的解法,會發現 $q$ (商)就是$n \times M$的前64位元,$r$ (餘)就是剩下來的64位元。 在驗證的地方運用到overflow的特性,剛剛說到商就是$n \times M$的前64位元,因為這裡沒有做位移的操作,所以單純的$n \times M$必定overflow,留下來的是後面的64位元。 換句話說,現在留下來的值是 $r$ (小數點以後的部分) $$r = \dfrac{n}{d} - q\space;\space r \in \{0/d, 1/d, 2/d,...,i/d\}\space;\space i \in \{0, 1, 2, ..., q-1\}$$ 欲判斷是否整除,$i$必須為$0$,由於會有誤差,可以改為判斷$r < 1/d$。 知道這些後,用程式碼表達就是`return n * M < M` 因為題目要求`return n * M <= YYY`,考慮到等號造成的誤差,==M-1==才會是適合這題的答案。 --- ## 5. strlower_vector ### 目的: 轉換字元編碼成小寫 ```c #include <ctype.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <string.h> #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]); } } #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) #define VV1 0x80 #define VV2 0 #define VV3 -1 #define VV4 0x80 /* 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); } int main() { /* quote from https://www.gutenberg.org/files/74/74-h/74-h.htm */ char str[] = // why not __char *str__, if so, str can not be modified "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); return 0; } ``` 比較字元編碼: [A-Z],[a-z] 字母大寫 ```0x41```, ```0x42```, … ```0x5A``` 二進位 `0100 0001`, `0100 0002`, … `0101 1010` 字母小寫 ```0x61```, ```0x62```, … ```0x7A``` 二進位 `0110 0001`, `0110 0002`, … `0111 1010` 巨集PACKED_BYTE的用意跟第1題(is_ascii)的變數==payload==相近,把小單位的變數拓展成較大的尺寸,達成一口氣比對更多位元的效果。 `if ((*chunk & PACKED_BYTE(VV1)) == 0)` 欲判斷一個字元是否符合ASCII,直接比對8-bit變數裡的**Most Significant Bit**。 $1000\ 0000_2 = 80_{16}$ 所以==VV1==是`0x80` 指標==chunk==型態為(uint64_t *),寬度達64-bit。 PACKED_BYTE用乘法把`0x80`展開,以符合==chunk==的大小,即`0x8080808080808080`。 ==VV2== 跟 ==VV3== 一起看,要圈出字母A~Z的範圍,直觀上分別判斷每個byte是否介於兩者之間($A \leq byte \leq Z$)。 換個角度想,就是要判斷`(byte - 'A' >= 0 && byte - 'Z' <= 0)`,只是光這樣仍無法簡單用bitwise操作處理。 於是再加一些變化,等式兩邊同加`0x80`,得到`(byte + 128 - 'A' >= 128 && byte + 128 - 'Z' <= 128)`。 考慮一下溢位,修改一些參數,融合bitwise的精神,整理後得`(byte + 128 - 'A' & 0x80)`跟`byte + 128 - 'Z' - 1 & 0x80)`這兩個條件。 由此可知題目的==VV2== = 0;==VV3== = -1。 變數`A`跟`Z`利用==XOR==的特性,做為mask以判斷`*chunk`是否同時符合條件(介於兩者之間)。 :::info **轉換小寫的另一個方法:** 只要每個byte都跟$0010\space0000_{2}$==OR==一下 大寫會轉小寫,小寫依然是小寫。 ==XOR==則比較適合大小寫的轉換。 ::: ==XOR==做完之後,如果是範圍內的數,**Most Significant Bit**必會是1,上面藍框內提到轉換小寫會用到這個$0010\space0000_{2}$,它跟MSB差了兩個shift,所以mask最後還需要向右位移兩次。 ### 延伸問題 `str[]` `*str` :::success 延伸問題: 如果把`char str[]`更換為`char *str`,會發生什麼事? ::: 一般這麼做會收到==Segmentation fault==的訊息,通常指動到不該動的記憶體位址。 `*str`是唯讀的,屬於靜態配置的string literal,如果你去改它就會觸發undefined behavior。 而`str[]`則會在初始化時,於記憶體裡的stack中建立可修改的副本,不會觸發UB。 6.4.5 String literals > It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to modify such an array, the behavior is undefined. 參考資料: [C語言規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf) --- ## 6. singleNumber ### 目的: 已知array所有元素必存在三個copies,僅一個元素沒有,求該元素 ```c #define KKK ~higher #define JJJ ~lower 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; } int main() { int data[] = {2,2,3,2}; return singleNumber(data, 4); } ``` **原理:** 這題的解法很不直觀,先來看看另一種做法的解題思路。 **Counting Set Bit** - 通過計算位元總數來求解。 以```data[] = {2,2,3,2}```為例可得下表: ($2_{10} = 10_2$;$3_{10} = 11_2$) |$\downarrow$|$\downarrow$| |:-:|:-:| |1|0| |1|0| |1|1| |1|0| 將所有數字的各個位元分別加總起來,在左邊這行(column)總合是4,右邊這行是1。分別對3取餘數,各會得到1。兩邊併在一起用二進位表達就是$11_2 = 3_{10}$。 這樣做可以很直觀的找到我們要找的single number,但問題是一個int不小,通常有32位元,這意味著有32個bit要數,實作上很可能比先排序再搜尋還要慢。 這裡就要用上一個特別的邏輯操作XOR,XOR有個特性,取任意一個數字,對另一個數字==XOR==兩次,其值不變,即**自反性**。 $$p \oplus q \oplus q = p \oplus 0 = p$$ 真值表如下: |$p$|$q$|$p \oplus q$| |:-:|:-:|:----------:| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 | 利用這個會自己回歸的特性,可以用來記錄狀態。如果題目的設定是所有數字兩兩成對,僅一個數單獨存在,那把整個數列(array)從頭XOR到底,就可以很快地得到答案。但由於題目是三個重複,所以還要加一點修改讓變數可以載運行三次後回歸為零。 **換句話說,這題的本質就是利用bitwise操作達成三進位的計算。** 這題利用兩個變數lower, higher來記錄位元的變化,暫時先用True/False簡化以理解其原理。這裡先畫出我們想要的規律。 |出現次數|lower|higher| |:----:|:----:|:----:| |1|True|False| |2|False|True| |3|False|False| 再來答案湊一湊就知道這樣寫可以滿足上面這張表所要的效果。 ```c for (int i = 0; i < numsSize; i++) { lower ^= nums[i]; lower &= ~higher; higher ^= nums[i]; higher &= ~lower; } ``` 實際去一步一步展開就會是以下結果: | i | nums[i] | action | lower | higher | |:-:|:-------:| :-------------: |:-----:|:------:| | 0 | 2 | - | 0 | 0 | | 0 | 2 |lower ^= nums[i] | 2 | 0 | | 0 | 2 |lower &= ~higher | 2 | 0 | | 0 | 2 |higher ^= nums[i]| 2 | 2 | | 0 | 2 |higher &= ~lower | 2 | 0 | | 1 | 2 |lower ^= nums[i] | 0 | 0 | | 1 | 2 |lower &= ~higher | 0 | 0 | | 1 | 2 |higher ^= nums[i]| 0 | 2 | | 1 | 2 |higher &= ~lower | 0 | 2 | | 2 | 3 |lower ^= nums[i] | 3 | 2 | | 2 | 3 |lower &= ~higher | 1 | 2 | | 2 | 3 |higher ^= nums[i]| 1 | 1 | | 2 | 3 |higher &= ~lower | 1 | 0 | | 3 | 2 |lower ^= nums[i] | 3 | 0 | | 3 | 2 |lower &= ~higher | 3 | 0 | | 3 | 2 |higher ^= nums[i]| 3 | 2 | | 3 | 2 |higher &= ~lower | 3 | 0 | 最終回傳==lower=3==。 ### 延伸問題 LeetCode :::success 延伸問題: 請撰寫不同上述的程式碼,並確保在 LeetCode 137. Single Number II 執行結果正確且不超時 ::: ```c int singleNumber(int* nums, int numsSize){ if (numsSize == 1) { return nums[0]; } int once = 0, twice = 0, thrice = 0; for (size_t i = 0; i < numsSize; i++) { twice |= once & nums[i]; once ^= nums[i]; thrice = once & twice; once &= ~thrice; twice &= ~thrice; } return once; } ``` 利用三個變數搭配邏輯操作,達成三次一循環的效果。 ==once==, ==twice== 分別表示出現的次數。 ==thrice==則在第三次出現時,作為MASK使用,修正==once==跟==twice==的狀態。 ```c twice |= once & nums[i]; ``` ==twice==必須比==once==先執行,確保值的正確性。 如果在```once```跟```nums[i]```同時有值,代表這個bit已經是第二次出現了,透過```AND```即可過濾出來。 ```c once ^= nums[i]; ``` ==once==與輸入值==nums==利用自反性,加入第一次出現的值,同時削去第二次出現的值。 ```c thrice = once & twice; once &= ~thrice; twice &= ~thrice; ``` 這三個操作是為了找出出現三次的bit,並記錄在==thrice==變數,再利用==thrice==變數反轉作為MASK去除==once==跟==twice==的狀態。 LeetCode執行結果: ![](https://i.imgur.com/2AfpFhG.png) ### 延伸問題 重複多次 :::success 延伸問題: 將重複 3 次改為改為重複 4 次或 5 次 ::: **重複 4 次** ```c int singleNumber(int* nums, int numsSize){ int r = 0; for (int i = 0; i < numsSize; i++) { r ^= nums[i]; } return r; } ``` 作法很單純,任何偶數次的都可以這樣處理。 只要一路```XOR```到底,利用自反性把位元消除,剩下的就會是多出來的那個數字的位元組態。 **重複 5 次** ```c #include<stdio.h> int singleNumber(int* nums, int numsSize){ if (numsSize == 1) { return nums[0]; } int once = 0, twice = 0, thrice = 0, quarce = 0, quince = 0; for (size_t i = 0; i < numsSize; i++) { quarce |= thrice & nums[i]; thrice |= twice & nums[i]; twice |= once & nums[i]; once ^= nums[i]; quince = once & twice & thrice & quarce; once &= ~quince; twice &= ~quince; thrice &= ~quince; quarce &=~quince; } return once; } int main() { int data[] = {0,2,1,2,0,1,0,2,2,1,9,0,1,2,0,1}; printf("Ans: %d\n", singleNumber(data, sizeof(data)/sizeof(int))); return 0; } ``` 一樣的道理,後面的變數先跟前一個處理(有點像進位),最後處理==once==。 ```c quarce |= thrice & nums[i]; thrice |= twice & nums[i]; twice |= once & nums[i]; once ^= nums[i]; ``` 生成出現五次的MASK。 ```c quince = once & twice & thrice & quarce; ``` 最後再消除前面各變數的狀態。 ```c once &= ~quince; twice &= ~quince; thrice &= ~quince; quarce &= ~quince; ``` 執行結果: ![](https://i.imgur.com/MlDEWSQ.png)