JavaScript 将两个升序数组合并为一个升序数组(经典面试题)

1,090 阅读4分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

现在有这样一道题目:

给定两个数组 a 和 b ,它们的值为别为[1,3,5,7][2,4,6,8],请你用最简单的方法将 a 与 b 合并为一个数组,合并后得到的数组的值应为[1,2,3,4,5,6,7,8]

这道题是在这几年的面试中比较容易出现的算法题之一,我在58同城以及区块链的一家公司的面试过程中都遇到过,而且不仅仅局限于 javascript,也可以作为 python ,java 等的面试题。

可能有的同学想,这有什么难的,不就是考察数组的合并和排序嘛~

于是落笔成文:

var c = a.concat(b).sort((a,b)=> a-b)

写完美滋滋,你看我,一行代码能搞定的,绝对不写成两行

然后面试官微微一笑,让你回去等通知

为什么呢?因为你没仔细审题


题目中说“请你用最简单的方法”,最简单,并不等于写的代码最少。

所以这里其实有个隐藏的前提,那就是不能把 a 和 b 合并之后再排序,这里最简单的解法应该是让循环执行的次数最少。

因为在算法当中,优化意味着要尽量避免无效的计算和多次的计算。

举个例子,我们经常使用循环来创建一系列的元素,并追加到页面中。

  • 第一种方式,利用数组存储创建的元素,最后利用数组方法转成字符串进行追加
function createGoodsList(num) {
    var i = 0, array = [];
    for( ; i<num; i++) {
        array.push('<li>' + (i+1) + '</li>')
    }
    return array
}
document.getElementById('goods_wraper').innerHTML = createGoodsList(10).join('')
  • 第二种方法,利用字符串拼接的方法来存储创建的元素,然后追加到页面里
function createGoodsList(num){
    var i = 0, str = '';
    for( ; i<num; i++){
        str += '<li>' + (i+1) + '</li>'
    }
    return str
}
document.getElementById('goods_wraper').innerHTML = createGoodsList(10)
  • 第三种方式,直接在循环里追加,每循环一次,则往页面中追加一次
function createGoodsList(parent,num){
    var i = 0
    for( ; i<num; i++){
        var liElement = document.createElement('li')
            liElement.innerText = i + 1
        parent.appendChild(liElement)
    }
}
createGoodsList(document.getElementById('goods_wraper'),10)

在上面的三种方法

第一种优于第二种,第二种优于第三种。

相比于第三种循环追加的方法,第二种减少了频繁的 DOM 操作,而第一种减少了中间变量的产生,避免了一些多余的运算。

这是因为早期浏览器中没有对于 '+' 运算符的优化,由于 String 类型是不可变的,所以要通过创建中间值来存储 '+' 连接的结果,频繁地创建和销毁字符串导制浏览器在运行程序时性能异常的差。

咳咳,跑题了


回到我们的题目中。

即然 a 和 b ,都是已经给定的升序数组,那最简单的方式,肯定要利用上 a 和 b 自身的升序排列,来减少在排序阶段将要进行的比较。

所以,我们只需要将 a 与 b 中的元素两两比较,而 a 中的元素需要互相比较大小吗?b 中的元素需要互相比较大小吗?

不需要。

于是就有了这样的思路:

  • 首先,针对 a 和 b 中具有相同索引的值进行大小比对
  • 然后将其中较小者放进 c 中
  • 较大者则进入下一次比较过程
  • 当某个给定数组中的值比较完毕,只需要把另一个数组的值全部放入 c 中
  • 拿到正确的结果

第一次实现:

虽然题目中给定的 a 和 b 的长度是相等的,但我们这里假设 a 与 b 的长度不相等,我们来设置两个下标来对应 a 和 b 各自的索引

var a = [1,3,5,7,9]
var b = [2,4,6,8]
var i = j = 0, c = [], a_length = a.length, b_length = b.length
while(i<a_length && j<b_length){
    if(b[j] > a[i]){
        c.push(a[i])
        i++
    }else{
        c.push(b[j])
        j++
    }
}
while(i<a_length){
    c.push(a[i])
    i++
}
while(j<b_length){
    c.push(b[j])
    j++
}
console.log(c)

现在已经完成了题目,但是仔细考虑一下上面的思路,因为我们并不知道 a 和 b 哪个数组长度长一点,才会多写一个循环(真正执行的只有两个),那么我们有没有办法把三个循环的写法简化一下呢?

其实这里我们只需要判断一下在读取数组中的值得时候,这个值是否存在

优化:

var a = [1,3,5,7,9]
var b = [2,4,6,8]

var i = j = 0, c = [], a_length = a.length, b_length = b.length

while(i<a_length || j<b_length){
    if((b[j] > a[i] && a[i]) || !b[j]){
        c.push(a[i])
        i++
    }else if((b[j] <= a[i] && b[j]) || !a[i]){
        c.push(b[j])
        j++
    }
}
console.log(c)

到这里,这道题就完成了。


bug 引发的思考:

经过一次偶尔的测试,我发现上面的代码其实有个不起眼的 bug,这个 bug 会导致我们的代码功亏一篑

比如:

var a = [0,2,3,5,9,10,12]
var b = [7,11,13]

...

console.log(c) // [7, 11, 13, 0, 2, 3, 5, 9, 10, 12]

这是因为我们在代码里判断了 a[i] 为真,而当a[i]值为 0 的时候,判断是不能成立的,所以出现了 bug。

既然这样,我们直接使用另一种思路来替代之前的解题思路,

首先,我们的循环条件要修改一下,只按照一个数组进行循环,这样就保证循环中每一次的a[i]都存在,这是什么意思呢?

假如我有如下的代码:

var a = [0,1,2,3,4,5],i=0

while(i<a.legnth){
    i++  
}

这里的判断条件能保证a[i]肯定是存在的。

那我们说一下整体的思路:

  • 首先,通过变更循环的判断条件将代码中针对值是否存在的判断去掉
  • 其次,我们先将一个数组放进 c 中 var c = [...a]
  • 接下来按照数组 b 进行循环
  • 为了优化逻辑,我们就倒序循环,以省略多余的变量ij
  • 在循转中,我们直接使用赋值的方式填充数组,不再使用push方法
var c = [...a]
var b_length = b.length - 1
var a_length = a.length - 1

var c_length = b_length + a_length + 1

while(b_length >= 0) {
    if(a_length < 0) {
        c[c_length--] = b[b_length--]
        continue
    }
    c[c_length--] = a[a_length] >= b[b_length] ? a[a_length--]: b[b_length--]
}
console.log(c) // [0, 2, 3, 5, 7, 9, 10, 11, 12, 13]