contributed by < kkkkk1109
>
1
測驗一主要為使用 bitwise
的方式來進行平方根的計算,以下為版本一
msb
的部分是用來計算 N
是由幾個 bit 所組成的,以 N
= 10 為例
使用 (int)
強制轉成整數
接著將 1 左移 3 位,得到 8。
以 result + a
的平方檢查目前是否小於 N,若小於,則代表此數可以構成平方根,並不斷將 a 右移以檢查下個 bit,直到 a 為零。
版本二為不需要使用 log
的計算,利用不斷右移的方式,計算 bit 個數。
版本三是使用 Digit-by-digit calculation的方式進行,其說明如下
若要求得 x
的平方根,可以將 x
表示成以下二進位的形式
也可以表示成2的冪相加
以 x = 36
舉例,
也可以表示為
此方法是將 展開,並從係數來回推。
另
平方為
其中 為
因此,當 時即為所求
且
只要從 試到 ,且滿足 ,即可求出平方根。
因此可以從 開始,持續加上 ,並於每次加上後檢查是否滿足 即可。
為了減少運算平方的成本,可以改成以下方式進行
以 36
舉例
第一輪:
第二輪
此輪結束時,差值,因此無須進行下一輪。不過在計算 時,仍需要計算平方,因此可再將 分解
令
可以求得
以上情況為時,若 ,則
這樣就可以使用移位的方式來迭代,不需要使用到平方
首先,檢查是否有平方根,由於此題輸入輸出為整數,因此只需檢查是否小於等於 1
由於 在迭代到最後會為 0,因此我們可以以 來判斷是否迭代結束
以 m
為 ,可利用 31 - CLZ
來獲得位數,再進行移位得到 。而每次迭代,,因此要右移兩位
以 z
為 , b
為 , x
為
b = z + m
求得 ,z >> 1
對 進行迭代,並以 (x >= b)
檢查 是否為 0。
迭代最後,會發現是平方根為 z
,解釋如下
這時會發現 就是 ,對應到最後一次迭代會發現
z
會右移,代表 ,又 z += m
代表加上了 ,即為所求 。
使用第 2 週測驗題-測驗三 中的ffs
可以取代 GNU_extension
中的 __builtin_clz
。
我使用了測驗三中的 ffs
和 clear_bit
來取代利用CLZ來找出最高位。
2
此題為進行 mod 10 和 div 10操作時,要如何降低運算成本
由於 ,使用 bitwise 進行除法時,不能夠完全整除。因此要找到一個除數 x
,求出來的結果會和以 10 為除數相同。
閱讀《Hacker's Delight》,提到可以以找出乘上倒數的方式來進行除法,如
以 3
做舉例
我們可以將被除數透過位移的方式來完成除法
但要注意的是,每次右移都有可能造成誤差,會直接忽略低位的位元,由於上述程式碼右移了 5 次,因此餘數 r
會介在 之間,其中 3 為除數。參考同學 vax-r
所做的實驗,記錄了最大餘數的變化
以 hexa
的形式來看
可以發現確實會隨著數字的增加,最大餘數會持續增加。
以數字 c9c6c9cf
來查看每次右移的結果
可以發現剛好右移的位數都是 1
,右移 4 ,0~3 bit 為 1;右移 8 ,0~7 bit 為 1,因此造成了誤差 15,也如同文章所做的實驗
Thus, using (33), for which q is too low by at most 5, we expect the remainder to be at most 5*3 + 2 = 17. Experiment reveals that it is actually at most 15
回到測驗題中,說明以下程式碼中的餘數是介在 0
~ 19
然而,觀察上面的程式碼後可以發現, tmp 不可能會大於 19 ,因此只需要考慮 19~0 的情況即可。
剛開始不太懂為何是 0 ~ 19,猜測是因為 carry
所造成的誤差最大就是 1,而餘數最大值為 9,因此最大可能餘數為
再以 Hacker's Delight
中的 divu10
測試,最大餘數也會到達 13
。
若我們想將控制精度在小數點後第一位,則可以表示為
接著找 x
的範圍,可以找到
接下來就可以透過以下方式實作
d0
、d1
、d2
是用來儲存因為右移而消失的位數,透過右移的方式來進行除以 13
,再左移來完成乘上 128
,最後再將 tmp
- 10q
。
包裝成以下方式
此方法為改成 的方式來運算。為了得到0.8,會先近似於0.75,再慢慢逼近到 0.8 最後再除上 8。
將100000000
作為輸入,並查看每次運算的結果
由於 0.8
在 32位元表示是 0.11001100110011001100110011001100
,可以先產生前面的1100
11
以下可以先產生 0.110011
再進行 4 次的位移和加法
最後,因為 q
目前是 ,右移二來除上 8 得到商 ,而餘數則是利用 來取得, q & ~0x7
是為了確保加上 div << 1
不會進位。
3
此題為計算以 2 為底的對數,且輸入和輸出均為整數。
版本一
逐位檢查是否為 1 ,來計算對數
版本二
括號中的數值為,此方法為一次大範圍的計算對數,比版本一逐位計算,此方法減少了計算成本。
版本三
利用 CLZ
來求得此數的前導零數量,以 0x00020100
舉例, CLZ
為 12,可快速透過 來得到對數。
同學 MathewSu-001 提醒了我,由於 gcc 中有說明道,若輸入為 0 ,會造成 undefined result,因此要加上 v | 1
來避免此行為發生。
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
4
Exponential moving average是種取平均的統計手法,並且使經過時間越久的歷史資料的權重也會越低,以下為 EWMA 的數學定義:
將其展開來如下
隨著時間 越來越長,越久以前的資料權重也會越低。
首先,EWMA的結構由三個參數組成,分別是 internal
, factor
和 weight
。 internal
是最後取的平均值,對應到公式中的 , factor
為 scaling factor,weight
即為加權重量。
我們可以藉由 ewma_init
來進行初始化,並檢查其中輸入的 factor
和 weight
是否為 2 的對數,接下來將他們的對數值存入factor
和 weight
中。
接著,可以使用 ewma_add
來增加樣本。
加入新樣本的公式為
對應到程式中的 internal
, 對應到 weight
, 對應到 val
。整理公式可以變成
即對應到程式中,當 avg->internal
存在時的敘述。
公式中的 介在 0 ~ 1 之間的數,但在此 weight
為大於零的 2 的冪,因此公式中的 原本進行乘法和除法,會在程式中相反。如 在程式中卻是進行左移,而括號外的乘上 卻變成了右移。
5
此測驗是延伸測驗 3
,已知輸入必為大於 0 的數值 (即 x
> 0),以下程式碼可計算 ,也就是 ceil 和 log2 的組合並轉型為整數:
以下為程式碼
首先,由於對數會比位數少一位,如 32 = 0b100000
位數是6,但對數是 5,因此要先減 1,以完成後續計算
在不斷的比較中, r
來儲存所有比較的結果, shift
儲存當前比對結果,利用 r |= shift
來紀錄每次的結果
在比較到最後,可以發現 x
最後一位沒有進行比較,且最後一次的 shift
沒有被儲存到 r
中, 直接在 return
中完成上述操作。
這邊注意到,最後再return 又再加上了 1 ,而若沒有第一步的 x--
,則會結果少一,試著改寫成以下方式
兩者測試發現,只有在 x
為 2的冪時才會相同,才想到此題還有 ceil
,才會造成兩者結果不一樣。
1
popcount可以用來計算二進位的表示式中,有多少位元為1,如 0b10101010
的 popcount 為 4。
popcount_naive
每次將 v
的 LSB 位設為零,直到 v
為零,使用迴圈計算 1 的個數
對於 32 bit 的無號整數,popcount的公式推導如下
以 x
= 0b101011
舉例
計算到最後可以得到 4。
假設 ,以 4 個位元 來看,其 popcount為
整理一下
最後公式可以整理成
popcount_naive
使用迴圈的方式來進行,而我們可以實作常數時間複雜度的做法 popcount_branchless
此方法是將 4 個位元(nibble) 為一個單位計算,因此只要使用計算即可
首先,相當於做 ,右移是為了表示 ,而 0x77777777
作為遮罩,完成一個 nibble 為單位計算
重複上述動作,完成
接著,將每組所算出的 popcount 加起來
以 代表第 n 個 nibble 中的數值
加總後為
使用 mask 0x0F0F0F0F
最後乘上 0x01010101
,右移 24 就會是最終的答案
對於要計算兩數的 Hamming distance,將兩數做 xor
再取 popcount 即可獲得。
針對 LeetCode 477. Total Hamming Distance,考慮以下程式碼:
因為此方法的 i
和 j
都是由 0
~ numsize-1
,因此會計算到重覆的 hamming distance,因此最後 totla
要右移一位。
此方法的時間複雜度為,我們可以改寫 j = i
做內部迴圈,但時間複雜度仍然是
換個角度思考,從位元的角度來看
n 'th bit | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|
input 7 | 0 | 1 | 1 | 1 | 1 |
input 5 | 0 | 0 | 1 | 0 | 1 |
input 10 | 0 | 1 | 0 | 0 | 1 |
input 17 | 1 | 0 | 0 | 0 | 1 |
0
位: 全 1 ,hamming distance = 01
位: 一個 1 ,hamming distance = 1 * numsize-1
2
位: 兩個 1 ,hamming distance = 2 * numsize-2
我們可以直接在位元內,計算 1 的個數 n
,hamming distance 為 n
* numsize - n
,再逐個位元加起來。時間複雜度縮短到
2
若要不使用除法,就獲得某數除以另一數的餘數,可以使用以下方法,只要除數滿足 。以除數為 3 做舉例, ,
而考慮某數 n
,以二進位表示式 ,,我們可以拆成位元來獲得其餘數,舉例,以 11
= 0b1011
,餘數為 。
因此我們可以利用 n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
,計算偶數位和奇數位的 popcount
,來求出餘數。可以透過以下方式化簡
因此 n = popcount(n & 0x55555555) - popcount(n & 0xAAAAAAAA)
可以寫成 n = popcount(n ^ 0xAAAAAAAA) - 16
,不過為了避免出現負數的情況,因此加上了 3 的倍數 39,再進行計算。
第一次的結果,n
會落在 23 ~ 55 之間,第二次會落在 -3 ~ 2 之間,最後查看是否為負數,若為負數,則再加上 3
。
這邊要注意,在 return 時, 右移運算的 n
需加上 (int),unsigned n 右移的話會是補 0,signed 右移才會補上 1,做 &
運算才能得到 3
。
也可以使用 lookup table
來完成
原先n = popcount(n ^ 0xAAAAAAAA) - 16
的範圍會介在 -16 ~ 16 之間,而這邊等同於直接加上了 16,會讓範圍介在 0~32之間,因此 lookup table 需要 33 個空間,而可以對應到 ,因此 table[0]
會從 2 開始,, table[1]
為 0,table[2]
為 1 … table[32]
為 1。
在Hacker's Delight 中提到,若不使用 popcount
,也可以使用除數轉換的方式來求得餘數
證明如下:
令 , 和 都是整數,且 ,則當 為 0、1、2 時都成立。
並且,由於 ,可以直接從二進位的方式來判斷餘數,也就是 00
~ 11
,剛好對到十進位 0
~ 3
,我們可以以 2 個 bit 為一組來計算;另一個概念是,將原本的數字乘上 ( 為偶數),所求得的餘數不會變,因為 ,因此我們可以將原先的數字分成許多組來相加計算,並且不會影響除數結果,舉例如下
,,把 分成, 來求得餘數,也就是將 N >> 6
+ (N >> 4)& 0b11
+ (N >> 2) & 0b11
+ N & 0b11
,因為 ,,,因此可以透過移位相加來計算餘數。
同理,3
個 bit 的話,可以表示 0~7 的餘數,
且 ,乘上 8 的倍數就不會改變餘數,看 tictactoe.c 中的 mod7
,就是使用 來求得。
接下來解釋 tictactoe.c
首先,move_mask
有九步,對應到棋盤上的九宮格。
可以看出,分別對應到 8 種獲勝方式,3 個 row
、 3 個 column
和 2 個 diagonal
。因此,對於是否獲勝,只要看 8 個 nibble
是否有達到 0111
,換句話說,加上 0x11111111
檢查各個 nibble 的最高位是否為 1。
3
AVL tree 是一種 Binary Search Tree,藉由計算每個節點的高度,並將節點的子節點的高度相減,以判斷是否平衡;若不平衡,則根據目前的情況,進行對應的 rotation 來達到平衡。
LL
型
RR
型
LR
型
RL
型
rbtree 也是一種二元搜索樹,而其遵循以下四個性質
在新增的過程中,若要進行新增或刪除,也是按照前述的 right rotation 和 left rotation 之外,還要調整節點的顏色。
AVL tree 在一般搜尋情況下會比還要快,但在新增和刪減的情況下,rb tree 所需要進行的 rotate 比較少。
對於一個兼具AVL tree 和 rb tree 的 XTree,著重在快速的 insert/delete 的操作和合理的 look up 速度。XTree 的平衡機制與 AVL tree 相似,利用 hint
來評估是否需要做 rotate,然而 xt_node 的 hint
的計算方式是,其左右節點 hint
最大值 +1 並且只有在被呼叫到 xt_update
時才會被更新。
接著對 XTree 的程式碼 做解釋:
XTree 繼承了 AVL Tree 和 rb tree 的優點,並有著以下四種操作 xt_rotate_left
、 xt_rotate_right
、 xt_replace_right
和 xt_replace_left
。
xt_rotate_left
和 xt_rotate_right
主要用於平衡二元樹的部分,因此在插入或刪除時都會使用;而 xt_replace_right
和 xt_replace_left
主要用在刪除的部分,會將需要被刪除的節點的左右子樹中,找到替代的節點。
而 xt_update
則判斷目前 XTree 中是否平衡,旋轉後會持續往親代進行 update。
於 xtree.h
中:
節點 xt_node
包含了計算其高度的 hint
、親代 parent
和左右節點 left
、right
。
整顆二元樹則是包含了根節點 root
、 用於比較鍵值的 cmp
、 包含鍵值的初始節點 create_node
和函式 destroy_node
指向被刪除的節點。
接著可以看 xtree.c 中的各個函式
xt_create
: 用於創建一棵 XTree
xt_insert
: 用於新增節點,由 __xt_find
由 key
值比對該插入到哪個位置。接著將 create_node
的鍵值設為 key
,判斷 root
是否存在,成立,則呼叫 __xt_insert
將節點 n
放置 p
的左或右子代,並進行 xt_update
來平衡二元樹。
xt_update
: 先呼叫 xt_balance
判斷目前是否平衡,並進行 rotation
平衡後,再呼叫 xt_max_hint
來儲存平衡後的 hint
,再對 parent
做平衡。
xt_remove
用於刪除節點,先輸入鍵值,並查詢是否存在,接著再呼叫 __xt_remove
來進行刪除。
在 __xt_remove
中,若此刪除節點有右子樹,則會將右子樹中的最小值進行替換
若沒有右子樹,則找出左子樹的最大值進行替換
再進行 xt_update
來完成平衡。