一、栈 stack
栈的概念
- 一个后进先出的数据结构
- JavaScript 没有栈,但可以用 Array 实现栈的所有功能
const stack = []
// 入栈操作 push()添加至数组最后一项
stack.push(1);
stack.push(2);
// 出栈操作 pop()移除数组最后一项,并返回
const item1 = stack.pop()
const item2 = stack.pop()
- 栈的应用场景
1.需要后进先出的场景
2.比如:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈.......
LeetCode:20.有效的括号
题目描述:
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
解题步骤:
1.新建一个栈
2.然后扫描字符串,遇到左括号入栈,遇到和栈顶括号类型匹配的右括号就出栈,类型不匹配就直接判定不合法
3.字符串扫描完了,最后栈为空就合法,否则不合法
var isValid = function (s) {
if (s.length % 2 === 1) { return false }
const stack = []
for (let i = 0; i < s.length; i++) {
const c = s[i];
const isLeft = c === '(' || c === '{' || c === '['
if (isLeft) {
stack.push(c)
}else {
const t = stack[stack.length - 1]
const isMatch = (t === '(' && c === ')') || (t === '{' && c === '}') || (t === '[' && c === ']')
if (isMatch) {
stack.pop()
}
else {
return false
}
}
}
return stack.length === 0
}
LeetCode:1047. 删除字符串中的所有相邻重复项
题目描述:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,
这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",
其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
解题步骤:
1.新建一个栈
2.然后扫描字符串,判断栈顶字符和扫描到的字符是否相等,如果相等,就出栈,否则扫描到的字符入栈
3.字符串扫描完了,在把栈转成字符串
var removeDuplicates = function (s) {
let stack = []
for (let i = 0; i < s.length; i++) {
if (stack[stack.length - 1] === s[i]) {
stack.pop()
}
else{
stack.push(s[i])
}
}
return stack.join('')
};
LeetCode:150. 逆波兰表达式求值
题目描述:
示例 1:
输入: ["2", "1", "+", "3", " * "]
输出: 9
解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
var evalRPN = function (tokens) {
let stack = []
for (const item of tokens) {
if (["+", "-", "*", "/"].includes(item) && stack.length >= 2) {
switch (item) {
case '+':
stack.push(stack.pop() + stack.pop())
break
case '-':
stack.push(stack[stack.length - 2] - stack[stack.length - 1])
stack.splice(stack.length - 3, 2)
break
case '*':
stack.push(stack.pop() * stack.pop())
break
case '/':
stack.push(parseInt(stack[stack.length - 2] / stack[stack.length - 1]))
stack.splice(stack.length - 3, 2)
break
}
} else {
stack.push(parseInt(item))
}
}
return stack.pop()
};
二、队列 queue
队列概念
- 队列是一个先进先出的数据结构
- JavaScript没有队列,但可以用Array实现队列的所有功能
const queue = [];
// 入队
queue.push(1)
queue.push(2)
// 出队 shift()移除数组第一个元素并返回
const item1 = queue.shift()
const item2 = queue.shift()
// 队头元素
queue[0]
- 应用场景:需要先进先出的场景,食堂排队打饭、js异步中任务队列、计算最近请求次数
LeetCode:933. 最近的请求次数
题目描述:
写一个 `RecentCounter` 类来计算特定时间范围内最近的请求。
越早发出的请求就越早不在最近3000ms内的请求里,满足先进先出,用队列
输入:
["RecentCounter", "ping", "ping", "ping", "ping"]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]
解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1); // requests = [1],范围是 [-2999,1],返回 1
recentCounter.ping(100); // requests = [1, 100],范围是 [-2900,100],返回 2
recentCounter.ping(3001); // requests = [1, 100, 3001],范围是 [1,3001],返回 3
recentCounter.ping(3002); // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3
var RecentCounter = function () {
this.q = []
};
RecentCounter.prototype.ping = function (t) {
this.q.push(t)
while (this.q[0] < t - 3000) {
this.q.shift()
}
return this.q.length
};
LeetCode:239. 滑动窗口最大值
题目描述
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
你只可以看到在滑动窗口内的 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
var maxSlidingWindow = function (nums, k) {
let queue = [] // 存放单调队列的下标
let result = []
for (let i = 0; i < nums.length; i++) {
// 在滑动窗口之外的直接从队头删掉
if (i - queue[0] >= k) {
queue.shift()
}
// 如果新加进来的数比单调队列中原来的数都要大,则直接弹出队列中的其他数
while (nums[queue[queue.length - 1]] <= nums[i]) {
queue.pop()
}
queue.push(i)
if (i >= k - 1) {
result.push(nums[queue[0]])
}
}
return result
};
三、链表 linkedList
链表概念
- 多元素组成的列表
- 元素存储是不连续的,用next指针连在一起
{ val: 1, next: { val: 2, next: { val: 3, next: null } } }
- 链表和数组的区别
数组:增删非收尾元素时往往需要移动元素
链表:增删非首尾元素,不需要移动元素,只需要更改next指向即可
- JavaScript中没有链表这个数据结构,但是可以用object来模拟
const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }
const d = { val: 'd' }
a.next = b
b.next = c
c.next = d
// 遍历链表
let p = a;
while (p) {
console.log(p.val)
p = p.next
}
// 插入 (将e插入c和d之间)
const e = { val: 'e' }
c.next = e
e.next = d
// 删除 (删除e)
c.next = d
console.log(a)
LeetCode:237.删除链表中的节点
题目描述
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,
该链表应变为 4 -> 1 -> 9
// 解题思路:无法直接获取被删除节点的上一个节点,那就将被删除节点转移到下一个节点
// 解题步骤:将被删除节点的值改成下一个节点的值,删除下一个节点
var deleteNode = function (node) {
node.val = node.next.val
node.next = node.next.next
};
LeetCode:206.反转链表
题目描述:
输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
// 解题思路:如果反转两个节点:将n+1的next指向n,反转多个节点,则需要双指针遍历,重复上述操作
// 解题步骤:第一步:新建两个指针,一前一后遍历链表,第二步:反转双指针
var reverseList = function (head) {
let p1 = head
let p2 = null
while (p1) {
const tmp = p1.next
p1.next = p2
p2 = p1
p1 = tmp
}
return p2
};
LeetCode:83. 删除排序链表中的重复元素
题目描述
输入: head = [1,1,2]
输出: [1,2]
// 解题思路:因为输入的链表是有序的,所以重复元素一定相邻;
// 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素
// 解题步骤:
// 第一步:遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素
// 第二步:遍历结束后,返回原链表的头部
var deleteDuplicates = function (head) {
let p = head
while (p && p.next) {
if (p.val === p.next.val) {
p.next = p.next.next
}
else {
p = p.next
}
}
return head
};
LeetCode:141. 环形链表
// 解题思路:用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表有环
var hasCycle = function (head) {
let p1 = head
let p2 = head
while (p1 && p2 && p2.next) {
p1 = p1.next
p2 = p2.next.next
if (p1 === p2) {
return true
}
}
return false
};
LeetCode:1290.二进制链表转整数
题目描述
输入: head = [1,0,1]
输出: 5
解释: 二进制数 (101) 转化为十进制数 (5)
var getDecimalValue = function (head) {
let p = head
let arr = []
// 链表转数组
while (p) {
arr.push(p.val)
p = p.next
}
// 数组转字符串,再转十进制
return parseInt(arr.join(''),2)
};
LeetCode:21. 合并两个有序链表
题目描述
输入: l1 = [1,2,4], l2 = [1,3,4]
输出: [1,1,2,3,4,4]
var mergeTwoLists = function (l1, l2) {
let p1 = l1
let p2 = l2
let l3 = new ListNode(0)
let p3 = l3
if (l1 == null) { return l2 }
if (l2 == null) { return l1 }
while (p1 || p2) {
if (p1 == null) {
p3.next = p2
return l3.next
}
if (p2 == null) {
p3.next = p1
return l3.next
}
if (p1.val <= p2.val) {
let p = new ListNode(p1.val)
p3.next = p
p1 = p1.next
p3 = p3.next
} else {
let p = new ListNode(p2.val)
p3.next = p
p2 = p2.next
p3 = p3.next
}
}
return l3.next
};
LeetCode:203. 移除链表元素
题目描述:
输入: head = [1,2,6,3,4,5,6], val = 6
输出: [1,2,3,4,5]
var removeElements = function (head, val) {
let l = new ListNode(0)
let p = head
let p1 = l
while (p) {
if (p.val !== val) {
let p2 = new ListNode(p.val)
p1.next = p2
p1 = p1.next
}
p = p.next
}
return l.next
};
四、集合 set
集合概念
- 无序唯一的数据结构
- ES6中有集合,名为Set
- 集合的常用操作:去重、判断某元素是否在集合中、求交集
// 去重
const arr = [1, 1, 2, 2]
const arr2 = [...new Set(arr)]
// 判断元素是否在集合中
const set = new Set(arr)
const has = set.has(1) //true
const has = set.has(3) //false
// 求交集
const set2 = new Set([2, 3])
const set3 = new Set([...set].filter(item => set2.has(item)))
LeetCode:349. 两个数组的交集
var intersection = function (nums1, nums2) {
return [...new Set(nums1)].filter(item => nums2.includes(item))
};
五、字典 map
字典概念
- 与集合类似,字典也是一种存储唯一值的数据结构,但是它是以键值对的形式来存储的
- ES6有字典,名为Map
- 字典的常规操作,键值对的增删改查
const m = new Map()
// 增
m.set('a', 'aa')
m.set('b', 'bb')
// 删
m.delete('b')
//删除所有的键值对
// m.clear()
//改
m.set('a', 'aaa')
// 查
m.get('a') //true
LeetCode:20.有效的括号
var isValid = function (s) {
if (s.length % 2 === 1) {
return false
}
const stack = []
const map = new Map()
map.set('(', ')')
map.set('{', '}')
map.set('[', ']')
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (map.has(c)) {
stack.push(c)
} else {
const t = stack[stack.length - 1]
if (map.get(t) === c) {
stack.pop()
} else {
return false
}
}
}
return stack.length === 0
}
LeetCode:1. 两数之和
var twoSum = function(nums, target) {
const map = new Map()
for (let i = 0; i < nums.length; i++) {
const n = target - nums[i]
if (map.has(n)) {
return [map.get(n), i]
} else {
map.set(nums[i], i)
}
}
};
LeetCode:13. 罗马数字转整数
var romanToInt = function(s) {
let num = 0
let map = new Map([
['I', 1], ['V', 5], ['X', 10], ['L', 50], ['C', 100], ['D', 500], ['M', 1000],
['IV', 4], ['IX', 9], ['XL', 40], ['XC', 90], ['CD', 400], ['CM', 900]
])
for (let i = 0; i < s.length; i++) {
if ([...map.keys()].includes(s.slice(i, i + 2))) {
num = num + map.get(s.slice(i, i + 2))
i++
}
else {
num = num + map.get(s[i])
}
}
return num
};
六、树 tree
树概念
- 一种分层数据的抽象模型
- 前端工作中常见的树包括:DOM树、级联选择、树形控件......
- JS中没有树,但是可以用 Object 和 Array 构建树
- 树的常用操作:深度/广度优先遍历,先中后序遍历
- 二叉树中每个节点最多只能有两个子节点
// 二叉树
const bt = {
val: 1,
left: {
val: 2,
left: {
val: 4,
left: null,
right: null,
},
right: {
val: 5,
left: null,
right: null,
},
},
right: {
val: 3,
left: {
val: 6,
left: null,
right: null,
},
right: {
val: 7,
left: null,
right: null,
},
},
};
module.exports = bt;
- 深度优先遍历:尽可能深的搜索树的分支
- 广度优先遍历:先访问离根节点最近的节点
深度优先遍历
// 深度优先遍历算法口诀(尽可能深的搜索树的分支)
// 1.访问根节点
// 2.对根节点的 children 挨个进行深度优先遍历
const dfs = (tree) => {
console.log(tree.var);
tree.children.forEach(dfs);
// 等价于 tree.children.forEach((child) => dfs(child));
};
dfs(tree);
广度优先遍历
// 广度优先遍历算法口诀 (先访问离根节点最近的节点)
// 1.新建一个队列,把根节点入队
// 2.把队头入队并访问
// 3.把队头的children挨个入队
// 4.重复第二第三步,直到队列为空
const bfs = (tree) => {
const q = [tree];
while (q.length > 0) {
const n = q.shift();
console.log(n.var);
n.children.forEach((child) => {
q.push(child);
});
}
};
bfs(tree);
二叉树先序遍历
const bt = require("./bt");
// 二叉树先序遍历算法口诀
// 1.访问根节点
// 2.对根结点的左子树进行先序遍历
// 3.对根节点的右子树进行先序遍历
const preorder = (root) => {
if (!root) {
return;
}
console.log(root.val);
preorder(root.left);
preorder(root.right);
};
// preorder(bt);
// 非递归版
const preorder2 = (root) => {
if (!root) {
return;
}
// 模拟 js 的函数执行栈
const stack = [root];
while (stack.length) {
const n = stack.pop();
console.log(n.val);
// 因为栈是后进先出的数据结构,所以先放right节点再放left节点
if (n.right) stack.push(n.right);
if (n.left) stack.push(n.left);
}
};
preorder2(bt);
二叉树中序遍历
const bt = require("./bt");
// 二叉树中序遍历算法口诀
// 1.对根节点的左子树进行中序遍历
// 2.访问根节点
// 3.对根节点的右子树进行中序遍历
const inorder = (root) => {
if (!root) {
return;
}
inorder(root.left);
console.log(root.val);
inorder(root.right);
};
// inorder(bt)
// 非递归版
const inorder2 = (root) => {
if (!root) return;
const stack = [];
let p = root;
while (stack.length || p) {
while (p) {
stack.push(p);
p = p.left;
}
const n = stack.pop();
console.log(n.val);
p = n.right;
}
};
inorder2(bt);
二叉树后序遍历
const bt = require("./bt");
// 二叉树后序遍历算法口诀
// 1.对根节点的左子树进行后序遍历
// 2.对根节点的右子树进行后序遍历
// 3.访问根节点
const postordder = (root) => {
if (!root) {
return;
}
postordder(root.left);
postordder(root.right);
console.log(root.val);
};
// postordder(bt);
// 非递归版
const postordder2 = (root) => {
if (!root) return;
// 模拟 js 的函数执行栈
const stack = [root];
const outputStack = [];
while (stack.length) {
const n = stack.pop();
outputStack.push(n);
if (n.left) stack.push(n.left);
if (n.right) stack.push(n.right);
}
while (outputStack.length) {
const n = outputStack.pop();
console.log(n.val);
}
};
postordder2(bt);
LeetCode:226.翻转二叉树
const flipTree = (root) => {
const inverNode = (left, right) => {
let temp = left;
left = right;
right = temp;
//需要重新给root赋值一下
root.left = left;
root.right = right;
}
if (root === null) return root
//确定节点处理逻辑 交换
inverNode(root.left, root.right);
flipTree(root.left)
flipTree(root.right)
return root
}
LeetCode:100. 相同的树
题目描述:
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
var isSameTree = function (p, q) {
if (p === null && q === null) return true
if (p === null || q === null) return false
return p.val === q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
};
LeetCode:101. 对称二叉树
const symmetric = (queue) => {
let arr = queue.map(item => {
if (!item) { return null }
else { return item.val }
});
for (let i = 0; i < arr.length / 2; i++) {
if (arr[i] !== arr[arr.length - 1 - i]) {
return false
}
}
return true
}
var isSymmetric = function (root) {
if (!root) return false
let queue = [root]
while (queue.length) {
let len = queue.length
for (let i = 0; i < len; i++) {
let n = queue.shift()
if (n) {
queue.push(n.left)
queue.push(n.right)
}
}
if (!symmetric(queue)) { return false }
}
return true
};
LeetCode:102. 二叉树的层序遍历
题目描述:
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
var levelOrder = function (root) {
let queue = []
let result = []
queue.push(root)
if (root === null) return result
while (queue.length) {
let len = queue.length // 当前层级点数
let curLevel = [] // 每一层节点
for (let i = 0; i < len; i++) {
let n = queue.shift()
curLevel.push(n.val)
n.left && queue.push(n.left)
n.right && queue.push(n.right)
}
result.push(curLevel)
}
return result
};
LeetCode:104. 二叉树的最大深度
题目描述:
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3
var maxDepth = function (root) {
if (root === null) return 0
let queue = [root]
let count = 0 // 二叉树深度
while (queue.length) {
let len = queue.length // 当前层级节点个数
for (let i = 0; i < len; i++) {
let n = queue.shift()
n.left && queue.push(n.left)
n.right && queue.push(n.right)
}
count++
}
return count
};
LeetCode:111. 二叉树的最小深度
var minDepth = function (root) {
if (root === null) return 0
let queue = [root]
let count = 1 // 二叉树的最小深度
while (queue.length) {
let len = queue.length // 当前层级节点个数
for (let i = 0; i < len; i++) {
let n = queue.shift()
n.left && queue.push(n.left)
n.right && queue.push(n.right)
if (!n.left && !n.right) {
return count
}
}
count++
}
};
LeetCode:515. 在每个树行中找最大值
题目描述:
给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
输入: root = [1,3,2,5,3,null,9]
输出: [1,3,9]
var largestValues = function (root) {
let queue = []
let result = []
if (root === null) return result
queue.push(root)
while (queue.length) {
let len = queue.length // 当前层级节点个数
let curLevelMax = null // 当前层级节点最大值
for (let i = 0; i < len; i++) {
let n = queue.shift()
if (curLevelMax === null) {
curLevelMax = n.val
}
else if (n.val > curLevelMax) {
curLevelMax = n.val
}
n.left && queue.push(n.left)
n.right && queue.push(n.right)
}
result.push(curLevelMax)
}
return result
};