# 2024q1 Homework4 (quiz3+4)
contributed by < ollieni >
## quiz3
### **測驗一 : 平方根**
版本一的程式是先對要找平方根的整數 N 取對數(以二為底),可以找出 msb (most significant bit) ,也就是用二進位表示這個整數時,最高位1是在從右開始數的第幾位。
有了 msb 後,因為整數 N 的平方根不可能超過 msb ,所以將 1 左移 msb 個位元後放入 a,接下來就可以開始用 while 迭代,在每一次迭代中,它會檢查 (result + a) * (result + a) 是否小於或等於 N。如果是,則將 result 加上 a,然後將 a 右移(除以二),迭代完就可以逼近整數平方根。
版本二就是不直接使用取對數的方法,用將 N 右移的方式,右移幾次 N 不大於 1,次數就是 msb,接下來就和版本一一樣了。
版本三
```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;
}
```
:::danger
bit 的翻譯是「位元」,而非「位」,後者是量詞。
:::
`__builtin_clz(x)` 函式返回 `x` 的二進制表示中從最高位到最低位第一個非零位前的零的個數。
這個版本用`__builtin_clz(x)` 函式找出 msb 。
`((31 - __builtin_clz(x)) & ~1UL)` ,用 31 減去 零的個數後 ,可以得到 msb , `& ~1UL` 把最後一位轉成 0 ,是因為每次 m 右移兩位,要強制對齊成偶數。
### **測驗二 : 針對正整數在相鄰敘述進行 mod 10 和 div 10 操作,如何減少運算成本?**
我覺得重點是 **"mod 10 就是 tmp 減去 div 10 的結果乘 10"**。
```c
#include <stdint.h>
void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod)
{
uint32_t x = (in | 1) - (in >> 2); /* div = in/10 ==> div = 0.75*in/8 */
uint32_t q = (x >> 4) + x;
x = q;
q = (q >> 8) + x;
q = (q >> 8) + x;
q = (q >> 8) + x;
q = (q >> 8) + x;
*div = (q >> 3);
*mod = in - ((q & ~0x7) + (*div << 1));
}
```
`uint32_t x = (in | 1) - (in >> 2);` 這行就等於,in最後一位強制為1 - (in/4),約等於 $x = \dfrac{3}{4} \times in$ 。
`uint32_t q = (x >> 4) + x;`,這一行程式等同於$q = \dfrac{3}{4} \times in + \dfrac{3}{64} \times in = \dfrac{51}{64} \times in \approx 0.79 \times in$。
接下來這幾行是為了讓 q 接近 $\dfrac{8}{10} \times in$,加了四次 $\dfrac{1}{256} \times 0.79 \times in$。
```c
x = q;
q = (q >> 8) + x;
q = (q >> 8) + x;
q = (q >> 8) + x;
q = (q >> 8) + x;
```
最後 `*div = (q >> 3);` 右移3位,等於除8,就變成$\dfrac{1}{10} \times in$。
而 *mod 就等於 `in - ((q & ~0x7) + (*div << 1)); `,in - 商數乘10。
### **測驗三 : 考慮 ilog2 可計算以 2 為底的對數,且其輸入和輸出皆為整數**
版本一是將要取對數的樹一直右移,並計算右移的次數,以此找出 msb,要從 -1 開始加是因為 log1 = 0。
版本二則是檢查要取對數的值大於65535、256、16、2時,可以一次右移16、8、4、1個 bits。
版本三是使用測驗一有提到的 `__builtin_clz(x)` 找出最高位到最低位第一個非零位前的零的個數,並用 31 減去即可得到以二為底的對數。
### 測驗四 : EWMA
<s>
Chatgpt 對於 EWMA 的解釋如下 :
> EWMA 是指指數加權移動平均。它是一種統計方法,用於分析數據點,將較近期的數據賦予更高的權重,而對較舊的數據賦予較低的權重。在 EWMA 中,每個數據點都以指數方式加權,隨著數據點越來越遠,權重呈指數衰減。這種方法特別適用於金融分析、信號處理和質量控制等領域,其中近期的數據點通常比舊的更具決策價值。EWMA 常用於平滑噪聲數據、檢測趨勢以及識別時間序列中的異常或變化。
</s>
:::danger
閱讀權威材料。
:::
```c
* ewma_add() - Exponentially weighted moving average (EWMA)
* @avg: Average structure
* @val: Current value
*
* Add a sample to the average.
*/
struct ewma *ewma_add(struct ewma *avg, unsigned long val)
{
avg->internal = avg->internal
? (((avg->internal << avg->weight) - avg->internal) +
(val << avg->factor)) >> avg->weight
: (val << avg->factor);
return avg;
}
```
根據題目
$S_t = \begin{cases}
Y_0, & t=0 \\
\alpha Y_t + (1-\alpha)\cdot S_{t-1}, & t > 0
\end{cases}$
題目中的三元條件運算式就是這個數學式。
也就是說在 `avg->internal` 是 0 的情況下,`avg->internel = (val << avg->factor)`。
對於為什麼要左移 factor bits,我問 Chatgpt後,有了以下的解釋 :
> factor 是用於縮放內部值的因子。這個因子的作用是調整內部值的大小,以便在內部表示中容納指定範圍內的數值。
>
> 具體來說,factor 用於計算加權平均的內部表示,這涉及將實際值(例如計數或度量)轉換為內部表示。通常,為了在 EWMA 中處理大範圍的數值,需要將這些值進行縮放,以確保它們可以被有效地處理。因此,factor 就是用於進行這種縮放的參數。
而 `avg->internal` 不是 0 的情況下,```(((avg->internal << avg->weight) - avg->internal) + (val << avg->factor)) >> avg->weight```,最後為什麼要右移 avg->weight 還不太明白。
### **測驗五 : 計算 $\lceil \log_2 x\rceil$**
```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;
}
```
前面的步驟都是和測驗三找對數時一樣的概念,重點在最後的回傳值,`return (r | shift | x > 1) + 1;`將 shift 和 x > 1 (如果 x 大於1,則為1,否則為0) 添加到 r 中,並將結果加1返回。這是因為在一開始計算2的冪前,我們將 x 減1,所以最終結果需要再加1。
## quiz4
### **測驗一 : popcount**
```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;
}
```
這段程式將 nums 裡的數,透過兩個 for 迴圈遍歷交叉使用`__builtin_popcount(nums[i] ^ nums[j])` 計算,`(nums[i] ^ nums[j])` 有幾個1,就可以得知在二進制時,每個位元的差。
最後右移 1 bits 的原因是因為會重複計算,比如 i = 1, j = 2 和 i = 2, j = 1 重複了,所以最後結果要除以2。
### **測驗二 : Remainder by Summing digits**
### **測驗三 : Xtree**
這題針對 `static void __xt_remove(struct xt_node **root, struct xt_node *del)` 這個函式作解釋。
```c
static void __xt_remove(struct xt_node **root, struct xt_node *del)
{
if (xt_right(del)) {
struct xt_node *least = xt_first(xt_right(del));
if (del == *root)
*root = least;
xt_replace_right(del, least);
xt_update(root, xt_right(least));
return;
}
if (xt_left(del)) {
struct xt_node *most = xt_last(xt_left(del));
if (del == *root)
*root = most;
xt_replace_left(del, most);
xt_update(root, xt_left(most));
return;
}
if (del == *root) {
*root = 0;
return;
}
/* empty node */
struct xt_node *parent = xt_parent(del);
if (xt_left(parent) == del)
xt_left(parent) = 0;
else
xt_right(parent) = 0;
xt_update(root, parent);
}
```
首先,檢查待刪除節點 del 是否有右子節點(xt_right(del))。
如果有右子節點,找到右子樹中的最小節點 least,並將其設為待刪除節點 del 的右子節點。
如果 del 為根節點,則更新根節點為 least。
然後,使用 xt_update 更新根節點,以確保樹的平衡性。
如果待刪除節點 del 沒有右子節點,則檢查它是否有左子節點(xt_left(del))。
如果有左子節點,找到左子樹中的最大節點 most,並將其設為待刪除節點 del 的左子節點。
如果 del 為根節點,則更新根節點為 most。
然後,使用 xt_update 更新根節點,以確保樹的平衡性。