owned this note
owned this note
Published
Linked with GitHub
# 2020q3 Homework2 (quiz2)
contributed by < `haoyu0970624763` >
## 1. ASCII 編碼判斷
```cpp
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.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 & 0x8080808080808080)
return false;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
***程式解說***
對於單個字元 1 byte = 8 bits , ASCII碼為0-126
若此字元 和 0x80 做 & 不等於0 則表示他從左邊數過來第二個bit != 0 也就不為ASCII碼
同樣的邏輯推廣到 8 個字元即是答案
:::info
memcpy 的使用不但是用來得到 64 bits 的 payload,memcpy會去檢查記憶體位址是否有
alignment ,可以避免因為對記憶體的 unaligned memory access 產生錯誤。
:::
### 延伸問題二
```cpp=
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u)
bool is_alphabet(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);
uint64_t A = payload + PACKED_BYTE(128 - 'A' );
uint64_t Z = payload + PACKED_BYTE(128 - 'Z' - 1);
uint64_t a = payload + PACKED_BYTE(128 - 'a' );
uint64_t z = payload + PACKED_BYTE(128 - 'z' - 1);
uint64_t lowerMask = (a ^ z) & PACKED_BYTE(0x80);
uint64_t upperMask = (A ^ Z) & PACKED_BYTE(0x80);
if (( lowerMask | upperMask ) ^ PACKED_BYTE(0x80)){
return false;
}
i += 8;
}
while (i < size) {
if ( (str[i] < 65 || (str[i] > 90 && str[i] < 97) || str[i] > 122))
return false;
i++;
}
return true;
}
```
***程式解說***
一開始想不到要怎麼寫 參考了別人一下也發現看不太懂
:::danger
看不懂就說看不懂,不要說模糊地「不太懂」。如果你認定其他同學的共筆寫不好,那就留訊息指出其不足之處。
再者,你的標點符號呢?
:notes: jserv
:::
但是把全部測驗都解釋整理一次之後 就是把測驗 5 的思考迴路跟解法搬過來這邊解
## 2. 16進位字元轉換
```cpp=
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & 0x40;
const uint8_t shift = (letter >> 3) | (letter >> 6);
return (in + shift) & 0xf;
}
```
***程式解說***
我們可以觀察回傳值 `(in + shift) & 0xf` 僅要後面四碼而已
而字元`'0'`到`'9'` 的僅取其後四個bit轉換過來即為答案
至於字元`'a'`到`'f'` 的其後四個bit轉換過來為 1-7 只要都再+9 即為對應之答案
我們先用 mask=0x40 如果你是字母(大小寫都可)letter都會等於 0x40 如果是 數字 letter=0x00
而shift則表示 如果你是字母 shift=0x09 數字的話則為0
接著就可求到答案了
## 3.快速除法
```cpp=
const uint32_t D = 3;
#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;
return n - quotient * D;
}
```
### 程式原理
$n\mod D = n - (n/D) \times D$
也就是說 `n - quotient * D` 的 `quotient` 就是 `n / D` 的整數部份。
從題目裡的提示
$$
\dfrac{n}{d} = n \times (\dfrac{2^N / d}{2^N})
$$
`quotient = ((__uint128_t) M * n) >> 64` 右移 64 bits 即為 $\dfrac{1}{2^N}$,所以 N = 64。
但是 $M={(2^{64}/D)}$ 跟 ${(2^{64} - 1) / D} + 1$ 略為不同 參考`Rsysz` 發現兩者算出來的答案是一樣的 所以 `M=0xFFFFFFFFFFFFFFFF`
## 4.倍數判斷
```cpp=
bool divisible(uint32_t n)
{
return n * M <= M-1;
}
```
### 程式原理
延伸上一題的觀念
因為D=3 令 `n=3k,3k+1,3k+2` (k為非負整數)
其中 `3M=0x10000000000000002` 已經overflow 所以會把 3M視為`3M=0x0000000000000002`
所以 case1: $n * M = 3kM = 2k$
case2 : $n * M = 3kM +M = 2k +M$
case3 : $n * M = 3kM +2M = 2k +2M$
所以答案 C,D皆符合
:::info
疑問:有點像是用答案硬推理 不知道為什麼當初會這樣構想程式
:::
:::danger
工程人員說話要精準,避免說「有點像是」,後者是文組^TM^用語
:notes: jserv
:::
## 5. 字串大寫轉換小寫
```cpp
#include <ctype.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u)
void strlower(char *s, size_t n);
/* vectorized implementation for in-place strlower */
void strlower_vector(char *s, size_t n);
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);
}
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]);
}
}
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(0x80)) == 0) { /* is ASCII? */
uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + 0);
uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + (-1));
uint64_t mask = ((A ^ Z) & PACKED_BYTE(0x80)) >> 2;
*chunk ^= mask;
} else
strlower(s, 8);
}
k = n % 8;
if (k)
strlower(s, k);
}
```
### 程式原理
`PACKED_BYTE(b)` 的作用是取出 `b` 的後 8 個位元(假設是 0x12),並且變成 0x1212121212121212
接著檢查這個字元是不是 ASCII 編碼 (運用到題一)
如果 `*chunk + PACKED_BYTE(128 - 'A' + 0) >= 128` 表示 chunk的ASCII 編碼大於等於`'A'`
如果 *chunk + PACKED_BYTE(128 - 'Z' -1) < 128表示 chunk的ASCII 編碼小於等於`'Z'`
把 A 跟 Z 做 Xor 如果等於 `10000000` 表示 chunk是介於 `'A'` 到 `'Z'` 之間
則 mask=`00100000`=`' '`
若大寫字母跟`' '` Xor 則會跑出小寫字母
如果chunk不介於`'A'`到 `'Z'` 之間 同時小於128 或是同時大於128
則 mask=`00000000`
任何東西跟`0` Xor 都等於自己
## 6. 單次數尋找
```cpp
int singleNumber(int *nums, int numsSize)
{
int lower = 0, higher = 0;
for (int i = 0; i < numsSize; i++) {
lower ^= nums[i];
lower &= ~higher;
higher ^= nums[i];
higher &= ~lower;
}
return lower;
}
```
### 程式原理
`lower` 是紀錄哪個位置的bit出現數為奇數次 出現奇數次的=1
`higher` 是紀錄哪個位置的bit出現數為偶數次 出現偶數次的=1
**實做邏輯**
lower ^= nums[i] 會先把num[i]的bit 出現奇數次的bit紀錄住
lower &= ~higher 會把已經出現過偶數次的bit扣掉
(當有一個bit出現第三次 lower會紀錄住 但是這個其實是不需要的 會透過上一個迴圈的higher把第三次出現的的消掉 因為我們只要出現一次的bit)
higher ^= nums[i] 會先把num[i]的bit 奇數次的bit的紀錄住
higher &= ~lower 會把已經出現過奇數次的bit扣掉
(當只出現一次 lower會記住 higher也會記住 但會被扣掉 而出現第二次時 lower會消掉
higher會記住 但不會被扣掉 也就是說higher會紀錄出現偶數次的bit)
:::danger
注意筆記書寫規範,中英文字元用一個空白區隔,從小處做起
:notes: jserv
:::
## 新增: 第2題 16進位字串轉換
```c=1
uint64_t HexToDigit(char* in) {
char *tmp_ptr=in;
char *reverse;
uint64_t payload = 0, value = 0;
if (in[0] == '0' && in[1] == 'x')
tmp_ptr=&in[2];
#if __BYTE_ORDER == __BIG_ENDIAN
reverse=tmp_ptr;
#else
memset(reverse,'\0',strlen(tmp_ptr)+1);
for (int i = 0; i < strlen(tmp_ptr); i++)
{
reverse[i]=tmp_ptr[strlen(tmp_ptr)-1-i];
}
#endif
memcpy(&payload, reverse , sizeof(char)*strlen(tmp_ptr));
uint64_t letter = payload & 0x4040404040404040;
uint64_t shift = (letter >> 3) | (letter >> 6);
uint64_t tmp=(payload + shift) & 0x0F0F0F0F0F0F0F0F;
int i=strlen(tmp_ptr);
for( int i=strlen(tmp_ptr); i!=0 ; i--){
value= value << 4;
value |= ((tmp >> (i-1)*8) & 0x0F);
}
return value;
}
```
根據電腦的endian架構去決定input的字串要不要反過來
把 input 的字串反過來是因為 電腦架構還有 uint64_t 的 endian 是不一樣的,會發現這個
是因為我在測試 `0x12` 的時候,答案一直都不是我預期的,反而是 `0x21` 的答案,所以就上
stackoverflow 尋找才發現是 endian 的問題
邏輯:
`strlen(tmp_ptr)` 表示需要轉換的長度
用 `tmp` 存著所有轉換完的 input ,每 8bits 表示一個數字
從轉換完的輸入最左邊 8bits 開始依序到最右邊 8bits ,將 8bits 右移到最右邊8 個 bits 並且跟 value 做 AND 接著再把 value 左移4個bits(因為是16進位)
把第8-15bits 右移到最右邊8 個 bits 並且跟 value 做 AND ....依此類推
`tmp >> (i-1)*8` 控制是哪一組 8bits 右移到最右邊
` & 0x0F` 只需要最右邊 4bits
## 新增: 第3題 快速除法(jemelloc原理)
$n$ 為**被除數**, $q$ 為**商**, $d$ 為**除數**
數學式子從`Rsysz`參考,但是他的 $r=(-2)^k mod \space d$ 應為筆誤或是錯誤
r 表示 $2^k mod \space d$ (也就是餘數)
假設$n = q \times d$,已知 n , d 求 q
$q = n / d$ $=\left \lfloor \left \lceil \dfrac{2^k}{d} \right \rceil \times \dfrac{n}{2^k} \right \rfloor = \left \lfloor \dfrac{2^k + r}{d} \times \dfrac{n}{2^k} \right \rfloor,k為任意整數,而r = -(2)^k mod \space d$
$將\left \lfloor \dfrac{2^k + r}{d} \times \dfrac{n}{2^k} \right \rfloor展開為\left \lfloor \dfrac{2^k}{d} \times \dfrac{n}{2^k} + \dfrac{r}{d} \times \dfrac{n}{2^k}\right \rfloor = \left \lfloor \dfrac{n}{d} + \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor$
$因假設n /d為整除,所以n/d的小數部分為0,因此式子可以化簡為\dfrac{n}{d} + \left \lfloor \dfrac{r}{d} \times \dfrac{n}{2^k} \right \rfloor$
也就是當$\dfrac{r}{d} \times \dfrac{n}{2^k} < 1$,$n/d = \left \lfloor \left \lceil \dfrac{2^k}{d} \right \rceil \times \dfrac{n}{2^k} \right \rfloor$就會成立
因 $r$ 是 $\dfrac{2^k}{d}$ 的餘數,因此 r < d 且 $r/d<1$總是成立,所以只需滿足$\dfrac{n}{2^k} < 1$
又 $n$ 為32bits的數,最大值為 $2^{32}-1$,因此可以令 k = 32(k之最大值)來確保$\dfrac{n}{2^k} < 1$,而這也是為什麼jemalloc bound 數值範圍在 $2^{32}$ 之間。
[code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/quickDiv.c)
div_info->magic = $\left \lceil \dfrac{2^k}{d} \right \rceil$
為何要取上高斯??
用例子說明 : $12 \times \dfrac{2^3}{6} >> 3$
若是 $\dfrac{2^3}{6}$ 四捨五入或取下高斯 , 答案皆為1,取上高斯答案則為 6(所以要取上高斯)
理論上最優的算法當然是 $12 \times 2^3 /6 >> 3$ 但程式語言無法做到這件事。
取下高斯或是捨去會有部份值被捨去,會造成相乘出來的結果變小,進而影響結果。
取上高斯或是進位,會造成相乘出來的結果變大,進而影響結果,但後來有右移所以把答案調回來。
## 新增: 快速除法跟一般除法之間的效能比較:
**最一開始的實驗:** 將快速除法跟除法分成兩個執行檔,除數為亂數設定,被除數也是亂數設定。
兩個檔案的亂數種子是設定為一樣的,所以兩個檔案的除數跟被除數都是一樣的,皆進行一億次運算
想像中的結果,quickdiv 應該快於 div 的運算,然而結果卻不一樣。
[div code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/div_test.c)
[quickdiv code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/quickdiv_test.c)
![](https://i.imgur.com/7PvxtuN.png)
**從上圖可以發現,一般的除法會略快於快速除法**
**為什麼會導致這樣的結果呢?**
* **1. 實驗設計錯誤**
* **2. 在實做上 quickdiv真的慢於div**
* **3. 實驗測資導致的結果**
### 針對實驗設計錯誤的問題,我仿照`RinHizakura`的實驗方式進行實驗
底下的 code 為 `RinHizakura` 的實驗基礎設計
```cpp
static uint64_t MAX = ((uint64_t)(1) << 32) - 1;
int main()
{
struct timespec tt1, tt2;
clock_gettime(CLOCK_MONOTONIC, &tt1);
for (uint64_t i = 2; i < 5000; i++) {
div_info_t DIV;
div_init(&DIV, i);
uint64_t num = rand() % MAX;
clock_gettime(CLOCK_MONOTONIC, &tt1);
size_t ans1 = div_compute(&DIV, num);
clock_gettime(CLOCK_MONOTONIC, &tt2);
long long time1 = (long long) (tt2.tv_sec * 1e9 + tt2.tv_nsec) -
(long long) (tt1.tv_sec * 1e9 + tt1.tv_nsec);
clock_gettime(CLOCK_MONOTONIC, &tt1);
size_t ans2 = num / i;
clock_gettime(CLOCK_MONOTONIC, &tt2);
long long time2 = (long long) (tt2.tv_sec * 1e9 + tt2.tv_nsec) -
(long long) (tt1.tv_sec * 1e9 + tt1.tv_nsec);
printf("%ld, %lld, %lld\n", i, time1, time2);
}
}
```
## 改良的第一版測試,測試 多個被除數 除以 單一除數
[test1 code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/TestDiv/test1.c)
下圖為測試結果
![](https://i.imgur.com/pZLWvGB.png)
可以發現在編譯器沒優化的情況下, quickdiv 明顯快於 div
而經過優化之後發現兩者速度皆減低不少,而根據 `RinHizakura`的實驗發現這是因為編譯器發現
除法的結果是不需要用到的,所以自動把它略過了。
## 改良的第二版測試,測試 多個被除數 除以 多個除數
[test2 code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/TestDiv/test2.c)
因為知道存在編譯器的優化,所以在程式過程中我們把編譯器的優化關掉。
同時為了避免測資的偏頗,設置數個不同的 divisor ,並分別計算兩種不同的除法的時間並取平均值,
quickDiv 暫時不包含 div_init 的執行時間(也就是僅計算除法的部份)。
![](https://i.imgur.com/ah7pZjC.png)
由上圖可發現 quickdiv 明顯快於 div
## 改良的第三版測試,測試2的小變形
[test3 code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/TestDiv/test3.c)
test2的變形,因為在程式執行過程中,其實不能忽略 div_init 的執行時間
因為他也算在 quickDiv 的部份環節,若是以 test2 為基準,每一層 loop 都找一個新的divisor
也就是每一個quickDiv都包含 div init的時間。
![](https://i.imgur.com/57dZeZQ.png)
由上圖就可以發現, quickdiv 的時間明顯慢於 div
關於 `RinHizakura` 的實驗筆記的這部份修正,我覺得是不必要的。
```cpp
int main()
{
struct timespec tt1, tt2;
clock_gettime(CLOCK_MONOTONIC, &tt1);
for (uint64_t i = 2; i < 5000; i++) {
div_info_t DIV;
div_init(&DIV, i);
uint64_t num = rand() % 10000;
num *= i;
clock_gettime(CLOCK_MONOTONIC, &tt1);
size_t ans1 = div_compute(&DIV, num);
clock_gettime(CLOCK_MONOTONIC, &tt2);
long long time1 = (long long) (tt2.tv_sec * 1e9 + tt2.tv_nsec) -
(long long) (tt1.tv_sec * 1e9 + tt1.tv_nsec);
clock_gettime(CLOCK_MONOTONIC, &tt1);
size_t ans2 = num / i;
clock_gettime(CLOCK_MONOTONIC, &tt2);
long long time2 = (long long) (tt2.tv_sec * 1e9 + tt2.tv_nsec) -
(long long) (tt1.tv_sec * 1e9 + tt1.tv_nsec);
printf("%ld, %lld, %lld, %ld, %ld\n", i, time1, time2, ans1, ans2);
}
}
```
為了符合 div 的假設,因此需讓被除數 n 可以整除 d,才可以得到正確的計算結果
**因為 jemalloc 其實也可以處理非整除的問題,只是要將 debug的那一行註解掉,而且我覺得 div_init 的時間是需要被考慮進去的,所以我在自己的測驗code3有加入 init , 還有一個地方是他的 divisor 是從 2 加到 4999 ,我覺得要用亂數才比較能符合真實狀況,所以有進行修改。**
:::info
小結論:我們從改良的實驗code可以發現,當遇到一個新的除數,因為要包含div_init的時間,在這個時候,quickdiv會較慢,在除同一個除數的時候 quickdiv 會較快,但這個結論跟我的最開始的實驗有產生矛盾。
:::
### 矛盾解法1.
仔細看 `RinHizakura` 跟我最開始設計的實驗有一個**巨大的差異**,就是他的變數宣告都是
`uint64_t` 而我的是 `int` ,但此題的除法實做應該只須用到 `int` , quickdiv 是用乘法跟右移 取代掉 32bit除法 ,在過程中的確需要 64bit ,但卻不是全部都需要。
[test4 code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/TestDiv/test4.c)
將test2進行微調,並把變數宣告成int 並且不包含 div_init
![](https://i.imgur.com/9gj0J5U.png)
可以發現,用 int 型別的確略快於 uint64_t , 然而卻不足以超越 quickdiv的時間
比對最初的資料跟 test1 , 1E 筆資料 int 型別大概要0.9秒 而 uint64_t 要1.5秒
### 矛盾解法2.
**重新 review 自己最開始的測試結果,我發現我測試的是整段 code ,而 code 當中還包含rand()
而 rand() 次數高達一億次,也因為包含這個非除法的部份,會導致自己偵測的時間變得不只包含除法,所以不準確。**
### hint
**在測驗過程中應盡量避免極端值得出現,EX:數字太小 , 數字太小在 fastdiv 測試 會有異常高的測驗時間值,但我因為測試的資料量較大,所以我認為平均下來應該還好,然而最好還是要撇去極端值存在才能更好得比較效能**
## 新增: 第5題 延伸問題 char str[] 更換為 char *str
結果: `Segmentation fault`
> char *p = “abc”;
>
>defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal
>If an attempt is made to use p to modify the contents of the array, the behavior is undefined.
string literal 是靜態配置的,也就是 const char *str 嘗試去更動其內容是 undefined behavior
## 新增 4.倍數判斷 解說
```cpp=
bool divisible(uint32_t n)
{
return n * M <= M-1;
}
```
延伸自上一題的觀念
若以 D=3 此題為例子來看:
其中 `3*M=0x10000000000000002` 會產生overflow ,也就是所以大於 3 的數字乘上 M 都會發生overflow , 3*M 在這裡被視為 `0x0000000000000002` , 4*M 則代表 2+M...依此類推
而 6*M 則視為 4 ,也就是說 當 3的倍數乘上 M , 最後的數值 會小於或小於 M (不確定是哪個) 。
但有一點很重要的是, M 是 $\left \lceil \dfrac{2^k}{d} \right \rceil$ (上高斯值),舉個例子說明
EX: 當我們取 k=4 (bit數為4的意思) 而 d=4 , 則 M = 4 = (0xF/4)+1
取 n=4 , n*M = 16 (overflow ,修正為 0 < M)
取 n=5 , n*M = 20 (overflow ,修正為 4 = M)
取 n=6 , n*M = 24 (overflow ,修正為 8 > M )
取 n=7 , n*M = 28 (overflow ,修正為 C > M )
取 n=8 , n*M = 32 (overflow ,修正為 0 < M )
(0xF/4) 實際上只有 3.x 的值 , 但取上高斯則為 4 ,會導致一個情況是 n * M 造成的overflow 進行修正後的值 = 取上高斯的值,導致 n*M=M ,但是這情況是不整除的。
所以要 -1 修正,把上高斯的值去掉。
## 新增: 6.改寫 singleNumber
這題舉例子說明的話
例1: 1,1,1,3
bit 0 =1 出現 4 次 , 而 bit1=1 出現 1 次 , 其餘 bit X=1 出現 0次 ,最後要回一個數字 bit 0,1 = 1
例2 : 1,1,1,3,5,5,5
bit 0 =1 出現 7 次 , 而 bit1=1 出現 1 次 , bit2=1 出現 3 次其餘 bit X=1 出現 0次 ,最後要回一個數字 bit 0,1 = 1
從這裡我們可以發現,要去紀錄 bit X 出現 3k+1 次的時候 ( k >= 0 ) , 出現 3 次的則是不重要
因此我們可以做簡化,紀錄出現 3k+1次的,紀錄出現 3k+2 次的,出現 3k+3 次的
```c=
int singleNumber(int *nums, int numsSize)
{
int apprear1 = 0, apprear2 = 0, apprear3 = 0;
int tmp1=0,tmp2=0,tmp3=0;
for(int i=0;i<numsSize;i++) {
apprear1 = (~(tmp2 | tmp1) & nums[i]) | tmp1 & ~nums[i] | tmp3 & nums[i];
apprear2 = tmp1 & nums[i] | tmp2 & ~nums[i];
apprear3 = tmp2 & nums[i] | apprear3 & ~nums[i];
tmp1 = apprear1;
tmp2 = apprear2;
tmp3 =apprear3;
}
return apprear1;
}
```
apprear 1 紀錄出現 3k+1 次的 , apprear2 紀錄出現 3k+2 次的 , apprear 1 紀錄出現 3k+1 次的。
apprea1 要紀錄的有
* 1.沒有出現在 3K+1 次 也沒有出現在 3K+2 次 但是 nums[i] 有的 (因為這些應該被紀錄在 apprear2 跟 apprear3 裡面)
* 2.出現 3K+1次 但在 nums[i] 沒出現的,
* 3.出現在 3K+3 次也有出現在 nums[i]裡面的(因為 3k+4 = 3(k+1) +1 )
apprea2 要紀錄的有
* 2.出現 3K+1次 在 nums[i] 也有出現的,
* 3.出現 3K+2 次 但在 nums[i] 沒出現的
apprea3 要紀錄的有
* 2.出現 3K+2次 在 nums[i] 也有出現的,
* 3.出現 3K+3 次 但在 nums[i] 沒出現的
最後回傳 出現 3K+1 次的即可。
[改寫版 code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/singleNumber.c)
[改寫擴充版 code 連結](https://github.com/haoyu0970624763/sysprog21-quiz2/blob/master/singleNumber_expansion.c)