JS 常见算法的解析和代码实现
博主wx: -GuanEr-
,加博主进前端交流群
一、欧几里得定理(辗转相除法)
1. 条件
求 A
和 B
的最大公约数,A
和 B
都是大于等于 0
的整数。
2. 应用场景
欧几里得定理,在数学上,用来计算两个非负整数的最大公约数。
辗转相除法就是 JS
实现欧几里得定理的具体方式。
3. 分析
欧几里得定理: 两个整数的最大公约数等于其中较小的那个数和两数相除余数的最大公约数。
用代码描述:a
和 b
相除余数如果为 0
,最大公约数直接是 b
,如果余数是个不为 0
的数 c
,接着往下计算,b
如果能整除 c
,代表 b
和 c
的最大公约数是 c
,a
和 b
的最大公约数也就是 c
。
4. 代码实现
假设求 2000 和 132 的最大公约数,数学计算过程为:
被除数 除数 余数
2000 132 20
132 20 12
20 12 8
12 8 4
8 4 0
所以 2000 和 132 的最大公约数是 4
我们可以将这个运算过程转换成代码逻辑,有两个变量 a
和 b
,做除法运算求余数,余数为 0
时,除数就是最大公约数,余数不为 0
时,用上一次的除数除以上一次的余数,继续求余数,直到某次运算余数为 0
,那么当次运算的除数就是 a
和 b
的最大公约数。
转换成代码:
function getGYS(a, b) {
let c = null;
while(c !== 0) {
c = a % b;
if(c === 0) return b;
a = b;
b = c;
}
}
console.log(getGYS(10, 8));
…写出来感觉代码很少,但实际上比起传统方式,用欧几里得算法求最大公约数,代码执行效率很高,循环次数非常少。
二、二分查找
二分查找是一种非常高效的数据查找方式,无论在哪种语言中,出现的频率都很高。
1. 条件
二分查找算法要求被查找序列要按照某种规律依照顺序排列。
2. 应用场景
- 电话簿里面找到一个指定首字母的联系人列表;
- 数据量较大的查找需求;
3. 分析
有一个数组:[{
id: 3,
name: '赵一'
}, {
id: 5,
name: '钱二'
}, {
id: 6,
name: '孙三'
}, {
id: 20,
name: '李四'
}, {
id: 55,
name: '周五'
}, {
id: 98,
name: '吴六'
}, {
id: 120,
name: '郑七'
}, {
id: 150,
name: '王九'
}, {
id: 199,
name: '冯八'
}, {
id: 230,
name: '陈十'
}, {
id: 401,
name: '魏零'
}]
查询目的:想要查询 id 为 230 的这条数据的 name 值
二分查找的思路:
1. 先找到一个中间键,id 为 98 的这条数据,然后与 230 做比较。
2. 确定要查找的 230 这条数据在整个列表中的位置,属于 98 左侧,还是 98 右侧。
3. 这样一次就过滤掉一半无用的数据。
4. 当 230 确定 在 98 右侧,继续找到下一个中间键 id 为 199 的这条数据。
5. 199 和 230 作比较,确定 230 的这条数据在 199 的左侧还是右侧。
6. 又过滤掉一半无用的数据。
7. 依次类推,最终确定 id 为 230 这条数据的位置,拿到它。
这样的查找过程比循环查找方便多了。
4. 代码实现
通过上面的分析,我们可以确定这样的代码逻辑,是我们需要一个中间键,来做过滤条件,中间键确定之后,我们就能确定要查找的数据在序列中的范围,范围肯定需要最大值和最小值,所以我们需要两个坐标来保留每次确定的范围。
简化一数组,我们可以这样分析逻辑
数组: [3, 5, 6, 20, 55, 98, 120, 150, 199, 230, 401]
要查找的目标数据: 120
范围最小下标 范围最大下标 中间键下标(小数取整) 中间键 比较
0 10 (10 + 0) / 2 : 5 98 98 < 120
5 + 1 = 6 10 (10 + 6) / 2 : 8 199 199 > 120
6 8 - 1 = 7 (6 + 7) / 2 : 6 120 120 === 120 return
范围最小下标和范围最大下标要进行加一减一的运算,是因为,在上一次的运算中,当前数据已经被运算了
没有必要进行二次运算。
转换成代码:
const list = [{
id: 3,
name: '赵一'
}, {
id: 5,
name: '钱二'
}, {
id: 6,
name: '孙三'
}, {
id: 20,
name: '李四'
}, {
id: 55,
name: '周五'
}, {
id: 98,
name: '吴六'
}, {
id: 120,
name: '郑七'
}, {
id: 150,
name: '王九'
}, {
id: 199,
name: '冯八'
}, {
id: 230,
name: '陈十'
}, {
id: 401,
name: '魏零'
}];
function getData(list, id) {
let min = 0, max = list.length - 1;
while(min <= max) {
let i = parseInt((max + min) / 2);
let res = list[i];
if(res.id === id) return res;
else if(res.id < id) min = i + 1;
else if(res.id > id) max = i + 1;
}
return null;
}
console.log(getData(arr, 230));
三、冒泡排序
1. 应用场景
将一组数据按照升序或者降序排列,比如商品列表,按照价格、销量升序或者降序排列。
2. 分析
冒泡排序的原理:
- 比较相邻的元素。如果第一个比第二个大(小),就交换这两个元素。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
比如有一组数据:[5, 31, 9, 0, 22, 16, 45]
如果希望他们升序排列
比较 5, 31 后者大,不变
比较 31, 9 交换位置
比较 31, 0 交换位置
...
这种相邻似的的比较第一轮只能确定最后一个位置的值,其他数据的顺序还是乱的,所以要比较很多轮,才能将这组数据正确排序
3. 代码实现
根据上面的分析,我们确定以下几点:
- 两两比较元素,根据排序规则交换位置,每次交换的元素都是当前拿到的元素和其下一个元素
- 一轮交换并不能完全的将数组中的数据正确排列,需要多轮交换
- 从最后一个位置,依次向前推动,每轮交换都能确定一个位置上的数据
原数组:[5, 31, 9, 0, 22]
排序要求:降序排列
排序规律:
假设每次交换的递增位置变量为 i,每轮交换的递增变量为 j,那么冒泡排序的排序规律如下
j i i + 1 值比较 是否交换 数组
0 0 1 5 < 31 是 [31, 5, 9, 0, 22]
0 1 2 5 < 9 是 [31, 9, 5, 0, 22]
0 2 3 5 > 0 否 [31, 9, 5, 0, 22]
0 3 4 0 < 22 是 [31, 9, 5, 22, 0]
1 0 1 31 > 9 否 [31, 9, 5, 22, 0]
1 1 2 9 > 5 否 [31, 9, 5, 22, 0]
1 2 3 5 < 22 是 [31, 9, 22, 5, 0]
1 4 4 5 > 0 否 [31, 9, 22, 5, 0]
2 0 1 31 > 9 否 [31, 9, 22, 5, 0]
2 1 2 9 < 22 是 [31, 22, 9, 5, 0]
2 2 3 9 > 5 否 [31, 22, 9, 5, 0]
...
转换成代码:
const arr = [5, 31, 9, 0, 22, 16, 45];
function sort(_arr) {
for(let i = 0; i < _arr.length; i++) {
for(let j = 0; j < _arr.length; j++) {
if(_arr[j] < _arr[j + 1]) {
const temp = _arr[j];
_arr[j] = _arr[j + 1];
_arr[j + 1] = temp;
}
}
}
return _arr;
}
console.log(sort([...arr]));
但是这种写法不是最优写法, 因为每次外层循环结束后,最终都能在数组的最后一个位置上确定一个数据,所以随着外层循环的执行,每次内层循环可以少判断几个位置,所以外层循环越执行到后面,内层循环的循环次数越少。
所以优化代码:
const arr = [5, 31, 9, 0, 22, 16, 45];
function sort(_arr) {
for(let i = 0; i < _arr.length; i++) {
for(let j = 0; j < _arr.length - i; j++) {
if(_arr[j] < _arr[j + 1]) {
const temp = _arr[j];
_arr[j] = _arr[j + 1];
_arr[j + 1] = temp;
}
}
}
return _arr;
}
console.log(sort([...arr]));
四、选择排序
1. 应用场景
选择排序和冒泡排序经常被一起提起,都是用来做序列排序的,只是内部执行机制不同而已。
但是很多人容易把冒泡排序和选择排序搞混,那就是还是对原理和机制了解不到位,只是记住了代码。
2. 分析
选择排序的执行原理:
- 从待排序的序列中拿到第一个数,假设它是当前序列的最小的数
- 然后依次对比序列中的其他数,如果该数比最小的数小,那就交换位置
- 第一轮结束之后进行第二轮比较,第二轮是从第二个位置开始,找剩余的数中最小的数
- 以此类推,直到全部待排序的数据元素的个数为零
比如有一组数据:[2, 5, 0, 1, 7, 9, 8, 6, 3]
如果升序排列,我们需要这个序列第一个位置的值最小,最后一个位置的值最大
那第一轮排序假设位置为 0 的元素 2 最小,对比其他元素,发现 0 实际最小,交换
那么 0 在整个序列中最小
第二轮在未排序的序列中,假设位置为 1 的元素 5 最小,对比其他元素,发现 1 最小,交换位置
一次类推
3. 代码实现
根据上面的分析,我们需要双层循环,外层循环控制每次在未排序的序列中找到一个最小的或者最大的数,内层循环控制当次寻找的两两对比。
还需要一个最小或者最大下标的保留值,用来找到实际最小或者实际最大的数时做数据交换
排序数组:[22, 51, 10, 43, 75, 90, 3]
排序要求:升序排列
假设每次在剩余数列里找到一个最小值的递增位置变量为 i,每次寻找最小下标交换位置的循环递增变量为 j
在寻找最小下标过程中,保留当前交换过的所有数的最小下标为 minIndex,每次初始值为当前剩余未排序序列中的最小下标
i j arr[j] arr[minIndex] 比较 minIndex保留值 数组
0 1 51 22 51 > 22 0 -
0 2 10 22 10 < 22 2 -
0 3 43 10 43 > 10 2 -
0 4 75 10 75 > 10 2 -
0 5 90 10 90 > 10 2 -
0 6 3 3 3 < 10 6 -
[3, 51, 10, 43, 75, 90, 22]
3 已经是所有数里的最小数,所以待排序的序列是数组中除 3 之外的数,minIndex 在第二循环时,假定值应为 1
而且第二轮循环不需要比较 3
i j arr[j] arr[minIndex] 比较 minIndex保留值 数组
1 2 10 51 10 < 51 2 -
1 3 43 10 43 > 10 2 -
1 4 75 10 75 > 10 2 -
1 5 90 10 90 > 10 2 -
1 6 22 10 22 > 10 2 -
[3, 10, 51, 43, 75, 90, 22]
[3, 10, 22, 43, 75, 90, 51]
i j arr[j] arr[minIndex] 比较 minIndex保留值 数组
2 3 43 51 43 < 51 3 -
2 4 75 43 75 > 43 3 -
2 5 90 43 90 > 43 3 -
2 6 22 43 22 < 43 6 -
[3, 10, 22, 43, 75, 90, 51]
...
还原成代码:
const arr = [5, 31, 9, 0, 22, 16, 45];
function sort(_arr) {
for(let i = 0; i < _arr.length; i++) {
let minIndex = i;
for(let j = i + 1; j < _arr.length; j++) {
if(_arr[j] < _arr[minIndex]) minIndex = j;
}
const temp = _arr[i];
_arr[i] = _arr[minIndex];
_arr[minIndex] = temp;
}
return _arr;
}
console.log(sort([...arr]));
五、指定数随机分成 N 份
1. 条件
有一个指定的数 M,将其随机分成指定的份额
2. 应用场景
微信红包算法。
3. 分析
假如要求把 M 分成非等额的 N 份,我们可以每次从 M 中拿出一个数,给 0 ~ n - 1 位置上的任一一个数,这样分 M 次即可。
4. 代码实现
转换成代码:
const num = 100, copies = 7;
let arr = new Array(copies).fill(0);
for(let i = 0; i < num; i++) {
const index = parseInt(Math.randon() * copies);
arr[index]++;
}
5. 拓展
如果要求变一下, 将 M 随机分成 N 份,要求每一份不能小于 x,且不能大于 y(逻辑条件:不存在 M 不够分或 M 分剩下的情况)。
思路:不能小于 x,那每个位置上的初始值就是 x。不能大于 y,那在分的这个过程中,如果某个位置上的数大于 y,那么这个位置,就不能再添加新的数据。
代码:
let sum = 100, copies = 20;
let min = 3, max = 6;
const arr = new Array(copies).fill(min);
while(sum !== 0) {
const index = parseInt(Math.random() * copies);
if(arr[index] < max) {
arr[index]++;
sum--;
}
}