Skip to content
📖预计阅读时长:0 分钟字数:0

代码随想录 — 栈与队列、二叉树

对应 leetcode-master:栈与队列理论基础、232/225/20/1047/150/239/347、二叉树理论、遍历、层序、翻转/对称/深度、构造、BST 等。


一、栈与队列理论基础

  • :先进后出(LIFO);不提供遍历,只提供 push/pop/top 等接口。底层可以是数组或链表(如 deque),属于容器适配器
  • 队列:先进先出(FIFO);同样不提供遍历。底层常用 deque。
  • 理解:栈适合「最近相关」「对称匹配」「深度优先」;队列适合「一层一层」「广度优先」。很多语言里栈/队列是对某种底层容器的封装,知道「只能一端进出一端出」即可。

二、栈与队列经典题(思路要点)

题目要点
232 用栈实现队列两个栈:一个负责入队(push),一个负责出队(pop/peek)。出队时若「出队栈」为空,先把「入队栈」全部倒入再出。
225 用队列实现栈用一个队列即可:每次 push 时把新元素入队,再把队内原有元素依次出队再入队,这样新元素就到了队头,等价于栈顶。
20 有效的括号左括号入栈,右括号时看栈顶是否匹配;最后栈空则有效。注意只有右括号、栈空时来右括号等情况。
1047 删除相邻重复项栈顶等于当前字符则弹栈并跳过,否则入栈;最后栈内字符逆序即结果(或再用一个栈/数组反转)。
150 逆波兰表达式遇到数字入栈,遇到运算符则弹出两个数运算后结果入栈;注意减法和除法的顺序(先弹出的是右操作数)。
239 滑动窗口最大值单调队列:维护一个从队头到队尾单调减的双端队列,存下标;窗口右移时去掉超出窗口的队头、新元素从队尾入队并踢掉比它小的,队头即当前窗口最大值。
347 前 K 个高频元素先用 Map 统计频率,再用(或排序):维护大小为 k 的小顶堆,堆顶是当前第 k 大频率,超过 k 就弹出堆顶;最后堆中即为前 k 个。

三、二叉树理论基础(有助于写题)

  • 种类:满二叉树(每层都满)、完全二叉树(最后一层从左到右连续)、二叉搜索树 BST(左 < 根 < 右)、平衡二叉搜索树(左右高度差 ≤ 1,如 AVL)。map/set 的底层可以是平衡二叉搜索树,所以增删查 O(log n)。
  • 存储:链式(left/right 指针)常用;顺序存储时父节点 i 的左子 2i+1、右子 2i+2。
  • 遍历
    • 深度优先:前序(中左右)、中序(左中右)、后序(左右中)— 「前中后」指的处理顺序。
    • 广度优先:层序遍历,用队列,每次把当前层节点出队并把其子节点入队。

递归与栈:前中后序的递归实现,本质是系统栈;迭代实现用显式栈。层序用队列。


四、二叉树 — 递归遍历与层序

  • 递归:终止条件一般是 root === null;先处理根(前序)或左或右,再递归左右。回溯是递归的副产品,在「修改状态 → 递归 → 恢复状态」时就是在回溯。
  • 迭代(栈):前序:先压根,每次弹出一个并访问,再先右后左入栈(这样先访问左)。中序:指针往左走到底并一路入栈,再弹栈访问,再往右。后序:可改为「中右左」再反转,或按「访问标记」区分已处理子树的节点。
  • 层序:队列 + 每层记个数(或每层一个数组),先入根,每次取一层节点,把其非空子节点入队,直到队列空。

五、二叉树 — 属性与简单递归

题目思路要点
226 翻转二叉树交换左右子,再递归翻转左右子树;前序或后序均可。
101 对称二叉树判断两棵树是否镜像:比较根值,再递归比较「左的左 vs 右的右」「左的右 vs 右的左」。
104 最大深度根为 null 返回 0;否则 1 + max(左深度, 右深度)。
111 最小深度注意:若某子为 null,不能算成 0 取 min,应只算另一侧;叶子才是「1」。
222 完全二叉树节点数可递归左右子树节点数相加 +1;完全二叉树可结合满二叉树公式优化。
110 平衡二叉树后序求高度,若左右高度差 >1 则返回 -1 表示不平衡,否则返回高度。
257 所有路径DFS,维护路径数组,到叶子时把路径加入结果;回溯时 pop。
404 左叶子之和判断「左叶子」:当前节点的左孩子存在且左孩子是叶子;递归累加。
112 路径总和到叶子时判断剩余 sum 是否等于当前值;否则递归 left/right 并传 sum - root.val。

六、二叉树 — 构造与 BST

  • 106 从中序与后序构造:后序最后一个为根,在中序里找到根,分出左子树区间和右子树区间,再递归构造左右子树(注意后序里左右子树的区间划分)。
  • 105 从前序与中序构造:前序第一个为根,其余同 106。
  • 654 最大二叉树:在区间内找最大值作为根,递归左右区间。
  • 700 二叉搜索树中的搜索:根据 val 与根比较,小则搜左,大则搜右。
  • 98 验证二叉搜索树:中序遍历应为严格递增;或递归时传「当前子树的值域上下界」。
  • 530 最小绝对差:BST 中序得到有序序列,相邻差最小即答案。
  • 501 众数:BST 中序,统计连续相同值的个数,更新众数。
  • 236 最近公共祖先:后序:若当前根为 p 或 q 或 null 则返回;否则递归左右,若左右都非空则当前根为 LCA;若一侧非空则返回该侧结果。
  • 235 BST 最近公共祖先:利用 BST 性质:若 p、q 都小于根则 LCA 在左;都大于根在右;否则根即为 LCA。
  • 701/450 插入/删除:插入到叶子;删除分三种情况(无子、单子、双子—用右子树最小节点替代)。
  • 108 有序数组转 BST:中点作根,递归左右半数组。
  • 538 累加树:反中序(右中左)遍历,累加和赋给当前节点。

七、二叉树总结

类型方法
遍历前中后序(递归/迭代)、层序(队列)
属性深度、对称、路径和、左叶子等 — 定义好递归含义,注意叶子与 null 的边界
构造用根划分中序得到左右区间,再划分前序/后序的对应区间递归
BST中序有序、搜索/插入/删除、LCA 利用大小关系

建议:层序遍历、226/101/104、106/105、98、236 要熟;写递归时先想「当前节点要返回什么、左右子返回什么」。


附:核心代码模板

二叉树节点定义

javascript
// JavaScript
class TreeNode {
  constructor(val = 0, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

前中后序递归遍历

javascript
// JavaScript
function preorder(root, res = []) {
  if (!root) return res;
  res.push(root.val);       // 前序:中 → 左 → 右
  preorder(root.left, res);
  preorder(root.right, res);
  return res;
}

function inorder(root, res = []) {
  if (!root) return res;
  inorder(root.left, res);
  res.push(root.val);       // 中序:左 → 中 → 右
  inorder(root.right, res);
  return res;
}

层序遍历(BFS)

javascript
// JavaScript — 时间 O(n)
function levelOrder(root) {
  if (!root) return [];
  const res = [], queue = [root];
  while (queue.length) {
    const level = [], size = queue.length;
    for (let i = 0; i < size; i++) {
      const node = queue.shift();
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    res.push(level);
  }
  return res;
}

将有序数组转为二叉搜索树

javascript
function sortedArrayToBST(nums) {
	function build(l, r) {
	  if (l > r) return null;
	  const mid = (l + r) >> 1;
	  const root = new TreeNode(nums[mid]);
	  root.left = build(l, mid - 1);
	  root.right = build(mid + 1, r);
	  return root;
	}
	return build(0, nums.length - 1);
}

计算高度 (深度)

javascript
  /**
   * 二叉树高度:从节点到最远叶子的最长路径边数
   * 空树高度 -1,单节点高度 0
   * 时间 O(n)
   */
  function treeHeight(root) {
    if (!root) return -1;
    return 1 + Math.max(treeHeight(root.left),
  treeHeight(root.right));
  }

  // 若定义「空树为 0,单节点为 1」:

  function treeHeight(root) {
    if (!root) return 0;
    return 1 + Math.max(treeHeight(root.left),
  treeHeight(root.right));
  }
  
   /**
   * 节点深度:从根到该节点的边数
   * 根深度 0
   */
  function nodeDepth(root, target) {
    function dfs(node, d) {
      if (!node) return -1;
      if (node === target) return d;
      const L = dfs(node.left, d + 1);
      if (L !== -1) return L;
      return dfs(node.right, d + 1);
    }
    return dfs(root, 0);
  }

  // 若用 val 比较:

  function nodeDepth(root, targetVal) {
    function dfs(node, d) {
      if (!node) return -1;
      if (node.val === targetVal) return d;
      const L = dfs(node.left, d + 1);
      if (L !== -1) return L;
      return dfs(node.right, d + 1);
    }
    return dfs(root, 0);
  }

最近公共祖先(LCA)

javascript
// JavaScript — 时间 O(n)
function lowestCommonAncestor(root, p, q) {
  if (!root || root === p || root === q) return root;
  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);
  if (left && right) return root;
  return left || right;
}

用栈实现队列

javascript
// JavaScript
class MyQueue {
  constructor() {
    this.inStack = [];
    this.outStack = [];
  }
  push(x) { this.inStack.push(x); }
  pop() {
    if (!this.outStack.length) {
      while (this.inStack.length) this.outStack.push(this.inStack.pop());
    }
    return this.outStack.pop();
  }
  peek() {
    const val = this.pop();
    this.outStack.push(val);
    return val;
  }
  empty() { return !this.inStack.length && !this.outStack.length; }
}