算法复杂度,分为空间复杂度和时间复杂度
所谓空间复杂度就是程序执行时需要的内存空间 时间复杂度就是程序执行时需要的计算量(CPU)(时间)
算法思维分为:贪心,二分,动态规划
复杂度是一个数量级不是一个具体的数字
O(1)一次就够 :是指不循环操作
如
fun function(item={}){
return item?.a + item.b
}
O(n):指的是有一个单循环
fn function(arr){
For(let I =0;let i <arr.length;i++){}
}
O(n^2):数据量的平方 指的是有双循环
如
fun function(arr){
for(let i=0;I<arr.length;i++){
for(let j = 0;j<arr.length;j++){
}
}
}
O(logn):数据量的对数 指的是二分
比如说在一个顺序数组里面查找一个数字
先取中间一个数字,如果比要找的大,把前部分的再二分,再比较,再二分。。。
O(nlogn):数据量数据量的对象
就是先二分再循环
前端是重时间轻空间
将一个数组旋转k步
将数组的元素翻转到数组的前面,k是翻转几个元素
两种思路
1.循环k步,将数组的元素从尾部删除,插到头部
此方法的时间复杂度是O(n^2)
空间复杂度是O(1)
export const arrAction= (arr=[],num)=>{
if(!arr.length||!num)return arr
const step = Math.abs(num%arr.length)
for(let i =0;i<step;i++){//for循环的复杂度是O(n)
let n = arr.pop()
n!=null&& arr.unshift(n)//数组是有顺序的,往数组前面添加那么后面的元素的顺序也会变动,所以unshift的时间复杂度是O(n)
}
return arr
}
arrAction([1,2,3,4,5,7],3) //【4,5,7,1,2,3】
2,将数组从后面截取k个,再取前面的,两者合在一起即可
此方法时间复杂度是O(1)
空间复杂度是O(n)
export const sliceAction=(arr=[],num)=>{
if(!arr.length||!num)return arr
const step = Math.abs(num%arr.length)
const arr1 = arr.slice(-step)//slice是返回新数组,并没有修改原数组,所以slice的速度很快 所以复杂度是 O(1)
const arr2 = arr.slice(0,arr.length-step)
return [...arr1,arr2]
}
sliceAction([1,2,3,4,5,7],3)//【4,5,7,1,2,3】
3.在数组元素原有的索引上面去操作
这样修改之后他们的索引有一个规律:
比如说传进去的k是3,数组的后面三个元素的索引各自减去k+1就是新的索引,那其他的元素的索引就是各自加3,入上图所示。
但是此方法的时间复杂度是O(n)
空间复杂度是O(1)
以时间复杂度来说,远不如第二种
这两个算法的若论差异好坏,这里可以做一下性能测试
console.time('arrAction')
arrAction(arr1,90000)
console.timeEnd('arr1'); //8000ms
console.time('sliceAction')
arrAction(arr1,90000)
console.timeEnd('sliceAction'); //1ms
当然是第二种更优
检验算法的鲁棒性(单元测试)
这里使用的jest
import {arrAction,sliceAction} from "./arrAction.js"
// describe测试描述
describe('数组旋转',()=>{
it('是否相等',()=>{
let res = arrAction([1,2,3,4,5,7],3)
expect(res).toEqual([4,5,7,1,2,3]) //断言
})
it('数组为空',()=>{
let res = arrAction([],3)
expect(res).toEqual([]) //断言
})
it('num是负值',()=>{
let res = arrAction([[1,2,3,4,5,7],-3)
expect(res).toEqual([4,5,7,1,2,3]) //断言
})
it('num不是数字',()=>{
let res = arrAction([1,2,3,4,5,7],'abc')
expect(res).toEqual([1,2,3,4,5,7]) //断言
})
})
// 运行单元测试用例
npm ject 'src/...写的测试用例的url'
判断字符串是否括号匹配
- 一个字符串s可能包括{},[],()
- 判断s是否是括号匹配的
- 如(a{b}c)匹配,而{a(b或{a(b}s)就不匹配
const matchFunc = (left,right){
if(left==='{' && right === '}') return true
if(left==='[' && right === ']') return true
if(left==='(' && right === ')') return true
return false
}
const matchBracket = (str){
let leftStr = "{[("
let rightStr = ")]}"
let stack = []
if(str.length==0)return true
for(let i = 0;i<str.length;i++){
let st = str[i]
if(leftStr.includes(st)){
stack.push(st)
}else if(rightStr.includes(st)){
let top = stack[stack.length-1]
if(matchFunc(top,st)){
stack.pop()
}else{
return false
}
}
}
return stack.length == 0
}
// 功能测试
const str = "a{b(2[f])}"
console.info(matchBracket(str));
// 单元测试
describe('括号匹配',()=>{
it('正常情况',()=>{
const str = 'a{b[c(e)]}'
const res = matchBracket(str)
expect(res).toBe(res) //toBe 就是boolean的判断
})
it('不匹配',()=>{
const str = 'a{b[c{e)]}'
const res = matchBracket(str)
expect(res).toBe(res) //toBe 就是boolean的判断
})
it('空字符',()=>{
const str = ''
const res = matchBracket(str)
expect(res).toBe(res) //toBe 就是boolean的判断
})
})
两个栈模拟一个队列
- 需要具有添加(入队) 删除头部的元素(出队),和获取长度的功能
栈:是先进后出,
队列是先进先出
代码:
export class MyQueue {
private stack1 = []
private stack2 = []
// add就是入队
add(n){
this.statck1.push(n)
}
// 出队
// 三步
// 队列的形式先进先出,但是栈是先进后出,所以这里通过两个栈做模拟这种先进先出
// 原则:保持先进先出
delete(){
let res
let stack1 = this.stack1
let stack2 = this.stack2
// 先将stack1中的值放到stack2中
while(stack1.length){
let t = stack1.pop()
if(t!==null){
stack2.push(t)
}
}
res = stack2.pop() //得到删除的元素
while(stack2.length){
let s = stack2.pop()
if(s!==null){
stack1.push(s)
}
}
return res||null
}
get length(){
return this.stack1.length
}
}
// 功能测试
const q = new MyQueue()
q.add(1)
q.add(2)
q.add(3)
console.log(q.length);
console.info(q.delete());
// 单元测试
describe('两个栈一个队列',()=>{
it('add And length',()=>{
const q = new MyQueue()
expect(q.length).toBe(0)
q.add(100)
q.add(200)
q.add(300)
expect(q.length).toBe(3)
})
it('delete',()=>{
const q = new MyQueue()
expect(q.delete()).toBe(0)
q.add(100)
q.add(200)
q.add(300)
expect(q.delete()).toBe(2)
})
})
反转单向链表
链表和数组的区别
链表和数组都是有序的结构
链表是查询慢O(n) 新增删除快O(1)
数组是查询快O(1) 新增删除慢O(n)
数组是连续存储的,查询快是因为它有下标
链表是零散存储的结构,不需要像数组一样去前后处理空间
首先根据数组创建单向链表
// 根据数组创建单向链表
export function createLinkList(arr){
let length = arr.length
if(length == 0) throw new Error('Array is empty')
let curNode = {
value:arr[length-1]
}
if(length ==1)return curNode
for(let i = length-2;i>=0;i--){
curNode = {
value:arr[i],
next:curNode
}
}
return curNode
}
// 功能测试
// let res = createLinkList([1,2,3,4])
// console.info(res);
反转单向链表
export function flipList(node){
let curNode = null
let preNode = null
let nextNode = node
while(nextNode){
if(curNode&&!preNode){
// 防止循环引用 改变方向之后第一个就没有next了
delete curNode.next
}
// 反转指针
if(curNode&&preNode){ //中间的内容 因为最后一个是没有next的,所以最后一个是走不到当前这一步的
curNode.next = preNode
}
// 整体向后移动指针
curNode = nextNode
nextNode = nextNode?.next
preNode = curNode
}
// 最后一个补充:当nextNode空时 此时curNode尚未设置next
curNode.next = preNode
return curNode
}
// 功能测试
// let res = createLinkList([1,2,3,4])
//const info = flipList(res)
//console.info(info)
//单元测试
describe('反转单向链表',()=>{
it('单个元素',()=>{
const node = flipList({value:100})
expect(node).toEqual({value:100})
})
it("多个元素",()=>{
const node = createLinkList([100,200,300])
const node1 = flipList(node)
expect(node1).toEqual({
value:300,
next:{
value:200,
next:{
value:100
}
}
})
})
})
链表实现队列
链表和数组,哪个实现队列更快
队列是一种逻辑结构,
链表和数组是一种物理结构
数组实现队列是有性能问题
链表是非连续存储,add,delete都很快,
数组是连续存储,push很快,shift很慢
所以链表实现队列更快一些
// 链表实现队列
export class myQueue{
private head = null
private tail = null
private len = 0 //维护length,遍历获取length,时间复杂度太高
// 入队操作
add(n){
const newNode = {
value:n,
next:null
}
// 处理head
if(this.head == null){
head = newNode
}
// 处理tail
let tailNode = this.tail
if(tailNode){
tailNode.next = newNode
}
this.tail = newNode
this.len ++
}
// 出队,在head位置
delete(){
const headNode = this.head
if(this.len<=0)return null
if(headNode == null) return null
// 取值
const value = headNode.value
this.head = headNode.next
this.len --
return value
}
get length(){
return this.len
}
}
// 功能测试
const q = new myQueue
q.add(100)
q.add(200)
q.add(300)
console.log('length',q.length)
// 单元 测试
descript('链表实现队列',()=>{
it('add and length',()=>{
const a = new myQueue()
a.add(100)
a.add(200)
a.add(300)
expect(a.length).toBe(3) //除对象之外的断言用toBe
})
it('delete',()=>{
const a = new myQueue()
expect( a.delete()).toBeNull() //toBeNull断言为null
a.add(100)
a.add(200)
a.add(300)
expect( a.delete()).toBe(3) //除对象之外的断言用toBe
expect( a.delete()).toBe(2) //除对象之外的断言用toBe
a.delete()
expect( a.delete()).toBe(1) //除对象之外的断言用toBe
a.delete()
expect( a.delete()).toBeNull() //toBeNull断言为null
})
})
性能测试
const q = new MyQueue()
console.time('queue with list')
for(let i = 0; i<10 *1000;i++){
q.add(i) //入队
}
for(let i = 0; i<10 *1000;i++){
q.delete()//出队
}
console.timeEnd('queue with list') //17ms
const q2 = []
console.time('queue with Array')
for(let i = 0; i<10 *1000;i++){
q2.push(i) //入队
}
for(let i = 0; i<10 *1000;i++){
q.shift() //出队
}
console.timeEnd('queue with Array') //420ms
- 数据结构的选择要比算法优化更重要
- 时间复杂度的敏感性,比如length不能遍历查找
二分查找
// 循环查找
export function binarySearch1(arr, target) {
let length = arr.length
if (length == 0) return -1
let startIndex = 0
let endIndex = length - 1
while (startIndex <= endIndex) {
let midIndex = Math.floor((startIndex + endIndex) / 2)
let midValue = arr[midIndex]
if (target > midValue) {
startIndex = midIndex + 1
} else if (target < midValue) {
endIndex = startIndex - 1
} else {
return midIndex
}
}
return -1
}
// 递归查找
export function binarySearch2(arr, target, startIndex, endIndex) {
let length = arr.length
if (length == 0) return -1
if (startIndex > endIndex) return -1
if (!startIndex) startIndex = 0
if (!endIndex) endIndex = length - 1
let midIndex = Math.floor((startIndex + endIndex) / 2)
let midValue = arr[midIndex]
if (midValue > target) {
return binarySearch2(arr, target, startIndex, midIndex - 1)
} else if (midValue < target) {
return binarySearch2(arr, target, midIndex + 1, endIndex)
} else {
return midIndex
}
}
// 功能测试
const arr = [10,20,30]
console.info(binarySearch2(arr,20)) //1
// 单元测试
describe('二分查找',()=>{
it('正常情况',()=>{
let arr = [1,2,3,4]
let index = binarySearch1(arr,3)
expect(index).toBe(2)
})
it('空数组',()=>{
let arr = []
let index = binarySearch1(arr,3)
expect(index).toBe(-1)
})
it('找不到target',()=>{
let arr = [1,2,3,4,5]
let index = binarySearch1(arr,6)
expect(index).toBe(-1)
})
})
二分循环与递归的性能比较
console.time('binarySearch1')
for(let i =0;i<100*1000;i++){
binarySearch1(arr,target)
}
console.timeEnd('binarySearch1') //17ms
console.time('binarySearch2')
for(let i =0;i<100*1000;i++){
binarySearch2(arr,target)
}
console.timeEnd('binarySearch2') //34ms
虽然他们数量级是一样的,时间复杂度都是是(Ologn),但是因为
循环是一个函数 ,递归要频繁调用函数, 因为函数的调用也会消耗时间。
所以所以非递归性能更好一点
但是相对来说 递归逻辑更清晰
- 所以凡是有序,都可以二分,
- 凡是二分,时间复杂度都包含O(logn)
二分查找查找两数之和
function findNum(arr, target) {
let res = []
let length = arr.length
let i = 0
let j = length-1
if(length === 0)return res
while (i < j) {
let n1 = arr[i]
let n2 = arr[j]
let n = n1 + n2
if (n > target) {
j--
} else if (n < target) {
i++
} else {
res.push(n1)
res.push(n2)
break
}
}
return res
}
二叉树前序中序后序遍历
const treeNode = {
value:10,
left:{
value:5,
left:null,
right:null,
},
right:{
value:7,
left:{
value:0,
left:null,
right:null
},
right:{
value:0,
left:null,
right:null
}
}
}
// 二叉树前序遍历
function preOrderTraverse(node){
if(node == null)return
console.log(node.value);
preOrderTraverse(node.left)
preOrderTraverse(node.right)
}
// 二叉树中序遍历
function midOrderTraverse(node){
if(node == null)return
preOrderTraverse(node.left)
console.log(node.value);
preOrderTraverse(node.right)
}
// 二叉树后序遍历
function afterOrderTraverse(node){
if(node == null)return
preOrderTraverse(node.left)
preOrderTraverse(node.right)
console.log(node.value);
}
动态规划
何为动态规划
- 就是把一个大问题,拆分成小问题,逐级向下拆解
- 用递归的思路去分析问题 再改为循环来实现
- 算法三大思维:贪心,二分,动态规划
递归实现
getMath(num){
if(num<=0)return 0
if(num ==1)return 1
return this.getMath(num-1)+this.getMath(num-2)
}
this.getMath(4) //3
使用递归的话时间复杂度是2的n次方,时间复杂度太高
这里可以使用循环降低时间复杂度为n的平方
let funcAction = (num)=>{
if(num<=0)return 0
if(num ==1)return 1
let n1 = 1
let n2 = 0
let res =0
for(let i =2;i<=num;i++){
res = n1 + n2
n2 = n1
n1 = res
}
return res
}
console.log("funcAction",funcAction(4)); //3
青蛙跳台阶
要跳到1级台阶,就一种方式:f(1) = 1
要跳到2级台阶,就二种方式:f(2) = 2
要跳到n级台阶,f(n) = f(n-1)+f(n-2)
将数组中的0转移到数组末尾
在原有的数组中操作,不能重建新数组
let moveZero=(arr)=>{
let length = arr.length
if(length == 0)return
let zeroLength = 0
for(let i =0;i<length-zeroLength;i++){
if(arr[i]==0){
arr.push(0)
arr.splice(i,1)
i--;
zeroLength++
}
}
return arr
}
const arr = [1,2,4,0,6,0,3,0,5]
console.log('3333',moveZero(arr));
双指针实现
- 定义j指向第一个0,i指向j后面第一个非0
- 交换j和i的值 向后移动
- 只遍历一次 所以时间复杂度是O(n)
const moveZero(arr){
const length = arr.length
if(length === 0) return
let i;
let j = -1 //让j指向第一个0
for(i=0;i<length;i++){
if(arr[i]==0){
if(j<0){
j = i
}
}
if(arr[i]>0&&j>=0){
let n = arr[i]
arr[i] = arr[j]
arr[j] = n
j++
}
}
}
求一个连续字符中最多的字符是什么,以及个数(如’aaabbbcccnnnnn’)
双指针思路
- 定义指针i 和 j,j不动,i继续移动
- 如果i和j一直相等 则i一直继续移动
- 直到i和j的值不相等,记录这个字符的长度和字符,并让j追上i, 继续进行第一步
时间复杂度是O(n)
const findStrMaxCountChar(str)=>{
let length = str.length
if(!length)return
let res = {
char:"",
length:0
}
let i =0;
let j =0;
let tempLenth =0;
for(;i<length;i++){
if(str[i]== str[j]){
tempLenth ++
}
// 如果i对应的字符和j对应的字符不相等或者到了末尾
if(str[i]!=str[j]||i==length-1){
if(tempLenth>res.length){
res.char = str[j]
res.length = tempLenth
}
tempLenth =0//reset
if(i<length-1){
j = i //让j追上i
i-- //细节,不然会错过当前这个字符对比
}
}
}
}
数组快速排序
固定算法,固定思路
找到中间位置midValue
遍历数组,小于midValue放在left,否则放到right
继续递归 最后concat拼接 返回
时间复杂度是O(n*logn)
const sortArr = (arr)=>{
let length = arr.length
if(!length)return 0
let leftArr = []
let rightArr = []
let midIndex = Math.floor(length/2)
let midValue = arr.splice(midIndex,1)[0]
for(let i=0;i<arr.length;i++){
let n = arr[i]
if(n>midValue){
leftArr.push(n)
}else{
rightArr.push(n)
}
}
return sortArr(leftArr).concat(
[midValue],
sortArr(rightArr)
)
}
如果是使用slice
更推荐使用slice,不修改原数组
const sortArr = (arr) => {
let length = arr.length
if (!length) return 0
let leftArr = []
let rightArr = []
let midIndex = Math.floor(length / 2)
let midValue = arr.slice(midIndex, midIndex + 1)[0]
//O(n)
for (let i = 0; i < length; i++) {
if (i !== midIndex) {
let n = arr[i]
log(n)
if (n > midValue) {
leftArr.push(n)
} else {
rightArr.push(n)
}
}
}
return sortArr(leftArr).concat([midValue], sortArr(rightArr))
}
求1-100中的回文
回文就是指正着读和反着读一模一样
- 思路1: - 使用数组反转、比较
数字转换为字符串 再转换为数组
数组reverse,再join为字符串
前后字符串做对比
findPalindromeNumber(max) {
let res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
const s = i.toString()
if (s == s.split('').reverse().join('')) {
res.push(i)
}
}
return res
},
- 思路二字符串头尾比较
数字转字符串
字符串头尾进行比较
也可以用栈 像括号匹配,但要注意奇偶数
findPalindromeNumber2(max) {
let res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
const s = i.toString()
const length = s.length
let flag = true
let startIndex = 0
let endIndex = length - 1
while (startIndex < endIndex) {
if (s[startIndex] !== s[endIndex]) {
flag = false
break
} else {
startIndex++
endIndex--
}
}
if (flag) res.push(i)
}
return res
},
- 思路三 生成翻转数
就是数字不变成字符串了,也不变成数组了,直接把数字生成翻转数
使用%和Math.floor生成翻转数
前后数字进行对比
findPalindromeNumber3(max) {
let res = []
if (max <= 0) return res
for (let i = 1; i <= max; i++) {
let n = i
let rev = 0
// 翻转数字 比如123
while (n > 0) {
rev = rev * 10 + (n % 10)//321
n = Math.floor(n / 10) //0
}
if (i == rev) res.push(i)
}
return res
},
console.log('====================================')
console.log(this.findPalindromeNumber3(100)) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99]
console.log('====================================')
思路1 看似时间复杂度是O(n),但是数组转换和翻转都需要时间,所以会更慢
思路2和思路3相比,操作数字更快,(因为电脑的原因就是计算机)
用栈不合适 因为用栈一般是用数组实现,会慢
所以尽量不要转换数据结构,尤其数组这种有序结构
尽量不要用内置API, 如reverse,不好识别复杂度
数字操作最快,其次是字符串
高效的字符串前缀匹配
-
比如说针对一个字符串
针对第一个字母对应26个字母,第二个对应26个字母
然后往下拆分继续26个 -
以hash的形式编写数据结构
对象的结构
或者map的结构
对象或hash通过key查询,时间复杂度是O(1) -
遍历数组,时间复杂度至少是O(n)起步,(n是数组长度)
而改为树 时间复杂度降低到O(m) (m是单词的长度)
PS:哈希表(对象)通过key查询,时间复杂度是O(1) -
考虑优化原始数据结构
有明确范围的数据(比如26个英文字母),考虑使用哈希表(对象)
以空间换时间,定义数据结构最重要 -
算法决定了数据的下限,数据结构决定了数据的上限
数字千分位格式化
- 将数字千分位格式化 输出字符串
如输入12050100 输出字符串12,050,100
(注意:逆序判断)
思路
- 转换为数组 reverse,每三位拆分
- 使用正则表达式
- 使用字符串拆分
- 计算机的东西能简单就别复杂
什么简单? 数字简单
就是说如果能通过数字,你就别转成字符串
如果能通过操作字符串实现,就别用数组
如果能通过手写API实现,就不要用语法糖或者正则表达式
使用数组 转换影响性能
使用正则表达式 性能性差
使用字符串 性能较好
** 使用数组**
format1(num) {
let n = Math.floor(num) //只操作整数
let s = num.toString()
let arr = s.split('').reverse()
return arr.reduce((prev, current, index) => {
if (index % 3 == 0) {
return current + ',' + prev
} else {
return current + prev
}
}, '')
},
使用字符传
format2(num) {
let n = Math.floor(num)
let s = n.toString()
let str = ''
let length = s.length -1 //下面的图解读这一句
for (let i = length - 1; i >= 0; i--) {
const j = length - i
if (j % 3 == 0) {
if (i == 0) {
str = s[i] + str
} else {
str = ',' + s[i] + str
}
} else {
str = s[i] + str
}
}
return str
},
let length = s.length -1
切换字母大小写
输入一个字符串 切换其中字母的大小写
如,输入字符串123abc,输出123ABC
思路: 正则表达式
ASCII码的方式
** 正则的方式**
switchLetterCase(s) {
let res = ''
let length = s.length
if (!length) return res
let reg1 = /[a-z]/
let reg2 = /[A-Z]/
for (let i = 0; i < length; i++) {
let n = s[i]
if (reg1.test(n)) {
res += n.toUpperCase()
} else if (reg2.test(n)) {
res += n.toLowerCase()
} else {
res += n
}
}
return res
},
ASCII码的方式
switchLetterCase2(s) {
let res = ''
let length = s.length
if (!length) return res
for (let i = 0; i < length; i++) {
let c = s[i]
let n = c.charCodeAt(0)
if (n >= 65 && n <= 90) {
res += c.toLowerCase()
} else if (n >= 97 && n <= 122) {
res += c.toUpperCase()
} else {
res += c
}
}
return res
},
为什么0.1 + 0.2 !== 0.3
- 计算机使用二进制存储数据
- 整数转换二进制没有误差,但是小数是有误差的,无法完全准确的二进制表示,是无尽的小数
- 项目中怎么避免呢 使用mathjs三方库,如果经常用小数的话
删除链表中连续相同值的节点
function ListNode (val, next = null) {
this.val = val
this.next = next
}
const getListFromArray = a => {
let dummy = new ListNode()
let pre = dummy
a.forEach(x => (pre = pre.next = new ListNode(x)))
return dummy.next
}
const getArrayFromList = node => {
let a = []
while(node){
a.push(node.val)
node = node.next
}
return a
}
function deleteDuplicates (head) {
// 空指针或者只有一个节点不需要处理
if (head == null || head.next === null) return head
let dummy = new ListNode()
let oldLinkCurrent = head
let newLinkCurrent = dummy
while (oldLinkCurrent) {
let next = oldLinkCurrent.next
if (next && oldLinkCurrent.val == next.val) {
while (next && oldLinkCurrent.val == next.val) {
next = next.next
}
oldLinkCurrent = next
} else {
newLinkCurrent = newLinkCurrent.next = oldLinkCurrent
oldLinkCurrent = oldLinkCurrent.next
}
}
logList(dummy.next)
return dummy.next
}
console.log(deleteDuplicates(getListFromArray([1,2,3,3,4,4,5]))); // {value:1,next:{value:2,next:{value:5}}}