在 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']
其中 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']
於是問題來了:
為什麼字串比較能夠輕鬆的實現排序?
在JavaScript中,字串之間無疑也是可以比較的。猜猜看下面這段程式碼輸出的結果是什麼?
console.log('5'>'1')
console.log('5'>'10')
答案是true
、true
。
比較字串是比較它們的 Unicode 值
這是因為在兩個字串進行比較時,是使用基於標準字典的 Unicode 值來進行比較的。通過String.prototype.codePointAt()
方法我們能拿到字串的 Unicode 值。所以'5'>'1'
的結果是true
;
而當字串長度大於1的時候比較則是逐位進行,因此'5'>'10'
進行比較時,首先比較第一位也就是'5'>'1'
,如果有結果則返回,沒有結果則繼續比較第二位。所以'5'>'10'
的結果與'5'>'1'
相同,也是true
。
回過頭來看問題,就不難理解了:.
的 Unicode 值為 46,0
的 Unicode 值為 48,其它數字在此基礎上遞增。所以在比較的時候10.1
是要大於1.1
的。
字串比較法適用範圍很小
上文解釋了為什麼題目中的 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*p2 + 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 那麼計算出來的結果顯然會不符合預期。而p
取1000
就能夠避免各個子版本加權之後產生汙染。
同理,有類似規則的版本號(如'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;
}
});
console.log(arr)
思考
我們總結並且對比了幾種用來比較版本號的方法,在不同的場景可以選擇合適的方式:
- 字串比較法
- 大數加權法
- 迴圈比較法
但是,我們知道生產環境中軟體的版本號通常並不全由陣列組成。比如我們可以在npm上釋出諸如1.0.0-beta
或者6.0.0-alpha
等格式的包,此時該如何比較版本號?相信聰明而又勤奮的你一定有自己的思路,不妨留言討論一下。