一道騰訊面試題竟引發我與好友兩人爭執不下

語言: CN / TW / HK

事情經過

下班坐地鐵的時候,好友突然給我發過來一條微信:

這道題是這樣:

咱們軟體開發不是經常不是有版本號嗎? 比如經典的相容IE瀏覽器的jQuery1.x最後的一個版本1.12.4,但有時咱們又不會精確到第三位,比如:vue3.0、react16.8…

然後實現一個方法,把兩個版本號傳進去,可以兩位(如:3.0)也可以三位(如:1.12.4),如果第一個版本比第二個版本大,就返回1。

如果第二個版本比第一個大,就返回-1。 其他情況返回0。

我很快就想出了一個思路:(先省略前期的各種判斷以及型別轉換)用split方法將其切割成陣列,然後把兩個陣列的第一位相比較,如果第一位就比較出結果就可以直接返回,沒有必要在比較第二位了,依此類推,直到比較到最後一位。如果比較到最後一位的時候兩個陣列的長度不一致,就為短的那一方加個0,比如3.0就會變成3.0.0。

本以為他也會這麼想,但萬萬沒想到他說出了一個腦回路跟我不太一樣的解法:

他這麼一說,我一下子就想到了JS小數計算不準確的問題,他肯定是從那獲得的靈感。
不過他居然說我是暴力解法,我怎麼覺得他那種才是暴力解法……
既然有爭議,那咱們就一不做二不休,實現一下吧!

我的split方法

function comparison (version1, version2) {
  // 引數的型別
  const types = ['string', 'number']

  // 第一個引數的型別
  const type1 = typeof version1

  // 第二個引數的型別
  const type2 = typeof version2
  
  // 引數不是字串或數字的情況下返回0
  if (!types.includes(type1) || !types.includes(type2)) return 0

  // 如果version1是number就將其轉成字串
  const ver1 = type1 === 'number' ? version1.toString() : version1

  // 如果version2是number就將其轉成字串
  const ver2 = type2 == 'number' ? version2.toString() : version2

  // 將version1變成陣列
  const versionArr1 = ver1.split('.')

  // 將version2變成陣列
  const versionArr2 = ver2.split('.')

  // 獲取長度最長的陣列的length
  const len = Math.max(versionArr1.length, versionArr2.length)

  // 迴圈對比版本號,如果前一位比較不出大小就繼續向後對比
  for (let i = 0; i < len; i++) {
    // 如果長度不一致將自動補0 同時將字串轉為數字
    const version1 = Number(versionArr1[i]) || 0
    const version2 = Number(versionArr2[i]) || 0

    if (version1 > version2) {
      // 如果version1大就返回1
      return 1
    } else if (version1 < version2) {
      // 如果version2大就返回-1
      return -1
    } else {
      // 如果比較到最後就返回0,否則繼續比較
      if (i + 1 === len) {
        return 0
      } else {
        continue
      }
    }
  }
}
複製程式碼

為了方便觀看這裡省略了用正則判斷傳進來的引數是否符合xx.xx.xx形式或者去除前面的0等一些繁瑣判斷(面試題應該也不用寫那麼細,寫出思路即可),所以只判斷了引數是否為string或number型別,來測試一下:

由於沒有寫死判斷,所以甚至可以實現無限版本號的比較:
好像沒啥毛病哈,反正面試題說的是其餘情況返回0,版本相等、傳錯引數應該都屬於其餘情況。

當然這個版本號過多(1.2.3.4.5.6)的情況嚴格意義上也算是傳錯引數,但為了驗證一下版本號過多的情況我的方法效能與朋友的方法效能哪個好(驗證一下是否為暴力解法),所以寫成了這樣(這種更難寫呢),而且寫成這樣更利於可持續化發展嘛,如果較真的朋友可以採用if(len === 2)和if(len === 3)的這種形式來解題。

接下來咱們再來看一下我朋友的方法:

朋友的replace方法

經測試有bug,後來又改了幾版,下面是最終版:

function compareVersion(version1, version2{
      let ver1 = version1.split('.')
      let ver2 = version2.split('.')
      let len1 = ''
      let len2 = ''
      ver1.forEach((item,index)=>{
        let zero = ''
        for (let i = 0; i < index; i++) {
          zero += '0'
        }
        len1 += zero + item
      })
      ver2.forEach((item,index)=>{
        let zero = ''
        for (let i = 0; i < index; i++) {
          zero += '0'
        }
        len2 += zero + item
      })
      let len = len1.length - len2.length
      if(len > 0){
        len2 = Number(len2) * Math.pow(10,Math.abs(len))
      }else{
        len1 = Number(len1) * Math.pow(10,Math.abs(len))
      }
      if(len1>len2){
        return 1
      }else if(len1<len2){
        return -1
      }else{
        return 0
      } 
}
複製程式碼
(最終版其實沒有用到replace方法,他也用了split)
這是他的原始碼,沒有寫註釋,後來他發微信過來說讓我把迴圈加0的那步去掉,只加一個0即可。

剛好我打算改造一下他的程式碼順便整理下格式然後加點註釋,改造後如下:

function compareVersion(version1, version2{
      // 將傳進的版本號切割為陣列
      const ver1 = version1.split('.')
      const ver2 = version2.split('.')

      // 將陣列相加中間補0最後變成一個數字(字串)
      let len1 = ver1.reduce((sum, item) => sum + '0' + item, '')
      let len2 = ver2.reduce((sum, item) => sum + '0' + item, '')

      // 得出兩個數字(字串)長度的差值
      const len = len1.length - len2.length

      // 如果差值大於0
      if (len > 0) {
        // 第二個數字就乘以十的差值次方
        len2 = Number(len2) * Math.pow(10, Math.abs(len))
      } else {
        // 否則第一個數字就乘以十的差值次方
        len1 = Number(len1) * Math.pow(10, Math.abs(len))
      }

      if (len1 > len2) {
        // 如果第一個數比第二個數大,返回1
        return 1
      } else if (len1 < len2) {
        // 如果第一個數比第二個數小,返回-1
        return -1
      } else {
        // 否則返回0
        return 0
      } 
}
複製程式碼

由於沒做判斷,導致傳入別的值會報錯,不過在傳參正確的情況下好像沒什麼毛病。

然後今早我們又爭執了一番:

我是這麼想的,他這個方法無論第一位是否相同,都是要迴圈整個陣列的。

而我的只要第一位不一樣瞬間就能得出結果,然後還說我的演算法複雜度是O(n),我就想不明白了一個for迴圈遍歷陣列怎麼就成O(n)了,咱們程式裡迴圈遍歷陣列多普遍的一件事啊,都成O(n)了?

而且如果不算前期判斷傳入的引數是否為陣列中的型別的話,我只迴圈了一次陣列,他這個迴圈兩個陣列,我說他肯定不服,那咱們用資料說話:

執行時長

首先在方法的第一行就加入一個console.time('執行時長:'),然後再在返回值之前加入console.timeEnd('執行時長:')

這是一個比較方便的測試程式執行時長的一種方式,注意 console.time()console.timeEnd() 裡面的引數一定要一模一樣才管用,大家可以去試試。

我的程式碼:

function comparison (version1, version2) {
  console.time('執行時長:')
  // 引數的型別
  const types = ['string', 'number']

  // 第一個引數的型別
  const type1 = typeof version1

  // 第二個引數的型別
  const type2 = typeof version2
  
  // 引數不是字串或數字的情況下返回0
  if (!types.includes(type1) || !types.includes(type2)) return 0

  // 如果version1是number就將其轉成字串
  const ver1 = type1 === 'number' ? version1.toString() : version1

  // 如果version2是number就將其轉成字串
  const ver2 = type2 == 'number' ? version2.toString() : version2

  // 將version1變成陣列
  const versionArr1 = ver1.split('.')

  // 將version2變成陣列
  const versionArr2 = ver2.split('.')

  // 獲取長度最長的陣列的length
  const len = Math.max(versionArr1.length, versionArr2.length)

  // 迴圈對比版本號,如果前一位比較不出大小就繼續向後對比
  for (let i = 0; i < len; i++) {
    // 如果長度不一致將自動補0 同時將字串轉為數字
    const version1 = Number(versionArr1[i]) || 0
    const version2 = Number(versionArr2[i]) || 0

    if (version1 > version2) {
      console.timeEnd('執行時長:')  
      // 如果version1大就返回1
      return 1
    } else if (version1 < version2) {
      console.timeEnd('執行時長:')
      // 如果version2大就返回-1
      return -1
    } else {
      // 如果比較到最後就返回0,否則繼續比較
      if (i + 1 === len) {
        console.timeEnd('執行時長:')
        return 0
      } else {
        continue
      }
    }
  }
}
複製程式碼

他的程式碼:

function compareVersion(version1, version2{
      console.time('執行時長:')  
      // 將傳進的版本號切割為陣列
      const ver1 = version1.split('.')
      const ver2 = version2.split('.')

      // 將陣列相加中間補0最後變成一個數字(字串)
      let len1 = ver1.reduce((sum, item) => sum + '0' + item, '')
      let len2 = ver2.reduce((sum, item) => sum + '0' + item, '')

      // 得出兩個數字(字串)長度的差值
      const len = len1.length - len2.length

      // 如果差值大於0
      if (len > 0) {
        // 第二個數字就乘以十的差值次方
        len2 = Number(len2) * Math.pow(10, Math.abs(len))
      } else {
        // 否則第一個數字就乘以十的差值次方
        len1 = Number(len1) * Math.pow(10, Math.abs(len))
      }

      if (len1 > len2) {
        console.timeEnd('執行時長:')
        // 如果第一個數比第二個數大,返回1
        return 1
      } else if (len1 < len2) {
        console.timeEnd('執行時長:')
        // 如果第一個數比第二個數小,返回-1
        return -1
      } else {
        console.timeEnd('執行時長:')
        // 否則返回0
        return 0
      } 
}
複製程式碼

測試結果:

按理說,應該是第一位就能比較出結果,並且位數很長的一個數值,執行的差距就越明顯,來試一下:

差了兩倍多,不過感覺怎麼沒有自己想象中的差距那麼明顯呢?換下位置再試試:
這回差了6倍……嗯。

而且我感覺我多比他寫的判斷引數型別和把數字型別引數換成字串變數也耗費了不少時間,懶得改了,拿一個極端的例子再試一下:

還是數倍的差距,大家怎麼看呢?有沒有更好的辦法實現這道題呢?