# 2024 Homework4 (quiz3+4) ## 第 3 週測驗題 ### isqrt1: 改寫第三版程式 ```c int i_sqrt(int x) { if (x <= 1) /* 假設 x 總是正數 */ return x; int z = 0; for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= 1) { int b = z + m; z >>= 1; if (x >= b) x -= b, z += m; } return z; } ``` 這段程式碼使用了一種稱為 "Digit-by-digit calculation" 的方法,來計算整數的平方根。基本上,它運用了二進位的特性,將要開平方的數拆成 2 的冪相加,然後進行運算,逐位計算結果。程式碼中的迴圈對應於這個逐位計算過程。 取代 GNU extension 如果我們要將 GNU extension 中的 __builtin_clz 替換為標準函式庫中的 ffs 或 fls,我們需要確保這兩個函式的功能和 __builtin_clz 一致。然後,我們只需要將程式碼中的 __builtin_clz(x) 替換為 ffs(x) - 1 即可。 ### 測驗2 這段程式碼是用來計算兩個以字元陣列表示的正整數的加法,並將結果存儲在另一個字元陣列中。其中,每個字元代表一個位數,並以字符的形式表示 0 到 9 之間的數字。 程式碼運作原理 首先,將兩個正整數的對應位數相加,並加上之前的進位(carry)。 檢查相加後的結果是否超過 10。如果超過 10,則需要將進位設置為 1,否則為 0。 將結果中的每個位數都取模 10,並將其轉換為字符表示形式。 將結果存儲在結果陣列中。 調整成不依賴除法的程式碼 為了減少運算成本,這段程式碼使用了一種技巧來代替除法操作。這種技巧的核心思想是通過位運算來實現除法,進而實現 mod 10 和 div 10 的操作。 在這段程式碼中,利用了除法原理,將 mod 10 和 div 10 合併為一個操作。然後使用 bitwise operation 來實現除法。這裡的關鍵是找到一個適合的除數,並使用位運算來模擬除法運算。最後,將結果存儲在結果陣列中。 這種方法的優勢在於它減少了除法操作的使用,進而減少了運算成本。這對於嵌入式系統和對性能要求較高的應用場景特別有用。 ### h2實作 % 9 和 % 5 程式碼 以下是分別計算 % 9 和 % 5 的程式碼: ```c uint32_t mod_9(uint32_t num) { return num - ((num >> 3) + (num & 0b111)); // num - (num / 9) } uint32_t mod_5(uint32_t num) { return num - ((num >> 2) + (num & 0b11)); // num - (num / 5) } ``` 這兩個函式分別計算了給定數字的模 9 和模 5。它們都避免了使用除法操作,而是利用了位運算的特性來實現模運算。這樣可以更有效地計算模運算,同時減少運算成本。 ### 測驗3 ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= 65536) { result += 16; i >>= 16; } while (i >= 256) { result += 8; i >>= 8; } while (i >= 16) { result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(v)); } ``` 程式碼運作原理 版本二: 此版本根據輸入的數值 i 的大小來選擇不同的迴圈進行。 首先檢查 i 是否大於等於 AAAA,如果是,則將 result 增加 16,並將 i 右移 16 位,以此類推,直到 i 小於 AAAA。 接著類似地檢查 i 是否大於等於 BBBB,CCCC,最後是 2,每次將 result 增加對應的位移數,並將 i 右移對應的位移數,直到 i 變為 0。 版本三: 此版本利用了 GNU extension __builtin_clz,該函式返回一個無符號整數的前導 0 的數量,即輸入的數值 v 在二進位表示中從最高位開始連續 0 的個數,並返回該值減去 31,即為對數的結果。 ### 測驗4 程式碼運作原理 ewma_init(): 此函式用於初始化 EWMA 結構的參數。 檢查 factor 和 weight 是否都是 2 的冪次方,如果不是,則產生斷言錯誤。 將 weight 和 factor 分別轉換為其對數值,使用 ilog2() 函式。 將 internal 初始化為 0。 ewma_add(): 此函式用於將一個新的樣本值添加到 EWMA 中。 如果 internal 不為 0(即 EWMA 已經有過樣本),則使用指數遞減的方式更新 internal。這是通過將前一個值的一部分(internal << EEEE)減去前一個值(avg->internal),並加上新的值(val << FFFF),然後右移 weight 位來實現的。 如果 internal 為 0(即第一個樣本),則將 val 左移 factor 位,並將結果存儲在 internal 中。 ewma_read(): 此函式用於讀取 EWMA 的平均值。 將 internal 右移 factor 位,然後返回結果。 ### 測驗5 ```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 > 0x1)) + 1; } ``` 程式碼運作原理 此程式碼實現了一個有效計算 ceil(log2(x)) 的方法,並將結果轉換為整數。以下是其運作原理: 將 x 減去 1,目的是將 x 轉換為 ceil(log2(x)) 中的上界數。 通過一系列右移操作來找到 ceil(log2(x)) 的值。首先,將 x 右移 4 位,然後將結果存儲在 r 中。接著,對 x 再進行右移和位運算,來尋找 log2(x) 的值,並將結果存儲在 shift 中。將 r 和 shift 進行位 OR 運算,得到 ceil(log2(x))。 最後,將 ceil(log2(x)) 加上 1,以得到 ceil(log2(x)) 的整數值。 ### h2改進程式碼 若要處理 x = 0 的情況且仍然保持無分支,可以將原始 x 的值與 1 進行比較,如果 x = 0,則將 r 和 shift 都設置為 0。修改後的程式碼如下: ```c int ceil_ilog2(uint32_t x) { uint32_t r = 0, shift = 0; x = (x == 0) ? 0 : x - 1; 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 > 0x1)) + 1; } ``` 這樣修改後,即使 x 為 0,也不會引發任何分支。 ## 第 4 週測驗題 ### 測驗1 #### 程式碼運作原理 這段程式碼的主要思想是根據每個數字的每個位元,計算所有數字中該位元為 1 和為 0 的數量,並將兩者相乘後加總起來。這樣就可以得到所有數字之間的 Hamming distance。 具體步驟如下: 從最低位元 (LSB) 開始,遍歷每個位元。 對於每個位元,遍歷所有的數字,並統計該位元為 1 的數量。 將該位元為 1 的數量乘以該位元為 0 的數量,並將結果加到總和中。 重複以上步驟,直到遍歷完所有的位元。 返回總和,即為所有數字之間的 Hamming distance 總和。 #### 改進程式碼 要進一步改進程式碼,可以優化內部的迴圈結構以減少時間複雜度。一種可能的方法是使用單個循環計算每個位元為 1 的數量。以下是改進後的程式碼: ```c int totalHammingDistance(int* nums, int numsSize) { int total = 0; for (int i = 0; i < 32; i++) { int countOnes = 0; for (int j = 0; j < numsSize; j++) { countOnes += (nums[j] >> i) & 1; } total += countOnes * (numsSize - countOnes); } return total; } ``` 這種方法在每次迭代中僅需進行一次內部迴圈,從而提高效率。 #### 不依賴 popcount,針對 LeetCode 477. Total Hamming Distance 撰寫出更高效的程式碼。 可以將數組 nums 的指針和其大小傳遞給這個函數,如下所示: ```c #include <stdio.h> int main() { int nums1[] = {4, 14, 2}; int size1 = sizeof(nums1) / sizeof(nums1[0]); printf("%d\n", totalHammingDistance(nums1, size1)); // 輸出: 6 int nums2[] = {4, 14, 4}; int size2 = sizeof(nums2) / sizeof(nums2[0]); printf("%d\n", totalHammingDistance(nums2, size2)); // 輸出: 4 return 0; } ``` 這個實現不依賴內置的 popcount 函數,並提供了一個更有效的解決方案,用於計算漢明距離的總和。 ### 測驗2 摘錄自《Hacker's Delight》: #### 餘數除以數字的和法 要在沒有除法的情況下計算一個數除以另一個數的餘數,可以使用以下技巧。如果除數符合 $(2^k \pm 1)$,那麼可以使用以下方法來實現這個目的。 如果 $(a \equiv a' (\bmod b))$ 且 $(b \equiv b' (\bmod b))$,則 $(a + b \equiv a' + b' (\bmod b))$ 和 $(a - b \equiv a' - b' (\bmod b))$。 假設 $(a = ab_0 + a_1)$,$(b = bb_0 + b_1)$,$(a_1 \equiv a' (\bmod b))$ 和 $(b_1 \equiv b' (\bmod b))$,那麼 $(a + b \equiv a' + b' (\bmod b))$。接下來進行運算。 以除數為 3 為例,1 $equiv 1 (\bmod 3)$ 且 2 $equiv -1 (\bmod 3)$,將後者不斷的乘上前者可得 $2^k \equiv \begin{cases} 1 (\bmod 3), & \text{若} k \text{為偶數} \\ -1 (\bmod 3), & \text{若} k \text{為奇數} \end{cases}$ 因此若 n 的二進位表示為 $(b_{31}b_{30} \dots b_1b_0)$,則 $n = \sum_{i=0}^{31} b_i \cdot 2^i$ 由以上的推論可得 $n \equiv \sum_{i=0}^{31} b_i \cdot (-1)^{i} (\bmod 3)$。 將每次得到的位元和遞迴的應用上式直到得到一個小於 3 的數即是餘數,若變成負數則要加上 3 的倍數。位元和通常又可以利用 population count 這類的函式來得到,因此寫成程式的話可以將上式表示為 $(n = \text{popcount}(n \& 0x55555555) - \text{popcount}(n \& 0xAAAAAAAA))$。 利用以下定理可以進一步化簡。 $\text{popcount}(n \& (n - 1)) - \text{popcount}(n) = \text{popcount}(n \oplus n) - \text{popcount}(n)$ 於是 $(n = \text{popcount}(n \oplus 0xAAAAAAAA) - 16)$,將此式重複應用於得到的 n 上直到 n 介於 0~2,但為了避免 n 變成負數,我們要將它加上一個足夠大的 3 的倍數,文中的例子是加上 39。 範例程式如下: ```c int mod3(unsigned n) { n = popcount(n ^ 0xAAAAAAAA) + 23; n = popcount(n ^ 0x2A) - 3; return n + ((n >> 31) & 3); } ``` 另一種變形是利用 lookup table 。 ```c int mod3(unsigned n) { static char table[33] = {2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1 }; n = popcount(n ^ 0xAAAAAAAA); return table[n]; } ``` ### 測驗3 這段程式碼實現了一種名為 XTree 的自平衡二元搜尋樹。它使用了二元搜尋樹的基本操作,如插入、刪除和查找,並且在查找操作中進行樹的平衡調整,從而實現了快速的插入和刪除操作。 XTree 通過四個基本的二元搜尋樹操作來實現平衡:rotate_left、rotate_right、replace_right 和 replace_left。其中,rotate_left 和 rotate_right 操作用於樹的平衡調整,而 replace_right 和 replace_left 則在刪除操作中使用,用於替換待刪除節點。 插入操作首先使用二元搜尋樹的插入方法將新節點插入到樹中,然後調用更新操作進行平衡調整。刪除操作則根據待刪除節點是否有左子節點和右子節點,分別進行不同的處理,並最終進行平衡調整。