前端面试系列三 - - 数据结构与算法

225 阅读8分钟

1、什么是算法复杂度?

image.png

时间复杂度 ?

image.png

image.png

空间复杂度 ?

image.png

image.png

image.png

image.png

2、如何把一个数组旋转 K 步 (代码演示和单元测试)?

性能分析

image.png

image.png

代码演示

/**
 * 思路一: 把末尾的元素挨个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 步 (性能分析)?

image.png

image.png

4、判断一个字符串是否括号匹配 ?

image.png

image.png

image.png

image.png

代码演示

/**
 * 匹配括号 "[{()}]"
 */
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)
    });
})

性能分析

image.png

5、用两个栈实现一个队列

image.png

代码演示

/**
 * 两个栈实现一个队列
 */
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)
    });
})

性能分析

image.png

6、使用 JS 反转单向链表-什么是链表

image.png

image.png

/**
 * 根据数组创建一个链表
 */
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 反转单向链表-分析解题思路

image.png

应用

image.png

image.png

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
                }
            }
        })
    });
})

image.png

【连环问】链表和数组哪个实现队列更快-分析解题思路

image.png

【连环问】链表和数组哪个实现队列更快-代码演示和单元测试

/**
 * 用链表实现队列
 */
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 实现二分查找-分析时间复杂度

image.png

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 实现二分查找-递归和循环哪个更好

image.png

image.png

12、找出一个数组中和为 n 的两个数-嵌套循环不是最优解

image.png

image.png

代码演示

/**
 * 求数组中两个数之和为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 的两个数-双指针的思路

image.png

image.png

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))

image.png

15、求二叉搜索树的第K小值-二叉树和三种遍历

image.png

image.png

image.png

代码演示

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小值-解题

image.png

代码演示

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() // 断言
    })
})

image.png

17、为什么二叉树很重要,而不是三叉树四岔树

image.png

image.png

image.png

image.png

image.png

image.png

18、堆有什么特点,和二叉树有什么关系

image.png

image.png

image.png

image.png

image.png

image.png🐭

19、求斐波那契数列的第n值-递归算法会导致运行崩溃

image.png

image.png

image.png

20、求斐波那契数列的第n值-优化时间复杂度-part1

image.png

代码演示

image.png

测试用例

image.png

动态规划

image.png

【连环问】青蛙跳台阶有几种方式

image.png

image.png

21、移动 0 到数组的末尾-splice 会导致性能问题

22、移动 0 到数组的末尾-使用双指针

23、获取字符串中连续最多的字符以及次数-使用嵌套循环

image.png

image.png

/**
 * 获取字符串中连续最多的字符以及次数
 * 使用双指针
 */
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、获取字符串中连续最多的字符以及次数-使用双指针

image.png

/**
 * 求连续最多的字符和次数
 * 指针实现
 */
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 实现快速排序并说明时间复杂度-代码演示和单元测试

image.png

image.png

27、用JS实现快速排序并说明时间复杂度-性能分析

28、获取1-10000之前所有的对称数(回文数)-代码演示和单元测试

29、获取1-10000之前所有的对称数(回文数)-性能分析

30、如何实现高效的英文单词前缀匹配

31、用 JS 实现数字千分位格式化

32、用JS 切换字母大小写

image.png

image.png

/**
 * 转换字母大小写
 * 正则表达式
 */
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

33、为什么0.1+0.2!==0.3