算常数总和-性能好-时间复杂度 O(1):
飞蛾公式:
1 | (n / 2) \*(1 + n) |
题库:leetcode
- 性能上:for > forEach > map
- for in 是键值的遍历,for of 是 es6 的产物,是迭代器的遍历方式,需要判断遍历对象是否存在
- 本质上 forEach 和 map 是用来遍历 Array
- 反转字符串:
1
str.split('').reduceRight((prev,curr)=>prev+curr)
时/空间复杂度
计算时间复杂度?
求数字的总和
1 | const numbers = [1, 3, 10, 50] |
斐波那契数列
1 | // n = 5 |
- 总结:使用渐进分析法给每种算法分析以下 3 种情况,最后得到时间复杂度
1
2
3Best Case: number = 1 OR number = 2 => O(1) // 数值少
Average Case: O(n)
Worst Case: number = 27,277 => O(n) (Improved: O(sqrt(n))) // 数值大
判断是否为 2 的幂
- 使用位运算符:二进制 与 &
- &:按位相乘,有 0 为 0,示例:4 & 7 ==> 100 & 111 ==> 100 ==> 4(最终结果)
1
2
3
4function isPowerOfTwo(number) {
if (number < 1) return false
return (number & (number - 1)) === 0 // O(1), (number & (number - 1))始终返回true
}
递归算法
阶乘算法
1 | function fact(number) { |
以递归实现斐波那契数列来证明递归不全是最好的实现方式
- 问题:建立了一个嵌套树,从 n=4 时会产生多余的相同结果
1
2
3
4
5
6
7function fib(n) {
if (n === 0 || n === 1) return 1
return fib(n - 1) + fib(n - 2)
// 每次执行回调函数都有两个函数要执行,表示时间长度是指数级增长
}
// 基于普通循环的时间复杂度为 O(n)
// 基于递归实现的时间复杂度为 O(n^2)
动态编程(Dynamic Programming)
- 在第一个函数也就是该方法的全局中添加存储空间的变量
- 在添加一个记忆变量的形式变量
- 在每次该回调函数结束前,把存储变量赋值给记忆变量
- 判断记忆变量有这个值就返回记忆变量的值不执行后面的内容避免冗余
以递归实现的斐波那契为例进行改造
1 | function fib(n, memo) { |
自下而上的方法
建立一个存储所有这些值的存储,将所有结果存储在此存储中的 bonachi 序列。执行方法的过程中,不断生成数据,知道目标值生成完成函数执行。当数据够多,一旦检索到 nf 元素然后我们取消完成函数执行。
- 使用场景:
- 在循环中使用底部应用程序方法
- 带有递归的动态规划方法
搜索算法
线性搜索(可以是任意类型的数据)
- 从头到尾遍历查找,找到对应的值就返回并停止,只返回第一个目标结果就停止
- 适用于有序和无序列表,所以列表不必排序
- 当然可以用系统内置的方法 find(), findIndex()
1 | // 搜索对象 |
二分搜索
需要先排序,然后根据中有两边的值,从中间划分查找
基于循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const arr = [1, 5, 9, 13, 99, 100]
function findElement(sortedArr, element) {
let startIndex = 0
let endIndex = sortedArr.length - 1
while (startIndex <= endIndex) {
const middleIndex = startIndex + Math.floor((endIndex - startIndex) / 2)
if (element === sortedArr[middleIndex]) return middleIndex
if (sortedArr[middleIndex] < element) startIndex = middleIndex + 1
else endIndex = middleIndex - 1
}
}
console.log(findElement(arr, 9))
// Best Case: 目标刚好正中间 O(1)
// Average Case: 趋于O(log n)
// Worst Case: 目标在开头或结尾 O(log n)递归的二分查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27const arr = [1, 5, 9, 13, 99, 100]
// sortedArr: 原数组 element: 目标元素 offset:索引偏移量,目标元素在新数组与旧数组的偏差
function findElement(sortedArr, element, offset) {
let startIndex = 0
let endIndex = sortedArr.length - 1
while (startIndex <= endIndex) {
const middleIndex = Math.floor((endIndex - startIndex) / 2)
if (element === sortedArr[middleIndex]) return middleIndex + offset // 返回目标元素在原数组的索引:当前元素索引+offest
if (sortedArr[middleIndex] < element) {
startIndex = middleIndex + 1
offset = offset + middleIndex + 1
} else {
endIndex = middleIndex
}
return findElement(
sortedArr.slice(startIndex, endIndex + 1),
element,
offset
) // endIndex +1 是为了防止元素出现在最末尾
}
}
console.log(findElement(arr, 9, 0))
// a: 1 (这里看return那一句时递归的,只执行一次,所以为1)
// b: 2 (这里看切分成多少次,从代码看数组被切分成两次)
// O(n^logb a) => O(n^log2 1) => O(n^0) => O(1)
// overall the algorithms:O(n^logb alog n) => O(1 * log n) => O(log n)
- 与基于循环的解决方案的主要区别: 是在循环中只是改变了索引然后计算不同的中间指标
- 在递归方案中总是计算相同的中间指标,改变 index 来调用新函数,再次使用原始数组的子集。即真的切数组为不同函数调用分成切片,在不同的数组切片上操作
主定理(Master Theorem)
- 递归的运行时间: O(nlogba)
- 公式里的变量知识面意思?
- a:等于子问题数(递归拆分数)多久拆分一次数组
- b:等于相对子问题大小(每次拆分的输入减少)
- f(n):等于递归外部运行时,指整个代码
- 使用主定理得到算法的整体时间复杂度的三种情况:
- case 1:递归步骤,完成最多工作,具有最大的运行时间,算法的运行时间为 O(nlogba)
- case 2:非递归部分做更多工作:O(fn(n))
- case 3:递归开始时的相同工作量推到整个算法的运行时间 O(nlogbalog n)
排序算法
冒泡算法
1 | function sort(arr) { |
快速排序
1 | function sort(arr) { |
归并排序
1 | function sort(arr) { |
空间复杂度
集合 sets(数组)算法
笛卡儿乘积算法
1 | // 衣服制造问题--衣服尺码颜色组合问题 |
全排列 无重复
1 | function getPermutations(options) { |
全排列 有重复
1 | function getPermutations(options, length) { |
复杂的算法和解决的方法
结构化方案
- 验证输入的问题
- 思考问题和提出解决方案
- 编写第一个版本的解决方案
- 验证结果
- 推到时间复杂度
- 探索替代方案
背包问题
排列出从所给的 items 中拿出不超过背包重量的 item
1 | function knapsack(items, cap, itemIndex) { |
改进版
1
2
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48function knapsackFn(items, cap, itemIndex, memo) {
if (memo[cap][itemIndex]) return memo[cap][itemIndex]
if (cap === 0 || itemIndex < 0) return { items: [], value: 0, weight: 0 }
if (cap < items[itemIndex].weight)
return knapsackFn(items, cap, itemIndex - 1, memo)
const sackWithItem = knapsackFn(
items,
cap - items[itemIndex].weight,
itemIndex - 1,
memo
)
const sackWithoutItem = knapsackFn(items, cap, itemIndex - 1, memo)
const valueWithItem = sackWithItem.value + items[itemIndex].value
const valueWithoutItem = sackWithItem.value
let resultSack
if (valueWithItem > valueWithoutItem) {
const updateSack = {
items: sackWithItem.items.concat(items[itemIndex]),
value: sackWithItem.value + items[itemIndex].value,
weight: sackWithItem.weight + items[itemIndex].weight,
}
resultSack = updateSack
} else {
resultSack = sackWithoutItem
}
memo[cap][itemIndex] = resultSack
return resultSack
}
// 生成空的数组,from从高的值创建一个新数组, fill不会创建新数组, 这是创建内存的最好方法
function knapsack(items, cap, index) {
const mem = Array.from(Array(cap + 1), () =>
Array(items.length).fill(undefined)
)
return knapsackFn(items, cap, index, mem)
}
const items = [
{ name: 'a', value: 3, weight: 3 },
{ name: 'b', value: 6, weight: 8 },
{ name: 'c', value: 10, weight: 3 },
]
const maxCap = 8 // 背包最大容量
const allPermutations = knapsack(items, maxCap, items.length - 1)
console.log(allPermutations)
// Time Complexity (without memoization): O(2^n)
// Time Complexity (with memoization): O(2*C) C指的是容量贪心算法 vs 动态算法
- 解决背包问题的方式: 贪心 和 动态
贪心背包算法
1 | // 一次只能返回一个满足要求的选项 |
找零问题
1 | // 存储对象的值 |
暴力找零
1 | // 存储对象的值 |
Set 和 new Map
Set
并集:利用 set 特性给两个数组去重
1
2
3
4
5const arr1 = [1, 22, 38, 31, 18]
const arr2 = [1, 122, 138, 19, 18]
const setArr1 = new Set(arr1)
const setArr2 = new Set(arr2)
const union = newSet([...setArr1, ...setArr2])交集:
1
2
3
4
5
6const intersection = new Set([...setArr1].filter((item) => setArr2.has(item)))
````
3. 差集:
```js
const intersection = new Set([...setArr1].filter((item) => !setArr2.has(item)))判断元素是否存在,时间复杂度为 O(1),速度快
1
2
3const nums = [11, 22, 38, 1, 8]
const newNums = new Set(nums)
console.log(newNums.has(11))实现队列,队尾添加,队头删除/栈的数据结构,在表尾即可完成添加和删除
1
2
3
4const alignment = new Set()
alignment.add(10)
alignment.add(11)
alignment.delete(alignment.values().next().value)
new Map
1 | // 这种数据结构适用 |
Object 与 new Map
- 创建一个 new Map(),可以适用.set(key,value)设置该 Map 的键值,使用.get(key)查找 Map 中该键的值,.has 是否存在该键
- Map 的键值支持正则,Map 是可迭代的,例如:forEach 循环 for…of…循环 ,Object 是不能直接迭代的
- 适用场景:
- 当插入的顺序是需要考虑的,并且使用除 String 和 Symbol 以外的键名是,使用 Map
- 需要遍历键值对并且需要考虑顺序,优先 Map
- 频繁增删改查的场景使用 Map
- 表单自定义字段使用 Map
1 | const map4 = new Map() |
元素的顺序和长度
- Map 保持对长度的跟踪,使其能够在 O(1)复杂度中进行访问
- Object 想要获得对象的属性长度,需要手动进行迭代,使其文 O(n)复杂度,属性长度为 n
- Map 始终保持按插入顺序返回键名
- Object 从 es6 开始,String 和 Symbol 键是按顺序保存起来的,但是通过隐式转换保存成 String 的键是乱序的
Set 的时间复杂度
Set 操作 | 时间复杂度 |
---|---|
Set.prototype.get() | O(1) ~ O(logn) |
Set.prototype.add() | O(1) ~ O(logn) |
Set.prototype.has() | O(1) ~ O(logn) |
Set.prototype.forEach() | O(n) |
Set.prototype.entries() | O(n) |
Set.prototype.values() | O(n) |
Set.prototype.keys() | O(n) |
哈希表
- 使用一层循环来获取需要双层循环的结果
- 列举:求两数之和:
- 使用 new Map() 创建一个空的 map 数组
- 在遍历的过程中通过 map 数组使用 has 方法判断 target-数组中的某一项得到的值是否存在
- 存在就使用 get 方法获取对应值的小标并返回
- 不存在就使用 set 方法设置下标给 map 数组
- 如果把 for 循环换成 forEach 有问题
1 | var twoSum = function (nums, target) { |
回溯算法
- 本质:暴力穷举算法
- 列如:给定两个整数 n 和 k,返回范围[1,n]中所有可能的 k 的组合,不要求顺序
- 其中:k 指的是组合的长度,如[2,4],并且按照数学规范[最小值,最大值], n 指最大区间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const combine = (n, k) => {
const ans = []
const getAns = (val, arr) => {
arr.push(val) // 将获取到的值添加到数组中,并计算如果数组长度和k的长度一样就直接添加到对应数组中
if (arr.length === k) {
ans.push(arr)
return
}
if (n - val < k - arr.length) return // n-val < k-arr.length ,是控制组合的展示形式符合数学规范,并且最大值不在选择范围
for (let i = val + 1; i <= n; i++) {
// 如果长度不满足就再次进入循环知道满足
getAns(i, [...arr])
}
}
for (let i = 1; i <= n - k + 1; i++) {
getAns(i, []) // 传一个空数组,用来存储k的组合,之后在把所有k组合存进ans中
}
return ans
}
动态规划 DP 算法
- 设定一个数组,存放到达第 n 层的跳法,假定只能从当前元素的左和下移动
- 找一个位置,分析到达这个位置的前置条件,并将这些条件以二维数组的方式相加
- 例如:
1
a[i][j] = a[i-1][j] + a[i][j-1]
- 倒着分析,找关系
- 处理极值
- arr[i][0] = 1
- arr[0][j] = 1
1 | // 生成一个二维数据的空数组 |