contributed by < AmyLin0210
>
在以下的程式碼中,a >> 1
與 b >> 1
分別代表將 a
與 b
除二並無條件捨去到整數位,而後方的 EXP1 要處理的就是進位的問題。
在 a
與 b
皆為奇數時,會需要把前面相加的結果加一,因此 EXP
在這裡是 a & b & 0x1
。若 a
與 b
都是奇數,那做完 and 之後最右邊的位元應該要是 1
,後面再與 0x1
做 and,得到最右邊的位元。
觀察二進位加法,以下面的範例為例:
a & b
可以找到 a 與 b 在相同位置是否都是 1,如果是的話,表示在那個位置他們需要進位。
a ^ b
則是找到在哪邊 a 與 b 分別有值,代表的是相加後不需要進位的地方。
在取平均數時,需要把 a 與 b 相加除二,因此我們把上述的算式改進一下:
在 max
這個函式裡,要做的就是回傳比較大的那個值,因此先來思考,在已經給定
這樣的算式時,我要如何分別回傳出 a
與 b
。
看到 xor 的特性:
0
這個數字做 xor ,那會得到的便是自己。0
。再來看看 and 的特性:
0
這個數字做 and ,那會得到 0 。1
的數字做 and ,那會得到自己。在二補數的系統中,-1
在使用二進制表示時,會全部都是 1 ,因此推論 EXP5
這個判斷式是回傳出 a
或者是 b
的關鍵。
先去考慮 a ^ (EXP)
會回傳出什麼東西
a ^ b
由於 a ^ a
會變成 0
,所以得到 0 ^ b
會是 b
。推測 EXP4
為 a ^ b
,這樣才有機會去拿到一個完整的 b
。
接下來看到 EXP5
,目前我們知道目標是如果我想回傳 a
,那會變成 a ^ ((a ^ b) & 0)
,如果我想回傳 b
,那會變成 a ^ ((a ^ b) & -1)
,這樣 EXP5
的答案就很顯而易見,就是 a < b
。
GCD 的演算法特性
以下的程式碼就是根據上述 GDB 演算法特性而改寫而來:
在第 4 行可以對照到 GDB 演算法特性的第 1 點與第 2 點,若 u
與 v
其中一方或雙方是 0 時, u | v
的結果便是 0 (在 u 和 v 皆為 0 時) 或者是非 0 的那個數 (在 u 和 v 僅有其中一方為 0 時)。
在第 6 行到第 8 行,可以對照到 GDB 演算法特性的第 3 點,把 u
與 v
共同對 2 的因數給取出來。
在第 9 行到第 10 行與第 12 行到第 13 行,可以對照到 GDB 演算法特性的第 4 點與第 5 點,代表把 u 與 v 對 2 的倍數給拿出來。
在第 14 到 19 行,就是經典的 GDB 操作,大數減去小數後,將結果存在 v
,因此第 21 行的 while 迴圈判斷條件為 v
,當減完的結果還有值,就表示 GDB 還沒完全完成。
第 22 行,回去看到第 6 到第 8 行,可以發現有把 u
與 v
共同的二的因數個數礎存在 shift
中,因此最後需要回傳 u << shift
把二的倍數部份乘回去。
我們直接跳到程式碼的第 10 行,在這裡想要使用 GNU extenstion 的 __builtin_ctzll
在這裡的 r 是從低位元往高位元需要連續遇上多少個 0 才碰的到 1。
然後第 12 行想要做的事情就是把已經數到的位元清零,從這邊判斷,EXP6 這裡想要做的事情是找到最低位元的 1 在哪裡,並把他設為 1,其餘位元設為 0。
因此最開始我猜 EXP6 為 1 << __builtin_ctzll(bitset)
,但顯然的不符合作答規範。
現在來思考在什麼狀況下可以拿到等同於 1 << __builtin_ctzll(bitset)
的數字。
由於想要把大部分的數字給清零,首先知道 a & ~a
會變成 0 ,但這還不是我們想要的,我們需要留下最低位元的 1 。
以下是範例:
然後根據二補數的定義, ~bitmap + 1
會等同於 -bitmap
,
所以可以得到 EXP6 = bitset & -bitset
這裡我們先來討論一般來說求循環小數會怎麼求。
在數學式部份,以 4 / 333
當作範例:
可以從上圖很直覺地看到,當我們做到 (4) 時,由於已經與 (1) 重複了,所以就會判斷他是一個循環小數。
現在來對照下方的程式碼:
首先我們來拆解一下這份程式碼
struct { char c; t _h; } *)0
,表示有個 struct { char c; t _h; }
型態的指標指向了記憶體的位置 0(struct { char c; t _h; } *)0)->_h
那到當中的 _h
參數(&((struct { char c; t _h; } *)0)->_h)
找到 _h
參數的位置((char *)(&((struct { char c; t _h; } *)0)->_h) - (char *)0)
強至轉形成 char *
的型態,並找到與 (char *)0
的距離差簡單作個小實驗,程式碼:
可以發現 int
的 align 為 4, int *
這個指標的 align 為 8。
再來看看下面的程式碼,在這邊我做的變化是,在 int
型態的變數前,多塞幾個 char
型態的變數,看看 int
型態變數的記憶體位置會不會有什麼變化。
從結果可以看到,int
型態變數的記憶體位置的確會以 4 為倍數對齊。
is_divisible 可以參考 Faster remainders when the divisor is a constant: beating compilers and libdivide 這篇文章
下面控制 FizzBuzz 的部份,先來看到 length
這個變數,以下有 3 種可能:
length
為 2 ( %u )。length
為 4 (Fizz 或 Buzz)。length
為 8 (FizzBuzz)。看到程式碼的第 20 行,這裡會是 (2 << div3) << div5
的原因為,div3 與 div5 分別表示是不是被 3 或被 5 整除,所以只會是 1 或 0,2 << 0 << 0
、 2 << 1
與 2 << 1 << 1
的結果分別為 2, 4, 8 ,對應到 length
為 2, 4, 8 的情形。
再看到程式碼第 23 行的地方,這裡表示了要從哪裡開始讀 FizzBuzz%u 這個字串。
我們先看到 8 >> div5
,這樣出來可能的結果為 8 或者是 4,換句話說就是,如果可以被五整除,那就是從 4 這個位置開始讀,如果不行,那就從 8 這個位置開始。
再來我們來看 div3
,如果它是 3 的倍數,那我無論如何都是從 0 開始讀,如果不是,則要維持上面是否為 5 的倍數的結果。在 div3 << 2
這裡,可以發現如果 div3
為 1,出來的數值為 4,若 8 >> 4 會變成 0,反之若 div3
為 0,則不會動。