1、什么是算法复杂度?
时间复杂度 ?
空间复杂度 ?
2、如何把一个数组旋转 K 步 (代码演示和单元测试)?
性能分析
代码演示
/**
* 思路一: 把末尾的元素挨个pop,然后unshift到数组前面
* @param arr
* @param k
*/
function rotate1(arr: number[], k: number): number[] {
// 首先判断
const length = arr.length
if(!k || length === 0) return arr
// 获取步数
const step = Math.abs(k % length)
for(let i = 0; i < step; i++) {
const n = arr.pop()
if(n) {
arr.unshift(n)
}
}
return arr
}
const arr = [1,2,3,4,5,6,7,8]
console.log(rotate1(arr, 3)) // [6,7,8,1,2,3,4,5]
/**
* 思路二: 把数组拆分,最后 concat 拼接到一起
*/
function rotate2(arr: number[], k: number): number[] {
// 首先判断
const length = arr.length
if (!k || length === 0) return arr
// 获取步数
const step = Math.abs(k % length)
const part1 = arr.slice(-step)
const part2 = arr.slice(0, length - step)
const part3 = part1.concat(part2)
return part3
}
const arr2 = [1,2,3,4,5,6,7]
console.log(rotate2(arr2, 4)) // [4,5,6,7,1,2,3]
测试用例
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import {rotate1, rotate2} from '@/utils/algorithm/kDemo'
describe('数组旋转', () => {
it('正常情况', () => {
const arr = [1,2,3,4,5,6]
const k = 3
const res = rotate1(arr, k)
expect(res).toEqual([4,5,6,1,2,3]) // 断言
})
it('数组为空', () => {
const res = rotate1([], 3)
expect(res).toEqual([4,5,6,1,2,3]) // 断言
})
it('k 为负值', () => {
const arr = [1,2,3,4,5,6]
const k = -3
// @ts-ignore
const res = rotate1(arr, k)
expect(res).toEqual([4,5,6,1,2,3]) // 断言
})
it('k 不是数组', () => {
const arr = [1,2,3,4,5,6]
const k = 'abc'
// @ts-ignore
const res = rotate1(arr, k)
expect(res).toEqual([4,5,6,1,2,3]) // 断言
})
})
3、如何把一个数组旋转 K 步 (性能分析)?
4、判断一个字符串是否括号匹配 ?
代码演示
/**
* 匹配括号 "[{()}]"
*/
function isMatch(left: string, right: string) {
if (left === '{' && right === '}') return true
if (left === '[' && right === ']') return true
if (left === '(' && right === ')') return true
return false
}
function matchBracket(str: string): boolean {
const length = str.length
if (length === 0) return true
const stack = []
const leftSymbols = '{[('
const rightSymbols = '}])'
for (let i = 0; i < length; i++) {
const s = str[i]
if (leftSymbols.includes(s)) {
stack.push(s)
} else if (rightSymbols.includes(s)) {
const top = stack[stack.length - 1]
// 如果匹配则出栈
if (isMatch(top, s)) {
stack.pop()
} else { // 否则不匹配
return false
}
}
}
return stack.length === 0
}
const str = '{[123}'
console.log('判断是否匹配:', matchBracket(str)) // false
const str2 = '{[123]}'
console.log('判断是否匹配:', matchBracket(str2)) // true
测试用例
import {matchBracket} from '@/utils/algorithm/match-bracket'
describe('测试括号匹配', () => {
it('匹配', () => {
const str = '{a(b[c]d)e}f'
const res = matchBracket(str)
expect(res).toBe(true)
});
it('不匹配', () => {
const str = '{a((b[c]d)e}f'
const res = matchBracket(str)
expect(res).toBe(false)
});
})
性能分析
5、用两个栈实现一个队列
代码演示
/**
* 两个栈实现一个队列
*/
export class myQueue {
private stack1: number[] = [] // 栈1
private stack2: number[] = [] // 栈2
// stack1 中添加数据
add(num: number) {
this.stack1.push(num)
}
// 删除数据
/**
* 逻辑:
* stack1中的数据出栈,
* stack2入栈,
* stack2删除栈顶元素,(模拟队列先进先出)
* stack2 数据 => stack1
*/
delete(): number | null {
let res = null;
const stack1 = this.stack1
const stack2 = this.stack2
//1、stack1中的数据出栈,stack2入栈,
while (stack1.length) {
const n = stack1.pop()
if (n != null) {
stack2.push(n)
}
}
// 2、stack2删除栈顶元素,(模拟队列先进先出)
res = stack2.pop()
// 3、stack2 数据 => stack1
while (stack2.length) {
const n2 = stack2.pop()
if (n2 != null) {
stack1.push(n2)
}
}
return res || null
}
// 获取栈的长度
get legnth(): number {
return this.stack1.length
}
}
const p = new myQueue()
console.log('长度:', p.legnth) // 0
p.add(100)
p.add(120)
p.add(140)
console.log('出栈:', p.delete()) // 100
console.log('长度:', p.legnth) // 2
console.log('出栈:', p.delete()) // 120
测试用例
import {myQueue} from '@/utils/algorithm/two-stack-one-queue'
describe('两个栈一个队列', () => {
it('add and length', () => {
const q = new myQueue()
expect(q.legnth).toBe(0)
q.add(100)
q.add(110)
q.add(120)
expect(q.legnth).toBe(3)
});
it('delete', () => {
const q = new myQueue()
expect(q.legnth).toBe(0)
q.add(100)
q.add(110)
q.add(120)
expect(q.delete()).toBe(100)
expect(q.legnth).toBe(2)
});
})
性能分析
6、使用 JS 反转单向链表-什么是链表
/**
* 根据数组创建一个链表
*/
interface ILinkListNode {
value: number,
next?: ILinkListNode
}
function createLinkList(arr: number[]): ILinkListNode {
const length = arr.length
if (length === 0) throw new Error('arr is empty !')
// 创建链表尾部节点
let curNode: ILinkListNode = {
value: arr[length - 1]
}
// 数组长度为1,则返回
if (length === 1) return curNode
// 数组长度大于1
for (let i = arr.length - 2; i >= 0; i--) {
curNode = {
value: arr[i],
next: curNode
}
}
return curNode
}
const arr = [100, 200, 300]
console.log(createLinkList(arr))
// {value: 100, next: {value: 200, next: {value: 300}}}
7、使用 JS 反转单向链表-分析解题思路
应用
8、使用 JS 反转单向链表-代码演示和单元测试
代码演示
/**
* 反转链表
* @param arr
*/
function reverseLinkList(linkList: ILinkListNode): ILinkListNode {
// 定义三个指针
let prevNode: ILinkListNode | undefined = undefined
let curNode: ILinkListNode | undefined = undefined
let nextNode: ILinkListNode | undefined = undefined
// 以nextLink为主,遍历链表
while (nextNode) {
// 第一个元素,删掉 next, 防止循环引用
if (curNode && !prevNode) {
// @ts-ignore
delete curNode.next
}
// 反转指针
if (curNode && prevNode) {
// @ts-ignore
curNode.next = prevNode
}
// 整体向后移动指针
prevNode = curNode
curNode = nextNode
// @ts-ignore
nextNode = nextNode?.next
}
// 最后一个的补充:当 nextNode 为空时, 此时curNode 尚未设置 next
curNode!.next = prevNode
return curNode!
}
单元测试
import {reverseLinkList, createLinkList, ILinkListNode} from '@/utils/algorithm/reverse-link-list'
describe('反转单项链表', () => {
it('单个元素', () => {
const node: ILinkListNode = {value: 100}
const node1 = reverseLinkList(node)
expect(node1).toEqual({value: 100})
})
it('多个元素', () => {
const node = createLinkList([100, 200, 300])
const node1 = reverseLinkList(node)
expect(node1).toEqual({
value: 300,
next: {
value: 200,
next: {
value: 100
}
}
})
});
})
【连环问】链表和数组哪个实现队列更快-分析解题思路
【连环问】链表和数组哪个实现队列更快-代码演示和单元测试
/**
* 用链表实现队列
*/
interface IListNode {
value: number,
next: IListNode | null
}
export class MyQueue {
private head: IListNode | null = null
private tail: IListNode | null = null
private len = 0
/**
* 入队,在tail位置
*/
add(n: number) {
const newNode: IListNode = {
value: n,
next: null
}
// 处理 head
if (this.head == null) {
this.head = newNode
}
// 处理 tail
const tailNode = this.tail
if (tailNode) {
tailNode.next = newNode
}
this.tail = newNode
// 记录长度
this.len++
}
/**
* 出队,在 head 位置
*/
delete(): number | null {
const headNode = this.head
if (headNode == null) return null
if (this.len <= 0) return null
// 取值
const value = headNode.value
// 处理 head
this.head = headNode.next
// 记录长度
this.len--
return value
}
get length(): number {
// length 要单独存储,不能遍历链表来获取,否则时间复杂度太高0(n))
return this.len
}
}
const p = new MyQueue()
p.add(100)
p.add(120)
p.add(140)
p.add(160)
console.log(p.length) // 4
console.log(p.delete()) // 100
console.log(p.length) // 3
【连环问】链表和数组哪个实现队列更快-性能分析
9、 用 JS 实现二分查找-分析时间复杂度
10、 用 JS 实现二分查找-代码演示和单元测试
代码演示
/**
* 二分查找 - 循环方法
*/
function binaryArray(arr: number[], target: number): number {
// 首先判断数组是否为空
const length = arr.length
if (length === 0) return -1
let startIndex = 0 // 开始位置
let endIndex = length // 结束位置
while (startIndex <= endIndex) {
const midIndex = Math.floor((startIndex + endIndex) / 2)
const midValue = arr[midIndex]
if (target < midValue) {
endIndex = midIndex - 1
} else if (target > midValue) {
startIndex = midIndex + 1
} else {
// 相等, 返回
return midIndex
}
}
return -1
}
/**
* 二分查找 - 递归
*/
function binarySearch2(arr: number[], target: number, startIndex?: number, endIndex?: number): number {
const length = arr.length
if (length === 0) return -1
if (startIndex === 0) startIndex = 0
if (endIndex === 0) endIndex = length - 1
// 中间位置
const midIndex = Math.floor((startIndex + endIndex) / 2)
const midValue = arr[midIndex]
if (target < midValue) {
return binarySearch2(arr, target, startIndex, midIndex - 1)
} else if (target > midValue) {
return binarySearch2(arr, target, startIndex, midIndex + 1)
} else {
// 返回相等
return midIndex
}
return -1
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
console.log(binaryArray(arr, 6)) // 5
const arr2 = [1, 2, 3, 4, 5, 6, 7, 8]
console.log('search2',binarySearch2(arr2, 4, 0, 0)) // 3
测试用例
import {shallowMount} from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import {binaryArray, binarySearch2} from '@/utils/algorithm/binaryArray'
describe('二分查找', () => {
it('正常情况', () => {
const arr = [1, 2, 3, 4, 5, 6]
const target = 3
const res = binaryArray(arr, target)
expect(res).toEqual(4) // 断言
})
it('数组为空', () => {
const res = binaryArray([], 3)
expect(res).toEqual(-1) // 断言
})
it('找不到 target', () => {
const arr = [1, 2, 3, 4, 5, 6]
const target = 400
const index = binaryArray(arr, target)
expect(index).toBe(-1) // 断言
})
})
11、 用 JS 实现二分查找-递归和循环哪个更好
12、找出一个数组中和为 n 的两个数-嵌套循环不是最优解
代码演示
/**
* 求数组中两个数之和为n
* 常规解法: 嵌套循环
*/
function getSum(arr: number[], n: number): number[] {
const length = arr.length
if (length === 0) return []
const sum: number[] = []
for (let i = 0; i < length - 1; i++) {
for (let j = i + 1; j < length; j++) {
if (i + j === n) {
sum.push(i)
sum.push(j)
return sum
}
}
}
return []
}
const arr = [1, 2, 3, 4, 5, 6]
console.log(getSum(arr, 18)) // []
console.log(getSum(arr, 18)) // [2,5]
测试用例
import {getSum} from '@/utils/algorithm/two-numbers-sum'
describe('两数之和', () => {
it('正常情况', () => {
const arr = [1, 2, 3, 4, 5, 6]
const res = getSum(arr, 7)
expect(res).toEqual([2, 5]) // 断言
})
it('空数组', () => {
const res = getSum([], 3)
expect(res).toEqual([]) // 断言
})
it('找不到结果', () => {
const arr = [1, 2, 3, 4, 5, 6]
const k = 100
const res = getSum(arr, k)
expect(res).toEqual([]) // 断言
})
})
13、找出一个数组中和为 n 的两个数-双指针的思路
14、找出一个数组中和为 n 的两个数-双指针的代码演示
/**
* 求数组中两个数之和为n
* 最优解: 双指针
*/
function getSumByLink(arr: number[], target: number): number[] {
const length = arr.length
if (length === 0) return []
const sum: number[] = []
let i = 0 // 头
let j = length - 1 // 尾部
while (i < j) {
const n1 = arr[i]
const n2 = arr[j]
const val = n1 + n2
if (target < val) {
// val大于target, 则 j 向前移动
j--
} else if (target > val) {
// val 小于 target, 则 i 向后移动
i++
} else {
sum.push(arr[i])
sum.push(arr[j])
break
}
}
return sum
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
console.log(getSumByLink(arr, 10))
15、求二叉搜索树的第K小值-二叉树和三种遍历
代码演示
interface ITreeNode {
value: number
left: ITreeNode | null
right: ITreeNode | null
}
const tree: ITreeNode = {
value: 5,
left: {
value: 3,
left: {
value: 2,
left: null,
right: null
},
right: {
value: 4,
left: null,
right: null
}
},
right: {
value: 7,
left: {
value: 6,
left: null,
right: null
},
right: {
value: 8,
left: null,
right: null
}
}
}
/**
* 前序遍历
*/
function preOrderTraverse(node: ITreeNode | null) {
if (node == null) return
console.log('前序遍历', node.value)
preOrderTraverse(node.left)
preOrderTraverse(node.right)
}
/**
* 中序遍历
*/
function midOrderTraverse(node: ITreeNode | null) {
if (node == null) return
midOrderTraverse(node.left)
console.log('中序遍历:', node.value)
midOrderTraverse(node.right)
}
/**
* 后序遍历
*/
function postOrderTraverse(node: ITreeNode | null) {
if (node == null) return
postOrderTraverse(node.left)
postOrderTraverse(node.right)
console.log('后序遍历:', node.value)
}
preOrderTraverse(tree) // 5,3,2,4,7,6,8
midOrderTraverse(tree) // 2,3,4,5,6,7,8
postOrderTraverse(tree) // 2,4,3,6,8,7,5
16、求二叉搜索树的第K小值-解题
代码演示
interface ITreeNode {
value: number
left: ITreeNode | null
right: ITreeNode | null
}
const tree: ITreeNode = {
value: 5,
left: {
value: 3,
left: {
value: 2,
left: null,
right: null
},
right: {
value: 4,
left: null,
right: null
}
},
right: {
value: 7,
left: {
value: 6,
left: null,
right: null
},
right: {
value: 8,
left: null,
right: null
}
}
}
/**
* 中序遍历
*/
function midOrderTraverse(node: ITreeNode | null) {
if (node == null) return
midOrderTraverse(node.left)
// console.log('中序遍历:', node.value)
arr.push(node.value)
midOrderTraverse(node.right)
}
/**
* 获取二叉树中的第K个值
* node: 二叉树
* K: 第几个值
* 中序遍历
*/
const arr: number[] = []
function getKthValue(node: ITreeNode, k: number): number | null {
midOrderTraverse(node)
return arr[k] || null
}
console.log('k值', getKthValue(tree, 4)) // 6
测试用例
import {getKthValue} from '@/utils/algorithm/binary-search-tree'
describe('二叉树获取K值', () => {
interface ITreeNode {
value: number
left: ITreeNode | null
right: ITreeNode | null
}
const bts: ITreeNode = {
value: 5,
left: {
value: 3,
left: {
value: 2,
left: null,
right: null
},
right: {
value: 4,
left: null,
right: null
}
},
right: {
value: 7,
left: {
value: 6,
left: null,
right: null
},
right: {
value: 8,
left: null,
right: null
}
}
}
it('正常情况', () => {
const res = getKthValue(bts, 3)
expect(res).toBe(4) // 断言
})
it('k不在正常范围之内', () => {
const res = getKthValue(bts, 0)
expect(res).toBeNull() // 断言
const res2 = getKthValue(bts, 1000)
expect(res2).toBeNull() // 断言
})
})
17、为什么二叉树很重要,而不是三叉树四岔树
18、堆有什么特点,和二叉树有什么关系
🐭
19、求斐波那契数列的第n值-递归算法会导致运行崩溃
20、求斐波那契数列的第n值-优化时间复杂度-part1
代码演示
测试用例
动态规划
【连环问】青蛙跳台阶有几种方式
21、移动 0 到数组的末尾-splice 会导致性能问题
22、移动 0 到数组的末尾-使用双指针
23、获取字符串中连续最多的字符以及次数-使用嵌套循环
/**
* 获取字符串中连续最多的字符以及次数
* 使用双指针
*/
interface IRes {
char: string,
length: number
}
/**
* 求连续最多的字符和次数
*/
export function findContinuousChar1(str: string): IRes {
const res: IRes = {
char: '',
length: 0
}
const length = str.length
if (length === 0) return res
let tempLength = 0 // 临时记录当前连续字符的长度
for (let i = 0; i < length - 1; i++) {
tempLength = 0 // 重置
for (let j = 0; j < length; j++) {
if (str[i] === str[i]) {
tempLength++
}
if (str[i] !== str[j] || j === length - 1) {
// 不相等,或者已经到了最后一个元素,要去判断最大值
if (tempLength > res.length) {
res.char = str[i]
res.length = tempLength
}
if (i < length - 1) {
i = j - 1
}
break
}
}
}
return res
}
// 功能测试
const str = 'aaabbbccc123456fff'
console.log(findContinuousChar1(str))
24、获取字符串中连续最多的字符以及次数-使用双指针
/**
* 求连续最多的字符和次数
* 指针实现
*/
export function findContinuousChar2(str: string): IRes {
const res: IRes = {
char: '',
length: 0
}
const length = str.length
if (length === 0) return res
let tempLength = 0 // 临时记录当前连续字符的长度
let i = 0
let j = 0
for (; i < length; i++) {
if (str[i] === str[j]) {
tempLength++
}
if (str[i] !== str[j] || i === length - 1) {
// 不相等,或者 i 到了字符串的末尾
if (tempLength > res.length) {
res.char = str[i]
res.length = tempLength
}
tempLength = 0 // reset
if (i < length - 1) {
j = i // 让 j '追上' i
i--
}
}
}
return res
}
let str2 = 'ccddEEfsdAA'
console.log(findContinuousChar2(str2))
26、用 JS 实现快速排序并说明时间复杂度-代码演示和单元测试
27、用JS实现快速排序并说明时间复杂度-性能分析
28、获取1-10000之前所有的对称数(回文数)-代码演示和单元测试
29、获取1-10000之前所有的对称数(回文数)-性能分析
30、如何实现高效的英文单词前缀匹配
31、用 JS 实现数字千分位格式化
32、用JS 切换字母大小写
/**
* 转换字母大小写
* 正则表达式
*/
export function switchLetterCase(s: string): string {
let res = ''
const length = s.length
if (length === 0) return res
const reg1 = /[a-z]/
const reg2 = /[A-Z]/
for (let i = 0; i < s.length; i++) {
const c = s[i]
if (reg1.test(c)) {
res += c.toUpperCase()
} else if (reg2.test(c)) {
res += c.toLowerCase()
} else {
res += c
}
}
return res
}
const str = 'sUpde234Sdcfr'
console.log(switchLetterCase(str)) // SuPDE234sDCFR
/**
* 转换字母大小写
* ASCII 编码
*/
function switchLetterCase2(s: string): string {
let res = ''
if (s.length === 0) return res
for (let i = 0; i < s.length; i++) {
const c = s[i]
const code = c.charCodeAt(0)
if (code >= 65 && code <= 90) {
res += c.toLowerCase()
} else if (code >= 97 && code <= 122) {
res += c.toUpperCase()
} else {
res += c
}
}
return res
}
const str2 = 'cbnfhr765GSYTDFR'
console.log(switchLetterCase2(str2)) // CBNFHR765gsytdfr