作者正处大三复习阶段,一方面要把只看过概念的 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 中
反转从位置 m 到 n 的链表。
/**
* 反转从位置 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
}
}
参考链接:
- 前端该如何准备数据结构和算法? —— ConardLi
- 部分数据结构有关习题来源于 LeetCode
- 部分数据结构有关习题来源于《剑指Offer》