面試官:如何對字符串版本號構成的數組進行排序?

array_sort.jpeg

在 segmentfault 有一個經典的面試題:面試

有一組版本號以下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。

如今須要對其進行排序,排序的結果爲 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']npm

問題連接segmentfault

其中 zzgzzg00 的回答大意以下,很是簡潔也很是有意思:數組

const arr=['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5'];
arr.sort((a,b)=>a>b?-1:1);
console.log(arr); // ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']

因而問題來了:spa

爲何字符串比較可以輕鬆的實現排序?

在JavaScript中,字符串之間無疑也是能夠比較的。猜猜看下面這段代碼輸出的結果是什麼?prototype

console.log('5'>'1')
console.log('5'>'10')

答案是truetruecode

比較字符串是比較它們的 Unicode 值

這是由於在兩個字符串進行比較時,是使用基於標準字典的 Unicode 值來進行比較的。經過String.prototype.codePointAt()方法咱們能拿到字符串的 Unicode 值。因此'5'>'1'的結果是true;blog

而當字符串長度大於1的時候比較則是逐位進行,所以'5'>'10'進行比較時,首先比較第一位也就是'5'>'1',若是有結果則返回,沒有結果則繼續比較第二位。因此'5'>'10'的結果與'5'>'1'相同,也是true排序

回過頭來看問題,就不難理解了:.的 Unicode 值爲 46,0的 Unicode 值爲 48,其它數字在此基礎上遞增。因此在比較的時候10.1是要大於1.1的。ip

字符串比較法適用範圍很小

上文解釋了爲何題目中的 case 可以經過字符串比較來實現。可是機智如你必定會發現,這種比較是存在問題的:若是修改題目中的arr以下:

const arr=[
    '0.5.1',
    '0.1.1',
    '2.3.3',
    '0.302.1',
    '4.2',
    '4.3.5',
    '4.3.4.5'
];

那字符串比較法會出錯:指望中版本號'0.302.1'應該大於'0.5.1',但實際比較的結果則是相反的,緣由就在於逐位比較

因此字符串比較這個技巧須要限定條件爲各個版本號均爲1位數字,它得出的結果纔是準備的,而常見的版本號並不符合這個條件。那麼有沒有適用性更強又簡潔的比較方式呢?

「大數」加權法

比較npm規則版本號

假設版本號遵循 npm 語義化規則,即版本號由MAJOR.MINOR.PATCH幾個部分組成::

const arr=['2.3.3', '4.3.4', '0.3.1'];

經過以下公式得出待比較的目標版本號:

MAJOR*p 2 + MINOR*p + PATCH

代碼以下:

const p = 1000;
const gen = (arr) => 
    arr.split('.').reduce(reducer,0);

const reducer = (acc,value,index) => 
    acc+(+value)*Math.pow(p,arr.length-index-1);

arr.sort((a,b)=> gen(a)>gen(b)?-1:1);

console.log(arr)

其中p爲常量,它的取值要大於MAJOR/MINOR/PATCH三者中最大值至少一個量級。譬如待比較的版本號爲1.0.1'0.302.1',此時若是p取值爲 10 那麼計算出來的結果顯然會不符合預期。而p1000就可以避免各個子版本加權以後產生污染。

同理,有相似規則的版本號(如'1.0.1.12')均可以經過上述方法進行排序。

更多的版本號

若是版本號數組以下:

const arr=[
    '1.1',
    '2.3.3',
    '4.3.5',
    '0.3.1',
    '0.302.1',
    '4.20.0',
    '4.3.5.1',
    '1.2.3.4.5'
];

上述數組不但不遵循MAJOR.MINOR.PATCH規則,其長度也沒有明顯的規則,這時該如何比較呢?

能夠在固定規則比較的方法基礎上進行擴展,首先須要獲取到版本號數組中子版本號最多有幾位maxLen。這裏咱們經過Math.max()獲取:

const maxLen = Math.max(
    ...arr.map((item)=>item.split('.').length)
);

拿到maxLen以後便可改寫 reducer 方法:

const reducer = (acc,value,index) => 
    acc+(+value)*Math.pow(p,maxLen-index-1);

const gen = (arr) =>
    arr.split('.').reduce(reducer,0);

arr.sort((a,b)=> gen(a)>gen(b)?-1:1);

console.log(arr)

上述方法足夠用於常規版本號的比較了。可是咱們知道,JavaScript 的 number 類型爲雙精度64位浮點類型,若是maxLen特別大、每一位的值又很大(好比某個子版本號用時間戳來標記),那麼上述方法則存在溢出而致使比較結果不許確的問題。

不過BigInt提案已經進入stage3規範,它可以表示任意大的整數。能夠預見的是,在不久的未來咱們無需考慮版本號取值範圍帶來的影響。

循環比較法

相對字符串比較法和大數加權法,循環比較法的適用性更強。思路仍然是逐位比較子版本號:若是當前版本號相同則比較下一位;若是版本號位數不相等而前幾位值一致則認爲位數多的版本號大。

代碼以下:

arr.sort((a, b) => {
    let i = 0;
    const arr1 = a.split('.');
    const arr2 = b.split('.');

    while (true) {
        const s1 = arr1[i];
        const s2 = arr2[i++];

        if (s1 === undefined || s2 === undefined) {
            return arr2.length - arr1.length;
        }

        if (s1 === s2) continue;

        return s2 > s1 ? -1 : 1;
    }
});

console.log(arr)

思考

咱們總結而且對比了幾種用來比較版本號的方法,在不一樣的場景能夠選擇合適的方式:

  • 字符串比較法
  • 大數加權法
  • 循環比較法

可是,咱們知道生產環境中軟件的版本號一般並不全由數組組成。好比咱們能夠在npm上發佈諸如1.0.0-beta或者6.0.0-alpha等格式的包,此時該如何比較版本號?相信聰明而又勤奮的你必定有本身的思路,不妨留言討論一下。

qr.001.jpeg

相關文章
相關標籤/搜索