[TypeScript 数据结构与算法]前端常见算法题🐗

187 阅读3分钟

本文github地址:JavaScript_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士


栈和数组的区别

二者没有联系。栈是逻辑结构,是一个理论模型。但数组是一个计算机中的物理实现。数组可以用来实现栈。

判断一个字符串是否括号匹配(栈)

思路:

  1. 遇到左括号({[进栈,遇到右括号]})出栈
  2. 判断两个字符是否相等,不相等直接返回为false
  3. 判断栈是否为空
const isEqual = (a:string,b:string):boolean => 
    (a === '[' && b === ']') || (a === '{' && b === '}') || (a === '(' && b === ')') ? true : false


function isValid(str:string):boolean {
    const len = str.length
    if(len === 0) return true

    const stack:string[] = []
    const leftSymbols:string = '({['
    const rightSymbols:string = '}])'

    for (let index = 0; index < len; index++) {
        let s:string = str[index];
        if(leftSymbols.includes(s)){
            stack.push(s)
        }
        if(rightSymbols.includes(s)){
            let top:string = stack.pop() as string
            if(!isEqual(top, s)) return false
        }
    }
    return stack.length ===  0
}

let s = '({[]})'
console.info(isValid(s)) // false

性能分析:

  1. 时间复杂度:O(n)
  2. 空间复杂度:O(n)

用两个栈实现一个队列(栈、队列)

class Myqueue  {
    private stack1:number[] = []
    private stack2:number[] = []
    add(n:number):void{
        this.stack1.push(n)
    }

    delete():number | null {
        const stack1 = this.stack1
        const stack2 = this.stack2
        // 将stack1中的元素腾挪到stack2
        while(stack1.length){
            const n = stack1.pop() as number
            if(n !== null) stack2.push(n)
        }
        // 栈顶出栈(队头出队)
        let res = stack2.pop()
        // 将stack2中的元素腾挪到stack1
        while(stack2.length){
            const n = stack2.pop() as number
            if(n !== null) stack1.push(n)
        }
        return res || null
    }

    get length():number{
        return this.stack1.length
    }
}

const q = new Myqueue()
q.add(1)
q.add(2)
console.log(q.delete())

性能分析:

  1. 时间复杂度:add是O(1),delete是O(n)
  2. 空间复杂度:O(n)

反转单向链表(链表)

构建节点类

class SingleNode<T>{
    value:T
    next?: SingleNode<T> | null = null
    constructor(value:T){
        this.value=value
    }
}

构建链表类

class SingleLink<T>{
    private head:SingleNode<T> | null = null
    append(val:T){
        let newNode = new SingleNode(val)
        if(!this.head){
            this.head = newNode
        }else{
            let current = this.head
            while(current.next){
                current = current.next
            }
            current.next = newNode
        }
    }
    appendArray(arr:T[]){
        arr.forEach(val => this.append(val))
    }
    showItems(){
        let current = this.head
        if(!current) return
        let res = ''
        while(current){
            res += `${current.value}->`
            current = current?.next || null
        }
        return res
    }
    getHead():SingleNode<T> | null{
        return this.head
    }
}

测试链表类,并将结果打印出来

const link:SingleLink<number> = new SingleLink()
link.appendArray([1,2,3,4,5])
console.log(link.showItems())

结果为:1->2->3->4->5->

然后实现链表反转的函数:

function reverseLink(link:SingleLink<number>):SingleNode<number> | null | undefined{
    let head:SingleNode<number> | null = link.getHead()
    if(!head) return null
    if(!head.next) return head
    let pre:SingleNode<number> | null | undefined = null
    let next:SingleNode<number> | null | undefined = null
    while (head !== null) {
        next = head.next;
        head.next = pre;
        pre = head;
        head = next!;
    }
    return pre
}

查看得到的新头结点,并打印新的链表

let newHead:SingleNode<number> | null | undefined = reverseLink(link)
let res = ''
while(newHead){
    res += `${newHead.value}->`
    newHead = newHead.next
}
console.log(res)

结果为:5->4->3->2->1->

二分查找(二分)

性能分析:

  1. 时间复杂度:O(logn)

先来写一个生成随机数的函数


// 生成指定长度的随机数数组
function random(len:number, max:number = 10000):number[]{
    const res:number[] = []
    for (let index = 0; index < len; index++) {
        let num = Math.floor(Math.random() * max)
        res.push(num)
    }
    return res
}

const randomArray = random(100)

二分查找函数:

function searchBinaryByWhile(arr:number[], target:number):number{
    const len = arr.length
    if(len === 0) return -1
    let startIndex = 0
    let endIndex = len - 1
    while(startIndex <= endIndex){
        const midIndex = Math.floor((startIndex + endIndex) / 2)
        const midVal = arr[midIndex]
        if(target < midVal){
            endIndex = midIndex - 1
        }else if(target > midVal) {
            startIndex = midIndex + 1
        }else{
            return midIndex
        }
    }
    return -1
}

要求数组是已经排序的,因此对随机数组进行一些处理

const randomArray = random(100, 100)
const sortedArray = randomArray.sort((a,b)=>a-b)
console.log(sortedArray)
const res = searchBinaryByWhile(sortedArray,10)
console.log(res)

用递归实现二分查找:

function searchBinaryByRecurve(arr:number[], target:number, startIndex:number = 0, endIndex?:number):number{
    const len = arr.length
    if(len === 0) return -1
    if(!endIndex) endIndex = len -1
    if(startIndex > endIndex) return -1
    const midIndex = Math.floor((startIndex + endIndex) / 2)
    const midVal = arr[midIndex]
    // 分治递归
    if(target < midVal){
        return searchBinaryByRecurve(arr, target, startIndex, midIndex - 1)
    }else if(target > midVal){
        return searchBinaryByRecurve(arr, target, midIndex + 1, endIndex)
    }else{
        return midIndex
    }
}

循环和递归进行比较:

const randomArray = random(100000, 1000000)
const sortedArray = randomArray.sort((a,b)=>a-b)

console.time('searchBinaryByWhile')
const res1 = searchBinaryByWhile(sortedArray,1000)
console.timeEnd('searchBinaryByWhile')

console.time('searchBinaryByRecurve')
const res2 = searchBinaryByRecurve(sortedArray,1000)
console.timeEnd('searchBinaryByRecurve')

注意📢:凡有序,必二分

找出一个排序数组中和为 n 的两个数(二分+双指针)

依然使用上面实现的生成随机数组的函数:

// 生成指定长度的随机数数组
function random(len:number, max:number = 10000):number[]{
    const res:number[] = []
    for (let index = 0; index < len; index++) {
        let num = Math.floor(Math.random() * max)
        res.push(num)
    }
    return res
}
const randomArray = random(100,100)
const sortedArray = randomArray.sort((a,b)=>a-b)

使用for嵌套循环暴力查找:

function findNumbers(arr:number[], target:number):number[]{
    const res: number[] = []
    const len = arr.length
    if(len === 0) return res
    for (let i = 0; i < len; i++) {
        const n1 = arr[i];
        let flag = false
        for (let j = i + 1; j < len; j++) {
            const n2 = arr[j];
            if(n1 + n2 === target){
                res.push(n1)
                res.push(n2)
                flag = true
                break
            }
        }
        if(flag) break
    }
    return res
}

测试:

const res = findNumbers(sortedArray, 100)
console.log(res)

时间复杂度分析:O(n^2)

我们可以利用递增的特性,想到二分的思想:选取第一个数和最后一个数相加,如果和大于目标数,则第一个数向右移动,反之,最后一个数向左移动。即使用双指针的思想,将时间复杂度降低到O(n)

function findNumbers(arr:number[], target:number):number[]{
    const res: number[] = []
    const len = arr.length
    if(len === 0) return res
    let left = 0, right = len - 1
    // 双指针移动
    while(left <= right){
        let sum = arr[left] + arr[right]
        if(sum > target){
            right -= 1
        }else if(sum < target){
            left += 1
        }else {
            res.push(...[arr[left] , arr[right]])
            return res
        }
    }
    return res
}

时间复杂度:O(n)

二叉树及二叉树遍历(二叉树+分治+递归)

前序遍历:root->left-right

中序遍历:left->root->right

后序遍历:left->right->root

构造树节点:

class TreeNode {
    public data:string | number
    public leftChild:TreeNode | null
    public rightChild:TreeNode | null
    constructor(data:string | number){
        this.data = data
        this.leftChild = null
        this.rightChild = null
    }
}

构建树(使用数组):

const createTree = (arr:string[] | number[]):TreeNode | null | undefined => { 
    // 创建二叉树
    let tree = new TreeNode(arr[0])
    let Nodes = [tree]
    let i = 1
    for (let node of Nodes){
        Nodes.push(node.leftChild = new TreeNode (arr[i]))
        i += 1
        if (i == arr.length) return tree
        Nodes.push(node.rightChild = new TreeNode(arr[i]))
        i += 1
        if (i == arr.length) return tree
    }
}

测试:

let datas = ['A','B','C','D','E','F','G','H']
let tree = createTree(datas)
console.info(tree)

前序遍历

function prevOrderTree(node:TreeNode){
    if(node === null) return
    console.log(node.val)
    prevOrderTree(node.leftChild!)
    prevOrderTree(node.rightChild!)
}

中序遍历

function midOrderTree(node:TreeNode){
    if(node === null) return
    midOrderTree(node.leftChild!)
    console.log(node.val)
    midOrderTree(node.rightChild!)
}

后续遍历

function afterOrderTree(node:TreeNode){
    if(node === null) return
    afterOrderTree(node.leftChild!)
    afterOrderTree(node.rightChild!)
    console.log(node.val)
}

求二叉搜索树的第K小值

二叉搜索树的特点:

left(包括其后代) val <= root.val

right(包括其后代) val >= root.val

可使用二分查找快速检索

分析:二叉搜索树的中序遍历是按照从小到大顺序,可以直接按照中序遍历的结果获取二叉搜索树的第K小值

利用刚才的二叉树函数快速构建二叉搜索树:

let datas = [5,3,7,2,4,6,8]
let tree:TreeNode = createTree(datas) as TreeNode
function midOrderTree(node:TreeNode){
    if(node === null) return
    midOrderTree(node.leftChild!)
    // @ts-ignore
    res.push(node.val)
    midOrderTree(node.rightChild!)
}

const res:number[] | string[] = []
function findKValue(tree:TreeNode,k:number):number{
    midOrderTree(tree)
    return res[k-1] as number
}

console.log(findKValue(tree,3)) // 4

本文github地址:JavaScript_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士