沒有除法的除法——LeetCode第29題

序言

7月初的時候挑戰了一下LeetCode的第29題(中等難度,彷佛沒什麼值得誇耀的),題目要求在不使用除、乘,以及模運算的狀況下,實現整數相除的函數。node

既然被除數和除數都是整數,那麼用減法就能夠實現除除法了(多麼naive的想法)。一個trivial的、用JavaScript編寫的函數能夠是下面這樣的(爲了簡單起見,只考慮兩個參數皆爲正整數的狀況)git

function divide(n, m) {
  let acc = 0;
  while (n >= m) {
    n -= m;
    acc += 1;
  }
  return acc;
}

如此樸素的divide函數提交給LeetCode是不會被接受的的——它會在像2147483648除以2這樣的測試用例上超時。能夠在本地運行一下感覺下究竟有多慢github

➜  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次。爲了提高運行速度,必須減小循環的次數。shell

既然每次從n中減去m須要執行n/m次,那麼若是改成每次從中減去2m,不就只須要執行(n/m)/2次了麼?循環的次數一會兒就減小了一半,想一想都以爲興奮啊。每次減2m,而且自增2的算法的代碼及其運行效果以下ide

➜  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,上述公式的結果也不會很大,所以能夠顯著地提高整數除法的計算效率。spa

在開始寫代碼前,讓我先來簡單地證實一下這個方法算出來的商與直接計算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的計算公式。

若是有哪位好心的讀者朋友知道的話,還望不吝賜教。

閱讀原文

相關文章
相關標籤/搜索