---
tags: 進階電腦系統理論與實作, NCKU Linux Kernel Internals, 作業系統
---
# 2020q3 Homework2 (quiz2)
contributed by <`RusselCK` >
###### tags: `RusselCK`
- ==[數值系統 & Bitwise 練習題](https://hackmd.io/@sysprog/2020-quiz2)==
- [作業繳交區](https://hackmd.io/@sysprog/2020-homework2)
## Q1.判斷指定的記憶體範圍內是否全是有效的 [ASCII](https://zh.wikipedia.org/wiki/ASCII) 字元
* 目前電腦中用得最廣泛的字元集及其編碼,是美國國家標準局 (ANSI) 制定的 ASCII 碼,被國際標準化組織 (ISO) 採納為 ISO 646 標準。7 位碼 ASCII 是以 7 位元二進位數字進行編碼,可表示 128 個字元,第 128~255 號為擴展字元 (extended ASCII)。
#### 逐一字元比對 (效率低)
```cpp
#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;
}
```
其中 `str` 是開頭的記憶體地址, `size` 則是要檢驗的範圍。
* 二進位表示 0~127 為 `0000 0000`~`0111 1111` ,也就是說只要第 8 個 bit 為 1 ,該欄位便不為有效的 ASCII 字元
* 因此,我們可以利用 bitwise 操作( i.e. `if (str[i] & 0x80)` ),判別第 8 個 bit 是否為 0
> 1個 `char` 的大小 = 1 bytes = 8 bits
> 0x80 = (1000 0000)~2~
#### 在 64 位元的架構下,一次比對 64 位元的資料 (即 word size)
```cpp=
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
bool is_ascii(const char str[], size_t size)
{
// Check size
if (size == 0)
return false;
int i = 0;
while ((i + 8) <= size) {
uint64_t payload;
memcpy(&payload, str + i, 8);
if (payload & MMM) // MMM = 0x8080808080808080
return false;
i += 8;
}
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
return true;
}
```
:::info
* **`size_t`**
* Unsigned integral type
* **`uint64_t`**
* **unsigned integer type** with width of exactly 64
* **`void * memcpy ( void * destination, const void * source, size_t num );`**
* Copy block of memory
* Copies the values of `num` bytes from the location pointed to by `source` directly to the memory block pointed to by `destination`.
* Source : http://www.cplusplus.com/
:::
* 程式碼解析:
```c
int i = 0;
while ((i + 8) <= size) {
uint64_t payload;
memcpy(&payload, str + i, 8);
if (payload & MMM)
return false;
i += 8;
}
```
> 64 bits = 8 bytes
* 每一次迴圈檢查 8 bytes ,`memcpy(&payload, str + i, 8);` 將起始位址 `str + i` 後的 8 個 bytes 複製到 `payload` ,再由 `if (payload & MMM)` 判斷這 8 個bytes 的每一個 bytes 是否都為有效 ASCII 字元
* 檢查一個 byte 可以用 `& 0x80` 來檢查,推理可得,要一次檢查 8 個 bytes ,可以用 `& 0x8080808080808080` 來檢查
* 因此, **MMM = `0x8080808080808080`**
```c
while (i < size) {
if (str[i] & 0x80)
return false;
i++;
}
```
* 若 `size` 不為 8 的倍數,前面一個迴圈跳出來後,會剩餘 1~7 個 bytes 沒檢查到
* 這裡使用傳統方式,一次檢查 1 個 byte
## Q1. 進階題
#### 給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母
* 只要檢測到英文字母,便可`return true`
```c=
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u)
bool has_alpha(char *s, size_t size)
{
if (size == 0)
return false;
for (size_t j = 0; j < size; j++) {
if ((s[j] >= 'A' && s[j] <= 'Z') || (s[j] >= 'a' && s[j] <= 'z'))
return true;
}
return false;
}
bool has_alpha_vector(char *s, size_t size)
{
if (size == 0)
return false;
size_t i = 0;
while ( (i + 8) <= size){
uint64_t *payload = (uint64_t *) s;
if ((*payload & PACKED_BYTE(0x80)) == 0) { /* case1 : is ASCII */
// Upper mask
uint64_t A = *payload + PACKED_BYTE(128 - 'A');
uint64_t Z = *payload + PACKED_BYTE(127 - 'Z');
uint64_t upper_mask = ((A ^ Z) & PACKED_BYTE(0x80));
// Lower mask
uint64_t a = *payload + PACKED_BYTE(128 - 'a');
uint64_t z = *payload + PACKED_BYTE(127 - 'z');
uint64_t lower_mask = ((A ^ Z) & PACKED_BYTE(0x80));
if ( (upper_mask | lower_mask) & PACKED_BYTE(0x80) )
return true;
}else{ /* case 2 */
if (has_alpha(s, 8))
return true;
}
i += 8;
s += 8;
}
i = size % 8;
if (i){ /* case 3 */
if (has_alpha(s, 8))
return true;
}
return false;
}
```
* 此題的運用的技巧及分 case 的方式與 Q5. 類似
```c
#27 while ( (i + 8) <= size)
```
* 若最後一組不滿 8 個,則不可進入迴圈,要改使用 case 3
```c
#37 uint64_t upper_mask = ((A ^ Z) & PACKED_BYTE(0x80));
```
* 這裡的 mask與 Q5. 不同,是因為不需要改變更動 bits,只須辨別是否為字母即可,因此不需要 `>>2`
* 若該 byte 為字母,則相對應的 mask 的 byte 為 0x80
#### 承上,考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對
>提示: Intel SSE 4.2 加入 STTNI (String and Text New Instructions) 指令,可加速字串比對,詳見: [Schema Validation with Intel® Streaming SIMD Extensions 4](https://software.intel.com/content/www/us/en/develop/articles/schema-validation-with-intel-streaming-simd-extensions-4-intel-sse4.html)
## Q2. 將十六進位表示的字串 (hexadecimal string) 轉為數值
#### 不需要分支 (branchless) 的實作:
已知 `in` 一定隸屬於 '0', '1', '2', …, '9' 及 'a', 'b', 'c', …, 'f' (小寫) 及 'A', 'B', 'C', …, 'F' (大寫) 這些字元。
```c=
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & MASK; // MASK = 0x40
const uint8_t shift = (letter >> AAA) | (letter >> BBB);
//AAA = 3 、 BBB = 6
return (in + shift) & 0xf;
}
```
> 預期 hexchar2val('F') 和 hexchar2val('f') 回傳 15 = (1111)~2~
> 並且 hexchar2val('0') 回傳 0 = (0000)~2~
> 其他輸入都有對應的數值。
* 程式碼解析:
1. 考慮以下表格:
| Character | Hexdecimal | Binary |
|:---------:| ---------- | ------------- |
| F | 0x46 | 0==1==00 0110 |
| f | 0x66 | 0==1==10 0110 |
| 0 | 0x30 | 0==0==11 0000 |
* 我們可以發現,光==看第 7 個 bit ,就能夠區分 `in` 是 字母 或是 數字==
* 我們可以利用 `in & (0100 0000)` ( i.e. `in & 0x40` ) 來達到此效果
* 若 `in` 為 數字,則 `letter` 為 0x00 = (0000 0000)~2~
* 若 `in` 為 字母,則 `letter` 為 0x40 = (0100 0000)~2~
* 因此, **MASK = 0x40**
2. 考慮回傳值 `(in + shift) & 0xf` :
> 0xf = (0000 1111)~2~
* 若 `in` 為 F , ( f 也可推得相同結果 )
* `(in + shift) & 0xf` = 15
* ( (0100 0110) + `shift` ) & (0000 1111) = (0000 1111)
* ( (0100 0110) + `shift` ) = (**** 1111)
* `shift` = (**** 1001)
* 若 `in` 為 0 ,
* `(in + shift) & 0xf` = 0
* ( (0011 0000) + `shift` ) & (0000 1111) = (0000 0000)
* ( (0011 0000) + `shift` ) = (**** 0000)
* `shift` = (**** 0000)
3. 考慮 `shift = (letter >> AAA) | (letter >> BBB)` :
* 若 `in` 為 字母,則 `letter` 為 (0100 0000)
* 我們可以湊出 AAA = 3 、 BBB = 6 使得答案成立 ( i.e. `shift` = (**** 1001) )
* `(letter >> AAA)` = `(letter >> 3)` = (0000 1000)
* `(letter >> BBB)` = `(letter >> 6)` = (0000 0001)
* 若 `in` 為 字母、AAA = 3 、 BBB = 6,則 `letter` 為 (0000 0000)
* `(letter >> AAA)` = `(letter >> 3)` = (0000 0000)
* `(letter >> BBB)` = `(letter >> 6)` = (0000 0000)
* 可得 `shift` = (**** 0000)
* 因此,**AAA = 3** 、 **BBB = 6**
## Q2. 進階題
#### 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值
>[Hexspeak](https://en.wikipedia.org/wiki/Hexspeak)
```c=
#include <ctype.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u)
uint8_t hexchar2val(uint8_t in)
{
const uint8_t letter = in & 0x40;
const uint8_t shift = (letter >> 3) | (letter >> 6);
return (in + shift) & 0xf;
}
size_t hexspeak2val(char str[]){
int len = strlen(str);
assert(len);
if (len == 1) /* case 1 */
return hexchar2val(str[0]);
assert(str[0] == '0' && (str[1] == 'x' || str[1] == 'X'));
size_t total = 0;
size_t i = 2; /* case 2 */
for (; (i + 8) <= len ; i += 8)
{
uint64_t payload;
memcpy(&payload, str + i, 8);
//printf("%lx\n", payload);
const uint64_t letter = payload & PACKED_BYTE(0x40);
const uint64_t shift = (letter >> 3) | (letter >> 6);
const uint64_t value = (payload + shift) & PACKED_BYTE(0x0f);
//printf("%lx\n", value);
//total = total << 64 ;
#if __BYTE_ORDER == __LITTLE_ENDIAN
for (size_t j = 0; j < 64 ; j += 8)
total += (( value << j ) >> 56) << (j >> 1);
#else
for (size_t j = 0; j < 64 ; j += 8)
total += (( value << j ) >> 56) << ((56 - j) >> 3);
#endif
//printf("%lx\n",total);
}
//printf("%lx\n",total);
for (; i < len ; i++) /* case 3 */
total = total << 4 | hexchar2val(str[i]));
//printf("%lx\n",total);
return total;
}
```
:::warning
* 當加總 `total` 超過 2^64 - 1 時,會造成 Overflow
* 如果可以解決 Overflow ,加上 `#42 total = total << 64 ;` ,則可以在更長串的 hexspeak 上執行
* 目前正在尋找更有效率完成 `#44#45`、`#47#48` 之功能的程式碼
:::
* 若 `payload` 為 `'8BADF00D'`
* 在 Little Endian , `value` 為 `0x0d 00 00 0f 0d 0a 0b 08`
* 在 Big Endian , `value` 為 `0x08 0b 0a 0d 0f 00 00 0d`
## Q3. 除法運算的改進的策略
#### 將除法改為乘法運算
$$
\frac{n}{d} = \frac{n\times2^N}{d\times2^N} = n \times \frac{\frac{2^N}{d}}{2^N}
$$
$$
= (n \times \frac{2^N}{d}) >> N
$$
其中,$2^N/d$ 可以先算好,節省時間
```c=
const uint32_t D = 3;
#define M ((uint64_t)(UINT64_C(XXX) / (D) + 1)) // XXX = 0xFFFFFFFFFFFFFFFF
/* 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;
}
```
>當我們想將 5 除以 4 時,就相當於乘以 0.25,所以我們可將 $5/4$ 改為 5×0.25,我們得到整數的部分 (即 1),和小數部分 (即 0.25),後者乘以 4 就是 1 ,也就是餘數。
:::info
* **`uint64_t`**
* unsigned integer type with width of exactly 64 bits
(provided only if the implementation directly supports the type)
* **`UINT64_C`**
* expands to an integer constant expression having the value specified by its argument and whose type is the promoted type of `uint_least64_t`
* **`uint_least64_t`**
* smallest unsigned integer type with width of at least 64 bits
* **`__uint128_t`**
* [GCC extension](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html),用以表示 128 位元的無號數值
:::
* 程式碼解析:
* 與數學式對比後,可以發現 N = 64 及 `M` 應該是擔任 $2^N/d$ 的角色,但仍須考量:
* $2^N/d$ 可能不整除
* `uint64` 無法表示小數,
* 應該可以用某些手法來避免上述困擾
* 因此,可以猜測 XXX 的值應該與 $2^N$ 差距不遠,故選 0xFFFFFFFFFFFFFFFF ( i.e. $2^N-1$ )
#### 證明正確性
$$
0 \leq n - q \times D < D ,
$$
$$
where \space n,D \space \epsilon \space \mathbb{N} \space and \space q = \left\lfloor\frac{n\times(\left\lfloor\frac{2^N -1}{d}\right\rfloor +1)} {2^N} \right\rfloor , \space N \space \epsilon \space \mathbb{N}
$$
* 若已知 n 、 D 為正整數,則存在唯一一組整數解 q 、 r 、且 $0\leq r<D$,使得 $n = q\times D+r$
* 因此,只要能完成上述證明,便能證明程式的正確性
## Q4. 呈 Q3 ,判斷某個數值能否被指定的除數所整除
* `D` 和 `M` 都沿用 Q3
```c=
const uint32_t D = 3;
#define M ((uint64_t)(UINT64_C(XXX) / (D) + 1))
bool divisible(uint32_t n)
{
return n * M <= YYY; // YYY = M-1
}
```
>divisible(7) 要得到 0 (即 7 無法被 3 整除)
>divisible(87) 要得到 1 (即白痴是三的倍數)
## Q5. 將指定的字串 (或部分字串) 的英文字母全改為小寫
#### 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]);
}
}
```
:::info
* **`int tolower ( int c );`**
* **Convert uppercase letter to lowercase (任何語系)**
* Converts c to its lowercase equivalent if c is an uppercase letter and has a lowercase equivalent. If no such conversion is possible, the value returned is c unchanged.
:::
#### 向量化 (vectorized) : (一次操作一整個 word )
在 64 位元處理器架構 (以下針對 `x86_64`, little endian),我們可引入向量化 (vectorized) 的手段,避免一次比對一個字元,而是直接操作一整個 word (對 `x86_64` 來說,就是 64 bits,或 8 個位元組)。
```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
#5 #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u)
```
>0xff = ( 0000 ... 0000 1111 1111 )~2~
* 若 `b` 為 0x * ... * cd = ( **** ... **** 1011 1100 )~2~ ,
則 `(uint64_t)(b) & (0xff)` 的結果為 ( 0000 ... 0000 1011 1100 ) = 0xcd,
即 **取出 `b` 在 16 進制的最後兩位數**
* `0xcd * 0x0101010101010101u` 結果為 0xcdcdcdcdcdcdcdcd ,即 **將前者複製 8 次**
```c
#10 size_t k = n / 8;
#11 for (size_t i = 0; i < k; i++, s += 8) {
#12 uint64_t *chunk = (uint64_t *) s;
```
* 8 bytes 一組,共有 `k` 組需要比對
* 當然,還有剩餘不滿 8 bytes 的部份,由下一段程式碼處理
##### case 1 : 8 個 bytes 皆為 有效 ASCII 字元
```c
#13 if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */
```
* 根據提示,`#13` 判斷 `*chunk` 是否為 有效的 ASCII 字元 ( 請參考 Q1 )
* 一次檢查 8 bytes ,因此 `PACKED_BYTE(VV1)` 必須為 0x8080808080808080
* 若 `*chunk & PACKED_BYTE(VV1)` 為 0,則 `*chunck` 8 bytes 皆為有效的 ASCII 字元
* 故 **VV1 = 0x80**
```c
#17 *chunk ^= mask;
```
* 根據 `#17` ,我們可以猜測,程式想用 `('A' ^ ' ') // 得到 'a'` 的原理,進行大小寫轉換
> `' '` = 0x20 = 0010 0000
* 接下來,們還需要辨別,哪些 bytes 裡頭存放著大寫,找出其在 `mask` 上的對應位置放上 0x20
反之則為小寫,在 `mask` 上的對應位置放上 0x00
```c
#16 uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2;
```
* 若只考慮 1 byte,且該 byte 存放著大寫字母,
則 `mask = ((A ^ Z) & VV4 ) >>2` 應為 0x20 = ( 0010 0000 )
* `((A ^ Z) & VV4 )` 為 ( 1000 0000 ) = 0x80
* 若只考慮 1 byte,且該 byte 存放著小寫字母,
則 `mask = ((A ^ Z) & VV4 ) >>2` 應為 0x00 = ( 0000 0000 )
* `((A ^ Z) & VV4 )` 為 ( 0000 0000 ) = 0x00
* 綜上所述,我們假設 **VV4= 0x80**,且
* 若存放著大寫字母,`(A ^ Z)` 應為 ( 1*** **** )
* `A` 應為 ( 1*** **** ) 、 `Z` 應為 ( 0*** **** )
* 若存放著小寫字母,`(A ^ Z)` 應為 ( 0*** **** )
* `A` 應為 ( 1*** **** ) 、 `Z` 應為 ( 1*** **** )
```c
#14 uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2);
```
* `#14` 應該要找出 ASCII 編碼 >= `'A'` ( i.e. ( 0100 0001 ) ) 的 bytes
* `128 - 'A'` = ( 1000 0000 ) - ( 0100 0001 ) = ( 0011 1111 )
* 若只考慮 1 byte,且該 byte 存放著大寫字母,
則 `*chunk + (128 - 'A' + VV2)` = ( 010* **** ) + ( 0011 1111 ) + VV2
* 因為 `*chunk` 是大寫字母,所以
* ( 010* **** ) + ( 0011 1111 ) >= `'A'` ( 0100 0001 ) + ( 0011 1111 ) = ( ==1==000 0000 )
* ( 010* **** ) + ( 0011 1111 ) <= `'Z'` ( 0101 1010 ) + ( 0011 1111 ) = ( ==1==001 1001 )
* 可達成我們假設的目標:`A` 應為 ( 1*** **** )
* 因此,假設 **VV2 = 0**
* 同理,若存放的是小寫字母, VV2 = 0 也能成立
```c
#15 uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3);
```
* `#15` 應該要找出 ASCII 編碼 <= `'Z'` ( i.e. ( 0101 1010 ) ) 的 bytes
* `128 - 'Z'` = ( 1000 0000 ) - ( 0101 1010 ) = ( 0010 0110 )
* 若只考慮 1 byte,且該 byte 存放著大寫字母,
則 `*chunk + (128 - 'Z' + VV3)` = ( 010* **** ) + ( 0010 0110 ) + VV3
* 因為 `*chunk` 是大寫字母,所以
* ( 010* **** ) + ( 0010 0110 ) >= `'A'` ( 0100 0001 ) + ( 0010 0110 ) = ( ==0==110 0111 )
* ( 010* **** ) + ( 0010 0110 ) <= `'Z'` ( 0101 1010 ) + ( 0010 0110 ) = ( 1000 0000 )
* 除了 `'Z'` 以外,其他大寫字母都能達成假設的目標:`Z` 應為 ( 0*** **** )
* 若也要讓 `'Z'` 達成目標,可以假設 **VV3 = -1**
* 同理,若存放的是小寫字母, VV3 = -1 也能成立
##### case 2 : 8 個 bytes 中,至少有一個不為 有效 ASCII 字元
```c
#18 else
#19 strlower(s, 8);
```
* 若 `*chunk` 中有幾個 bytes 不為有效的 ASCII 字元,則無法一次轉換 8 bytes
* 用傳統方式一次檢查一個 bytes
##### case 3 : 剩餘不滿 8 個 bytes 的部份
```c
#22 k = n % 8;
#23 if (k)
#24 strlower(s, k);
```
* 所有能 8 個 bytes 一組的轉換完之後,可能會有 1~7 個 bytes 尚未轉換
* 用傳統方式一次檢查一個 bytes
## Q5. 進階題
#### 將前述測試程式 `main` 函式的 char `str[]` 更換為 char `*str`,會發生什麼事?
* 會出現
```
程式記憶體區段錯誤 (核心已傾印)
Segmentation fault
```
* [ISO/IEC 9899:1999](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) :
:::success
$§\space 6.4.5\space String \space\space literals\space$ ( p.62 )
* In translation phase 7, a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals.
==The multibyte character sequence is then used to initialize **an array of static storage** duration and length just sufficient to contain the sequence.==
For character string literals, the array elements have type `char`, and are initialized with the individual bytes of the multibyte character sequence; ...
* ==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**.==
:::
:::success
$§\space 6.7.8\space EXAMPLE \space\space 8\space$ ( p.130 )
The declaration
```
char s[] = "abc", t[3] = "abc";
```
defines ‘‘plain’’ `char` array objects `s` and `t` whose elements are initialized with character string literals.
This declaration is identical to
```
char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };
```
The contents of the arrays are modifiable.
On the other hand, the declaration
```
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**.==
:::
## q6. LeetCode [137. Single Number I](https://leetcode.com/problems/single-number/)
Given a non-empty array of integers, every element appears twice except for one. Find that single one.
>Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
```c=
int singleNumber(int* nums, int numsSize){
int note = 0;
for (int i=0 ; i< numsSize ; i++)
note ^= nums[i];
return note;
}
```
* ==XOR 運算 = 看到 1 就反轉,看到 0 不變==
* 從 bitwise 的角度出發,`note` 的每個 bit 分別紀錄以檢查的 `nums[i]` 各 bit 出現 1 的次數
* 若 出現次數 mod 2 = 0 , 則 note~i~ 設為 0
* 若 出現次數 mod 2 = 1 , 則 note~i~ 設為 1
* 狀態圖:( 圓圈中的值代表 note~i~ )
```graphviz
digraph {
rankdir = LR
0 -> 1 [label = "input = 1"]
1 -> 0 [label = "input = 1"]
0 -> 0 [label = "input = 0"]
1 -> 1 [label = "input = 0"]
}
```
* Truth table
| A | B | C |
| ------- | ------ |:----------- |
| note~i~ | num~i~ | new note~i~ |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
* $C = 01 + 10$
$= A'B + AB'$
$= A \oplus B$
* 因此, **new note~i~ = note~i~ $\oplus$ num~i~**
## Q6. 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?
```c=
int singleNumber(int *nums, int numsSize)
{
int lower = 0, higher = 0;
for (int i = 0; i < numsSize; i++) {
lower ^= nums[i];
lower &= KKK; // KKK = ~higher
higher ^= nums[i];
higher &= JJJ; // JJJ = ~lower
}
return lower;
}
```
* 與 q6 相同概念,不同的是,數字可能出現 1~3 次,需要 `lower` ( 下方簡稱 L )及 `higher` ( 下方簡稱 H ) 兩個值來幫忙紀錄:
* 若 出現次數 mod 3 = 0 , 則 H~i~L~i~ 設為 0==0==
* 若 出現次數 mod 3 = 1 , 則 H~i~L~i~ 設為 0==1==
* 若 出現次數 mod 3 = 2 , 則 H~i~L~i~ 設為 1==0==
* 狀態圖:( 圓圈中的值代表 H~i~L~i~ )
```graphviz
digraph {
rankdir = LR
00 -> 01 [label = "input = 1"]
01 -> 10 [label = "input = 1"]
10 -> 00 [label = "input = 1"]
00 -> 00 [label = "input = 0"]
01 -> 01 [label = "input = 0"]
10 -> 10 [label = "input = 0"]
}
```
* Truth table
| A | B | C | D | E |
|:---- | ---- | ------ | -------- |:-------- |
| H~i~ | L~i~ | num~i~ | new H~i~ | new L~i~ |
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 | 1 |
| 0 | 1 | 0 | 0 | 1 |
| 0 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 0 |
* $E = 001 + 010$
$= A'B'C + A'BC'$
$= A'(B'C + BC')$
$= A'(B \oplus C)$
* 因此,**new L~i~ = ~ H~i~ & ( L~i~ $\oplus$ num~i~ )**
* $D = 001 + 100$
$= A'E'C + AE'C'$
$= E'(A'C + AC')$
$= E'(A \oplus C)$
* 因此,**new H~i~ = ~ (new L~i~) & ( H~i~ $\oplus$ num~i~ )**
* 故,**KKK = ~higher** 、 **JJJ = ~lower**
## Q6. 進階題
#### 請撰寫不同上述的程式碼,並確保在 LeetCode 137. Single Number II 執行結果正確且不超時
##### 解法一
```c=
int singleNumber(int *nums, int numsSize)
{
int lower = 0, higher = 0;
for (int i = 0; i < numsSize; i++) {
int tmp = lower;
lower ^= nums[i];
lower &= ~higher;
higher = (~higher & tmp & nums[i]) | (higher & ~tmp & ~(nums[i]));
}
return lower;
}
```
* Q6. 最後的數學推導中, new H~i~ 還有另一種寫法:
* $D = 011 + 100$
$= A'BC + AB'C'$
* 因此, new H~i~ = ( ~ H~i~ & L~i~ & num~i~ ) | ( H~i~ & ~ L~i~ & ~ (num~i~) )
* [檢測結果](https://leetcode.com/submissions/detail/400937625/):
![](https://i.imgur.com/lxrngA1.png)
##### 解法二: ( 參考 [RinHizakura 同學](https://hackmd.io/@RinHizakura/SJ5NuIANP#%E4%B8%8D%E5%90%8C%E7%9A%84%E8%A7%A3%E9%A1%8C%E6%80%9D%E8%B7%AF) )
* 數字只可能出現 1、3 次,可能只需要 `note` 一個值紀錄即可:
* 若 出現次數 mod 3 = 0 or 2 , 則 note~i~ 設為 0
* 若 出現次數 mod 3 = 1 , 則 note~i~ 設為 1
#### 將重複 3 次改為改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼
##### 重複 4 次 ( 偶數次 )
* 重複 2的倍數 次的情形,都可以使用 [q6](#q6-LeetCode-137-Single-Number-I) 的解法
* 同理,重複 3的倍數 次的情形,也都可以使用 [Q6](#Q6-LeetCode-137-Single-Number-II) 解法
##### 重複 5 次 ( 5的倍數次 )
```c=
int singleNumber(int *nums, int numsSize)
{
int left = 0, middle = 0, right = 0;
for (int i = 0; i < numsSize; i++) {
right = ~left & (nums[i] ^ right);
middle = ~left & ((~right & (middle ^ nums[i])) | (middle & right));
left = ~middle & ~right & (left ^ nums[i]);
}
return right;
}
```
* 數字可能出現 1~5 次,需要 `left`( 下方簡稱 L )、`middle`( 下方簡稱 M )、`right`( 下方簡稱 R ) 三個值來幫忙紀錄:
* 若 出現次數 mod 5 = 0 , 則 L~i~M~i~R~i~ 設為 0==00==
* 若 出現次數 mod 5 = 1 , 則 L~i~M~i~R~i~ 設為 0==01==
* 若 出現次數 mod 5 = 2 , 則 L~i~M~i~R~i~ 設為 0==10==
* 若 出現次數 mod 5 = 3 , 則 L~i~M~i~R~i~ 設為 0==11==
* 若 出現次數 mod 5 = 4 , 則 L~i~M~i~R~i~ 設為 1==00==
* 狀態圖:( 圓圈中的值代表 L~i~M~i~R~i~ )
```graphviz
digraph {
rankdir = LR
000 -> 001 [label = "input = 1"]
001 -> 010 [label = "input = 1"]
010 -> 011 [label = "input = 1"]
011 -> 100 [label = "input = 1"]
100 -> 000 [label = "input = 1"]
000 -> 000 [label = "input = 0"]
001 -> 001 [label = "input = 0"]
010 -> 010 [label = "input = 0"]
011 -> 011 [label = "input = 0"]
100 -> 100 [label = "input = 0"]
}
```
* Truth table:
| A | B | C | D | E | F | G |
| ---- | ---- |:---- | ------ | -------- | -------- |:-------- |
| L~i~ | M~i~ | R~i~ | num~i~ | new L~i~ | new M~i~ | new R~i~ |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 0 | 1 | 0 | 0 | 1 |
| 0 | 0 | 1 | 0 | 0 | 0 | 1 |
| 0 | 0 | 1 | 1 | 0 | 1 | 0 |
| 0 | 1 | 0 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 | 1 | 1 |
| 0 | 1 | 1 | 0 | 0 | 1 | 1 |
| 0 | 1 | 1 | 1 | 1 | 0 | 0 |
| 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| 1 | 0 | 0 | 1 | 0 | 0 | 0 |
![](https://i.imgur.com/7vBeHtN.jpg)
* 由上圖的推導,我選擇下列這組解:
* new R~i~ = ~ L~i~ & ( R~i~ $\oplus$ num~i~ )
* new M~i~ = ~ L~i~ & ( ( ~ (new R~i~) & ( M~i~ $\oplus$ num~i~ ) ) | ( M~i~ & new R~i~ ) )
* new L~i~ = ~ (new M~i~) & ~ (new R~i~) & ( L~i~ $\oplus$ num~i~ )