沒有除法的除法——LeetCode第29題
7月初的時候挑戰了一下LeetCode的 第29題 (中等難度,似乎沒什麼值得誇耀的),題目要求在不使用除、乘,以及模運算的情況下,實現整數相除的函數。
既然被除數和除數都是整數,那麼用減法就可以實現除除法了(多麼naive的想法)。一個trivial的、用JavaScript編寫的函數可以是下面這樣的(爲了簡單起見,只考慮兩個參數皆爲正整數的情況)
function divide(n, m) { let acc = 0; while (n >= m) { n -= m; acc += 1; } return acc; }
如此樸素的 divide
函數提交給LeetCode是不會被接受的的——它會在像2147483648除以2這樣的測試用例上超時。可以在本地運行一下感受下究竟有多慢
➜ nodejs time node divide.js 2147483648/2=1073741824 node divide.js 1.14s user 0.01s system 99% cpu 1.161 total
那麼有沒有更快的計算兩個整數的商的算法呢?答案當然是肯定的。
嘗試優化
一眼就可以看出,運行次數最多的是其中的 while
循環。以2147483648除以2爲例, while
循環中的語句要被執行1073741824次。爲了提升運行速度,必須減少循環的次數。
既然每次從 n
中減去 m
需要執行 n/m
次,那麼如果改爲每次從中減去 2m
,不就只需要執行 (n/m)/2
次了麼?循環的次數一下子就減少了一半,想想都覺得興奮啊。每次減 2m
,並且自增2的算法的代碼及其運行效果如下
➜ nodejs cat divide2.js function divide(n, m) { let acc = 0; let m2 = m << 1; // 因爲題目要求不能用乘法,所以用左移來代替乘以2。 while (n >= m2) { n -= m2; acc += 2; } while (n >= m) { n -= m; acc += 1; } return acc; } console.log(`2147483648/2=${divide(2147483648, 2)}`); ➜ nodejs time node divide2.js 2147483648/2=1073741824 node divide2.js 2.65s user 0.01s system 99% cpu 2.674 total
儘管耗時不降反升,令場面一度十分尷尬,但根據理論分析可知,第一個循環的運行次數僅爲原來的一半,而第二個循環的運行次數最多爲1次,可以知道這個優化的方向是沒問題的。
如果計算 m2
的時候左移的次數爲2,那麼 acc
的自增步長需要相應地調整爲4,第一個循環的次數將大幅下降至268435456,第二個循環的次數不會超過4;如果左移次數爲3,那麼 acc
的步長增至8,第一個循環的次數降至134217728,第二個循環的次數不會超過8。
顯然,左移不能無限地進行下去,因爲 m2
的值早晚會超過 n
。很容易算出左移次數的一個上限爲
對數符號意味着即便對於很大的 n
和很小的 m
,上述公式的結果也不會很大,因此可以顯著地提升整數除法的計算效率。
在開始寫代碼前,讓我先來簡單地證明一下這個方法算出來的商與直接計算 n/m
是相等的。
一個簡單的證明
記被減數爲 n
,減數爲 m
。顯然,存在一個正整數 N
,使得
令
,再令
,那麼 n
除以 m
等價於
證明完畢。
從上面的公式還可以知道,新算法將原本規模爲 n
的問題轉換爲了一個規模爲 r
的相同問題,這意味着可以用遞歸的方式來優雅地編寫最終的代碼。
完整的代碼
最終的 divide
函數的代碼如下
function divide(n, m) { if (n < m) { return 0; } let n2 = n; let N = 0; // 用右移代替左移,避免溢出。 while ((n2 >> 1) > m) { N += 1; n2 = n2 >> 1; } // `power`表示公式中2的N次冪 // `product`代表`power`與被除數`m`的乘積 let power = 1; let product = m; for (let i = 0; i < N; i++) { power = power << 1; product = product << 1; } return power + divide(n - product, m); }
這個可比最開始的 divide
要快得多了,有圖有真相
➜ nodejs time node divide3.js 2147483648/2=1073741824 node divide3.js 0.03s user 0.01s system 95% cpu 0.044 total
後記
如果以 T(n, m)
表示被除數爲 n
,除數爲 m
時的算法時間複雜度,那麼它的遞推公式可以寫成下列的形式
但這玩意兒看起來並不能用主定理直接求出解析式,所以很遺憾,我也不知道這個算法的時間複雜度究竟如何——儘管我猜測就是 N
的計算公式。
如果有哪位好心的讀者朋友知道的話,還望不吝賜教。