现在有这样一道题目:
给定两个数组 a 和 b ,它们的值为别为
[1,3,5,7]
和[2,4,6,8]
,请你用最简单的方法将 a 与 b 合并为一个数组,合并后得到的数组的值应为[1,2,3,4,5,6,7,8]
这道题是在这几年的面试中比较容易出现的算法题之一,我在58同城以及区块链的一家公司的面试过程中都遇到过,而且不仅仅局限于 javascript,也可以作为 python ,java 等的面试题。
可能有的同学想,这有什么难的,不就是考察数组的合并和排序嘛~
于是落笔成文:
varc=a.concat(b).sort((a,b)=>a-b)
写完美滋滋,你看我,一行代码能搞定的,绝对不写成两行
然后面试官微微一笑,让你回去等通知
为什么呢?因为你没仔细审题
题目中说“请你用最简单的方法”,最简单,并不等于写的代码最少。
所以这里其实有个隐藏的前提,那就是不能把 a 和 b 合并之后再排序,这里最简单的解法应该是让循环执行的次数最少。
因为在算法当中,优化意味着要尽量避免无效的计算和多次的计算。
举个例子,我们经常使用循环来创建一系列的元素,并追加到页面中。
第一种方式,利用数组存储创建的元素,最后利用数组方法转成字符串进行追加
functioncreateGoodsList(num){vari=0,array=[];for(;i<num;i++){array.push('<li>'+(i+1)+'</li>')}returnarray}document.getElementById('goods_wraper').innerHTML=createGoodsList(10).join('')
第二种方法,利用字符串拼接的方法来存储创建的元素,然后追加到页面里
functioncreateGoodsList(num){vari=0,str='';for(;i<num;i++){str+='<li>'+(i+1)+'</li>'}returnstr}document.getElementById('goods_wraper').innerHTML=createGoodsList(10)
第三种方式,直接在循环里追加,每循环一次,则往页面中追加一次
functioncreateGoodsList(parent,num){vari=0for(;i<num;i++){varliElement=document.createElement('li')liElement.innerText=i+1parent.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 各自的索引
vara=[1,3,5,7,9]varb=[2,4,6,8]vari=j=0,c=[],a_length=a.length,b_length=b.lengthwhile(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 哪个数组长度长一点,才会多写一个循环(真正执行的只有两个),那么我们有没有办法把三个循环的写法简化一下呢?
其实这里我们只需要判断一下在读取数组中的值得时候,这个值是否存在
优化:
vara=[1,3,5,7,9]varb=[2,4,6,8]vari=j=0,c=[],a_length=a.length,b_length=b.lengthwhile(i<a_length||j<b_length){if((b[j]>a[i]&&a[i])||!b[j]){c.push(a[i])i++}elseif((b[j]<=a[i]&&b[j])||!a[i]){c.push(b[j])j++}}console.log(c)
到这里,这道题就完成了。
bug 引发的思考:
经过一次偶尔的测试,我发现上面的代码其实有个不起眼的 bug,这个 bug 会导致我们的代码功亏一篑
比如:
vara=[0,2,3,5,9,10,12]varb=[7,11,13]...console.log(c)//[7,11,13,0,2,3,5,9,10,12]
这是因为我们在代码里判断了 a[i]
为真,而当a[i]
值为 0 的时候,判断是不能成立的,所以出现了 bug。
既然这样,我们直接使用另一种思路来替代之前的解题思路,
首先,我们的循环条件要修改一下,只按照一个数组进行循环,这样就保证循环中每一次的a[i]
都存在,这是什么意思呢?
假如我有如下的代码:
vara=[0,1,2,3,4,5],i=0while(i<a.legnth){i++}
这里的判断条件能保证a[i]
肯定是存在的。
那我们说一下整体的思路:
首先,通过变更循环的判断条件将代码中针对值是否存在的判断去掉
其次,我们先将一个数组放进 c 中 var c = [...a]
接下来按照数组 b 进行循环
为了优化逻辑,我们就倒序循环,以省略多余的变量i
和j
在循转中,我们直接使用赋值的方式填充数组,不再使用push
方法
varc=[...a]varb_length=b.length-1vara_length=a.length-1varc_length=b_length+a_length+1while(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]
作者:晴天同学