栈
栈的概念
- 栈是一种遵从后进先出原则的有序集合。
- 添加新元素的一端称为栈顶,另一端称为栈底。
- 操作栈的元素时,只能从栈顶操作(添加、移除或取值)。
栈的实现
我们需要实现以下功能
- push() 入栈方法
- pop() 出栈方法
- top() 获取栈顶值
- size() 获取栈的元素个数
- clear() 清空栈
class Stack {
constructor () {
// 存储栈的数据
this.data = {}
// 记录栈的数据个数(相当于数组的 length)
this.count = 0
}
// push() 入栈方法
push (item) {
// 方式1:数组方法 push 添加
// this.data.push(item)
// 方式2:利用数组长度
// this.data[this.data.length] = item
// 方式3:计数方式
this.data[this.count] = item
// 入栈后,count 自增
this.count++
}
// pop() 出栈方法
pop () {
// 出栈的前提是栈中存在元素,应先行检测
if (this.isEmpty()) {
console.log('栈为空!')
return
}
// 移除栈顶数据
// 方式1:数组方法 pop 移除
// return this.data.pop()
// 方式2:计数方式
const temp = this.data[this.count - 1]
delete this.data[--this.count]
return temp
}
// isEmpty() 检测栈是否为空
isEmpty () {
return this.count === 0
}
// top() 用于获取栈顶值
top () {
if (this.isEmpty()) {
console.log('栈为空!')
return
}
return this.data[this.count - 1]
}
// size() 获取元素个数
size () {
return this.count
}
// clear() 清空栈
clear () {
this.data = []
this.count = 0
}
}
const s = new Stack()
s.push('a')
s.push('b')
s.push('c')
Leet Code 面试题30:包含 min 函数的栈
-
不使用数组相关方法
// 在存储数据的栈外,再新建一个栈,用于存储最小值 class MinStack { constructor () { // stackA 用于存储数据 this.stackA = [] this.countA = 0 // stackB 用于将数据降序存储(栈顶值为最小值) this.stackB = [] this.countB = 0 } // 入栈 push (item) { // stackA 正常入栈 this.stackA[this.countA++] = item // stackB 如果没有数据,直接入栈 // 如果 item 的值 <= stackB 的最小值,入栈 if (this.countB === 0 || item <= this.min()) { this.stackB[this.countB++] = item } } // 最小值函数 min () { return this.stackB[this.countB - 1] } // 获取栈顶值 top () { return this.stackA[this.countA - 1] } // 出栈 pop () { // 先进行 stackB 的检测 // 如果 stackA 的栈顶值 === stackB 的栈顶值,stackB 出栈 if (this.top() === this.min()) { delete this.stackB[--this.countB] } // stackA 出栈 delete this.stackA[--this.countA] } } const m = new MinStack()
-
使用数组相关方法
class MinStack { constructor () { this.stack = [] } // 入栈 push (item) { this.stack.push(item) } // 查看栈顶值 top () { return this.stack[this.stack.length - 1] } // 实现最小值功能 min () { return Math.min.apply(null, this.stack) } // 出栈方法 pop () { return this.stack.pop() } } const m = new MinStack()
Leet Code 739、每日温度:
请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
/**
* @param {number[]} T 每日温度数组 [73, 74, 75, 71, 69, 72, 76, 73]
* @return {number[]} 等待天数列表 [1, 1, 4, 2, 1, 1, 0, 0]
*/
var dailyTemperatures = function(T) {
// 创建单调栈用于记录(存储索引值,用于记录天数)
const stack = [0]
let count = 1
// 创建结果数组(默认将结果数组使用 0 填充)
const len = T.length
const arr = new Array(len).fill(0)
// 遍历 T
for (let i = 1; i < len; i++) {
let temp = T[i]
// 使用 temp 比较栈顶值,如果栈顶值小,出栈(计算日期差,并存储),并重复操作
// - stack[count - 1] 代表栈顶值
while (count && temp > T[stack[count - 1]]) {
// 出栈
let index = stack.pop()
count--
// 计算 index 与 i 的差,作为 index 位置的升温日期的天数使用
arr[index] = i - index
}
// 处理完毕,当前温度入栈(等待找到后续的更大温度)
stack.push(i)
count++
}
return arr
}
队列
队列的概念
- 队列是一种遵从先进先出原则的有序集合。
- 添加新元素的一端称为队尾,另一端称为队首。
队列的实现
- enqueue() 入队方法
- dequeue() 出队方法
- top() 取队首值
- seize() 获取队列的元素个数
- clear() 清空队列
基于数组实现队列
class Queue {
constructor () {
// 用于存储队列数据
this.queue = []
this.count = 0
}
// 入队方法
enQueue (item) {
this.queue[this.count++] = item
}
// 出队方法
deQueue () {
if (this.isEmpty()) {
return
}
// 删除 queue 的第一个元素
// delete this.queue[0]
// 利用 shift() 移除数组的第一个元素
this.count--
return this.queue.shift()
}
isEmpty () {
return this.count === 0
}
// 获取队首元素值
top () {
if (this.isEmpty()) {
return
}
return this.queue[0]
}
size () {
return this.count
}
clear () {
// this.queue = []
this.length = 0
this.count = 0
}
}
const q = new Queue()
基于对象实现队列
class Queue {
constructor () {
this.queue = {}
this.count = 0
// 用于记录队首的键
this.head = 0
}
// 入队方法
enQueue (item) {
this.queue[this.count++] = item
}
// 出队方法
deQueue () {
if (this.isEmpty()) {
return
}
const headData = this.queue[this.head]
delete this.queue[this.head]
this.head++
this.count--
return headData
}
isEmpty () {
return this.count === 0
}
clear () {
this.queue = {}
this.count = 0
this.head = 0
}
}
const q = new Queue()
双端队列
双端队列指的是允许同时从队尾与队首两端进行存取操作的队列,操作更加灵活。
双端队列与 JavaScript 中的数组操作十分相似,只是不允许在数组两端以外的位置进行存取操作。
我们要实现以下新功能:
- addFront / addBack
- removeFront / removeBack
- frontTop / backTop
class Deque {
constructor () {
this.queue = {}
this.count = 0
this.head = 0
}
// 队首添加
addFront (item) {
this.queue[--this.head] = item
}
// 队尾添加
addBack (item) {
this.queue[this.count++] = item
}
// 队首删除
removeFront () {
if (this.isEmpty()) {
return
}
const headData = this.queue[this.head]
delete this.queue[this.head++]
return headData
}
// 队尾删除
removeBack () {
if (this.isEmpty()) {
return
}
const backData = this.queue[this.count - 1]
delete this.queue[--this.count]
// this.count-- 与 上一步 this.count - 1 合并
return backData
}
// 获取队首值
frontTop () {
if (this.isEmpty()) {
return
}
return this.queue[this.head]
}
// 获取队尾值
backTop () {
if (this.isEmpty()) {
return
}
return this.queue[this.count - 1]
}
isEmpty () {
return this.size() === 0
}
size () {
return this.count - this.head
}
}
const deq = new Deque()
剑指 Offer 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例 1
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
var MaxQueue = function() {
// 存储队列数据
this.queue = {}
// 双端队列维护最大值(每个阶段的最大值)
this.deque = {}
// 准备队列相关的数据
this.countQ = this.countD = this.headQ = this.headD = 0
};
/** 队尾入队
* @param {number} value
* @return {void}
*/
MaxQueue.prototype.push_back = function(value) {
// 数据在 queue 入队
this.queue[this.countQ++] = value
// 检测是否可以将数据添加到双端队列
// - 队列不能为空
// - value 大于队尾值
while (!this.isEmptyDeque() && value > this.deque[this.countD - 1]) {
// 删除当前队尾值
delete this.deque[--this.countD]
}
// 将 value 入队
this.deque[this.countD++] = value
};
/** 队首出队
* @return {number}
*/
MaxQueue.prototype.pop_front = function() {
if (this.isEmptyQueue()) {
return - 1
}
// 比较 deque 与 queue 的队首值,如果相同,deque 出队,否则 deque 不操作
if (this.queue[this.headQ] === this.deque[this.headD]) {
delete this.deque[this.headD++]
}
// 给 queue 出队,并返回
const frontData = this.queue[this.headQ]
delete this.queue[this.headQ++]
return frontData
};
/** 获取队列最大值
* @return {number}
*/
MaxQueue.prototype.max_value = function() {
if (this.isEmptyDeque()) {
return -1
}
// 返回 deque 队首值即可
return this.deque[this.headD]
};
/** 检测队列 deque 是否为空
*
*/
MaxQueue.prototype.isEmptyDeque = function () {
return !(this.countD - this.headD)
};
/** 检测队列 Queue 是否为空
*
*/
MaxQueue.prototype.isEmptyQueue = function () {
return !(this.countQ - this.headQ)
};
剑指 Offer 59 - I. 滑动窗口的最大值
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
/**
* @param {number[]} nums 传入数组
* @param {number} k 滑动窗口宽度
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
if (k <= 1) {
return nums
}
const result = []
const deque = []
// 1 将窗口第一个位置的数据添加到 deque 中,保持递减
deque.push(nums[0])
let i = 1
for (; i < k; i++) {
// - 存在数据
// - 当前数据大于队尾值
// - 出队,再重复比较
while (deque.length && nums[i] > deque[deque.length - 1]) {
deque.pop()
}
deque.push(nums[i])
}
// 将第一个位置的最大值添加到 result
result.push(deque[0])
// 2 遍历后续的数据
const len = nums.length
for (; i < len; i++) {
// 同上进行比较
while (deque.length && nums[i] > deque[deque.length - 1]) {
deque.pop()
}
deque.push(nums[i])
// 检测当前最大值是否位于窗口外
if (deque[0] === nums[i - k]) {
deque.shift()
}
// 添加最大值到 result
result.push(deque[0])
}
return result
};
链表
链表的概念
- 链表是有序的数据结构,链表中的每个部分称为节点。
- 链表可以从首、尾、中间进行数据存取。
- 链表的元素在内存中不必是连续的空间。
- 优点:添加与删除不会导致其余元素位移。
- 缺点:无法根据索引快速定位元素。
小结:
- 获取、修改元素时,数组效率高。
- 添加、删除元素时,链表效率高。
链表的实现
我们需要实现以下功能:
- 节点类:value、next
- 链表类:
- addAtTail 尾部添加节点
- addAtHead 头部添加节点
- addAtIndex 指定位置添加节点
- get 获取节点
- removeAtIndex 删除指定节点
// 节点类
class LinkedNode {
constructor (value) {
this.value = value
// 用于存储下一个节点的引用
this.next = null
}
}
// 链表类
class LinkedList {
constructor () {
this.count = 0
this.head = null
}
// 添加节点 (尾)
addAtTail (value) {
// 创建新节点
const node = new LinkedNode(value)
// 检测链表是否存在数据
if (this.count === 0) {
this.head = node
} else {
// 找到链表尾部节点,将最后一个节点的 next 设置为 node
let cur = this.head
while (cur.next != null) {
cur = cur.next
}
cur.next = node
}
this.count++
}
// 添加节点(首)
addAtHead (value) {
const node = new LinkedNode(value)
if (this.count === 0) {
this.head = node
} else {
// 将 node 添加到 head 的前面
node.next = this.head
this.head = node
}
this.count++
}
// 获取节点(根据索引)
get (index) {
if (this.count === 0 || index < 0 || index >= this.count) {
return
}
// 迭代链表,找到对应节点
let current = this.head
for (let i = 0; i < index; i++) {
current = current.next
}
return current
}
// 添加节点(根据索引)
addAtIndex (value, index) {
if (this.count === 0 || index >= this.count) {
return
}
// 如果 index <= 0,都添加到头部即可
if (index <= 0) {
return this.addAtHead(value)
}
// 后面为正常区间处理
const prev = this.get(index - 1)
const next = prev.next
const node = new LinkedNode(value)
prev.next = node
node.next = next
this.count++
}
// 删除(根据索引)
removeAtIndex (index) {
if (this.count === 0 || index < 0 || index >= this.count) {
return
}
if (index === 0) {
this.head = this.head.next
} else {
const prev = this.get(index - 1)
prev.next = prev.next.next
}
this.count--
}
}
// 测试代码
const l = new LinkedList()
l.addAtTail('a')
l.addAtTail('b')
l.addAtTail('c')
常见的链表形式有:
- 双向链表
- 双向链表指的是在普通链表的基础上增加一个用于记录上一个节点的属性 prev,可进行双向访问。
- 双向链表指的是在普通链表的基础上增加一个用于记录上一个节点的属性 prev,可进行双向访问。
- 循环链表(环形链表)
- 循环链表又称为环形链表,指的是链表最后一个节点的 next 指向第一个节点,形成首尾相连的循环结构,称为循环链表。
- 在实际使用中,环的结束点可以为链表的任意节点。
LeetCode: 206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
// 声明变量记录 prev、cur
let prev = null
let cur = head
// 当 cur 是节点时,进行迭代
while (cur) {
// 先保存当前节点的下一个节点
const next = cur.next
cur.next = prev
prev = cur
cur = next
}
return prev
};
递归实现反转链表
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
if (head === null || head.next === null) {
return head
}
const newHead = reverseList(head.next)
// 能够第一次执行这里的节点为 倒数第二个 节点
head.next.next = head
// head 的 next 需要在下一次递归执行时设置。当前设置为 null 不影响
// - 可以让最后一次(1)的 next 设置为 null
head.next = null
return newHead
};
LeetCode 面试题 02.08. 环路检测
给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。若环不存在,请返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
if (head === null || head.next === null) {
return null
}
// 声明快慢指针
let slow = head
let fast = head
while (fast !== null) {
// 慢每次指针移动一位
slow = slow.next
// 如果满足条件,说明 fast 为尾部结点,不存在环
if (fast.next === null) {
return null
}
// 快指针每次移动两位
fast = fast.next.next
// 检测是否有环
if (fast === slow) {
// 找到环的起点位置
let ptr = head
while (ptr !== slow) {
ptr = ptr.next
slow = slow.next
}
// ptr 和 slow 的交点就是环的起始节点
return ptr
}
}
// while 结束,说明 fast 为 null,说明链表没有环
return null
};
树
二叉树的遍历
- 二叉树的遍历从根节点开始,根据数据访问的顺序不同存在3中遍历形式:前序遍历、中序遍历、后序遍历。
- 这里的序表示树根节点的访问顺序。
- 前序遍历:按根节点 -> 左子树 -> 右子树 顺序进行遍历。
- 上述二叉树前序遍历结果为:ABDGHECFI
- 中序遍历:按 左子树 -> 根节点 -> 右子树 顺序进行遍历。
- 上述二叉树中序遍历结果为:GDHBEACIF
- 后序遍历:按 左子树 -> 右子树 -> 根节点 顺序进行遍历。
- 上述二叉树后序遍历结果为:GHDEBIFCA
LeetCode: 144. 二叉树的前序遍历
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]
-
递归方式实现
/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {number[]} */ var preorderTraversal = function(root) { // 用于存储遍历的结果 const res = [] // 设置函数用于进行递归遍历 const preorder = (root) => { // 当前结点为空时,无需进行递归 if (!root) { return } // 记录根节点值 res.push(root.val) // 前序遍历左子树 preorder(root.left) // 前序遍历右子树 preorder(root.right) } preorder(root) return res };
-
迭代算法方式实现
const preorderTraversal = function(root) { const res = [] const stk = [] while (root || stk.length) { while (root) { // 右子结点入栈 stk.push(root.right) // 记录根节点 res.push(root.val) // 下一步处理左子节点 root = root.left } // 左子树处理完毕,将 stk 出栈,处理右子树 root = stk.pop() } return res }
LeetCode: 104、二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
if (!root) {
return 0
}
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};
LeetCode: 102. 二叉树的层序遍历
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层序遍历结果:
[
[3],
[9,20],
[15,7]
]
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
const res = []
if (!root) {
return res
}
// 声明队列用于存储后续数据
const q = []
q.push(root)
// 遍历队列
while (q.length !== 0) {
// 针对本轮操作,创建一个新的二维数组
res.push([])
let len = q.length
for (let i = 0; i < len; i++) {
// 将本次操作的结点出队
const node = q.shift()
res[res.length - 1].push(node.val)
// 检测是否存在左右子结点,如果有,入队即可
if (node.left) {
q.push(node.left)
}
if (node.right) {
q.push(node.right)
}
}
}
return res
};
LeetCode: 98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
-
利用递归解题
/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {boolean} */ var isValidBST = function(root) { // 通过一个辅助函数来统一设置左右子树的比较 return helper(root, -Infinity, Infinity); }; const helper = (root, lower, upper) => { if (root === null) { return true } // 当前节点值超出边界,说明二叉树为非 BST if (root.val <= lower || root.val >= upper) { return false; } // 否则,递归处理左右子节点,并更新大小范围 // 同时根据左右子节点的返回值进行返回,只有全部递归结果均为 true, 才说明二叉树为 BST return helper(root.left, lower, root.val) && helper(root.right, root.val, upper); }
-
利用中序遍历解题
/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {boolean} */ var isValidBST = function(root) { let stk = [] // 用于记录上一次取得的节点值,BST 中这个值应小于当前节点 // 设置默认值为 -Infinity 避免对比较结果产生干扰 let oldNode = -Infinity while (root || stk.length) { while (root) { stk.push(root) root = root.left } root = stk.pop() // 如果任意节点比上个节点值小,说明二叉树不是 BST if (root.val <= oldNode) { return false } // 通过比较,记录当前节点值 oldNode = root.val root = root.right } return true };