Left-pad的实现
版本一
// 传入三个参数,分别为需要填充的字符串str,填充后的长度len、填充字符ch
const leftpad = (str, len, ch) => {
str = String(str) //保证str为字符串
//对ch的处理,防止null等不合法的值
if(!ch && ch !== 0) ch = ''
let i = 0
//计算需要填充字符的个数
len = len - str.length
while(i++ < len) str = ch + str
return str
}
这个版本为命令式编程,虽然逻辑清晰,但是代码冗余,效率低下。
版本二
//ch设置了默认值''
const leftpad = (str, len, ch='') => {
str = "" + str //保证str为字符串
len = len - str.length
if (len <= 0) return str
return ch.repeat(len) + str
}
这个版本代码比较精简,使用声明式方法repeat,更加高效。
版本三
const leftpad = (str, len, ch='') => {
str = "" + str //保证str为字符串
len = len - str.length
if (len <= 0) return str
let rpt = ''
while(true){
if ((len & 1) == 1) rpt += ch
len >>>= 1 //二进制向右移一位
if (len === 0) break
ch += ch //将自身重复一倍,如 'aaa' -> 'aaaaaa'
}
return rpt + str
}
这个版本虽然代码略长一些,但是把时间复杂度从O(n)降低到了O(logn)
版本四
const leftpad = (str, len, ch='') => {
str = "" + str //保证str为字符串
len = len - str.length
if (len <= 0) return str
let rpt = ''
do{
rpt += ch
ch += ch
len &= (len-1)
}while(len)
return rpt + str
}
思路:减去1后的数和原数相与,能将最低位的1置0,每次操作都是如此。所以**能操作的次数为原数二进制表示中1的个数**。因此,可以使用这中方法快速得到循环的次数。
交通信号灯算法
现在有个需求,有三个信号灯,按照一定的时间间隔进行切换。
setTimeout回调嵌套
很容易想到的就是使用setTimeout进行递归延迟,完整代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
#traffic li{
height: 40px;
width: 40px;
background-color: gray;
border-radius: 20px;
list-style: none;
margin: 10px;
}
#traffic.stop li:nth-child(1){
background-color: red;
}
#traffic.wait li:nth-child(2){
background-color: yellow;
}
#traffic.pass li:nth-child(3){
background-color: green;
}
</style>
</head>
<body>
<ul id="traffic" class="pass">
<li></li>
<li></li>
<li></li>
</ul>
<script>
const traffic = document.getElementById('traffic');
(function reset(){
traffic.className = 'stop';
setTimeout(() => {
traffic.className = 'wait';
setTimeout(() => {
traffic.className = 'pass';
setTimeout(reset, 1000)
}, 1000)
}, 1000)
})()
</script>
</body>
</html>
但是这样有一种回调地狱的感觉,而且要是改成了四个五个六个信号等,reset部分的代码要大改,改起来很麻烦。
状态列表法
我们可以把状态和持续时间放到一个列表中,这样我们循环遍历这个列表,并将对应的类名和持续时间进行应用即可。js部分的代码如下:
const traffic = document.getElementById('traffic');
const stateList = [
{state: 'stop', last: 1000},
{state: 'wait', last: 2000},
{state: 'pass', last: 3000}
]
const start = (traffic, stateList) => {
const len = stateList.length
const applyState = stateIdx => {
const {state, last} = stateList[stateIdx]
traffic.className = state
setTimeout(() => {
applyState((stateIdx + 1) % len)
}, last)
}
applyState(0)
}
start(traffic, stateList)
可以看到,如果我们想修改或增加状态,对stateList进行修改即可,且代码清晰易懂。
Promise封装
我们可以使用Promise对这个过程进行扁平化,代码如下:
const traffic = document.getElementById('traffic');
const wait = time => new Promise(resolve => setTimeout(resolve, time))
const setState = state => traffic.className = state
const start = async () => {
for(;;){
setState('stop')
await wait(1000)
setState('wait')
await wait(2000)
setState('pass')
await wait(3000)
}
}
start()
这种方法更加直接,将时间的切换和状态的改变进行拆解,更加灵活,且无需占用空间存贮状态,简明。
洗牌算法
如果我们想让一个数组里的元素随机排序,很容易想到以下方法:利用sort函数的参数和Math.random方法
// 创建数组[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let cards = Array(10).fill(0).map((_, index) => index)
const shuffle = cards => [...cards].sort(() => Math.random() > 0.5 ? -1 : 1)
但是这种方法并不平均,使用以下代码,将这个算法执行1000次,我们统计每个位置的数值之和,以查看每个位置产生的数字概率是否和其他位置的一致。
const result = Array(10).fill(0)
for(let i = 0; i < 1000; i++){
const s = shuffle(cards)
for(let j = 0; j < 10; j++){
result[j] += s[j]
}
}
这种方法产生的结果如下,我们发现,每个位置的数字之和差距很大,越靠后的数字之和越大,且还需要sort函数排序,时间和空间复杂度较高。
[3938, 3906, 4489, 4507, 4670, 4309, 4416, 4736, 4882, 5147]
下面我们换一种思路,从前开始,随机和后面的数字进行交换,这样保证了随机性。
const shuffle = cards => {
for(let i = 0, len = cards.length; i < len; i++){
const index = Math.floor(Math.random()*(len-1)) + 1;
[cards[i], cards[index]] = [cards[index], cards[i]]
}
return cards
}
而用这种方法统计出来的结果,其数值基本上都是在4500左右,比较接近,说明这种方法比较公平,且算法的复杂度很低。
[4542, 4504, 4481, 4479, 4338, 4531, 4575, 4548, 4536, 4466]
分红包算法
const generate = (amount, count) => {
let ret = [amount]
while(count > 1){
let cake = Math.max(...ret)
let idx = ret.indexOf(cake)
let part = 1 + Math.floor((cake / 2) * Math.random())
let rest = cake - part
ret.splice(idx, 1, part, rest)
count --
}
return ret
}
这种算法的思路是每次只对最大的那份红包进行切分,这样能够保证大家抢到的红包数额相对来说比较均衡。
个人总结
虽然这些代码都是老师PPT上的, 一些经过了我个人的修改,但是自己思考一遍,敲出来,自己实现一把,是有很多收获的, 尤其是在细节方面,只有自己敲出来的才能体会到,对于知识的理解也会更加深入。其中一些关键带啊吗增加了我的个人注释,方便大家理解。代码都是可以执行的,没有伪代码,希望可以帮助到大家。