# 2024q1 Homework4 (quiz3+4)
contributed by < [`56han`](https://github.com/56han/lab0-c) >
## 第 3 週測驗題
[2024q1 第 3 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz3)
### 測驗 `1`
$Y_m = \begin{cases}
c_m + d_m, & \text{if } a_m = 2^m \\
0, & \text{if } a_m = 0
\end{cases}$
在每次迴圈更新 $c_m$、$d_m$ 來計算 $Y_m$ 進而推得 $a_m$
$c_{m-1} = {P_m}{2^{m}} = (P_{m+1} + a_{m})2^{m} = P_{m+1}2^{m} + a_{m}2^{m} =
\begin{cases}
\frac{c_{m}}{2} + d_{m} & \text{if } a_{m} = 2^{m} \\
\frac{c_{m}}{2} & \text{if } a_{m} = 0
\end{cases}$
$d_{m-1} = \frac{d_{m}}{4}$
透過閱讀數學算式,進而得到幾個重點:
* 函式中 `z` 即是 $C_m$,而 `m` 即是 $d_m$,`b` 即是 $Y_m$,`x` 則是代表前一輪的差值,也就是 $X_{m+1}$。
* 函式中的迴圈每次迭代時 `m` 往右位移 2 位,是在表示上面公式中的 $d_{m-1} = \frac{d_{m}}{4}$。
* `b = z + m; ` 表示 $Y_m = c_m + d_m$。
* `if` 條件是判斷:當 $X_{m+1}$ 比 $Y_{m}$ 大時,則更新其值。
```c
int i_sqrt(int x)
{
if (x <= 1) /* Assume x is always positive */
return x;
int z = 0;
for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) {
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
}
return z;
}
```
#### 延伸問題 - 嘗試用[第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2)提到的 ffs / fls 取代 __builtin_clz,使程式不依賴 GNU extension
* `__ffs()` 函數的目的是找到參數 (unsigned long) 中**最低位**的 1 所在的位置(從 0 開始計算)。
* 將`__ffs()` 函數改寫成下面的 `fls()` 函數的目的是找到參數 (unsigned long) 中**最高位**的 1 所在的位置(注意:從 1 開始計算)。
* `__builtin_clz()` 為 GCC 的內建函式,功能為找到 x 最高位數前面有幾個 0 , x 為 0 時,未定義。
所以將 `BITS_PER_LONG - fls(x)` 即可得到 `__builtin_clz(x)`。
```c
static inline unsigned long fls(unsigned long x)
{
int num = 32;
if(!x)
return 0;
if ((x & 0xffff0000) == 0) {
num -= 16;
x <<= 16;
}
if ((x & 0xff000000) == 0) {
num -= 8;
x <<= 8;
}
if ((x & 0xf0000000) == 0) {
num -= 4;
x <<= 4;
}
if ((x & 0xc0000000) == 0) {
num -= 2;
x <<= 2;
}
if ((x & 0x80000000) == 0) {
num -= 1;
x <<= 1;
}
return num;
}
```
因此可以將 `i_sqrt()` 改寫成以下
```diff
int i_sqrt(int x)
{
if (x <= 1) /* Assume x is always positive */
return x;
int z = 0;
- for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 2) {
+ for (int m = 1UL << (fls(x)-1 & ~1UL); m; m >>= 2) {
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
}
return z;
}
```
### 測驗 `2`
### 測驗 `3`
考慮 `ilog2()` 可計算以 2 為底的對數,且其輸入和輸出皆為整數。以下是其一可能的實作: (版本一)
透過一次又一次的右移(除以 2 ),直到 `i=0`,去計算 i 以 2 為底時有多少位數。
```c
int ilog2(int i)
{
int log = -1;
while (i) {
i >>= 1;
log++;
}
return log;
}
```
改寫為以下程式碼: (版本二)
透過右移的方式去計算 i 以 2 為底時有多少位數。
:::info
我認為這裡的 while 可以改成 if,因為 i 透過右移會越來越小,因此不會進入相同迴圈中,只會往下走,進入別的判斷式中。
:::
```c
static size_t ilog2(size_t i)
{
size_t result = 0;
while (i >= (1<<16)) { //2^16
result += 16;
i >>= 16;
}
while (i >= (1<<8)) { //2^8
result += 8;
i >>= 8;
}
while (i >= (1<<4)) { //2^4
result += 4;
i >>= 4;
}
while (i >= 2) {
result += 1;
i >>= 1;
}
return result;
}
```
亦可運用 GNU extension `__builtin_clz` 來改寫,注意輸入若是 0 則無定義: (版本三)
透過 `31-__builtin_clz()` 找到最高位數,即是取 log 以後的值。
>`__builtin_clz()` 為 GCC 的內建函式,功能為找到 x 最高位數前面有幾個 0 , x 為 0 時,未定義。
```c
int ilog32(uint32_t v)
{
return (31 - __builtin_clz(v | 1));
}
```
### 測驗 `4`
### 測驗 `5`
延伸測驗 3,已知輸入必為大於 0 的數值 (即 x > 0),以下程式碼可計算 $\lceil \log_2(x)\rceil$,也就是 ceil 和 log2 的組合並轉型為整數:
```c
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
x--;
r = (x > 0xFFFF) << 4;
x >>= r;
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
shift = (x > 0xF) << 2;
x >>= shift;
r |= shift;
shift = (x > 0x3) << 1;
x >>= shift;
return (r | shift | x > 1) + 1;
}
```
函數首先 `x - 1`,是為了對於 2 的冪的數值也能夠正確計算對數。
`shift = (x > 0xFF) << 3;` 表示,若 x > 15 則為 `1 << 2`,否則為 `0 << 2`。
大部分和測驗 3 類似,當我們看到以下片段
```c
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
```
首先判斷 `x > 0xFF` ,並使用 shift 記錄要移動多少。
`x >>= shift;` 即是測驗 3 的 `i >>= 16;`,`r |= shift;` 即是 `r = r + shift;` 之意。
最後 return 可拆成3個部份看
```c
return (r | shift | x > 1) + 1;
```
1. `r | shift ` 即是 `r = r + shift;` 之意
2. 判斷 x 是否還有剩,因為前面`shift = (x > 0x3) << 1;` 會忽略 `x = 0x2` 的情況。
3. 最後再 +1 作為取上界 (ceil) 。
## 第 4 週測驗題
[2024q1 第 4 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz4)
### 測驗 `1`
此題是 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/submissions/1219875025/) ,透過兩個for 迴圈可以逐一比較兩個數字之間的 Hamming Distance,最後要除以2 (右移1位),因為重複計算ab和ba。
GCC 提供對應的內建函式:
>`__builtin_popcount(x)`: 計算 x 的二進位表示中,總共有幾個 1
```c
int totalHammingDistance(int* nums, int numsSize)
{
int total = 0;;
for (int i = 0;i < numsSize;i++)
for (int j = 0; j < numsSize;j++)
total += __builtin_popcount(nums[i] ^ nums[j]);
return total >> 1;
}
```
#### 延伸問題 - 不依賴 popcount,嘗試以上述歸納的規則,撰寫出更高效的程式碼。
從 popcount 角度出發,時間複雜度就被限制在 $O(n^2)$。
以下嘗試從位元展現的樣貌,觀察 Total Hamming Distance 的規則。
| n'th bit | 4 | 3 | 2 | 1 | 0 |
| -------- | --| --| --| --| --|
| input 7 | 0| 0| 1| 1| 1|
| input 5 | 0| 0| 1| 0| 1|
| input 10 | 0| 1| 0| 0| 1|
| input 17 | 1| 0| 0| 0| 1|
1. 第 0 個 bit 觀察 :
全部都是 1 -> Hamming Distance = 0
2. 第 1 個 bit 觀察 :
1 有一個,其餘皆為 0
可以直接用單個 bit 判斷 Hamming Distance (1)* (numsize - 1)
3. 第 2 個 bit 觀察 :
1 有兩個,其餘皆為 0
Hamming Distance (2)* (numsize - 2)
以 bit 找規則:
>所有 Number 中從第 0 個 bit 開始計算全部該 bit 的 Hamming Distance ,在全部累加起來就是 Total Hamming Distance。
可以改寫成以下程式碼,其中:
* `(nums[i] >> bit) & 1` 判斷 `nums[i]` 該 bit 是否為 1
* `bitCount` 統計出所有數字的該 bit 為 1 有幾個
* `bitCount * (numsSize - bitCount)` 統計該 bit 的 Hamming Distance
```c
int totalHammingDistance(int* nums, int numsSize) {
int total = 0;
for (int bit = 0; bit < 32; bit++) {
int bitCount = 0;
for (int i = 0; i < numsSize; i++) {
bitCount += (nums[i] >> bit) & 1;
}
total += bitCount * (numsSize - bitCount);
}
return total;
}
```
### 測驗 `2`
:::info
我理解 `n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)` 可以寫為 `n = popcount(n ^ 0xAAAAAAAA) - 16` ,其中也可以理解「我們要將它加上一個足夠大的 3 的倍數」,但為何是選擇加上39?使其為 `n = popcount(n ^ 0xAAAAAAAA) + 23;`
:::
```c
int mod3(unsigned n)
{
n = popcount(n ^ 0xAAAAAAAA) + 23;
n = popcount(n ^ 0x2A) - 3;
return n + ((n >> 31) & 3);
}
```
### 測驗 `3`