owned this note
owned this note
Published
Linked with GitHub
# 2022q1 Homework2 (quiz2)
contributed by < [haoyu0970624763](https://github.com/haoyu0970624763) >
### 測驗1
避免overflow 的初始解法
```c
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
缺點: 限制 input 的輸入 , low 的值必定要小於 high 才可以正常運行
改進解法一 : 使 input順序可以不受限制
```c
uint32_t average(uint32_t a, uint32_t b) {
// input scan order doesn't limit
return (a >> 1) + (b >> 1) + (a & b & 0x01);
}
```
思路: 兩數相加取平均有3種 case
* 奇數+奇數
* 奇數+偶數(不考慮順序)
* 偶數+偶數
**case1:**
右移1位元會捨棄原本數字的 LSB , 若兩數都為奇數 , 則會捨棄 2 個 LSB , 造成結果少2 , 取平均答案則少1。
所以要把答案+1
例如: 3+5 取平均為 4 , 把兩數先右移1位是 1+2 結果為3 再加1
**case2:**
會捨棄一個 LSB , 不影響答案
**case3:**
兩數 >> 1 相加即為答案
所以 EXP1 可推得要處理兩數皆為奇數的情況 EXP1 => a&b&1
改進解法二:
參考 [你所不知道的 C 語言:數值系統篇 當中的 運用 bit-wise operator 部份](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator)
先用1位元加法器的概念下去想像 a+b
設 a 的 LSB=x , b 的 LSB=y
* x=0 , y=0 相加為 0
* x=0 , y=1 相加為 1
* x=1 , y=0 相加為 1
* x=1 , y=1 相加為 0 , 進位值=1
可看出一位元加法不考慮進位之結果為 x ^ y
推廣至 n bit來看 , 不考慮每一個位元間的進位值相加結果為`a^b`
一位元進位結果是 (x&y) << 1 , 推廣至 n 位則是 (a&b) << 1
所以 a + b = a ^ b + ( a & b ) << 1
(a+b) / 2 = ((a ^ b) >> 1) + (a & b)
```c
uint32_t average2(uint32_t a, uint32_t b) {
// input scan order doesn't limit
return (a & b ) + ((a ^ b) >> 1);
}
```
將第一個寫出來的 code 開啟最佳化進行編譯
使用 `gcc -O3 -S average.c` 指令
會生成 average.s (組合語言的檔案) , 組合語言中的函式如下
```clike
average:
.LFB39:
.cfi_startproc
endbr64
movl %edi, %eax
movl %esi, %edx
andl %esi, %edi
shrl %eax
shrl %edx
andl $1, %edi
addl %edx, %eax
addl %edi, %eax
ret
.cfi_endproc
```
上面為 (a >> 1) + (b >> 1) + (a & b & 0x01) 的組合語言
使用 `gcc -O3 -S average2.c` 指令
會生成 average2.s (組合語言的檔案) , 組合語言中的函式如下
```clike
average2:
.LFB39:
.cfi_startproc
endbr64
movl %edi, %eax
andl %esi, %edi
xorl %esi, %eax
shrl %eax
addl %edi, %eax
ret
.cfi_endproc
```
上面為 (a & b) + ((a ^ b) >> 1) 的組合語言
:::info
暫時還看不懂 之後研讀
:::
### 測驗2
先從 <[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation)〉一文的「不需要分支的設計」一節提供的程式碼 min 開始解讀
```c
int32_t min(int32_t a, int32_t b) {
int32_t diff = (a - b);
return b + (diff & (diff >> 31));
}
```
倘若輸入數值的資料型態是 32 位元有號整數時,向右位移採用算數位移
此題只會有 2 種情況
* a - b >= 0 , MSB=0 且 a 是較大的值 , diff >> 31 = 0
所以 b + (diff & (diff >> 31)) => b + (diff&0) => b
* a - b < 0 , MSB = 1 且 b 是較大的值 , diff >> 31 = -1 (bit 全為1) 所以 , b + (diff & (diff >> 31)) => b+(diff & 0xFFFFFFFF)
=> b+diff => b+(a-b) => a
解析下面的程式
```c
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
跟上面那題同個邏輯 , 此題會有兩種 case
* a - b >= 0 也就是 a >= b
當 a ^ ((EXP4) & -(EXP5)) 為 a ^ ((EXP4) & -(0)) 時
=> a ^ 0 = a
由此判斷 EXP5 為使 a >= b 為 非的敘述
所以 `EXP5是 a < b`
* a - b < 0 也就是 a < b
把上面判斷的EXP5 納入考量 , 則
a ^ ((EXP4) & -(a<b)) 為 a ^ ((EXP4) & -1)
=> a ^ ((EXP4) & 0xFFFFFFFF ) => a ^ (EXP4)
a ^ (EXP4) 要使答案為 b 所以 `EXP4 為 a ^ b `
```c
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
上述的推導邏輯是從有號數取min值的想法類推而來
如果要適用於有號數應改成下列程式才更為正確
```c
int32_t max(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
不然在 一負數與一正數相比的情況 , 用無號數當資料型態會有問題。
例如: 輸入 -1 時 , bits 為 0xFFFFFFFF , 若用無號數的比大小 ,
會發生負數比正數大的情況
無號數取 max 值 就適用於下列程式
```c
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
### 測驗3
```c
uint64_t gcd64(uint64_t u, uint64_t v) {
/* if u or v equal zero , then return the number which is not equal to zero
*/
if (!u || !v)
return u | v;
/* shift is used to record common factor of 2^k */
int shift;
/* if u or v is odd number , then out of the loop*/
for (shift = 0; !((u | v) & 1); shift++) {
u >>= 1, v >>= 1;
}
// 更相減損法
while (!(u & 1))
u >>= 1;
do {
while (!(v & 1))
v >>= 1;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
```
**程式解析:**
```c
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
在 2 進位表示法中可以簡單快速的判斷兩數字是否可被 $2^k$ 整除,所以可以先把 $2^k$ 的公因數提出來再做輾轉相除法。
用`shift`變數紀錄 k 為多少,之後再把算出來之結果乘回原先提出的公因數
透過類似於輾轉相除法的求 GCD 方法:**更相減損法**
主要分成 3 種 case , 且因為我們先把 2 的公因數提出 , 所以不會有皆為偶數的情況
* x 為偶數,y 為奇數
$gcd(x, y) = gcd(x/2, y)$
* x 為奇數,y 為偶數
$gcd(x, y) = gcd(x, y/2)$
* x 為奇數,y 為奇數
$gcd(x, y) = gcd(x - y, y)=gcd((x - y)/2), y)$
因為 x-y 必為偶(2奇數相減) , 回到 case1 , 可再直接化簡
下面即為更相減損法之實做
```c
while (!(u & 1))
u /= 2;
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
```
最後答案再回傳 $u*2^k$ 也就是 $u << shift$
leetCode相關題 [Water and Jug Problem](https://leetcode.com/problems/water-and-jug-problem/)
使用 `__builtin_ctz` 改寫 gcd
運用到 **測驗2** 同樣邏輯的 min
先找出 2 數分別 right most zero 個數
在 rightmost zero 個數數量很多時 , 法二的效率較好
因為法一的 loop 數要跑很多次
並且法二的實做方法是 deterministic
```c
法一:
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
等價於
法二:
int shift_u = __builtin_ctz(u);
int shift_v = __builtin_ctz(v);
int shift = min(shift_u, shift_v);
u >> shift;
v >> shift;
```
```c
uint64_t min(int64_t a, int64_t b) { return a ^ ((a ^ b) & -(a >= b)); }
uint64_t gcd64(uint64_t u, uint64_t v) {
/* if u or v equal zero , then return the number which is not equal to zero
*/
if (!u || !v)
return u | v;
/*
__builtin_ctz is used to count the rightmost zero number
shift is used to record common factor of 2^k which means the smaller __builtin_ctz
*/
int shift_u = __builtin_ctz(u);
int shift_v = __builtin_ctz(v);
int shift = min(shift_u, shift_v);
u >> shift;
v >> shift;
// 更相減損法
if (!(u & 1))
u >>= __builtin_ctz(u);
do {
if (!(v & 1))
v >>= __builtin_ctz(v);
if (u < v) {
v -= u;
} else {
uint32_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
#### Linux kernel 中的 gcd 實做
```c
unsigned long gcd(unsigned long u, unsigned long v) {
unsigned long r = u | v;
/* if a = 0 or b = 0 , then return a | b
* which means return the number is not zero
*/
if (!u || !v)
return r;
v >>= __ffs(v);
if (v == 1)
return r & -r;
for (;;) {
/*check whether u is the form of 2^k
* if so , just return r & -r , like the code above it*/
u >>= __ffs(u);
if (u == 1)
return r & -r;
/* 如果提出 2的冪次方之後的 u 等於 提出 2的冪次方之後的 v
* 則表示他們的最大公因數為
* 提出之後的結果 乘上 兩數分別提出的冪次方中較小的2的冪次方
* 即 v << __ffs(r) 要寫成 u << __ffs(r) 也行
* */
if (u == v)
return v << __ffs(r);
/* 確保 u >= v , 使 u-=b 成立
* 且運用 gcd(x, y) = gcd(x - y, y)
* */
if (u < v)
swap(u, v);
u -= v;
}
}
```
**要先知道 __ffs(b) 等價於 __builtin_ctz(b)**
**r & -r** 解析 :
簡單假設 r 的 bit 形式為 00010010 ,
那麼 -r 就是 11101101再加1(取 not 再+1) 所以是 11101110
00010010 & 11101110 為 00000010 也就是取出最低位非0 的數字
r 又等於 a | b , 所以 r & -r 表示 取出 a | b 最低位非 0 的數
```c
int shift_u = __builtin_ctz(u);
int shift_v = __builtin_ctz(v);
int shift = min(shift_u, shift_v);
u >> shift;
v >> shift;
```
上面這段程式跟下面這段 linux kernel 意思有一些接近
```c
v >>= __ffs(v);
if (v == 1)
return r & -r;
```
如果用上面的 code 去理解 linux kernel 中的 code
可理解成我們先算出 v 的 rightmost zero number , 然後直接假設他是 u 跟 v 當中較小的 __ffs 並進行 shift
也就是先提出 $2^k$ 的概念
如果提出 $2^k$ , v 等於 1 的話 代表 v 的值就是 $2^k$
若 v 等於 $2^k$ , 也就是 bits 的形式 有唯一一個bit等於1
這時候 u 跟 v 的分佈會有以下情況
* u 比 v 小的情況
* u 為偶數 , 此時 u 跟 v 的最大公因數即取出 u | v 中 非0 的最低位數字的 bit 形式。
例如: v = 32 = $2^5$ = u = 6 , 則 u | v = 00100110
取出 u | v 中 非0 的最低位數字的 bit 形式為 00000010
最大公因數為 2
* u 為奇數 , 此時最大公因數必為 1 , 取出 u | v 中 非0 的最低位數字的 bit 形式 也會是 1
* u 比 v 大的情況
* u 為 偶數 , 此時 u 跟 v 的最大公因數即取出 u | v 中 非0 的最低位數字的 bit 形式。
例如: v = 32 = $2^5$ = u = 68 , 則 u | v = 01100100
取出 u | v 中 非0 的最低位數字的 bit 形式為 00000100
最大公因數為 4
* u 為奇數 , 此時最大公因數必為 1 , 取出 u | v 中 非0 的最低位數字的 bit 形式 也會是 1
我們可以發現 若 v 為 $2^k$ 的形式 , 不論 u 的值為多少
最大公因數皆為 `r & -r`
若 v 不為 $2^k$ 的形式 , 則繼續底下的 code
```c
for (;;) {
/*check whether u is the form of 2^k
* if so , just return r & -r , like the code above it*/
u >>= __ffs(u);
if (u == 1)
return r & -r;
/* 如果提出 2的冪次方之後的 u 等於 提出 2的冪次方之後的 v
* 則表示他們的最大公因數為
* 提出之後的結果 乘上 兩數分別提出的冪次方中較小的2的冪次方
* 即 v << __ffs(r) 要寫成 u << __ffs(r) 也行
* */
if (u == v)
return v << __ffs(r);
/* 確保 u >= v , 使 u-=b 成立
* 且運用 gcd(x, y) = gcd(x - y, y)
* */
if (u < v)
swap(u, v);
u -= v;
}
```
### 測驗4
先從函數的原始版本開始解析:
```c
#include <stddef.h>
#include <stdint.h>
size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) {
size_t pos = 0;
for (size_t k = 0; k < bitmapsize; ++k) {
/* bitset 的 value等於原始陣列的第 k 個元素的 value
* p 用來紀錄原始陣列的第 k 個元素 對應到 out 陣列的初始位置
*/
uint64_t bitset = bitmap[k];
size_t p = k * 64;
/* 檢查原始陣列的第 k 個元素的 bit 形式(64位元)
* 紀錄 第 k 個元素的 bit 形式當中 總共有幾個 bit 為 1
* pos 用來紀錄 bitmap 的每一個數值的二進位形式中 1 的總數量
* 將 out 陣列的值 透過函數的轉換即可知道當初在原始陣列中的值的哪個 bit 為
* 1
* */
for (int i = 0; i < 64; i++) {
if ((bitset >> i) & 0x1)
out[pos++] = p + i;
}
}
return pos;
}
```
本函數的目標是: 紀錄 bitmap 數值的2進位形式 為 1 的位元編號於 out 陣列 並回傳 bit 為 1 的總數量 (也就是 out 陣列的大小)
**優化版**
```c
#include <stddef.h>
#include <stdint.h>
size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
uint64_t bitset;
for (size_t k = 0; k < bitmapsize; ++k) {
bitset = bitmap[k];
while (bitset != 0) {
uint64_t t = bitset & -bitset;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
改寫前的程式碼中是對於 bitmap 中的每一個值的每一個 bit (64位元) 所以一個值要跑 64 次的 loop
然而其實不用針對每一個 bit 都去跑一次 loop
改寫後的版本是透過 `t = bitset & -bitset`
把 bitset 中 rightmost 的 1 保存起來 , 在測驗三當中已經說明過了
透過`r = __builtin_ctzll(bitset)` 去看是在 64位元中的哪個位置
並將其紀錄在 out 陣列
最後對 bitset 做 XOR 會把 剛剛保存的 `1` 變成 `0` ,其餘的部份則會複製原本的 bitset
這樣可以減少迴圈跑的次數。
**此 code 為 漢明權重(又稱漢明重量)** , 跟 bit 全為 0 之間的 漢明距離 , 在密碼學當中有廣泛的運用
### 測驗5
```c
#include "list.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct rem_node {
int key;
int index;
struct list_head link;
};
static int find(struct list_head *heads, int size, int key) {
struct rem_node *node;
int hash = key % size;
list_for_each_entry(node, &heads[hash], link) {
if (key == node->key)
return node->index;
}
return -1;
}
char *fractionToDecimal(int numerator, int denominator) {
int size = 1024;
char *result = malloc(size);
char *p = result;
// 如果分母等於 0 , 則字串馬上接終止符號
if (denominator == 0) {
result[0] = '\0';
return result;
}
// 如果分子等於 0 , 字串為0 然後馬上接終止符號
if (numerator == 0) {
result[0] = '0';
result[1] = '\0';
return result;
}
/* using long long type make sure there has no integer overflow */
long long n = numerator;
long long d = denominator;
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
bool sign = (float)numerator / denominator >= 0;
// set negative sign
if (!sign)
*p++ = '-';
long long remainder = n % d;
long long division = n / d;
/* 原本是 sprintf(p, "%ld", division > 0 ? (long)division : (long)-division);
* 但因為 n 跟 d 我們在上面處理過了有負號的情況
* 所以這邊不需要再判斷正負號
* */
sprintf(p, "%ld", (long)division );
if (remainder == 0)
return result;
p = result + strlen(result);
*p++ = '.';
/* Using a map to record all of reminders and their position.
* if the reminder appeared before, which means the repeated loop begin,
*/
char *decimal = malloc(size);
memset(decimal, 0, size);
char *q = decimal;
size = 1333;
struct list_head *heads = malloc(size * sizeof(*heads));
for (int i = 0; i < size; i++)
INIT_LIST_HEAD(&heads[i]);
for (int i = 0; remainder; i++) {
int pos = find(heads, size, remainder);
if (pos >= 0) {
while (pos-- > 0)
*p++ = *decimal++;
*p++ = '(';
while (*decimal != '\0')
*p++ = *decimal++;
*p++ = ')';
*p = '\0';
return result;
}
struct rem_node *node = malloc(sizeof(*node));
node->key = remainder;
node->index = i;
list_add(&node->link, &heads[remainder % size]);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
}
strcpy(p, decimal);
return result;
}
```