面试官:如何对字符串版本号构成的数组进行排序?

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

相关文章
相关标签/搜索