contributed by < StevenChou499
>
1
考慮以下對二個無號整數取平均值的程式碼:
我們再次改寫為以下等價的實作:
題目中的第一題因為想到要做平均,所以應該要用 >>
來做除以二的動作。但是因為再二進位制中,若兩數的同一位皆為 1
則需要進位,而只有 &
可以提取出兩數同時出現的 1
,因此 EXP1
應該是 a & b
才對。
而下一題的在再次改寫,因為後面有一個 >> 1
,應該是除以二的結果,而如果 a
與 b
皆需要除以二,應該以 ^
的方式提取兩者各單獨擁有的位元,再使用 &
來找出兩者共同擁有的位元,而因為\兩者皆擁有此位元,所以不需要進行除以二的動作。所以 EXP2
為 a & b
,且 EXP3
為 a ^ b
。
EXP1 : a & b
EXP2 : a & b
EXP3 : a ^ b
比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
2
改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
因為答案需要回傳 a
或是 b
,所以首先想到的是若要回傳 a
則需要利用 ^
的特性,也就是任何數值在與 0
做 EXR
後,還會是自己本身。因此我們可以知道 ((EXP4) & -(EXP5))
等於 0
。而若是需要回傳 b
,則可以想到 b
在與 a
做兩次 ^
後又會回到 b
,因此若需要回傳 b
,則 ((EXP4) & -(EXP5))
等於 a ^ b
,以下為兩種結果的表格:
情況 | a > b (回傳a) | a < b (回傳b) |
---|---|---|
((EXP4) & -(EXP5)) | 0 | a ^ b |
而因為需要回傳 0
或是 a ^ b
且中間還需要做 &
的操作,因此 (EXP4)
應該要等於 a ^ b
,且 -(EXP5)
在 a > b
時應該要等於 0x0
,在 a < b
時應該要等於 0xFFFFFFFF
,也就是 -1
。依據這樣的條件,在加上去除加上負號後的轉換, (EXP5)
應該要是 0
或 1
,因此 (EXP5)
應該為 (a < b)
,此時若 a > b
,會回傳 0
,加上負號後為 0
,與 a ^ b
做 ^
後為 0
,而 a ^ 0
等於 a
,就會回傳 a
。若 a < b
,會回傳 1
,加上負號後為 0xFFFFFFFF
,與 a ^ b
做 ^
後為 a ^ b
,最後則會回傳 b
,以下為詳細講述計算過程的表格:
情況 | a > b (回傳a) | a < b (回傳b) |
---|---|---|
a < b | 0 | 1 |
-(a < b) | 0x0 | 0xFFFFFFFF |
a ^ b | a ^ b | a ^ b |
(a ^ b) & -(a < b) | 0 | a ^ b |
a ^ (a ^ b) & -(a < b) | a | b |
因此我們可以得知 EXP4
等於 a ^ b
, EXP5
等於 a < b
。
EXP4 : a ^ b
EXP5 : a < b
3
考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式:
因為是要求兩數之最大公因數,因此前面的程式碼:
此階段為利用 shift
紀錄下總共有幾次方的 2
兩數可以進行整除,
接著下方的程式碼則是將剩下無法成為公因數的 2
去除。
而最後一段的 do while
迴圈則可以看出是用來計算最大公因數的輾轉相除法。而輾轉相除法的結束條件為兩數字相等也就是兩者相減為 0
,且因為當最後兩數相等時, do while
迴圈中會進入 else
的流程,此時 v
會等於 0
,因此 while
結束的條件為 v == 0
,所以 COND
為 v
,當 v
不為 0
時代表還需要進行下一次的輾轉相除法,直到 v
等於 0
為止。而因為最後要回傳兩者之最大公因數,此時之 u
為去除二倍數之最大公因數,所以最後還需要再利用一開始求出的 shift
乘回算出來的 u
,因此 RET
為 u << shift
。
COND : v
RET : u << shift
4
改寫的程式碼如下:
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b,那 t 就會變成000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
因為變數 t
是想要將原數字中二進位最低位的 1
保留下來,而由二補數系統(two's complement)的定義,一數字在加上 -
(負號)並與原本的數相加後,會因為一直進位而變成零,而最一開始進位的位置就是該數在二進位制中最低位的 1
。因此 EXP6
等於 bitset & -bitset
,這樣第一次進位的位置即是兩數同時擁有 1
的位置。
6 | -6 | 6 & -6 |
---|---|---|
0110 | 1010 | 0010 |
EXP6 : bitset & -bitset
5
以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作:
程式碼講解:
因為其程式碼有一定的長度,因此需要經過 trace code
再做完整的判斷會比較好。
先從程式開始執行的第24行開始:
此段是在配置足夠的記憶體來存取在做運算後的小數點,並先將分母以及分子為零的狀況排除。
接著是第55行:
此段主要是先計算出該數的商數與餘數,接著利用 sprintf
將 division
存入 %ld
再轉成 char
存入 p
。
第62行:
此段是將小數點加入 p
,再定義一個存放小數的變數 decimal
,配置空間後再用 memset
轉換其全部內容成 0
。
第72行:
此段開始要真正做小數運算的動作,先重新定義 size
為 1333
,接著定義一個 heads
並配置記憶體,在利用 INIT_LIST_HEAD
,依序進行初始化。
第77行:
接著利用 for
迴圈開始進行計算,透過呼叫 find
函式找出是否有相同的 key
,若有則回傳他的 index
,若無則回傳 -1
。
find
函式:而獲得 pos
後,變可以印出小數點的數值,並回傳 result
。
第89行:
若沒有找到相同的 key
,則需要將其 remainder
與 i
記錄下來,並將其存入配置圍成的 struct list_head*
中,接著最後兩行是存入當前餘數 remainder
的數值至 q
與更新 remainder
的數值。此處的 + '0'
,其實為加上 48
,將餘數算出後型態從 long long
轉換至 char
。
以下為解釋(以 remainder = 3, d = 6
為例):
0~9 之ASCII編碼:
Binary | Decimal | Character |
---|---|---|
0110000 | 48 | 0 |
0110001 | 49 | 1 |
0110010 | 50 | 2 |
0110011 | 51 | 3 |
0110100 | 52 | 4 |
0110101 | 53 | 5 |
0110110 | 54 | 6 |
0110111 | 55 | 7 |
0111000 | 56 | 8 |
0111001 | 57 | 9 |
由上述的運作情況,我們可以知道 PPP
為 pos--
,因為 pos
為記住小數點後不重複的位數,因此當 pos
等於零時,代表不重複的小數已經計算完了。而在後面的 MMM
與 EEE
則分別是 list_add
與 &heads[remainder % size]
。此處的用意是將餘數與位置放入 struct list_head
的結構 node*
當中,再利用 list_add
加入 head[remainder % size]
中。
整個運作上其實為一個雜湊表,透過雜湊表找出循環與非循環的小數。
以下為實際運作的示意圖(以分母為 90
,分子為 102
為例):
result
並同時加入小數點decimal
並準備存入小數for
迴圈 (i = 0):pos
回傳為 -1
,引此不會進入 if
條件式,並且把 remainder
與 i
加入 node*
,並透過 list_add
加入 heads[remainder % size]
,即為 heads[12]
。
加入雜湊表後,接著將餘數記住並加入 q
中,並且重新計算下一次的餘數
for
迴圈 (i = 1):for
迴圈後, pos
會回傳 -1
,接著再次加入 heads[30]
中。接著再次紀錄商數至 q
,並刷新餘數。
for
迴圈(i = 2):for
迴圈後, find
函式會進入 heads[30]
尋找相同的 key
,找到相同的 key
後,便會回傳 1
給 pos
。接著進入 if
判斷式,將 decimal
的內容複製給 p
,因為 pos
等於 1
,所以只會將 decimal
的第一位的 1
複製給 p
,此時 result
為 1.1
。加上 (
並複製剩下的內容直到遇到 \0
,最後在加上 )\0
即可回傳。PPP : pos–
MMM : list_add
EEE : &heads[remainder % size]
6
__alignof__ 是 GNU extension,以下是其可能的實作方式:
其程式碼講解:
先定義一個結構體其中擁有 char c
與 t _h
。
再將其結構之起始位置設定為 0
。
接著找出其結構中元素 _h
的位置。
最後與 X
的位置相減。
因為 __alignof__
就是計算 type t
的長度,所以 M
就是需要求的 _h
,而 X
就是 0
。
以 _h
是 int
為例:
其結構實際樣貌:
其執行結果:
M : _h
X : 0
7
考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
- 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
- 如果是 5 的倍數,印出 “Buzz”
- 如果是 15 的倍數,印出 “FizzBuzz”
- 如果都不是,就印出數字本身
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (
bitwise.c
)
因為題目要求依據各數字是否為 3
或 5
的倍數去做變化,而若只是 3
或是 5
的倍數只需要印出 Fizz
或是 Buzz
,所以我們可以知道 length
在皆否的情況下為 2
,只為其中一數之倍數為 4
,同時為兩數之倍數的話為 8
,而剛好 2 << 1
等於 4
且 2 << 2
等於 8
。因此我們可以知道 KK1
與 KK2
分別為 div3
和 div5
,也可以互換,因為不管先後都會作到 <<
的動作。
情況 | 3 的倍數 |
5 的倍數 |
15 的倍數 |
皆不是 |
---|---|---|---|---|
length 的數值 |
4 | 4 | 8 | 2 |
搞定 length
的問題後,後面的 strncpy
則是依據不同情況去複製不同位置以及大小之字串。若是 3
或是同時是 3
與 5
的倍數,則要從 0
的位置開始複製,若是單純 5
的倍數,要從 4
的位置開始複製,而若兩數皆不是的話則要從 8
的位置開始複製。
情況 | 3 的倍數 |
5 的倍數 |
同時為 3 與 5 的倍數 |
皆不是 |
---|---|---|---|---|
開始複製之位置 | 0 | 4 | 0 | 8 |
而因為 div5
已經決定好了,所以若是單純為 5
的倍數, 9 >> 5
等於 4
,剛好是開始複製的位置,此時 KK3
應該要為 0
。而若是 3
的倍數或是 15
的倍數,則需要一直位移直到數值為 0
,便可以從起始位置開始複製,所以 KK3
應該是 div3 << 2
,這樣在單純只為 3
的倍數的情況下,也可以讓 9
位移 4
次,變成 0
的結果。
但是當兩數皆不為倍數時,其值為 9
,此時只會印出 u
的結果,與 8
的結果不符,因此 9
應該改為 8
,這樣不會影響原本推導的結果,又可以讓兩數皆不為倍數的情況下複製並輸出正確的結果。
KK1 : div3
KK2 : div5
KK3 : div3 << 2