时间复杂度
-
O(1) 一次就够(数量级)
ex: 只走了一次处理逻辑const func = (a) => { return a }
-
O(n) 和传输的数据量一样
ex:时间复杂度和当前arr的长度一致const func = (arr) => { for(let i = 0; i < arr.length; i++){ console.log(arr[i]) } }
-
O(n^2) 数据量的平方
exconst func = (arr) => { for(let i = 0; i < arr.length; i++){ for(let j = 0; j < arr.length; j++){ console.log(arr[j]) } } }
-
O(logn) 数据量的对数(数量级) 100 => 10
ex: 二分算法,每次把数据砍掉二分之一。
[1,2,3,4,5,6,7,8,…] 找到6,可以通过砍掉中间的来比对 -
O(nlogn)
空间复杂度
- O(1) 一次就够(数量级)
ex: 像这样,不管数组的长度是多少,数组的内存大小是相对固定的const func = (arr: []) => { arr[1] = 1 arr[2] = 2 arr[3] = 3 arr[4] = 4 return a }
- O(n)
ex:这里定义了一个新的数组,并且对相关的数组进行了赋值const func = (arr) => { let arr2 = [] for(let i = 0; i < arr.length; i++){ arr2[i] = arr[i] } }
单元测试
jest
判断数组是否相等 toEquel
判断bool是否相等 toBe
二分查找
/**
* desc: 给一个有序的数组,[10, 20, 50, 70, 90] 查到50下标
* 算法:二分法查找
*/
// 循环的方式
export const binarySearch1 = (arr: number[], target: number) => {
let startIndex: number = 0
let endIndex: number = arr.length - 1
while (startIndex < endIndex) {
let mdIndex = Math.floor((endIndex + startIndex) / 2)
let value = arr[mdIndex]
// 如果正好是中间的数字
if (value === target) return mdIndex
// 在左边
if (target < value) {
endIndex = mdIndex - 1
}
// 在右边
if (target > value) {
startIndex = mdIndex - 1
}
}
return -1
}
// 递归的方式
const binarySearch2 = (arr: number[], target: number, startIndex?: number, endIndex?: number): number => {
if(arr.length <= 0) return -1
if(startIndex == null) startIndex = 0
if(endIndex == null) endIndex = arr.length - 1
if(startIndex > endIndex) return -1
// 找到中间下标和value
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, 40, 50, 70, 90, 120]
let res = binarySearch2(arr, 90)
console.log(res, 'res');
这里写了两种方式,递归和循环都可以完成问题,时间复杂度为O(logn),但是如果一定得使用最好的方式,循环更好,因为递归调用方法也会耗时。
将一个数组旋转k步
方案一 unshift pop
时间复杂度:O(n^2)
unshift 也是一个O(n) 的结构
方案二 contact
时间复杂度:O(1)
/**
* descripbe:
* 1. 完成两个算法,k位数往数组的前面加
* 2. 测试时间 log time endTime
*/
// 通过unshift的方法 [1,2,3,4,5,6,7] 3 => [5,6,7,1,2,3,4]
export const rodate1 = (arr: number[], len: number): number[] => {
// 去k的绝对值
let k = Math.abs(len % arr.length)
for (let i = 0; i < k; i++) {
let chu = arr.pop()
if (chu) {
arr.unshift(chu)
}
}
return arr
}
// 通过contact的方法
export const rodate2 = (arr: number[], len: number): number[] => {
let k = Math.abs(len % arr.length)
len = k
let l = arr.length
let left = arr.slice(0, l - len)
let right = arr.slice(l - len, l)
return [...right, ...left]
}
let arrHuge = []
for (let i = 0; i < 100000; i++) {
arrHuge.push(i)
}
let step = 8 * 10000
// 这个过程执行了 4000ms 也就是4s 时间复杂度 O(n^2)
console.time()
rodate1(arrHuge, step)
console.timeEnd()
// 这个过程仅仅2.7毫秒 时间复杂度 O(1)
console.time()
rodate2(arrHuge, step)
console.timeEnd()
数据结构
栈的应用
判断是否匹配 夸号类型
/**
* descripbe: 算法 {a[b(c)d]e}
* 判断是否匹配 夸号类型
*/
const isMatch = (top: string, s: string): boolean => {
if (top === "{" && s === '}') return true
if (top === "(" && s === ')') return true
if (top === "[" && s === ']') return true
return false
}
export const matchFunc = (str: string) => {
let length = str.length
if (length <= 0) return false
let leftSyb = '{[('
let rightSyb = '}])'
let stack = []
for (let i = 0; i < length; i++) {
let s = str[i]
if (leftSyb.includes(s)) {
// 碰到左边的字符串先入栈
stack.push(s)
} else if (rightSyb.includes(s)) {
// 碰到右边的字符串先判断是否和顶层的匹配
let top = stack[stack.length - 1]
if (isMatch(top, s)) {
// 匹配的话,就删除顶层
stack.pop()
} else {
return false
}
}
}
return stack.length === 0
}
let str1 = '{1[2(3)4]5}' // 匹配
let str2 = '{1[2(34]5}19)' // 顺序不一样
console.log(matchFunc(str1),matchFunc(str2), // true, false
找数组中两个字符串相加为10的值
O(n^2)的方式两个循环如下:
/**
* desc: 找数组中两个字符串相加为10的一个值
*/
export const twoNumberAdd = (arr: number[], total: number): number[] => {
if(arr.length === 0) return []
if(total === 0) return []
let length = arr.length
const res: number[] = []
let flag = false
for (let i = 0; i < length - 1; i++) {
for (let j = i; j < length - 1; j++) {
if (arr[i] + arr[j] == total) {
res.push(arr[i])
res.push(arr[j])
flag = true
break;
}
}
if (flag) break // for循环中,break可以停止循环。
}
return res
}
const arrlog = [1,2,3,4,5,6,8]
const res = twoNumberAdd(arrlog, 10)
console.log(res, 'res');
二分理念去查,时间复杂度 O(N)
export const twoNumberAdd2 = (arr: number[], total: number): number[] => {
if(arr.length === 0) return []
if(total === 0) return []
const res: number[] = []
let i = 0
let j = arr.length - 1
while (i < j) {
let a = arr[i]
let b = arr[j]
let t = a + b
if (t > total) {
j--
} else if (t < total) {
i++
} else {
res.push(a)
res.push(b)
break
}
}
return res
}
const arrlog = [1, 2, 3, 4, 5, 6, 8]
const res = twoNumberAdd2(arrlog, 10)
console.log(res, 'res');
时间复杂度对比,可以看出,二分法的时间复杂度更低。选第二个方法。
const arrlog = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 4, 5, 6, 8]
console.time()
for (let i = 0; i < 100 * 10000; i++) {
twoNumberAdd(arrlog, 10)
}
console.timeEnd()
console.time()
for (let i = 0; i < 100 * 10000; i++) {
twoNumberAdd2(arrlog, 10)
}
console.timeEnd()
// default: 288.071044921875 ms
// default: 14.84619140625 ms
链表
js实现单向链表结构
// 实现一个单项链表功能
// 变成 {value: '100', next: { value: "200", next: { value: '300', next: { value: '400' } } }}
const arrInit = [100, 200, 300, 400, 500]
const func = (arr) => {
let length = arr.length
if (length <= 0) return []
let curNode = {
value: arr[length-1],
}
for (let i = length-2; i >= 0; i--) {
console.log(i, 'i');
curNode = {
value: arr[i],
next: curNode
}
}
return curNode
}
console.log(func(arrInit), 'arr')
实现反转单向链表算法
/**
* {value: 100, next: { value: 200, next: { value: 300, next: { value: 400, next: { value: 500 } } } }}
* 处理成为下面结构
* {value: 500, next: { value: 400, next: { value: 300, next: { value: 200, next: { value: 100 } } } }}
* @param arr 反转单向链表结构结构
*/
interface INode {
value: number,
next?: INode
}
export const reverseLinkNode = (linkNode: INode) => {
// 定义三个指针用于接收
let prevNode: INode | undefined = undefined
let curNode: INode | undefined = undefined
let nextNode: INode | undefined = linkNode
// 循环赋值引用
while(nextNode){
console.log('1');
// 第一种情况
if (curNode && !prevNode) {
delete curNode.next
}
// 第二种和倒数第二种情况
if (curNode && prevNode) {
curNode.next = prevNode
}
prevNode = curNode
curNode = nextNode
// @ts-ignore
nextNode = nextNode.next
}
curNode!.next = prevNode
return curNode
}
const list = {value: 100, next: { value: 200, next: { value: 300, next: { value: 400, next: { value: 500 } } } }}
let list2 = reverseLinkNode(list)
console.log(list2, 'list2');
链表中,查询慢删除快
数组中,查询快删除慢。
队列
用两个栈实现队列(数组方式)
/**
* 两个栈实现队列
*/
export class MyQuene {
constructor(stack1: number[]){
this.stack1 = stack1
}
stack1: number[] = []
stack2: number[] = []
add(n: number) {
this.stack1.push(n)
}
delete(): number | null {
let stack1 = this.stack1
let stack2 = this.stack2
// 1. 压栈处理成stack2
while(stack1.length){
let n = stack1.pop()
if (n) stack2.push(n)
}
// 2. stack2.pop
let deleteItem = stack2.pop()
// 3. stack2 压栈成stack1
while(stack2.length){
let n = stack2.pop()
if(n) stack1.push(n)
}
return deleteItem as number
}
get length() {
return this.stack1.length
}
}
const q = new MyQuene([1,2,3,4,5])
let dItem = q.delete()
console.log(dItem, q.stack1); // 1, [2,3,4,5]
q.add(6)
console.log('add', q.stack1); // add, [2,3,4,5,6]
console.log(q.length); // 5
大致思路如下:
首先有两个栈,一个栈用于存初始值。一个用于转换的时候用,stack1压栈成stack2,顺序改变,A在首部变成尾部,直接pop删除最后一个元素,再压栈成stack1 得到的就是 删掉了A的 BCDE。
一些数据量庞大的场景中,由于数组
unshift
和shift
消耗的性能比较大时间复杂度(O(n^2)),使用栈的概念,可以通过 两个栈+队列 的方式完成数组的unshift操作。
用链表实现队列
/**
* 链表实现队列
*/
interface INode {
value: number,
next: INode | null
}
export class MyQuene {
head: INode | null = null
tail: INode | null = null
len: number = 0
add(n: number) {
let newNode: INode = {
value: n,
next: null
}
// 处理head
if (this.head == null) {
this.head = newNode
}
// // 处理tail
let tailNode = this.tail
if (tailNode) {
// 第二次进入已经新add了一个newNode,可以指定next为新节点
tailNode.next = newNode
}
this.tail = newNode
this.len++
}
delete(): number | null {
const headNode = this.head
if (this.len == 0) return null
if (this.head == null) return null
let value = headNode!.value
// 处理head
this.head = headNode!.next
this.len--
return value
}
get length(): number {
return this.len
}
}
数组和链表哪个更快?
- 数组是连续存储,push很快,shift很慢
- 链表是非连续存储,add和delete都很快(查找很慢)
- 所以链表实现队列更快
二叉树结构
题目:给一个有序的二叉树结构,找到第k位的树
/**
* 二叉搜索树中的第k位
*/
interface INode {
value: number,
left: INode | null,
right: INode | null
}
const bst: INode = {
value: 5,
left: {
left: {
value: 2,
left: null,
right: null
},
right: {
value: 4,
left: null,
right: null
},
value: 3
},
right: {
left: {
value: 6,
left: null,
right: null
},
right: {
value: 8,
left: null,
right: null
},
value: 7
},
}
let arr: number[] = []
/**
* 前序遍历
* Preorder traversal
*/
const preorderTraversal = (node: INode | null) => {
if (!node) return
if (!node) return
arr.push(node.value)
preorderTraversal(node.left)
preorderTraversal(node.right)
}
/**
* 中序遍历
* Preorder traversal
*/
const inorderTraversal = (node: INode | null) => {
if (node === null) return
inorderTraversal(node.left)
arr.push(node.value)
inorderTraversal(node.right)
}
/**
* 后序遍历
* Preorder traversal
*/
const postorderTraversal = (node: INode | null) => {
if (!node) return
if (!node) return
postorderTraversal(node.left)
postorderTraversal(node.right)
arr.push(node.value)
}
export const biranyTreeSearch1 = (node: INode, k: number): number | null=> {
inorderTraversal(node)
return arr[k - 1] || null
}
console.log(biranyTreeSearch1(bst, 3));
单元测试
一般react-create-app
有集成jest
包,可以直接通过 yarn test
运行
describe:描述内容
it: 提示内容
expect: 期望的方法
toBe: 期望得到某个数字
toBeNull: 期望得到null
import { INode, biranyTreeSearch1, bst } from './index'
describe('二叉搜索树求key位的值', () => {
it('普通情况', () => {
expect(biranyTreeSearch1(bst, 3)).toBe(4)
});
it('为0的情况', () => {
expect(biranyTreeSearch1(bst, 0)).toBeNull()
});
})
// Test Suites: 1 passed, 1 total
// Tests: 2 passed, 2 total
三种遍历
- 前序遍历
- 中序遍历
- 后序遍历
重点:
- 二叉树和三种遍历
- 二叉搜索树的特点:left <= root;right >= root
- 二分搜索树的价值:可使用二分法进行快速查找
算法三大规则
- 贪心
- 二分
- 动态规划
斐波拉切数列 找第 N 位
递归方式
这种方式 时间复杂度(O(n^2))
export function fiboracheSequence1(n: number): number {
if (n <= 0) return 0
if (n === 1) return 1
return fiboracheSequence1(n - 1) + fiboracheSequence1(n - 2)
}
// 测试
console.log(fiboracheSequence1(8)) // 21
循环的方式
export function fiboracheSequence2(n: number): number {
if (n <= 0) return 0
if (n === 1) return 1
let n1 = 1
let n2 = 0
let res = 0
for (let i = 2; i < n; i++) {
res = n1 + n2
n2 = n1
n1 = res
}
return res
}
移动数组中的0到末尾
O(n^2) 不好用
export const moveNumber1 = (arr: number[]) => {
if (arr.length === 0) return []
let length = arr.length
let blLen = 0
for (let i = 0; i < length - blLen; i++) {
if (arr[i] === 0) {
arr.push(0)
arr.splice(i, 1)
i--
blLen ++
}
}
return arr
}
let initArr = [1, 0, 1, 0, 1, 0, 234, 1, 34521, 0, 1, 90, 0]
moveNumber1(initArr)
console.log(initArr, 'arr')
O(n)时间复杂度
// O(n)的时间复杂度
export const moveNumber2 = (arr: number[]) => {
let i, j = -1, length = arr.length
for (i = 0; i < length; i++){
if (arr[i] === 0) {
if (j < 0) {
j = i
}
}
if(arr[i] !== 0 && arr[j] >= 0){
// 交换
const n = arr[i]
arr[i] = arr[j]
arr[j] = n
j++
}
}
}
let initArr = [1, 0, 1, 0, 1, 0, 234, 1, 34521, 0, 1, 90, 0]
moveNumber2(initArr)
console.log(initArr, 'arr')
字符串中连续最多的字符,以及次数
O(n)
// abcdddddfffg
interface IRes {
char: string,
length: number
}
export const findStr = (str: string) => {
let length = str.length
let res: IRes = {
char: '',
length: 0
}
let temLength
for (let i = 0; i < length; i++) {
console.log('---------------');
temLength = 0
for (let j = i; j < length; j++) {
console.log(`i:${i}\nj:${j}\ntemp:${temLength}\nres.length:${res.length}\nres.char:${res.char}`);
if (str[i] === str[j]) {
temLength++
}
if (str[i] !== str[j] || j === length - 1) {
if (temLength > res.length) {
res.length = temLength
res.char = str[i]
}
if (i < length - 1) {
i = j - 1
}
break;
}
}
}
return res
}
let str = 'abcdddefg'
console.log(findStr(str));
O(n)的时间复杂度
思路: 通过判断当前元素和上一个元素是否相等,来决定要不要累加。
export const findStr2 = (str: string): IRes => {
let length = str.length
let res: IRes = {
char: '',
length: 0
}
if (length === 0) return res
let tempLength = 1
for (let i = 0; i < length - 1; i++){
if (str[i] === str[i+1]) {
tempLength++
}else if(str[i] !== str[i+1]){
if (tempLength > res.length) {
res = {
char: str[i],
length: tempLength
}
tempLength = 1
}
}
}
return res
}
let str = 'abcdddeeeeeeefffg'
console.log(findStr2(str)); // char: e, length: 7
双指针
export const findStr3 = (str: string): IRes => {
let length = str.length
let res: IRes = {
char: '',
length: 0
}
if (length === 0) return res
let tempLength = 0
let i = 0,j = 0
for (; i < length; i++){
if (str[i] === str[j]) {
tempLength++
}else if(str[i] !== str[j] || i === length - 1){
if (tempLength > res.length) {
res = {
char: str[j],
length: tempLength
}
}
tempLength = 0
if (i < length - 1){
j = i
i --
}
}
}
return res
}
单元测试
import { findStr1, findStr2 } from './index'
describe('寻找重复的字串第一种方法:跳步', () => {
it('普通情况', () => {
expect(findStr1('abcdddedff')).toEqual({char: 'd', length: 3})
})
it('都是连续字符', () => {
expect(findStr1('dddeeeeddd')).toEqual({char: 'e', length: 4})
})
it('字符串为空', () => {
expect(findStr1('')).toEqual({char: '', length: 0})
})
it('无连续字符', () => {
expect(findStr1('abcdefghijk')).toEqual({char: 'a', length: 1})
})
})
describe('寻找重复的字串第二种方法:一次循环判断是否为相同字符', () => {
it('普通情况', () => {
expect(findStr2('abcdddedff')).toEqual({char: 'd', length: 3})
})
it('都是连续字符', () => {
expect(findStr2('dddeeeeddd')).toEqual({char: 'e', length: 4})
})
it('字符串为空', () => {
expect(findStr2('')).toEqual({char: '', length: 0})
})
it('无连续字符', () => {
expect(findStr2('abcdefghijk')).toEqual({char: 'a', length: 1})
})
})
快速排序
// 快速排序 slice
export const quickSort1 = (arr: number[]): number[] => {
let length = arr.length
if (length === 0) return []
let left = [], right = []
let midIndex = Math.floor(arr.length / 2)
let midValue = arr.slice(midIndex, midIndex + 1)[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midValue) {
left.push(arr[i])
} else if (arr[i] > midValue) {
right.push(arr[i])
}
}
return quickSort1(left).concat([midValue], quickSort1(right))
}
// 快速排序 splice
export const quickSort2 = (arr: number[]): number[] => {
let length = arr.length
if (length === 0) return []
let left = [], right = []
let midIndex = Math.floor(arr.length / 2)
let midValue = arr.splice(midIndex, 1)[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midValue) {
left.push(arr[i])
}
if (arr[i] > midValue) {
right.push(arr[i])
}
}
return quickSort2(left).concat([midValue], quickSort2(right))
}
const arr = [4,5,7,1,2,3,6,3,15,5,123,8,9,1,9,8]
console.log(quickSort2(arr), 'arr')
求回文数(aba 121 454)
// 求一个范围内的回文数 转字符串 转数字判断是否相等
export const getPalindromeNumberFunc1 = (max: number): number[] => {
let res: number[] = []
if (max <= 0) return []
for (let i = 1; i <= max; i++) {
let n = i.toString()
if (n === n.split('').reverse().join('')) {
res.push(i)
}
}
return res
}
// 求一个范围内的回文数 依次判断首位和末尾是否一直相等
export const getPalindromeNumberFunc2 = (max: number): number[] => {
let res: number[] = []
if (max <= 0) return []
for (let i = 1; i <= max; i++) {
let n = i.toString()
let startIndex = 0
let endIndex = n.length - 1
let flag = true
while (startIndex < endIndex) {
if (n[startIndex] === n[endIndex]) {
startIndex++
endIndex--
} else {
flag = false
break
}
}
if (flag) {
res.push(i)
}
}
return res
}
// 求一个范围内的回文数 反转数字
export const getPalindromeNumberFunc3 = (max: number): number[] => {
let res: number[] = []
if (max <= 0) return []
for (let i = 1; i <= max; i++) {
let n = i
let rev = 0
while (n > 0) {
rev = rev * 10 + n % 10
n = Math.floor(n / 10)
}
if (rev === i) res.push(i)
}
return res
}
console.log(getPalindromeNumberFunc2(500));
测试效果:方法1:400ms;方法二:50ms;方法三:42ms
时间复杂度分析:方法一最慢,因为数组转换需要时间
数字转成千分位的字符串
export const numberToStr = (num: number): string => {
let res = ''
let str = num.toString()
let length = str.length
let times = 0
for (let i = length - 1; i >= 0; i--) {
times ++
if (times == 3 && i !== 0) {
res = ',' + str[i] + res
times = 0
}else{
res = str[i] + res
}
}
return res
}
console.log(numberToStr(13880000000));
大小写转换
// 正则表达式大小写转换
export const toggleCase1 = (str: string): string => {
let res = ''
let length = str.length
for (let i = 0;i < length; i++){
let s = str[i]
let reg1 = /[a-z]/
let reg2 = /[A-Z]/
if (reg1.test(s)) {
res = res + s.toUpperCase()
}else if(reg2.test(s)){
res = res + s.toLowerCase()
}else {
res = res + s
}
}
return res
}
// 通过ASCII编码
export const toggleCase2 = (str: string): string => {
let res = ''
let length = str.length
for (let i = 0;i < length; i++){
let s = str[i]
let code = s.charCodeAt(0)
if (code >= 65 && code <= 90) {
res = res + s.toLowerCase()
}else if(code >= 97 && code <= 122){
res = res + s.toUpperCase()
}else {
res = res + s
}
}
return res
}
console.log(toggleCase2('avheDF!DsadSFEWF'));
0.1 + 0.2 !== 0.3
整数转换二进制没有误差
有些小数可能是无法用二进制精准转化
各个计算机语言的通病
通过math.js 进行准确的计算