阅读 675

Typescript 强化数据结构

作者正处大三复习阶段,一方面要把只看过概念的 TypeScript实践一下,一方面要巩固一下数据结构基础,最后一拍大腿,为什么不用TypeScript来实现一遍数据结构呢!

特别提醒: 本文篇幅较长,可分为小节浏览。

仓库地址为: DataStructure-Algorithm-TS,可在此处查看详细代码,如有问题,欢迎提 issue !后续会持续更新一些算法方面的知识。

二叉树

二叉树是一种典型的树状结构,二叉树是每个节点最多有两个子树的树结构,通常被称为“左子树”与“右子树”。

二叉树的数据结构

首先我们定义二叉树节点的数据结构

以下相关代码在 BinaryTree.ts 中

class TreeNode {
    val: any //节点内容
    left: TreeNode //左子树
    right: TreeNode // 右子树


    constructor(val:any) {
        this.val = val
        this.left = this.right = undefined
    }
}
复制代码

接着我们定义二叉树的数据结构

class Tree <T> {
    root : TreeNode

    constructor (data: T[]){
        // 临时存储所有节点,方便寻找父子节点
        let nodeList : TreeNode[] = []
        // 顶节点
        let root : TreeNode
        for(let i = 0, len = data.length; i < len; i++) {
            let node = new TreeNode(data[i]);
            nodeList.push(node);
            
            if (i > 0) {
                // 计算节点所在层级的指数 即每一层都是 2^k-1 个  k为层数 n = k -1
                let n : number = Math.floor(Math.sqrt(i+1))
                // 当前层的启始值
                let q = Math.pow(2, n) - 1 // 索引值 减一
                // 记录上一层的起始点
                let p = Math.pow(2, n-1) - 1 //索引值 减一
                // 找到当前节点的父节点
                let parent: TreeNode = nodeList[p + Math.floor((i - q) / 2)]
                // 将当前节点和上一次节点做关联
                if (parent.left !== undefined) {
                    parent.right = node
                } else {
                    parent.left = node
                }
            }
        }
        this.root = nodeList.shift()
        nodeList.length = 0
    }
    

}
复制代码

二叉树的中序遍历

遍历顺序 左子树 >> 节点 >> 右子树

// 在 Tree 类中
inOrderTraversal (root : TreeNode, array : T[]) : T[] {
        if (root) {
            this.inOrderTraversal(root.left, array)
            array.push(root.val)
            this.inOrderTraversal(root.right, array)
        }
        return array
    }
复制代码

二叉树的先序遍历

遍历顺序 节点 >> 左子树 >> 右子树

// 在 Tree 类中
public preOrderTraversal (root : TreeNode, array: T[]) : T[] {
        if (root) {
            array.push(root.val)
            this.preOrderTraversal(root.left, array)
            this.preOrderTraversal(root.right, array)
        }
        return array
    }
复制代码

二叉树的后序遍历

遍历顺序 左子树 >> 右子树 >> 节点

// 在 Tree 类中
public postOrderTraversal (root: TreeNode, array: T[]) : T[] {
        if (root) {
            this.postOrderTraversal(root.left, array)
            this.postOrderTraversal(root.right, array)
            array.push(root.val)
        }
        return array
    }
复制代码

二叉树的深度

// 在 Tree 类中
public treeDepth (root: TreeNode) : number {
        // 一个二叉树的深度为 左子树深度和右子树深度的最大值 + 1
        return (root === undefined || root.val === null)  ? 0 : Math.max(this.treeDepth(root.left), this.treeDepth(root.right)) + 1
    }
复制代码

二叉树的最小深度

// 在 Tree 类中
public minDepth (root: TreeNode) : number {
        if (root === undefined || root.val === null) {
            return 0
        }
        let left = this.minDepth(root.left)
        let right = this.minDepth(root.right)
        return (left && right) ? 1 + Math.min(left, right) : 1 + left + right
    }
复制代码

判断是否为平衡二叉树

// 判断二叉树是否为平衡二叉树
    public isBalanced (root: TreeNode) : boolean {
        if (!root || root.val === null) {
            return true;
        }
        let left = this.isBalanced(root.left)
        let right = this.isBalanced(root.right)
        // 如果存在不平衡情况即都不平衡
        if (left === false || right === false || Math.abs(this.treeDepth(this.root.left) - this.treeDepth(this.root.right)) > 1) {
            return false
        }
        return true
    }
复制代码

判断是否为对称二叉树

此部分代码在 Symmetry.ts

对称二叉树的要求:

  • 根结点相等
  • 左子树的右节点和右子树的左节点相同。
  • 右子树的左节点和左子树的右节点相同。
import Tree, {TreeNode} from './BinaryTree'

/**
 * 判断是否为对称二叉树
 * 对称二叉树条件为:
 * - 根节点相等
 * - 左子树的右节点和右子树的左节点相同
 * - 右子树的左节点和左子树的右节点相同
 * 
 * @param {Tree} tree
 */
function isSymmetry (tree: Tree<Number>) : Boolean {
    return isSymmetryTree(tree.root.left, tree.root.right)
}
/**
 * 
 * @param {TreeNode} node1 
 * @param {TreeNode} node2
 */
function isSymmetryTree (node1 : TreeNode, node2: TreeNode ) : Boolean {
    // 如果两个节点都不存在
    if (!node1 && !node2) {
        return true;
    } 
    // 如果只有一个节点不存在
    else if (!node1 || !node2) {
        return false;
    }
    // 如果节点值不相同
    else if (node1.val !== node2.val) {
        return false;
    }

    //向下递归调用
    return isSymmetryTree(node1.left, node2.right) && isSymmetryTree(node1.right, node2.left);
}

export default isSymmetry
复制代码

二叉树的镜像

此部分代码在 Mirror.ts

镜像即二叉树所有的的左右节点交换位置

import {TreeNode} from './BinaryTree'

/**
 * 使一个二叉树变化为他的镜像
 * 即交换左右节点位置
 * 
 * @param {TreeNode} root 
 */
function Mirror (root: TreeNode) : void {
    if (root) {
        let temp = root.right;
        root.right = root.left;
        root.left = temp;
        Mirror(root.right);
        Mirror(root.left);
    }
}

export default Mirror
复制代码

二叉树层次遍历

此部分代码在 Tree 类中

// 二叉树层次遍历
    public levelTraversal (root: TreeNode) : number[][] | number[] {
        if (!root) return []
        // 使用 queue 来存储当前层级的节点
        let result = [], queue = [root]
        while (queue.length) {
            let levelSize = queue.length
            let currentLevel = []
            while (levelSize--) {
                let node = queue.shift()
                currentLevel.push(node.val)
                if (node.left && node.left.val !== null) queue.push(node.left)
                if (node.right && node.right.val !== null) queue.push(node.right)
            }
            result.push(currentLevel)
        }
        return result
    }
复制代码

重建二叉树

此部分代码在 BuildTree.ts 中

根据先序遍历和中序遍历结果构建二叉树

/**
 * 根据前序遍历与中序遍历序列构造二叉树
 * 
 * @param {number[]} preorder 先序遍历序列
 * @param {number[]} inorder 中序遍历序列
 * @return {TreeNode}
*/
function buildTreeByPreAndIn (preorder: number[], inorder: number[]): TreeNode {
    if (!preorder.length && !inorder.length) {
        return null;
    }
    // 前序遍历的第一个节点即为二叉树根节点
    let root = new TreeNode(preorder[0])
    // 获取根节点在中序序列中的位置
    let position = inorder.indexOf(root.val)
    // 中序遍历中根节点之前的节点为左子树节点
    let midLeft = inorder.slice(0, position)
    // 中序遍历中根节点之后的节点为右子树节点
    let midRight = inorder.slice(position+1)
    // 前序序列中根节点之后与中序遍历左子树节点相同长度为左子树
    let preLeft = preorder.slice(1, position + 1)
    // 前序序列中的右子树
    let preRight = preorder.slice(position + 1)
    // 递归生成左子树和右子树
    root.left = buildTreeByPreAndIn(preLeft, midLeft)
    root.right = buildTreeByPreAndIn(preRight, midRight)
    return root
}
复制代码

根据中序遍历和后序遍历结果构建二叉树

function buildTreeByPostAndIn (postOrder: number[], inOrder: number[]) : TreeNode {
    if (!postOrder.length && !inOrder.length) {
        return null
    }
    // 后序遍历的最后一个节点为根节点
    let root = new TreeNode(postOrder[postOrder.length - 1])
    // 获取根节点在中序遍历中的位置
    let position = inOrder.indexOf(root.val)
    // 中序序列根节点之前的均为左子树
    let midLeft = inOrder.slice(0, position)
    // 中序序列根节点之后的均为右子树
    let midRight = inOrder.slice(position+1)
    // 后序序列从前开始的左子树长度与中序序列相同
    let postLeft = postOrder.slice(0, position)
    // 后序序列的右子树
    let postRight = postOrder.slice(position, postOrder.length - 1)
    root.left = buildTreeByPostAndIn(postLeft, midLeft)
    root.right = buildTreeByPostAndIn(postRight, midRight)

    return root
}
复制代码

路径总和

此部分代码在 RouteSum.ts 中

/**
 *  计算是否有一条路径上的总和等于目标和
 * @param {TreeNode} root 
 * @param {number} sum 
 * @return {boolean}
 */
function RouteSum (root : TreeNode, sum : number) : boolean {
    const getSum =  (root: TreeNode, sum: number, total: number) : boolean => {
        // 判断是否为叶子节点,若是叶子节点计算当前路径上的和
        if (!root.left && !root.right) {
            total += root.val
            if (total === sum) {
                return true
            } else {
                return false
            }
        } else {
            // 如果只存在左子树
            if (root.left && !root.right) {
                return getSum(root.left, sum, total+root.val)
            } 
            // 如果只存在右子树
            else if (root.right && !root.left) {
                return getSum(root.right, sum, total+root.val)
            } 
            else {
                return (getSum(root.left, sum, total+root.val) || getSum(root.right, sum, total+root.val))
            }
        }
        
    }
    // 如果传入的是空树
    if (!root || root.val === undefined) {
        return false
    }
    return getSum(root, sum, 0);
}
复制代码

路径总和附带路径

此部分代码在 RouteSum.ts 中

/**
 * 给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
 * 
 * @param {TreeNode} root 
 * @param {number} sum 
 * @return {number[][]}
 */
 function RouteSumWithRoute(root: TreeNode, sum: number) : number[][] {
     let result = []
     const getSumRoute = (root: TreeNode, sum: number,total: number, array: number[] = []) => {
         // 判断是否为叶子节点,若是叶子节点计算当前路径上的和
        if (!root.left && !root.right) {
            total += root.val
            
            if (total === sum) {
                array.push(root.val)
                result.push(array)
            }
        } else {
            array.push(root.val)
            // 如果只存在左子树
            if (root.left && !root.right) {
                
                getSumRoute(root.left, sum, total+root.val, [...array])
            } 
            // 如果只存在右子树
            else if (root.right && !root.left) {
                 getSumRoute(root.right, sum, total+root.val, [...array])
            } 
            else {
                 getSumRoute(root.left, sum, total+root.val, [...array])
                 getSumRoute(root.right, sum, total+root.val, [...array])
            }
        }
    }
    // 如果传入的是空树
    if (!root) {
        return []
    }
    getSumRoute(root, sum, 0)
    return result
 }
复制代码

二叉树展开为链表

此部分代码在 TreeToList.ts中

/**
 * 给定一个二叉树,原地将它展开为链表。
 * @param {TreeNode} root 
 */
function TreeToList(root: TreeNode) : void {
    while (root != null) {
        // 如果左子树为 null ,直接考虑右子树
        if (root.left === null) {
            root = root.right
        } else {
            // 寻找左子树最右的节点
            let pre = root.left
            while (pre.right !== null) {
                pre = pre.right
            }
            // 将原来的右子树插入左子树最右边节点的右子树
            pre.right = root.right
            // 将左子树插入到右子树
            root.right = root.left
            root.left = null
            root = root.right
        }
    }
}
复制代码

链表

链表是用一组任意存储的单元来存储线性表的数据元素。一个对象存储着本身的值和下一个元素的地址。

  • 查询元素需要遍历到指定元素,查询慢
  • 插入元素只需要断开链接重新赋值,插入快

类似于 React 16 的 Fiber Node 连接而成的 Fiber Tree ,就是个单链表结构。

链表的数据结构

此部分代码在 LinkList.ts 中

首先我们定义链表内节点的数据结构

class ListNode {
    val: any
    next: ListNode

    /**
     * 
     * @param {*} val 
     * @param {ListNode} next 
     */
    constructor (val: any, next: ListNode) {
        this.val = val
        this.next = next
    }
}
复制代码

之后我们定义链表的数据结构和一些基础操作(查询,插入,删除)

class List  {
    head: ListNode
    constructor(arr ? : any[]) {
        if (arr) {
            this.head = new ListNode(arr[0], null)
            let current = this.head
            for(let i = 1; i < arr.length; i++) {
                current.next = new ListNode(arr[i], null)
                current = current.next
            }
        } else {
            this.head = new ListNode(null, null)
        }
        
    }

    /**
     * 从 0 开始计算,找到包括head 头节点 在哪的位于 index 位置的节点
     * @param { Number } index
     * @return {ListNode}
     */
    public find(index: number): ListNode {
        let current = this.head;
        for (let i = 0; i < index; i++) {
            current = current.next;
        }
        return current;
    }
    /**
     * 在指定位置插入节点
     * 
     * @param {any} value 
     * @param {number} index 
     */
    public insert(value: any, index: number): void {
        // 获取当前位置前一个节点
        let prev = this.find(index-1)
        let next = new ListNode(value, prev.next)
        prev.next = next
    }
    /**
     *  删除指定位置的节点
     * 
     * @param {number} index 
     */
    public delete(index: number): void {
        // 如果要删除头节点
        if (index === 0) {
            this.head = this.head.next
        } else if (this.find(index) === null || this.find(index).val === null) {
            throw Error('输入节点不存在')
        } else {
            // 获取要删除节点的前一个节点
            let prev = this.find(index-1)
            prev.next = prev.next.next
        }
    }

}
复制代码

链表去重

此部分代码在 List 类中

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

/**
     * 删除重复元素
     */
    public DeleteDuplicates(): void {
        let current = this.head
        // 暂存值前一个节点的值
        let temp: any
        // 要删除节点的
        let toDelete: ListNode

        while(current && current.next !== null) {
            temp = current.val
            // 如果重复, 删除重复节点
            if (current.next.val === temp) {
                toDelete = current.next
                current.next = toDelete.next
            } else {
                current = current.next
                temp = current.val
            }
        }
    }
复制代码

正向遍历与反向遍历

此部分代码在 List 类中

 /**
     * 正序遍历链表,返回一个数组
     * 
     * @return {any[]}
     * 
     */
    public PositiveTraverse(): any[] {
        let arr = []
        let current = this.head
        while (current !== null) {
            arr.push(current.val)
            current = current.next
        }
        return arr
    }

    /**
     * 逆序遍历链表,返回一个数组
     * 
     * @return {any[]}
     */
    public NegativeTraverse(): any[] {
        let arr = []
        let current = this.head;
        while (current !== null) {
            arr.unshift(current.val)
            current = current.next
        }
        return arr
    }
复制代码

反转链表

此部分代码在 ReverseList.ts 中

将链表进行翻转,尾节点变为新的头节点,输入一个链表的头节点,返回反转后的新的头节点

/**
 * 反转链表
 * 
 * @param {ListNode} head 
 * @return {ListNode}
 */
function ReverseList (head: ListNode): ListNode {
    let headNode = head
    let preNode = null
    let next = null
    // 遍历所有的节点
    while(headNode) {
        // 暂存原来的next 用于遍历
        next = headNode.next
        // 将节点的next指向反转
        headNode.next = preNode
        preNode = headNode
        headNode = next
    }

    return preNode
}
复制代码

反转链表 II

此部分代码在 ReverseList.ts 中

反转从位置 mn 的链表。

/**
 * 反转从位置 m 到 n 的链表
 * @param {ListNode} head
 * @param {number} m 反转开始位置
 * @param {number} n 反转结束位置
 * @return {ListNode}
 */

 function ReverseBetween (head: ListNode, m: number, n: number): ListNode {
    if (!head || head.next == null) {
        return head
    }
    let current = head
    let prev = null
    // 首先向后遍历使 current 为要反转的首个节点
    while ( m > 1) {
        prev = current
        current = current.next
        m--
        n--
    }
    // 保存要反转节点的前一个节点
    let prevTail = prev
    // 保存第 m 个节点,即反转后的尾节点
    let theTail = current
    // 保存next节点用于遍历
    let theNext = null
    while (n > 0) {
        theNext = current.next
        current.next = prev
        prev = current
        current = theNext
        n--
    }
    // 如果从首个节点开始反转
    if (prevTail === null) {
        head = prev
    } 
    // 否则反转前一个节点连接反转后的头节点
    else {
        prevTail.next = prev
    }
    // 连接反转尾节点与原链表后序节点
    theTail.next = current
    
    return head

 }
复制代码

合并链表

此部分代码在 MergeList.ts 中

传入两个有序链表的头节点,返回一个合并两个链表的有序链表

/**
 * 合并两个有序链表
 * 
 * @param {ListNode} pHead 
 * @param {ListNode} qHead 
 * @return {List}
 */
function MergeList(pHead: ListNode, qHead: ListNode): List {
    let list = new List()
    if (!pHead) {
        list.head = qHead
        return list
    } else  if (!qHead) {
        list.head = pHead
        return list
    }
    let node = list.head
    // 当两个链表均为完全合并时
    while(pHead && qHead) {
        if (pHead.val < qHead.val) {
            node.next = pHead
            pHead = pHead.next
        } else {
            node.next = qHead
            qHead = qHead.next
        }

        node = node.next
    }
    // 链接未完全合并链表的后序节点
    if (!pHead) {
        node.next = qHead
    } else {
        node.next = pHead
    }
    list.head = list.head.next

    return list
    
}
复制代码

删除链表的倒数第N个节点

这部分代码在 DeleteN.ts 中

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

/**
 * 删除链表的倒数第N个节点
 * 
 * @param {ListNode} head 
 * @param {number} n 
 * @return {ListNode}
 */
function DeleteNFromEnd (head: ListNode, n: number) :ListNode {
    if (!head || n <= 0) {
        return null
    }
    let node = new ListNode(0, null)
    node.next = head
    // 利用双指针法,首先领 a b 节点都指向头节点
    let a: ListNode = node
    let b: ListNode = node
    let c: ListNode = null
    // a提前移动 n 个节点
    for(let i = 0; i < n; i++) {
        a = a.next
        // 如果链表长度不超过 n
        if (!a) {
            return null
        }
    }
    // 向后遍历直到a到达尾部,此时b就为要删除的节点,c始终为b的前一个节点
    while(a) {
        c = b
        b = b.next
        a = a.next
    }
    // 删除b节点
    c.next = c.next.next

    return node.next
}

复制代码

旋转链表指定的长度

此部分代码在 RotateRight.ts 中

给定一个链表,让每个节点向右移动k个位置。

例如:

输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
复制代码
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
复制代码
/**
 * 给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
 * 
 * @param {ListNode} head 
 * @param {number} k 
 * @return {ListNode}
 */
function RotateRight (head: ListNode, k: number):ListNode {
    // 判断链表是否为空或只有一个节点
    if (head === null || head.next === null) {
        return head
    }
    // 定义n为链表长度
    let n : number
    // 定义一个节点用来遍历链表
    let old_tail = head
    // 首先获取链表内节点个数
    for(n = 1; old_tail.next !== null; n++) {
        old_tail = old_tail.next
    }
    // 形成闭环
    old_tail.next = head

    // 新的尾节点为 (n - k % n - 1) 个节点
    // 新的头节点为 (n - k % n) 个节点
    // 定义新的尾节点
    let new_tail = head
    for(let i = 0; i < (n - k % n - 1); i++) {
        new_tail = new_tail.next
    }
    // 定义新的头节点
    let new_head = new_tail.next;
    // 断开新的尾节点与后面的连接
    new_tail.next = null

    return new_head

}
复制代码

两两交换链表中的节点

此部分代码在 Exchange.ts 中

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

/**
 * 递归两两交换相邻的节点
 * 
 * @param {ListNode} head 
 * @return {ListNode}
 */
function Exchange(head: ListNode) : ListNode {
    // 如果遍历到最后一个或最后为单数节点
    if (head === null || head.next === null) {
        return head
    }
    // 找到后面要交换的节点
    let next = head.next
    // head链接后面已经完成的子链
    head.next = Exchange(next.next)
    // next 链接head
    next.next = head
    return next
}
复制代码

分隔链表

此部分代码在 SeparateList.ts 中

给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。

同时保留两个分区中每个节点的初始相对位置。

/**
 * 给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
 * 
 * @param {ListNode} head 
 * @param {number} x 
 */
function SeparateList (head: ListNode, x: number) {
    // 使用双指针法
    let before_x = new ListNode(0, null)
    let after_x = new ListNode(0, null)
    let before_head = before_x
    let after_head = after_x
    // 遍历原链表,小于的放在 before 大于等于的放在 after
    while(head) {
        if (head.val < x) {
            before_x.next = head
            before_x = before_x.next
        } else {
            after_x.next = head
            after_x = after_x.next
        }
        head = head.next
    }
    // 结束尾节点
    after_x.next = null
    // 合并原链表
    before_x.next = after_head.next
    return before_head.next
}
复制代码

栈是一种先入后出的数据结构。只能通过栈顶进行入栈和出栈操作。

栈的实现和基础操作

此部分代码在 Stack.ts 中

class Stack<T> {
    // 栈的存储空间
    data: Array<T>
    // 栈顶元素索引
    top: number

    constructor() {
        this.data = []
        this.top = 0
    }
    // 入栈
    public push (item: any):void {
        this.data.push(item)
        this.top++
    }
    // 出栈
    public pop (): T {
        this.top--
        return  this.data.pop()
    }
    // 返回栈顶的元素
    public peek (): T {
        return this.data[this.top - 1]
    }
    // 判断栈是否为空
    public isEmpty (): Boolean {
        return this.top === 0 
    }
    // 返回栈的大小
    public size (): number {
        return this.top
    }
    // 清空栈
    public clear (): void {
        delete this.data
        this.top = 0
        this.data = []
    }
    // 打印栈
    public print (): void {
        console.log(this.data.toString())
    }
}
复制代码

栈的入栈和出栈序列检测

此部分代码在 StackCheck.ts 中

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。

例如 入栈顺序为 [1,2,3,4,5] 那么 [4,3,5,2,1]是可以为出栈序列的,但[4,3,5,1,2]却不是

/**
 * 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
 * 
 * @param {Array} pushOrder 
 * @param {Array} popOrder 
 * @return {Boolean}
 */
function StackCheck<T> (pushOrder: Array<T>, popOrder: Array<T>): Boolean {
    // 如果长度不同,则直接证明不对应
    if (pushOrder.length !== popOrder.length) {
        return false
    }
    // 做一个辅助栈
    const stack = new Stack()
    // 设置入序指针和出序指针
    let push = 0, pop = 0;
    // 将入栈序列元素一次进入辅助栈
    // 检查辅助栈顶元素和出栈序列栈顶元素是否一致:
    // 元素一致,弹出辅助栈元素,出栈序列指针后移
    // 不一致,则证明不匹配
    while(pop < popOrder.length) {
        for(; push < pushOrder.length && popOrder[pop] !== stack.peek(); push++) {
            stack.push(pushOrder[push])
        }
        // 如果插入完毕但无匹配 则证明部位匹配的序列
        if (popOrder[pop] !== stack.peek()) {
            return false
        }
        // 若一致 则辅助栈弹出栈元素 
        stack.pop()
        pop++
    }
    return true
}
复制代码

队列

队列是一种先进先出的数据结构。通过队首出队列,通过队尾入队列。

队列的实现和基础操作

此部分代码在 Queue.ts 中

class Queue<T> {
    // 队列的存储空间
    queue: Array<T>

    constructor () {
        this.queue = []
    }
    // 入队列
    public push(item: T):void {
        this.queue.push(item)
    }

    // 出队列
    public pop(): T {
        if (this.queue.length === 0) {
            return null
        }
        return this.queue.shift()
    }

    // 返回队首元素
    public peek(): T {
        return this.queue[0]
    }

    // 队列是否为空
    public isEmpty() : Boolean {
        return this.queue.length === 0
    }
}

复制代码

循环队列

此部分代码在 CircleQueue.ts 中

循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

class CircleQueue<T> {
    // 队列长度限制
    k: number
    // 头部指针
    head: number
    // 尾部指针
    tail: number
    // 队列长度
    size: number
    // 存储空间
    data: Array<T>
    constructor(k: number) {
        this.k = k
        this.head = -1
        this.tail = -1
        this.size = 0
        this.data = new Array(k)
    }

    // 获取队首元素,如果队列为空,返回-1
    public getFront(): T | number {
        return this.size === 0 ? -1 : this.data[this.head]
    }
    // 获取队尾元素,如果队列为空,返回-1
    public getRear(): T | number {
        return this.size === 0 ? -1 : this.data[this.tail]
    }
    // 入循环队列
    public enQueue(item: T): Boolean {
        // 队列已满
        if (this.size === this.k) {
            return false
        }
        if (this.tail === this.head && this.tail === -1) {
            this.head++
        }
        // 判断是否尾节点是否是队列最后一个节点,如果是,通过改变为第一个来实现循环
        this.tail = this.tail === this.k -1 ? 0 : this.tail + 1
        this.size++
        this.data[this.tail] = item
        return true
    }
    // 出循环队列
    public deQueue () {
        if (this.size === 0) {
            return false
        }
        delete this.data[this.head]
        // 头节点向后移动
        this.head = this.head === this.k - 1 ? 0 : this.head + 1
        this.size--
        // 如果清空后为空
        if (this.size === 0) {
            this.head = -1
            this.tail = -1
        }
        return true
    }

    // 检查队列是否为空
    isEmpty (): Boolean {
        return this.size === 0
    }

    // 检查循环队列是否已满
    isFull () {
        return this.size === this.k
    }
}

复制代码

循环双端队列

此部分代码在 CircularDeque.ts

设计一个双端循环队列,头部可以插入和删除,尾部可以插入和删除,同时可以循环,我们可以直接使用限制长度的数组直接实现。

class CircularDeque <T> {
    // 队列长度限制
    k: number
    // 队列长度
    size: number
    // 存储空间
    data: Array<T>

    constructor(k: number) {
        this.k = k
        this.size = 0
        this.data = new Array()
    }

    // 将一个元素添加到双端队列头部
    public insertFront(item: T): Boolean {
        if (this.size === this.k) {
            return false
        } else {
            this.size++
            this.data.unshift(item)
            return true
        }
    }

    // 将一个元素添加到双端队列尾部
    public insertLast(item: T): Boolean {
        if (this.size === this.k) {
            return false
        } else {
            this.size++
            this.data.push(item)
            return true
        }
    }

    // 从头部删除一个元素
    public deleteFront(): Boolean {
        if (this.size === 0) {
            return false
        } else {
            this.size--
            this.data.shift()
            return true
        }
    }

    // 从尾部删除一个元素
    public deleteLast(): Boolean {
        if (this.size === 0) {
            return false
        } else {
            this.size--
            this.data.pop()
            return true
        }
    }

    // 从双端队列头部获得一个元素
    public getFront() : T | number {
        if (this.size === 0) {
            return -1
        } else {
            return this.data[0]
        }
    }

    // 从双端队列尾部获取一个元素
    public getRear(): T | number {
        if (this.size === 0) {
            return -1
        } else {
            return this.data[this.size - 1]
        }
    }

    // 检查双端队列是否为空
    public isEmpty(): Boolean {
        return this.size === 0 ? true : false
    }

    // 检查双端队列是否满了
    public isFull(): Boolean {
        return this.size === this.k ? true : false
    }
}
复制代码

堆的底层其实是一棵完全二叉树。

堆有两种情形,最大堆最小堆

  • 最大堆:每个节点的元素值不小于其子节点
  • 最小堆:每个节点的元素值不大于其子节点

堆的数据结构

此部分代码在 Heap.ts 中

enum Type { 
    Min = 'min', 
    Max = 'max'
}

class Heap {
    // 堆的类型
    type: Type
    value: number[]

    constructor(type: Type) {
        this.type = type
        this.value = []
    }

    // 传入数组,创建一个堆
    public create(numbers: number[]): void {
        this.value = numbers
        const length = this.value.length
        // 从第一个非叶子节点开始调整结构
        for(let i = Math.floor((length/2) -1); i>=0;i--) {
            this.adjust(i, length)
        }
    }

    // 调整堆的结构
    public adjust(index: number, length:number): void {
        const array = this.value
        for(let i = 2 * index + 1; i < length; i = 2 * i + 1) {
            if (i + 1 < length) {
                // 如果符合堆的规则
                if ((this.type === Type.Max && array[i + 1] > array[i]) || (this.type === Type.Min && array[i+1] < array[i])) {
                    i++
                }
            }
            // 如果不符合规则 则进行交换
            if ((this.type === Type.Max && array[index] < array[i]) || (this.type === Type.Min && array[index] > array[i])) {
                [array[index], array[i]] = [array[i], array[index]]
                index = i
            }
        }
    }

    // 向堆中添加元素
    public add (item: number): void {
        const array = this.value
        array.push(item)
        if (array.length > 1) {
            let index = array.length - 1
            let target = Math.floor((index - 1) / 2)
            while(target >= 0) {
                if ((this.type === Type.Min && array[index] < array[target]) || (this.type === Type.Max && array[index] > array[target])) {
                    [array[index], array[target]] = [array[target], array[index]]
                    index = target
                    target = Math.floor((index - 1) / 2)
                } else {
                    break
                }
            }
        }
    }

    // 弹出堆顶元素
    public pop(): number {
        const array = this.value
        let result = null
        if (array.length > 1) {
            result = array[0]
          // 将最后一个叶子结点置于堆顶 之后调整结构
            array[0] = array.pop()
            this.adjust(0, array.length)
        } else if (array.length === 1) {
            return array.pop()
        }
        return result
    }
}

复制代码

参考链接:

关注下面的标签,发现更多相似文章
评论