# 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 更新根節點,以確保樹的平衡性。