这是我参与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 进行循环
- 为了优化逻辑,我们就倒序循环,以省略多余的变量
i
和j
- 在循转中,我们直接使用赋值的方式填充数组,不再使用
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]