---
# System prepended metadata

title: 2024 Homework4 (quiz3+4)

---

# 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 則在刪除操作中使用，用於替換待刪除節點。

插入操作首先使用二元搜尋樹的插入方法將新節點插入到樹中，然後調用更新操作進行平衡調整。刪除操作則根據待刪除節點是否有左子節點和右子節點，分別進行不同的處理，並最終進行平衡調整。