---
title: '2020q3 Homework4 (quiz4)'
tags: linux2020
---
# 2020q3 Homework4 (quiz4)
contributed by < `ChongMingWei` >
## Outline
[TOC]
## 環境
```shell
$ uname -a
Linux cmw-System-Product-Name 5.4.0-47-generic #51~18.04.1-Ubuntu SMP Sat Sep 5 14:35:50 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
## 作業要求
## 測驗 1
要計算每個位元的差,就使用 `XOR` 來檢查哪些 bit 不是同時為 set 或是 clear ,最後在使用 popcount 來計算 set bit 的數量。
```c=
int hammingDistance(int x, int y) {
return __builtin_popcount(x ^ y);//OP
}
```
:::success
1.解釋上述程式碼運作原理,並舉出不使用 gcc extension 的 C99 實作程式碼;
:::
- Line 5 可得到最低位的 set bit ,其他 bits 設為 clear 。
- Line 6 計算 set bit 的數量
- Line 7 計算完 set bit 的數量,將剛剛得到的最低位 set bit 用 `XOR` 來消除
```c=
int hammingDistance(int x, int y){
int a = x^y;
int b = 0;
while (a){
int t = a & (-a);
b++;
a ^= t;
}
return b;
//return __builtin_popcount(x ^ y);
}
```
:::success
2.練習 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/),你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時
:::
- Version 1: Using double for loop to find hamming distance of each pair.
- Result: Time Limit Exceeded
```c=
int totalHammingDistance(int* nums, int numsSize){
int sum=0;
for (int i = 0;i < numsSize - 1; ++i){
for (int j = i + 1; j < numsSize; ++j){
sum += __builtin_popcount(nums[i]^nums[j]);
}
}
return sum;
}
```
- Version 2: Replace the `__builtin_popcount` by constant time implementation
- Result
![](https://i.imgur.com/RyRrAbb.png)
```c=
int totalHammingDistance(int* nums, int numsSize){
int sum=0;
for (int i = 0;i < numsSize - 1; ++i){
for (int j = i + 1; j < numsSize; ++j){
int a = nums[i]^nums[j];
unsigned n;
unsigned v = (unsigned)a;
n = (v >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
n = (n >> 1) & 0x77777777;
v -= n;
v += v >> 4;
v &= 0x0F0F0F0F;
v *= 0x01010101;
v >>= 24;
sum +=v;
}
}
return sum;
}
```
- Version3: For each bit, we can find the hamming distance of each pair is `countOfOne*countOfZero`.
So we traverse 32 bits and computes the hamming distance of each one bit. Finally, sum all hamming distance.
- Result
![](https://i.imgur.com/NTslLha.png)
> 參考 [O(n) Simple Solution](https://leetcode.com/problems/total-hamming-distance/discuss/865629/O(n)-Simple-Solution)
```c=
int totalHammingDistance(int* nums, int numsSize){
int sum=0;
for(int i = 0; i < 32;++i){
int count1=0;
for (int j = 0;j < numsSize;++j)
count1 += (nums[j]&(1u<<i))>>i;//if bit i of nums[j] is 1, count1++
sum += count1*(numsSize-count1);//compute hamming distance of bit i
}
return sum;
}
```
## 測驗 2
> 參考 [YLowy](https://hackmd.io/@YLowy/S1PmArmLD)
LeetCode [1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) 大意:
>給你一棵樹,樹上有 n 個節點,編號自 0 到 n−1。樹以父節點陣列的形式提供,其中 parent[i] 是節點 i 的父節點。樹的根節點是編號為 0 的節點。請設計 treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k) 函式,回傳節點 node 的第 k 個祖先節點。若不存在這樣的祖先節點,回傳 -1。樹節點的第 k 個祖先節點是從該節點到根節點路徑上的第 k 個節點
```c=
#include <stdlib.h>
typedef struct {
int **parent;//AAA
int max_level;
} TreeAncestor;
TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize)
{
TreeAncestor *obj = malloc(sizeof(TreeAncestor));
obj->parent = malloc(n * sizeof(int *));
int max_level = 32 - __builtin_clz(n) + 1;
for (int i = 0; i < n; i++) {
obj->parent[i] = malloc(max_level * sizeof(int));
for (int j = 0; j < max_level; j++)
obj->parent[i][j] = -1;
}
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
for (int j = 1;; j++) {
int quit = 1;
for (int i = 0; i < parentSize; i++) {
obj->parent[i][j] = obj->parent[i][j + (-1)] == -1//BBB
? -1
: obj->parent[obj->parent[i][j - 1]][j - 1];
if (obj->parent[i][j] != -1) quit = 0;
}
if (quit) break;
}
obj->max_level = max_level - 1;
return obj;
}
int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)
{
int max_level = obj->max_level;
for (int i = 0; i < max_level && node != -1; ++i)
if (k & (1<<i))//CCC
node = obj->parent[node][i];
return node;
}
void treeAncestorFree(TreeAncestor *obj)
{
for (int i = 0; i < obj->n; i++)
free(obj->parent[i]);
free(obj->parent);
free(obj);
}
```
### 破題
根據題意,首先面臨的問題就是要建立怎麼樣的一個資料結構,來儲存如以下圖的 tree:
![](https://i.imgur.com/AVnjv3f.png)
在此實作中,我們採用的是查表的方式,建立一個名為 `TreeAncestor` 的 struct ,使用裡面的表格 `parent` ,來供我們查詢,那麼查詢的方法是甚麼呢?
### 查詢方法
我們會建立一個 $node\times (\lceil{log_{_2}node}\rceil+1)$ (此處 node=7)大小的表格,以上圖為範例:
| Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | |
| -------- | -------- | -------- | -------- | -------- |
| 0 | | | | |
| 1 | | | | |
| 2 | | | | |
| 3 | | | | |
| 4 | | | | |
| 5 | | | | |
| 6 | | | | |
這邊其實多出一個 column ,==用途是讓我們有修改的空間==
在表格中, `parent[node][j]` 中儲存的是 `node` 的 2^j^ th 祖先,我們把要找第 k 個祖先的 "k" 拿出來討論:
k 是一個正整數,而正整數 k 事實上可以被表示成數個 2^n^ 的和
E.g.
要找第 7 個祖先,就是去找 ((第 1 個祖先) 的第 2 個祖先) 的第 4 個祖先
>因為 7 = 2^0^ + 2^1^ + 2^2^
這樣一來,我們就可以達到 O(logn) 的搜尋時間
> 假設我們今天的資料結構是用 linked list ,每個 node 都有著其 parent 的位址,那麼我們的搜尋時間就會是 O(n)
### Code Details
- 計算樹的高度以及將表格 `parent` 初始化,全部都設成-1
```c=13
int max_level = 32 - __builtin_clz(n) + 1;
for (int i = 0; i < n; i++) {
obj->parent[i] = malloc(max_level * sizeof(int));
for (int j = 0; j < max_level; j++)
obj->parent[i][j] = -1;
}
```
| Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | |
| -------- | -------- | -------- | -------- | -------- |
| 0 | -1 | -1 | -1 | -1 |
| 1 | -1 | -1 | -1 | -1 |
| 2 | -1 | -1 | -1 | -1 |
| 3 | -1 | -1 | -1 | -1 |
| 4 | -1 | -1 | -1 | -1 |
| 5 | -1 | -1 | -1 | -1 |
| 6 | -1 | -1 | -1 | -1 |
- 將每一個 node 的父親填入表格中
```c=19
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
```
| Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | |
| -------- | -------- | -------- | -------- | -------- |
| 0 | -1 | -1 | -1 | -1 |
| 1 | 0 | -1 | -1 | -1 |
| 2 | 0 | -1 | -1 | -1 |
| 3 | 1 | -1 | -1 | -1 |
| 4 | 1 | -1 | -1 | -1 |
| 5 | 2 | -1 | -1 | -1 |
| 6 | 2 | -1 | -1 | -1 |
- 將表格 `parent` 中,剩下的欄位填入資料
- Line 25: 如果第2^n-1^個祖先不存在 (-1),那麼後續第2^n^個祖先也不存在 (設為-1)
- Line 27: 如果第2^n-1^個祖先存在 (!= -1),那麼第2^n^個祖先就是**第2^n-1^個祖先的第2^n-1^個祖先**。
> 2^n^ = 2^n-1^ + 2^n-1^
```c=22
for (int j = 1;; j++) {
int quit = 1;
for (int i = 0; i < parentSize; i++) {
obj->parent[i][j] = obj->parent[i][j + (-1)] == -1//BBB
? -1
: obj->parent[obj->parent[i][j - 1]][j - 1];
if (obj->parent[i][j] != -1) quit = 0;
}
if (quit) break;
}
```
| Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor | |
| -------- | -------- | -------- | -------- | -------- |
| 0 | -1 | -1 | -1 | -1 |
| 1 | 0 | -1 | -1 | -1 |
| 2 | 0 | -1 | -1 | -1 |
| 3 | 1 | 0 | -1 | -1 |
| 4 | 1 | 0 | -1 | -1 |
| 5 | 2 | 0 | -1 | -1 |
| 6 | 2 | 0 | -1 | -1 |
- 樹的高度,前面 Line 13 沒必要+1的(會使得表格多出一個欄位),這邊再-1也就顯得多餘。
```c=32
obj->max_level = max_level - 1;
```
- 搜尋第 k 個祖先
- Line 38: 先得到樹的最大高度
- Line 39: for 迴圈的跳出條件中, i 的搜尋次數限定不超過 `max_level` 且判斷目前的 node 是否已經是 root (-1)
- Line 40: if 使用 k 的二進位表示法來判斷是否要去找之前的祖先
> E.g.
> k = 3, 表示要找第1個祖先的第2個祖先,
> 因為3 = 2^0^ + 2^1^,會在 i = 0和1 的時候更新 node 的值
> k = 5, 表示要找第1個祖先的第4個祖先,
> 因為5 = 2^0^ + 2^2^,會在 i = 0和2 的時候更新 node 的值
```c=38
int max_level = obj->max_level;
for (int i = 0; i < max_level && node != -1; ++i)
if (k & (1<<i))//CCC
node = obj->parent[node][i];
return node;
```
-Result
![](https://i.imgur.com/GTr7bDS.png)
:::success
2. 在 treeAncestorCreate 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析
:::
:::success
3. 在 [LeetCode 1483. Kth Ancestor of a Tree Node](https://leetcode.com/problems/kth-ancestor-of-a-tree-node/) 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者
:::
- Line 13-14: 修改之前發現建立表格 `parent` 時多出來的一個欄位,也就是移除計算 `max_level` 時多餘的部分
- Line 24-31: 之前在建立表格的最後一個步驟時,是每一個欄位都會更新一次,將其更改為,在更新每一個 node 的祖先欄位時,發現出現 -1 就跳出該個迴圈。
| Node | 2^0^ th ancestor | 2^1^ th ancestor | 2^2^ th ancestor |
| -------- | -------- | -------- | -------- |
| 0 | -1 | -1 | -1 |
| 1 | 0 | -1 | -1 |
| 2 | 0 | -1 | -1 |
| 3 | 1 | 0 | -1 |
| 4 | 1 | 0 | -1 |
| 5 | 2 | 0 | -1 |
| 6 | 2 | 0 | -1 |
```c=
#include <stdlib.h>
typedef struct {
int **parent;//AAA
int max_level;
} TreeAncestor;
TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize)
{
TreeAncestor *obj = malloc(sizeof(TreeAncestor));
obj->parent = malloc(n * sizeof(int *));
int max_level = 32 - __builtin_clz(n);
obj->max_level = max_level;
for (int i = 0; i < n; i++) {
obj->parent[i] = malloc(max_level * sizeof(int));
for (int j = 0; j < max_level; j++)
obj->parent[i][j] = -1;
}
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
for (int i = 0; i < parentSize; i++) {
for (int j = 1;j<max_level; j++) {
if(obj->parent[i][j + (-1)] == -1)
break;
else
obj->parent[i][j] = obj->parent[obj->parent[i][j - 1]][j - 1];
}
}
return obj;
}
int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)
{
int max_level = obj->max_level;
for (int i = 0; i < max_level && node != -1; ++i)
if (k & (1<<i))//CCC
node = obj->parent[node][i];
return node;
}
void treeAncestorFree(TreeAncestor *obj)
{
for (int i = 0; i < obj->max_level; i++)
free(obj->parent[i]);
free(obj->parent);
free(obj);
}
```
![](https://i.imgur.com/ogGmvYR.png)
## 測驗 3
考慮貌似簡單卻蘊含實作深度的 [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) 題目:
+ 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
+ 如果是 5 的倍數,印出 “Buzz”
+ 如果是 15 的倍數,印出 “FizzBuzz”
+ 如果都不是,就印出數字本身
直覺的實作程式碼如下: (`naive.c`)
```cpp
#include <stdio.h>
int main() {
for (unsigned int i = 1; i < 100; i++) {
if (i % 3 == 0) printf("Fizz");
if (i % 5 == 0) printf("Buzz");
if (i % 15 == 0) printf("FizzBuzz");
if ((i % 3) && (i % 5)) printf("%u", i);
printf("\n");
}
return 0;
}
```
利用 bitwise 和由 i 來控制字串起始和長度所實作的 FizzBuzz 程式碼: (`bitwise.c`)
```c=
#define MSG_LEN 8
static inline bool is_divisible(uint32_t n, uint64_t M) {
return n * M <= M - 1;
}
static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;
static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1;
int main(int argc, char *argv[]) {
for (size_t i = 1; i <= 100; i++) {
uint8_t div3 = is_divisible(i, M3);
uint8_t div5 = is_divisible(i, M5);
// 如果是3或5的倍數,要輸出的長度皆為4,如果是15的倍數,要輸出長度為8
unsigned int length = (2 << div3) << div5;
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> div5) >> (div3 << 2)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
- 根據是否為3, 5, 15的倍數,分別給予不同的起始 index 。
```c=18
strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length);
```
- 可以整理成以下的表格
| div3 | div5 |start |
| -------- | -------- | -------- |
| 0 | 0 | 8 |
| 0 | 1 | 4 |
| 1 | 0 | 0 |
| 1 | 1 | 0 |
- 從`8`開始的,表示既不是3的倍數,也不是5的倍數
- 從`4`開始的,表示只能是5的倍數
- 從`0`開始的,表示可能是15的倍數,也可能只是3的倍數,這邊要輸出甚麼樣的字串是由 Line 15 計算長度時所控制,所以把他當成同一種狀況就好。
- 回到程式碼,`KK1` 要填的就是 `div5` ,表示在只有能被5整除時,起始點為 `8>>1`(也就是4)
- 而後面的 `(KK2 << KK3)` 則需要考慮,如果能被3整除時要讓起始點變成0,反之要能讓起始點保持在8,當我們填 `(div3 << 2)` 就滿足了以上的考慮。
:::success
1. 解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差
- 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
:::
- 實驗設計: i 從 1 到 100,將每次執行的時間相加後,最後除以100
- 使用 printf 來做實驗 (**加速 1.21x**)
| Naive | Bitwise |
| -------- | -------- |
| 10601 ns | 8769 ns |
- 使用 sprintf 來做實驗 (**加速 1.69x**)
| Naive | Bitwise |
| -------- | -------- |
| 584 ns | 345 ns |
可以見得,做 printf 會比 sprintf 耗時,會消耗掉改成 bitwise 操作所加速的時間。
## 測驗 4
測驗4介紹了一個 GCC 的擴充函式 `int __builtin_ffs (int x)` ,說明如下:
>摘自 [Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
>Built-in Function: int __builtin_ffs (int x)
Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero.
也就是說這個函式返回的值是最低位 set bit 的 index 值再加上1
> E.g.
> $6_{10} = 110_2$
> $\_\_builtin\_ffs (6) = 2(index\ of\ the\ least\ significant\ set\ bit = 1)$
回到題目,這題是在事先知道除數 (PAGE_QUEUES) 有哪些的情況下,要將原本的整數除法修改成兩步驟:
1. 先除以除數中的因數:2^n^ (使用 right shift 來做)
2. 再除以除數中的剩餘因數
PAGE_QUEUES 如下:
- (1 or 2 or 3 or 4)
- (5 or 6 or 7 or 8) × (2^n^), n from 1 to 14
給定 `size_t offset`,試著將原本運算:
```cpp
#include <stdint.h>
size_t blockidx = offset / PAGE_QUEUES;
```
更改成
```c=
#include <stdint.h>
#define ffs32(x) ((__builtin_ffs(x)) - 1)
size_t blockidx;
size_t divident = PAGE_QUEUES;
blockidx = offset >> ffs32(divident);
divident >>= ffs32(divident);
switch (divident) {
case 3: blockidx /= 3;
break;
case 5: blockidx /= 5;
break;
case 7: blockidx /= 7;
break;
}
```
前面有敘述過 `int __builtin_ffs (int x)` 返回值,這邊的 `ffs32(x)` 做的就是計算最低位 set bit 的 index ,**代表著這個數 x 有 2^ffs32(x)^ 這個因數**。
```c=2
#define ffs32(x) ((__builtin_ffs(x)) - 1)
```
在看這兩行之前,我們先觀察上述 PAGE_QUEUES 的數值:
- (1 or 2 or 3 or 4)
- (5 or 6 or 7 or 8) × (2^n^), n from 1 to 14
我們將其做整理:
| Original | After |
| -------- | -------- |
| 1 | 2^0^ |
| 2 | 2^1^ |
| 3 | 3 x 2^1^ |
| 4 | 2^2^ |
| 5 x 2^n^ n from 1 to 14 | 5 x 2^n^ n from 1 to 14 |
| 6 x 2^n^ n from 1 to 14 | 3 x 2^n+1^ n from 1 to 14 |
| 7 x 2^n^ n from 1 to 14 | 7 x 2^n^ n from 1 to 14 |
| 8 x 2^n^ n from 1 to 14 | 2^n+3^ n from 1 to 14 |
而這是我們要計算的值:
$\dfrac{offset}{PAGE\_QUEUES}$
我們可以同時對分子分母除以2^n^(n = ffs32(divident)),也就是 Line 5-6 在做的事情
```c=5
blockidx = offset >> ffs32(divident);
divident >>= ffs32(divident);
```
而根據上面表格,當我們做完 分子分母同時 right shift `ffs32(divident)`之後,會有4種情況:
1. divident = 1
2. divident = 3
3. divident = 5
4. divident = 7
所以我們要接著再做除數為 `3`, `5`, `7`的除法(`1`就不用繼續做了)
```c=7
switch (divident) {
case 3: blockidx /= 3;
break;
case 5: blockidx /= 5;
break;
case 7: blockidx /= 7;
break;
}
```