栈(Stack)
概念介绍
栈是一种**后进先出(LIFO - Last In First Out)**的线性数据结构。可以把栈想象成一个只有一个开口的容器,元素只能从顶部进入和离开。
栈的特点
- 后进先出:最后压入栈的元素最先被弹出
- 只能在栈顶操作:插入和删除操作都只能在栈顶进行
- 线性结构:元素之间存在一对一的关系
JavaScript 中的栈实现
在 JavaScript 中没有内置的栈数据结构,但我们可以通过数组来模拟栈的行为:
js
// 创建一个空栈
const stack = []
// 入栈操作 - push方法在数组末尾添加元素
stack.push(1) // [1]
stack.push(2) // [1, 2]
stack.push(3) // [1, 2, 3]
// 出栈操作 - pop方法移除并返回数组末尾元素
const top = stack.pop() // 返回 3,栈变为 [1, 2]
// 查看栈顶元素(不移除)
const peek = stack[stack.length - 1] // 返回 2
// 检查栈是否为空
const isEmpty = stack.length === 0 // false
// 获取栈的大小
const size = stack.length // 2
栈的基本操作
js
class Stack {
constructor() {
this.items = []
}
// 入栈
push(element) {
this.items.push(element)
}
// 出栈
pop() {
if (this.isEmpty()) {
return undefined
}
return this.items.pop()
}
// 查看栈顶元素
peek() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.items.length - 1]
}
// 检查是否为空
isEmpty() {
return this.items.length === 0
}
// 获取栈大小
size() {
return this.items.length
}
// 清空栈
clear() {
this.items = []
}
}
栈的使用场景
栈在计算机科学和日常编程中有着广泛的应用:
1. 进制转换
- 十进制转二进制:通过不断除以2,将余数压入栈中,最后依次弹出得到二进制表示
- 表达式求值:中缀表达式转后缀表达式
2. 括号匹配
- 有效括号判断:检查括号是否正确配对和嵌套
- 编译器语法分析:检查代码中的括号、大括号、方括号是否匹配
3. 函数调用栈
- 递归函数:系统自动管理函数调用和返回
- 程序执行:保存函数的局部变量和返回地址
4. 浏览器历史记录
- 前进后退功能:浏览器的历史记录管理
- 撤销重做操作:文本编辑器的撤销功能
5. 算法应用
- 深度优先搜索(DFS):图和树的遍历
- 回溯算法:解决组合、排列等问题
- 单调栈:解决下一个更大元素等问题
算法题: LeetCode 第20题 - 有效的括号
题目描述
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合
- 左括号必须以正确的顺序闭合
- 每个右括号都有一个对应的相同类型的左括号
示例:
ini
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
提示:
1 <= s.length <= 10^4
s
仅由括号'()[]{}'
组成
解题思路
这是一个典型的栈应用问题:
- 遇到左括号:将其压入栈中
- 遇到右括号 :检查栈顶是否为对应的左括号
- 如果匹配,弹出栈顶元素
- 如果不匹配或栈为空,返回 false
- 遍历结束 :检查栈是否为空
- 栈为空说明所有括号都匹配
- 栈不为空说明有未匹配的左括号
代码实现
js
/**
* @param {string} s
* @return {boolean}
*/
function isValid(s) {
// 如果字符串长度为奇数,直接返回 false
if (s.length % 2 === 1) {
return false
}
const stack = []
const map = {
')': '(',
']': '[',
'}': '{'
}
for (let char of s) {
// 如果是右括号
if (char in map) {
// 检查栈是否为空或栈顶元素是否匹配
if (stack.length === 0 || stack.pop() !== map[char]) {
return false
}
} else {
// 如果是左括号,压入栈中
stack.push(char)
}
}
// 栈为空说明所有括号都匹配
return stack.length === 0
}
// 测试用例
console.log(isValid("()")) // true
console.log(isValid("()[]{}")) // true
console.log(isValid("(]")) // false
console.log(isValid("([])")) // true
复杂度分析
- 时间复杂度:O(n),其中 n 是字符串的长度,需要遍历字符串一次
- 空间复杂度:O(n),最坏情况下栈中存储所有的左括号
进阶:十进制转二进制
js
/**
* 使用栈将十进制转换为二进制
* @param {number} decimal
* @return {string}
*/
function decimalToBinary(decimal) {
if (decimal === 0) return '0'
const stack = []
while (decimal > 0) {
stack.push(decimal % 2) // 将余数压入栈
decimal = Math.floor(decimal / 2)
}
let binary = ''
while (stack.length > 0) {
binary += stack.pop() // 依次弹出得到二进制
}
return binary
}
// 测试
console.log(decimalToBinary(10)) // "1010"
console.log(decimalToBinary(8)) // "1000"
栈的总结
栈是一种简单而强大的数据结构,其**后进先出(LIFO)**的特性使其在以下场景中特别有用:
- ✅ 函数调用管理:递归和函数调用栈
- ✅ 表达式处理:括号匹配、中缀转后缀
- ✅ 算法实现:深度优先搜索、回溯算法
- ✅ 撤销操作:编辑器的撤销功能
- ✅ 浏览器历史:前进后退功能
💡 记忆口诀:只要遇到"后进先出"的场景,就考虑使用栈!
队列(Queue)
概念介绍
队列是一种**先进先出(FIFO - First In First Out)**的线性数据结构。可以把队列想象成排队买票,先来的人先服务,后来的人排在队尾等待。
队列的特点
- 先进先出:最先进入队列的元素最先被移除
- 两端操作:一端进行插入(队尾),另一端进行删除(队头)
- 线性结构:元素之间存在一对一的关系
JavaScript 中的队列实现
在 JavaScript 中没有内置的队列数据结构,我们可以通过数组来模拟队列的行为:
js
// 创建一个空队列
const queue = []
// 入队操作 - push方法在数组末尾添加元素(队尾)
queue.push(1) // [1]
queue.push(2) // [1, 2]
queue.push(3) // [1, 2, 3]
// 出队操作 - shift方法移除并返回数组第一个元素(队头)
const first = queue.shift() // 返回 1,队列变为 [2, 3]
// 查看队头元素(不移除)
const front = queue[0] // 返回 2
// 查看队尾元素(不移除)
const rear = queue[queue.length - 1] // 返回 3
// 检查队列是否为空
const isEmpty = queue.length === 0 // false
// 获取队列大小
const size = queue.length // 2
队列的基本操作
js
class Queue {
constructor() {
this.items = []
}
// 入队(在队尾添加元素)
enqueue(element) {
this.items.push(element)
}
// 出队(移除队头元素)
dequeue() {
if (this.isEmpty()) {
return undefined
}
return this.items.shift()
}
// 查看队头元素
front() {
if (this.isEmpty()) {
return undefined
}
return this.items[0]
}
// 查看队尾元素
rear() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.items.length - 1]
}
// 检查是否为空
isEmpty() {
return this.items.length === 0
}
// 获取队列大小
size() {
return this.items.length
}
// 清空队列
clear() {
this.items = []
}
}
性能优化:双端队列实现
使用数组的 shift()
方法时间复杂度为 O(n),对于大量数据可能性能较差。可以使用对象来实现更高效的队列:
js
class OptimizedQueue {
constructor() {
this.items = {}
this.head = 0
this.tail = 0
}
enqueue(element) {
this.items[this.tail] = element
this.tail++
}
dequeue() {
if (this.isEmpty()) {
return undefined
}
const item = this.items[this.head]
delete this.items[this.head]
this.head++
return item
}
front() {
return this.items[this.head]
}
isEmpty() {
return this.head === this.tail
}
size() {
return this.tail - this.head
}
}
队列的使用场景
队列在计算机科学和实际应用中有着广泛的用途:
1. 任务调度
- 操作系统进程调度:按照先来先服务的原则调度进程
- 打印队列:多个打印任务按顺序执行
- CPU任务调度:多任务系统中的任务管理
2. 前端开发
- 事件循环:JavaScript 的事件队列机制
- 微任务队列:Promise、async/await 的执行顺序
- 宏任务队列:setTimeout、setInterval 等异步任务
- 请求队列:控制并发请求数量
3. 广度优先搜索(BFS)
- 图的遍历:层序遍历图的节点
- 树的层序遍历:按层访问树的节点
- 最短路径:在无权图中寻找最短路径
4. 缓存和缓冲
- IO缓冲区:数据的临时存储
- 网络数据包:按顺序处理网络请求
- 流媒体缓冲:视频、音频的播放缓冲
5. 实际应用场景
- 银行排队系统:客户按到达顺序服务
- 呼叫中心:电话按接入顺序处理
- 消息队列:系统间异步通信(如 RabbitMQ、Kafka)
算法题: LeetCode 第933题 - 最近请求的次数
题目描述
写一个 RecentCounter
类来计算特定时间范围内最近的请求。
请你实现 RecentCounter 类:
RecentCounter()
初始化计数器,请求数为 0int ping(int t)
在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在[t-3000, t]
内发生的请求数- 保证每次对 ping 的调用都使用比之前更大的 t 值
示例:
scss
输入:
["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
提示:
1 <= t <= 10^9
- 保证每次对 ping 调用所使用的 t 值都严格递增
- 至多调用 ping 方法
10^4
次
解题思路
这是一个典型的队列应用问题:
- 维护时间窗口:我们需要维护一个 3000 毫秒的滑动时间窗口
- 队列特性:由于时间是递增的,可以使用队列来存储请求时间
- 清理过期请求:每次新请求到来时,移除队列中超出时间窗口的请求
- 返回计数:队列的长度就是时间窗口内的请求数
算法步骤:
- 将新请求时间加入队列
- 从队头开始移除所有小于
t - 3000
的请求时间 - 返回队列的长度
代码实现
js
/**
* 最近请求计数器
*/
class RecentCounter {
constructor() {
this.requests = [] // 使用数组模拟队列
}
/**
* @param {number} t
* @return {number}
*/
ping(t) {
// 将新请求时间加入队列
this.requests.push(t)
// 移除超出时间窗口的请求(小于 t - 3000 的请求)
while (this.requests.length > 0 && this.requests[0] < t - 3000) {
this.requests.shift()
}
// 返回时间窗口内的请求数
return this.requests.length
}
}
// 测试用例
const recentCounter = new RecentCounter()
console.log(recentCounter.ping(1)) // 1
console.log(recentCounter.ping(100)) // 2
console.log(recentCounter.ping(3001)) // 3
console.log(recentCounter.ping(3002)) // 3
优化版本(使用对象实现队列)
js
class OptimizedRecentCounter {
constructor() {
this.requests = {}
this.head = 0
this.tail = 0
}
ping(t) {
// 入队
this.requests[this.tail] = t
this.tail++
// 出队过期请求
while (this.head < this.tail && this.requests[this.head] < t - 3000) {
delete this.requests[this.head]
this.head++
}
return this.tail - this.head
}
}
复杂度分析
- 时间复杂度 :
- 每次 ping 操作:O(1) 平均情况,O(n) 最坏情况(需要清理大量过期请求)
- 总体:O(n),其中 n 是总的 ping 调用次数
- 空间复杂度:O(W),其中 W 是时间窗口内的最大请求数(最多 3000 个)
进阶:用队列实现栈
js
/**
* 用两个队列实现栈
*/
class MyStack {
constructor() {
this.queue1 = []
this.queue2 = []
}
push(x) {
this.queue1.push(x)
}
pop() {
// 将 queue1 中除最后一个元素外的所有元素移到 queue2
while (this.queue1.length > 1) {
this.queue2.push(this.queue1.shift())
}
// 弹出最后一个元素
const result = this.queue1.shift()
// 交换两个队列
[this.queue1, this.queue2] = [this.queue2, this.queue1]
return result
}
top() {
// 类似 pop,但不删除元素
while (this.queue1.length > 1) {
this.queue2.push(this.queue1.shift())
}
const result = this.queue1[0]
this.queue2.push(this.queue1.shift())
[this.queue1, this.queue2] = [this.queue2, this.queue1]
return result
}
empty() {
return this.queue1.length === 0
}
}
队列的总结
队列是一种重要的线性数据结构,其**先进先出(FIFO)**的特性使其在以下场景中不可或缺:
- ✅ 任务调度:操作系统进程调度、打印队列
- ✅ 异步编程:JavaScript 事件循环、Promise 队列
- ✅ 算法实现:广度优先搜索、层序遍历
- ✅ 系统设计:消息队列、缓冲区管理
- ✅ 实际应用:排队系统、请求限流
💡 记忆口诀:只要遇到"先进先出"或"排队"的场景,就考虑使用队列!
链表(Linked List)
概念介绍
链表是一种非连续存储的线性数据结构,通过指针将各个节点连接起来。与数组不同,链表的元素在内存中不是连续存储的,而是通过指针链接。
链表的特点
- 动态大小:可以在运行时动态增加或减少节点
- 非连续存储:节点在内存中可以分散存储
- 指针连接:通过 next 指针指向下一个节点
- 插入删除高效:在已知位置插入/删除的时间复杂度为 O(1)
链表的结构
css
[数据|指针] -> [数据|指针] -> [数据|指针] -> null
节点1 节点2 节点3
JavaScript 中的链表实现
在 JavaScript 中没有内置的链表数据结构,我们可以通过对象来模拟:
基础链表操作
js
// 定义链表节点
class ListNode {
constructor(val, next = null) {
this.val = val
this.next = next
}
}
// 创建链表
const node1 = new ListNode(1)
const node2 = new ListNode(2)
const node3 = new ListNode(3)
// 连接节点
node1.next = node2
node2.next = node3
// 链表结构:1 -> 2 -> 3 -> null
// 遍历链表
function traverseList(head) {
let current = head
const values = []
while (current !== null) {
values.push(current.val)
current = current.next
}
return values
}
console.log(traverseList(node1)) // [1, 2, 3]
完整的链表类实现
js
class LinkedList {
constructor() {
this.head = null
this.size = 0
}
// 在链表头部插入节点
prepend(val) {
const newNode = new ListNode(val, this.head)
this.head = newNode
this.size++
}
// 在链表尾部插入节点
append(val) {
const newNode = new ListNode(val)
if (!this.head) {
this.head = newNode
} else {
let current = this.head
while (current.next) {
current = current.next
}
current.next = newNode
}
this.size++
}
// 在指定位置插入节点
insert(index, val) {
if (index < 0 || index > this.size) {
throw new Error('Index out of bounds')
}
if (index === 0) {
this.prepend(val)
return
}
const newNode = new ListNode(val)
let current = this.head
for (let i = 0; i < index - 1; i++) {
current = current.next
}
newNode.next = current.next
current.next = newNode
this.size++
}
// 删除指定位置的节点
removeAt(index) {
if (index < 0 || index >= this.size) {
throw new Error('Index out of bounds')
}
if (index === 0) {
this.head = this.head.next
this.size--
return
}
let current = this.head
for (let i = 0; i < index - 1; i++) {
current = current.next
}
current.next = current.next.next
this.size--
}
// 删除指定值的节点
remove(val) {
if (!this.head) return false
if (this.head.val === val) {
this.head = this.head.next
this.size--
return true
}
let current = this.head
while (current.next && current.next.val !== val) {
current = current.next
}
if (current.next) {
current.next = current.next.next
this.size--
return true
}
return false
}
// 查找节点
find(val) {
let current = this.head
let index = 0
while (current) {
if (current.val === val) {
return index
}
current = current.next
index++
}
return -1
}
// 获取指定位置的值
get(index) {
if (index < 0 || index >= this.size) {
throw new Error('Index out of bounds')
}
let current = this.head
for (let i = 0; i < index; i++) {
current = current.next
}
return current.val
}
// 获取链表大小
getSize() {
return this.size
}
// 检查链表是否为空
isEmpty() {
return this.size === 0
}
// 转换为数组
toArray() {
const result = []
let current = this.head
while (current) {
result.push(current.val)
current = current.next
}
return result
}
// 清空链表
clear() {
this.head = null
this.size = 0
}
}
// 使用示例
const list = new LinkedList()
list.append(1)
list.append(2)
list.append(3)
list.prepend(0)
console.log(list.toArray()) // [0, 1, 2, 3]
list.insert(2, 1.5)
console.log(list.toArray()) // [0, 1, 1.5, 2, 3]
list.remove(1.5)
console.log(list.toArray()) // [0, 1, 2, 3]
链表的使用场景
链表在计算机科学和实际开发中有着重要的应用:
1. 动态内存管理
- 内存分配:操作系统的内存管理
- 垃圾回收:标记清除算法中的对象链接
- 缓存实现:LRU 缓存的实现
2. 数据结构基础
- 栈和队列:可以用链表实现栈和队列
- 图的邻接表:表示图的边关系
- 哈希表:解决哈希冲突的链地址法
3. 实际应用
- 音乐播放器:歌曲播放列表的管理
- 浏览器历史:前进后退功能的实现
- 文本编辑器:撤销重做功能
- 区块链:区块之间的链接关系
4. 算法应用
- 递归算法:树和图的遍历
- 动态规划:状态转移的表示
- 字符串处理:模式匹配算法
链表 vs 数组对比
特性 | 数组 | 链表 |
---|---|---|
内存存储 | 连续存储 | 分散存储 |
访问元素 | O(1) 随机访问 | O(n) 顺序访问 |
插入删除 | O(n) 需要移动元素 | O(1) 已知位置 |
内存开销 | 较小 | 较大(需要存储指针) |
缓存友好 | 是 | 否 |
大小限制 | 固定大小 | 动态大小 |
算法题: LeetCode 第2题 - 两数相加
题目描述
给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照逆序 的方式存储的,并且每个节点只能存储一位数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
ini
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
解释:9999999 + 9999 = 10009998
提示:
- 每个链表中的节点数在范围
[1, 100]
内 0 <= Node.val <= 9
- 题目数据保证列表表示的数字不含前导零
解题思路
这道题模拟了我们手工计算加法的过程:
- 从低位开始:由于链表是逆序存储,正好从个位开始计算
- 处理进位:当两位相加大于等于10时,需要向高位进位
- 处理不同长度:两个链表长度可能不同,需要特殊处理
- 最终进位:计算完成后可能还有最后一位进位
算法步骤:
- 初始化进位变量
carry = 0
- 同时遍历两个链表,计算当前位的和
- 创建新节点存储结果的个位数
- 更新进位值
- 处理剩余的进位
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
function addTwoNumbers(l1, l2) {
const dummy = new ListNode(0) // 哨兵节点,简化边界处理
let current = dummy
let carry = 0 // 进位
// 当还有节点需要处理或还有进位时继续循环
while (l1 !== null || l2 !== null || carry !== 0) {
// 获取当前位的值,如果节点为空则为0
const val1 = l1 ? l1.val : 0
const val2 = l2 ? l2.val : 0
// 计算当前位的和
const sum = val1 + val2 + carry
// 创建新节点存储结果的个位数
current.next = new ListNode(sum % 10)
current = current.next
// 更新进位
carry = Math.floor(sum / 10)
// 移动到下一个节点
if (l1) l1 = l1.next
if (l2) l2 = l2.next
}
return dummy.next // 返回结果链表(跳过哨兵节点)
}
// 辅助函数:数组转链表
function arrayToList(arr) {
const dummy = new ListNode(0)
let current = dummy
for (const val of arr) {
current.next = new ListNode(val)
current = current.next
}
return dummy.next
}
// 辅助函数:链表转数组
function listToArray(head) {
const result = []
let current = head
while (current) {
result.push(current.val)
current = current.next
}
return result
}
// 测试用例
const l1 = arrayToList([2, 4, 3]) // 表示数字 342
const l2 = arrayToList([5, 6, 4]) // 表示数字 465
const result = addTwoNumbers(l1, l2)
console.log(listToArray(result)) // [7, 0, 8] 表示数字 807
复杂度分析
- 时间复杂度:O(max(m, n)),其中 m 和 n 分别是两个链表的长度
- 空间复杂度:O(max(m, n)),新链表的长度最多为较长链表长度 + 1
进阶:反转链表
js
/**
* 反转链表 - 迭代方法
* @param {ListNode} head
* @return {ListNode}
*/
function reverseList(head) {
let prev = null
let current = head
while (current !== null) {
const next = current.next // 保存下一个节点
current.next = prev // 反转指针
prev = current // 移动 prev
current = next // 移动 current
}
return prev // prev 成为新的头节点
}
/**
* 反转链表 - 递归方法
* @param {ListNode} head
* @return {ListNode}
*/
function reverseListRecursive(head) {
// 基础情况
if (head === null || head.next === null) {
return head
}
// 递归反转剩余部分
const newHead = reverseListRecursive(head.next)
// 反转当前连接
head.next.next = head
head.next = null
return newHead
}
// 测试反转链表
const originalList = arrayToList([1, 2, 3, 4, 5])
console.log('原链表:', listToArray(originalList)) // [1, 2, 3, 4, 5]
const reversedList = reverseList(originalList)
console.log('反转后:', listToArray(reversedList)) // [5, 4, 3, 2, 1]
链表的总结
链表是一种灵活的动态数据结构,其指针连接的特性使其在以下场景中特别有用:
- ✅ 动态内存管理:大小可变,内存利用率高
- ✅ 高效插入删除:在已知位置操作时间复杂度为 O(1)
- ✅ 实现其他数据结构:栈、队列、图的基础
- ✅ 处理大数据:避免数组的内存限制
- ✅ 算法应用:递归、回溯、动态规划
链表的权衡:
- ❌ 随机访问慢:访问第 n 个元素需要 O(n) 时间
- ❌ 额外内存开销:需要存储指针
- ❌ 缓存不友好:节点在内存中分散存储
💡 选择建议:当需要频繁插入删除且很少随机访问时,选择链表;当需要频繁随机访问时,选择数组。
集合(Set)
概念介绍
集合是一种不允许重复元素的数据结构,它基于数学中集合的概念。集合中的每个元素都是唯一的,常用于去重、成员检测和集合运算。
集合的特点
- 元素唯一性:不允许重复元素
- 无序性:元素没有固定的顺序(在某些实现中)
- 快速查找:检查元素是否存在的时间复杂度通常为 O(1)
- 集合运算:支持并集、交集、差集等数学运算
JavaScript 中的集合实现
JavaScript 提供了内置的 Set
对象,同时我们也可以用其他方式模拟集合:
使用 ES6 Set
js
// 创建集合
const set = new Set()
// 添加元素
set.add(1)
set.add(2)
set.add(3)
set.add(2) // 重复元素,不会被添加
console.log(set) // Set(3) {1, 2, 3}
// 从数组创建集合(自动去重)
const setFromArray = new Set([1, 2, 2, 3, 3, 4])
console.log(setFromArray) // Set(4) {1, 2, 3, 4}
// 检查元素是否存在
console.log(set.has(2)) // true
console.log(set.has(5)) // false
// 删除元素
set.delete(2)
console.log(set.has(2)) // false
// 获取集合大小
console.log(set.size) // 2
// 遍历集合
for (const value of set) {
console.log(value)
}
// 转换为数组
const array = [...set] // [1, 3]
const array2 = Array.from(set) // [1, 3]
// 清空集合
set.clear()
console.log(set.size) // 0
自定义集合类实现
js
class MySet {
constructor(iterable = []) {
this.items = {}
this.count = 0
// 从可迭代对象初始化
for (const item of iterable) {
this.add(item)
}
}
// 添加元素
add(element) {
const key = this._getKey(element)
if (!this.items.hasOwnProperty(key)) {
this.items[key] = element
this.count++
return true
}
return false
}
// 删除元素
delete(element) {
const key = this._getKey(element)
if (this.items.hasOwnProperty(key)) {
delete this.items[key]
this.count--
return true
}
return false
}
// 检查元素是否存在
has(element) {
const key = this._getKey(element)
return this.items.hasOwnProperty(key)
}
// 获取集合大小
get size() {
return this.count
}
// 清空集合
clear() {
this.items = {}
this.count = 0
}
// 获取所有值
values() {
return Object.values(this.items)
}
// 转换为数组
toArray() {
return this.values()
}
// 并集
union(otherSet) {
const unionSet = new MySet()
// 添加当前集合的所有元素
for (const value of this.values()) {
unionSet.add(value)
}
// 添加另一个集合的所有元素
for (const value of otherSet.values()) {
unionSet.add(value)
}
return unionSet
}
// 交集
intersection(otherSet) {
const intersectionSet = new MySet()
for (const value of this.values()) {
if (otherSet.has(value)) {
intersectionSet.add(value)
}
}
return intersectionSet
}
// 差集
difference(otherSet) {
const differenceSet = new MySet()
for (const value of this.values()) {
if (!otherSet.has(value)) {
differenceSet.add(value)
}
}
return differenceSet
}
// 子集判断
isSubsetOf(otherSet) {
for (const value of this.values()) {
if (!otherSet.has(value)) {
return false
}
}
return true
}
// 生成键值(处理不同数据类型)
_getKey(element) {
if (typeof element === 'string') {
return `string:${element}`
} else if (typeof element === 'number') {
return `number:${element}`
} else if (typeof element === 'boolean') {
return `boolean:${element}`
} else if (element === null) {
return 'null'
} else if (element === undefined) {
return 'undefined'
} else {
return `object:${JSON.stringify(element)}`
}
}
// 迭代器支持
[Symbol.iterator]() {
const values = this.values()
let index = 0
return {
next() {
if (index < values.length) {
return { value: values[index++], done: false }
} else {
return { done: true }
}
}
}
}
}
// 使用示例
const set1 = new MySet([1, 2, 3])
const set2 = new MySet([3, 4, 5])
console.log('set1:', set1.toArray()) // [1, 2, 3]
console.log('set2:', set2.toArray()) // [3, 4, 5]
// 集合运算
const union = set1.union(set2)
console.log('并集:', union.toArray()) // [1, 2, 3, 4, 5]
const intersection = set1.intersection(set2)
console.log('交集:', intersection.toArray()) // [3]
const difference = set1.difference(set2)
console.log('差集:', difference.toArray()) // [1, 2]
集合的使用场景
集合在实际开发和算法中有着广泛的应用:
1. 数据去重
- 数组去重:快速移除重复元素
- 用户去重:避免重复的用户ID或邮箱
- 标签系统:文章标签的唯一性管理
2. 成员检测
- 权限验证:检查用户是否有特定权限
- 黑白名单:IP地址或用户的访问控制
- 关键词过滤:敏感词检测
3. 集合运算
- 数据分析:用户群体的交集、并集分析
- 推荐系统:基于共同兴趣的推荐
- A/B测试:实验组和对照组的用户管理
4. 算法优化
- 图算法:已访问节点的记录
- 动态规划:状态去重
- 回溯算法:避免重复搜索
集合的实际应用示例
数组去重的多种方法
js
// 方法1:使用 Set(最简洁)
function uniqueWithSet(arr) {
return [...new Set(arr)]
}
// 方法2:使用 filter + indexOf
function uniqueWithFilter(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index)
}
// 方法3:使用 reduce
function uniqueWithReduce(arr) {
return arr.reduce((unique, item) => {
return unique.includes(item) ? unique : [...unique, item]
}, [])
}
// 方法4:使用 Map(保持插入顺序)
function uniqueWithMap(arr) {
const map = new Map()
arr.forEach(item => map.set(item, true))
return [...map.keys()]
}
// 测试
const testArray = [1, 2, 2, 3, 4, 4, 5]
console.log(uniqueWithSet(testArray)) // [1, 2, 3, 4, 5]
console.log(uniqueWithFilter(testArray)) // [1, 2, 3, 4, 5]
console.log(uniqueWithReduce(testArray)) // [1, 2, 3, 4, 5]
console.log(uniqueWithMap(testArray)) // [1, 2, 3, 4, 5]
权限管理系统
js
class PermissionManager {
constructor() {
this.userPermissions = new Map() // 用户ID -> Set(权限)
this.rolePermissions = new Map() // 角色 -> Set(权限)
}
// 为用户分配权限
grantPermission(userId, permission) {
if (!this.userPermissions.has(userId)) {
this.userPermissions.set(userId, new Set())
}
this.userPermissions.get(userId).add(permission)
}
// 撤销用户权限
revokePermission(userId, permission) {
if (this.userPermissions.has(userId)) {
this.userPermissions.get(userId).delete(permission)
}
}
// 检查用户是否有权限
hasPermission(userId, permission) {
const userPerms = this.userPermissions.get(userId)
return userPerms ? userPerms.has(permission) : false
}
// 获取用户所有权限
getUserPermissions(userId) {
const userPerms = this.userPermissions.get(userId)
return userPerms ? [...userPerms] : []
}
// 获取有特定权限的所有用户
getUsersWithPermission(permission) {
const users = []
for (const [userId, permissions] of this.userPermissions) {
if (permissions.has(permission)) {
users.push(userId)
}
}
return users
}
}
// 使用示例
const pm = new PermissionManager()
pm.grantPermission('user1', 'read')
pm.grantPermission('user1', 'write')
pm.grantPermission('user2', 'read')
console.log(pm.hasPermission('user1', 'write')) // true
console.log(pm.hasPermission('user2', 'write')) // false
console.log(pm.getUsersWithPermission('read')) // ['user1', 'user2']
算法题: LeetCode 相关集合问题
1. 两个数组的交集
js
/**
* 给定两个数组,编写一个函数来计算它们的交集
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
function intersection(nums1, nums2) {
const set1 = new Set(nums1)
const set2 = new Set(nums2)
return [...set1].filter(num => set2.has(num))
}
// 或者使用集合运算
function intersectionWithSet(nums1, nums2) {
const set1 = new Set(nums1)
const result = new Set()
for (const num of nums2) {
if (set1.has(num)) {
result.add(num)
}
}
return [...result]
}
// 测试
console.log(intersection([1, 2, 2, 1], [2, 2])) // [2]
console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])) // [9, 4]
2. 快乐数
js
/**
* 判断一个数是否为快乐数
* 快乐数:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,
* 然后重复这个过程直到这个数变为1,或是无限循环但始终变不到1
* @param {number} n
* @return {boolean}
*/
function isHappy(n) {
const seen = new Set() // 记录已经出现过的数字
while (n !== 1 && !seen.has(n)) {
seen.add(n)
n = getSquareSum(n)
}
return n === 1
}
function getSquareSum(n) {
let sum = 0
while (n > 0) {
const digit = n % 10
sum += digit * digit
n = Math.floor(n / 10)
}
return sum
}
// 测试
console.log(isHappy(19)) // true (1^2 + 9^2 = 82, 8^2 + 2^2 = 68, ...)
console.log(isHappy(2)) // false
3. 存在重复元素
js
/**
* 给定一个整数数组,判断是否存在重复元素
* @param {number[]} nums
* @return {boolean}
*/
function containsDuplicate(nums) {
const seen = new Set()
for (const num of nums) {
if (seen.has(num)) {
return true
}
seen.add(num)
}
return false
}
// 更简洁的写法
function containsDuplicateSimple(nums) {
return new Set(nums).size !== nums.length
}
// 测试
console.log(containsDuplicate([1, 2, 3, 1])) // true
console.log(containsDuplicate([1, 2, 3, 4])) // false
console.log(containsDuplicateSimple([1, 1, 1, 3, 3, 4, 3, 2, 4, 2])) // true
集合的总结
集合是一种强大的数据结构,其元素唯一性 和快速查找的特性使其在以下场景中不可或缺:
- ✅ 数据去重:快速移除重复元素,时间复杂度 O(n)
- ✅ 成员检测:O(1) 时间复杂度检查元素是否存在
- ✅ 集合运算:并集、交集、差集等数学运算
- ✅ 算法优化:避免重复计算,提高算法效率
- ✅ 权限管理:用户权限、角色管理的理想选择
集合的性能特点:
- ✅ 查找快速:平均 O(1) 时间复杂度
- ✅ 插入删除高效:平均 O(1) 时间复杂度
- ✅ 自动去重:无需手动处理重复元素
- ❌ 无序性:元素没有固定顺序(ES6 Set 保持插入顺序)
- ❌ 额外内存:需要额外的哈希表空间
💡 使用建议:当需要快速查找、去重或进行集合运算时,优先考虑使用集合!
总结
本文详细介绍了四种重要的数据结构:栈、队列、链表和集合。每种数据结构都有其独特的特性和适用场景:
数据结构对比表
数据结构 | 主要特性 | 时间复杂度 | 适用场景 |
---|---|---|---|
栈 | 后进先出(LIFO) | 插入/删除: O(1) | 函数调用、表达式求值、撤销操作 |
队列 | 先进先出(FIFO) | 插入/删除: O(1) | 任务调度、BFS、事件处理 |
链表 | 指针连接、动态大小 | 插入/删除: O(1), 查找: O(n) | 动态内存管理、实现其他数据结构 |
集合 | 元素唯一、快速查找 | 查找/插入/删除: O(1) | 去重、成员检测、集合运算 |
选择指南
- 🔄 需要撤销/回退功能 → 选择栈
- 📋 需要排队/调度功能 → 选择队列
- 🔗 需要频繁插入删除 → 选择链表
- 🎯 需要去重/快速查找 → 选择集合
学习建议
- 理解原理:掌握每种数据结构的核心特性
- 动手实现:自己编写代码实现这些数据结构
- 刷题练习:通过 LeetCode 等平台巩固理解
- 实际应用:在项目中合理选择和使用数据结构
- 性能分析:了解不同操作的时间和空间复杂度
掌握这些基础数据结构是成为优秀程序员的必经之路,它们不仅是算法的基础,也是解决实际问题的重要工具。希望这份总结能帮助你更好地理解和应用这些数据结构!