在面试中遇到了这么两道题,当时没有做出来,挺有意思,记录下来。
一、数组中找出项的组合
当时乍一看想到把所有情况枚举一边,但是仔细看不知道怎么下手,回来仔细想了下,确定了个思路,可以把每一个组合转化成二进制,比如[1, 2, 3, 4]中数组第一项和第三项的组合就可以用1010表示,这样就可以快速列出所有组合的情况,然后算出对应项的和,等于M的就返回。
这个思路需要解决这么几个问题:
- 如何把所有的组合罗列出来进行遍历?
- 如何根据二进制的表示计算出这个组合中项的和?
- 如果,规定组合只能有几项,而不是要求所有情况该怎么处理?
我们来一个一个看。
首先,如何把所有组合罗列出来进行遍历?
既然决定采用二进制表示,那么最简单的方式实际上就是累加。
例如,[1, 2, 3, 4]组合有16种:
| 12
 3
 4
 
 | 0000、0001、0010、0011、0100、0101、0110、0111、
 1000、1001、1010、1011、
 1100、1101、1110、1111
 
 | 
其实换算成10进制就是0到15,共16中情况,也就是 2的数组长度的次方Math.pow(2, arr.length),只要一个循环累加就可以。
接着,如何根据这个二进制表示计算项的和?
我第一时间想到的,就是把这个字符串一位一位遍历,如果字符串的项是1,则进行累加,并把对应的数组中的项记录下来。
| 12
 3
 4
 5
 6
 7
 
 | let binStr = i.toString(2);binStr.split('').forEach((item, index) => {
 if (item === '1') {
 s += arr[index]
 temp.push(arr[index])
 }
 })
 
 | 
这里有个问题,在把一个数利用toString()转换成二进制字符串时,是不会出现0110这样的字符串,而是会出现110,需要补零。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | let binStr = i.toString(2);
 binStr = '0'.repeat(arr.length - binStr.length) + binStr;
 binStr.split('').forEach((item, index) => {
 if (item === '1') {
 s += arr[index]
 temp.push(arr[index])
 }
 })
 
 | 
最后,如果限定项数,就需要多加一层过滤,增加一个用来计数的counter来计算每个字符串的项数。
| 12
 
 | const counter = num => num.toString(2).replace(/0/g, '').length;
 
 | 
这三个问题解决后,来看下所有代码。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | function GetCombBySumFromArray(arr, sum, count) {
 const counter = num => num.toString(2).replace(/0/g, '').length;
 let len = arr.length, res = [];
 
 for (let i = 0; i < Math.pow(2, len); i++) {
 if (!count || counter(i) == count) {
 let s = 0, temp = [], binStr = i.toString(2);
 
 binStr = '0'.repeat(arr.length - binStr.length) + binStr;
 
 binStr.split('').forEach((item, index) => {
 if (item === '1') {
 s += arr[index]
 temp.push(arr[index])
 }
 })
 
 if (s == sum) {
 res.push(temp)
 }
 }
 }
 return res;
 }
 
 | 
来测试下,看下执行结果。
| 1
 | console.log(GetCombBySumFromArray([-1, 3, 9, 4, 6, -4, 5, 8, 1, 7], 5, 0));
 | 

看起来是正常的,接下来,考虑一个问题,既然已经采用二进制,是不是可以考虑用位运算?答案肯定是可以的,我本身对位运算使用不多,所以借助了下网上的资料,发现有两个地方可以优化。
- 原本在内循环中是通过拆字符串一项一项比较,其实可以通过位运算去判断这一项是不是在这个组合中
| 12
 3
 4
 5
 6
 7
 
 | for (let j = 0; j < len; j++) {
 if (0b0110 & 1 << (len - 1 - j)) {
 s += arr[j]
 temp.push(arr[j])
 }
 }
 
 | 
循环还是大同小异的循环,只不过,判断的方法变成了(0b0110 & 1 << (len - 1 - j)),这里才是重点,根据循环,0b0110需要依次和1000、100、10、1进行&运算,这样结果不为0的就表示这项在这个组合中存在。
- counter的升级,也是运用了&运算
| 12
 3
 4
 5
 6
 7
 8
 
 | const counter = num => {let count = 0
 while (num) {
 num = num & (num - 1)
 count++
 }
 return count
 }
 
 | 
来看下升级过后的函数:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 
 | const GetCombBySumFromArray = (arr, sum, count) => {
 const counter = num => {
 let count = 0
 while (num) {
 num = num & (num - 1)
 count++
 }
 return count
 }
 let len = arr.length, res = [];
 
 for (let i = 0; i < Math.pow(2, len); i++) {
 if (!count || counter(i) == count) {
 let s = 0, temp = [];
 
 for (let j = 0; j < len; j++) {
 
 if (i & 1 << (len - 1 - j)) {
 s += arr[j]
 temp.push(arr[j])
 }
 }
 
 if (s == sum) {
 res.push(temp)
 }
 }
 }
 return res;
 }
 
 | 
位运算还是挺有意思的,可是平时用的还是不多,导致解决问题时不一定能想起来采用位运算,有机会需要更深入了解下位运算的应用。
这个题应该还有其他解法,先放在这,等后面有功夫再想想其他解法。
二、同心圆

| 1
 | 这样一个同心圆,三种颜色,你用几个div可以做到?
 | 
这个题,当时听道,第一反应就是一个,div+border+box-shadow就可以做到:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | #circle {margin:100px auto;
 width: 300px;
 height: 300px;
 border-radius: 50%;
 background: aqua;
 position: relative;
 border: 50px solid orange;
 box-shadow: 0px 0px 0px 50px yellow;
 }
 
 | 
然后后面继续问道,四种颜色呢?
这时候我第一反应是,CSS的渐变。

| 12
 3
 4
 5
 6
 7
 8
 
 | #circle {margin:100px auto;
 width: 300px;
 height: 300px;
 border-radius: 50%;
 background: repeating-radial-gradient(circle, rgb(255, 255, 255) 10%, rgb(0, 0, 0) 20%);
 position: relative;
 }
 
 | 
渐变的我话,别说四个,更多的也可以解决。但是要求是不能使用渐变。这个时候就有点懵,面试官这时候提示道CSS的伪元素可以么,想了下,确实可以。

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | #circle::before {content: ' ';
 position: absolute;
 width: 100px;
 height: 100px;
 border-radius: 50%;
 top: 50%;
 left: 50%;
 margin-top: -50px;
 margin-left: -50px;
 background: purple;
 z-index: 20;
 }
 #circle::after {
 content: ' ';
 position: absolute;
 width: 200px;
 height: 200px;
 border-radius: 50%;
 top: 50%;
 left: 50%;
 margin-top: -100px;
 margin-left: -100px;
 background: chartreuse;
 z-index: 10;
 }
 
 | 
问题解决。以往自己用到before和after多的还是icon或者清除浮动等,很久不接触,突然想不起来,其实伪元素的作用真的挺大,自己在CSS方面的基础真的还需要再去细细梳理下。前端还是非常有意思的。