LeetCode JS 常用辅助数据结构 + 本地模拟代码
刷题高频辅助:链表、栈、队列、单调栈、双端队列、哈希表、Set、堆 (优先队列)、前缀和数组、并查集,逐个给出「本地可直接运行模拟代码」。
LeetCode 环境自带
ListNode,本地需要自己定义,其余 JS 原生自带 Set/Map/Array,堆需要手动封装。
一、链表(ListNode,链表题必备)
1. 本地定义 ListNode
// 本地必须手写,LC内置不用写
function ListNode(val, next = null) {
this.val = val;
this.next = next;
}
// 数组转链表(造测试用例)
function arrToList(arr) {
return arr.reduceRight((next, val) => new ListNode(val, next), null);
}
// 链表转数组(打印调试)
function listToArr(head) {
const res = [];
let p = head;
while(p) {
res.push(p.val);
p = p.next;
}
return res;
}
// 测试
let head = arrToList([1,2,3,4]);
console.log(listToArr(head)); // [1,2,3,4]
二、栈 Stack(数组模拟:尾进尾出)
场景:有效括号、字符串反转、单调栈、二叉树迭代遍历
class Stack {
#arr = []
push(v) {this.#arr.push(v)}
pop() {return this.#arr.pop()}
peek() {return this.#arr.at(-1)}
isEmpty() {return this.#arr.length === 0}
size() {return this.#arr.length}
}
// 测试:有效括号
const s = "()[]{}";
const st = new Stack();
const map = {')':'(', ']':'[', '}':'{'};
let flag = true;
for(let c of s) {
if(['(','[','{'].includes(c)) st.push(c)
else {
if(st.isEmpty() || st.pop() !== map[c]) flag = false
}
}
console.log(flag);
三、队列 Queue(数组模拟:尾进头出)
BFS 层序遍历、滑动窗口、任务队列
class Queue {
#arr = []
enqueue(v) {this.#arr.push(v)}
dequeue() {return this.#arr.shift()} // 低效,大数据用双端队列
front() {return this.#arr[0]}
isEmpty() {return this.#arr.length===0}
}
// BFS二叉树示例(简略)
// 本地TreeNode定义
function TreeNode(val,left=null,right=null){
this.val=val; this.left=left; this.right=right;
}
const root = new TreeNode(1,new TreeNode(2),new TreeNode(3))
const q = new Queue()
q.enqueue(root)
while(!q.isEmpty()){
let cur = q.dequeue()
console.log(cur.val)
if(cur.left) q.enqueue(cur.left)
if(cur.right) q.enqueue(cur.right)
}
四、双端队列 Deque(优化队列,首尾 O (1) 增删,滑动窗口最大值)
单调队列、滑动窗口经典题
class Deque {
#head = 0
#arr = []
// 队尾入
push(v) {this.#arr.push(v)}
// 队首出
shift() {
if(this.#head >= this.#arr.length) return undefined
return this.#arr[this.#head++]
}
// 队尾出
pop() {return this.#arr.pop()}
// 队首值
front() {return this.#arr[this.#head]}
// 队尾值
back() {return this.#arr.at(-1)}
isEmpty() {return this.#head >= this.#arr.length}
}
// 滑动窗口最大值示例 leetcode239
function maxSlidingWindow(nums,k){
const dq = new Deque(), res=[]
for(let i=0;i<nums.length;i++){
while(!dq.isEmpty() && nums[dq.back()] <= nums[i]) dq.pop()
dq.push(i)
while(dq.front() <= i-k) dq.shift()
if(i >= k-1) res.push(nums[dq.front()])
}
return res
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7],3))
五、哈希集合 Set / 哈希表 Map(原生自带,不用封装)
去重、两数之和、判环、查找
// Set 去重
const arr = [1,2,2,3]
const s = new Set(arr)
console.log([...s])
// Map 两数之和
function twoSum(nums,target){
const map = new Map()
for(let i=0;i<nums.length;i++){
let need = target - nums[i]
if(map.has(need)) return [map.get(need),i]
map.set(nums[i],i)
}
}
console.log(twoSum([2,7,11,15],9))
六、优先队列(最小 / 大堆,JS 无原生,本地封装,topK、数据流中位数)
LeetCode topK、贪心、Dijkstra 最短路径
// 小顶堆
class MinHeap {
#heap = []
push(val) {
this.#heap.push(val)
this.bubbleUp(this.#heap.length-1)
}
bubbleUp(idx) {
while(idx>0){
let p = (idx-1)>>1
if(this.#heap[p] <= this.#heap[idx]) break
[this.#heap[p],this.#heap[idx]] = [this.#heap[idx],this.#heap[p]]
idx = p
}
}
pop() {
let top = this.#heap[0], last = this.#heap.pop()
if(this.#heap.length) {
this.#heap[0] = last
this.sinkDown(0)
}
return top
}
sinkDown(idx){
let len = this.#heap.length
while(true){
let l = idx*2+1, r = idx*2+2, min = idx
if(l<len && this.#heap[l]<this.#heap[min]) min=l
if(r<len && this.#heap[r]<this.#heap[min]) min=r
if(min===idx) break
[this.#heap[idx],this.#heap[min]] = [this.#heap[min],this.#heap[idx]]
idx = min
}
}
top(){return this.#heap[0]}
size(){return this.#heap.length}
}
// topK 取前3大
function topK(arr,k){
const heap = new MinHeap()
for(let v of arr){
heap.push(v)
if(heap.size()>k) heap.pop()
}
let ans = []
while(heap.size()) ans.unshift(heap.pop())
return ans
}
console.log(topK([3,2,1,5,6,4],3))
七、并查集 UnionFind(连通分量、岛屿数量、朋友圈)
class UnionFind {
parent = []
constructor(n){
for(let i=0;i<n;i++) this.parent[i]=i
}
find(x){
if(this.parent[x]!==x) this.parent[x]=this.find(this.parent[x])
return this.parent[x]
}
union(x,y){
let fx = this.find(x), fy=this.find(y)
if(fx!==fy) this.parent[fy]=fx
}
}
// 岛屿连通模拟
const uf = new UnionFind(5)
uf.union(0,1)
console.log(uf.find(0)===uf.find(1)) // true
八、前缀和数组(子数组和、区间求和)
function preSum(arr){
let pre = [0]
for(let n of arr) pre.push(pre.at(-1)+n)
return pre
}
const arr = [1,2,3,4]
let pre = preSum(arr)
// [l,r]区间和:pre[r+1]-pre[l]
console.log(pre[3]-pre[1]) // 2+3=5
本地调试使用规范
- VSCode 新建
.js文件,粘贴上面对应结构代码; - 用
arrToList快速生成链表用例,listToArr打印结果; - BFS 用 Queue/Deque,滑动窗口固定 Deque,topK 用 Heap,连通题 UF
1. 为什么 class 里能直接写 #arr = []?
这叫 ** 类字段(Class Field)** 语法,是 JS 官方允许的写法。
作用:直接给类实例声明属性,不用写在 constructor 里。
以前老写法(必须写在 constructor):
class Stack {
constructor() {
this.arr = []
}
}
现在新写法(直接写在 class 顶层):
class Stack {
arr = [] // 直接写!
}
效果完全一样,只是语法更简洁。
2. 为什么加 #?#arr 是什么?
# 代表 私有属性(private field)
- 公有属性:外部能改、能访问
- 私有属性(# 开头):外部不能改、不能访问,只能在 class 内部用
这是为了安全 + 封装。
const stack = new Stack()
stack.#arr = 123 // 报错!私有属性,外部不能碰
刷题时你可以不加 #,也能正常运行:
class Stack {
arr = []
}
3. 为什么能用 this.#arr?
因为:
this 永远指向当前创建的实例对象
当你写
const s = new Stack()
new Stack() 会创建一个空对象,然后:
#arr = []会自动绑定到这个对象上- 所有方法里的
this都指向这个对象
所以:
push(v) {
this.#arr.push(v)
}
这里的 this 就是你创建的实例对象 ,所以能访问到 #arr。
4. 一句话总结(最关键)
class Stack {
#arr = [] // 1. 声明实例属性(私有)
push() {
this.#arr // 2. this 指向实例,所以能访问
}
}
class 里直接写变量 = 给实例添加属性 方法里的 this = 指向实例
5. 最简单等价写法(帮助你理解)
下面两种写法完全一样:
写法 A(现代简洁)
class Stack {
#arr = []
}
写法 B(传统构造函数)
class Stack {
constructor() {
this.#arr = []
}
}
最终总结
- class 里直接写变量 → 声明实例属性
- # 开头 → 私有属性,外部不能访问
- this → 指向当前实例,所以能访问到类里声明的变量
JS Class 字段前缀汇总(# 之外所有常用写法)
原生 JS 只有 # 是符号前缀,剩下靠关键字修饰:static、_(约定)、get/set 存取器,分 4 大类讲清楚👇
一、# :原生硬私有(ES2022 唯一符号前缀)
class Stack {
#arr = [] // 实例私有,外部访问报错
#fn(){} // 私有方法
}
#是名字的一部分 ,访问必须写this.#arr- 语言层面锁死,外部拿不到
二、static 关键字前缀(静态成员,不属于实例、属于类)
无特殊符号,关键字修饰,分「公有静态 / 私有静态 #+static」
class Stack {
// 1.公有静态属性:全类共用,Stack.count,实例拿不到
static count = 0
static info(){}
// 2.私有静态:# + static,只能类内部访问
static #innerCache = []
}
Stack.count = 10
Stack.info()
刷题场景:堆、工具类全局常量、计数器
三、下划线 _name:开发约定私有(非语言强制,业界惯例)
JS语法不生效,只是程序员约定「以下划线开头不要在外部修改」,仍能直接访问:
class Stack {
_arr = [] // 只是规范,stack._arr 外部照样能改
}
const s = new Stack()
s._arr.push(1) // 合法代码
- 老项目大量使用,不能替代 #真正私有
四、存取器 get /set(属性伪装成方法)
不加符号,get/set修饰,读写拦截:
class Stack {
#arr = []
get size() { // 读:stack.size
return this.#arr.length
}
set size(n) { // 写:stack.size=5
this.#arr.length = n
}
}
const s = new Stack()
console.log(s.size)
s.size = 2
五、补充:TypeScript 才有额外修饰符(JS 原生没有!)
TS 独有:private/protected/public,编译后变成普通 JS,无真正私有
// TS代码,编译后JS无任何限制
class Stack{
private arr:number[]=[]
}
纯 JS 不用这三个关键字,刷题写 leetcode 原生 JS 忽略
六、速记表格
表格
| 写法 | 标识 | 作用 | 外部可访问? |
|---|---|---|---|
#arr |
#符号 | 实例真私有 | ❌ |
static arr |
static | 公有静态 | ✅ 类名.arr |
static #arr |
static+# | 私有静态 | ❌ |
_arr |
下划线约定 | 伪私有 | ✅(规范不建议改) |
get arr(){} |
get | 属性拦截 | ✅ |
刷题最优写法(你之前 Stack 改造)
class Stack {
#arr = []
static totalCreate = 0 // 全局计数
constructor(){
Stack.totalCreate++
}
push(v){this.#arr.push(v)}
get size(){return this.#arr.length}
}