JavaScript当中的数据结构--字典,二叉树

120 阅读11分钟

数据结构--字典

什么是字典

字典(Dictionary)是一种以键值对(key-value)形式存储数据的数据结构。在 JavaScript 中,Map 对象就是字典的实现,它提供了高效的数据存储和检索机制。

字典的特点

  • 键的唯一性:每个键在字典中都是唯一的
  • 快速查找:通过键可以快速找到对应的值,时间复杂度为 O(1)
  • 动态大小:可以动态添加和删除键值对
  • 任意类型键:Map 允许使用任意类型作为键(包括对象、函数等)

JavaScript 中字典(Map)的基本操作

Map 提供了丰富的 API 来操作键值对数据:

// 1. 创建字典的几种方式
const map1 = new Map(); // 创建空字典
const map2 = new Map([
  ["name", "张三"],
  ["age", 25],
  ["city", "北京"],
]); // 通过数组初始化

// 2. 添加/更新键值对
map1.set("key", "value");
map1.set("number", 42);
map1.set("object", { id: 1 });
map1.set(Symbol("sym"), "symbol key"); // 支持Symbol作为键

// 3. 获取值
const val = map1.get("key");
console.log(val); // 输出: 'value'
console.log(map1.get("nonexistent")); // 输出: undefined

// 4. 检查键是否存在
console.log(map1.has("key")); // true
console.log(map1.has("test")); // false

// 5. 删除键值对
map1.delete("key"); // 删除指定键值对
console.log(map1.has("key")); // false

// 6. 获取字典大小
console.log(map1.size); // 当前键值对数量

// 7. 清空字典
map1.clear(); // 删除所有键值对
console.log(map1.size); // 0

Map 的高级操作

// 遍历Map的几种方式
const userMap = new Map([
  ["user1", { name: "张三", age: 25 }],
  ["user2", { name: "李四", age: 30 }],
  ["user3", { name: "王五", age: 28 }],
]);

// 1. 遍历键值对
for (const [key, value] of userMap) {
  console.log(`${key}: ${value.name}`);
}

// 2. 遍历键
for (const key of userMap.keys()) {
  console.log(key);
}

// 3. 遍历值
for (const value of userMap.values()) {
  console.log(value.name);
}

// 4. 使用forEach
userMap.forEach((value, key) => {
  console.log(`${key}: ${value.name}`);
});

// 5. 转换为数组
const entries = Array.from(userMap.entries());
const keys = Array.from(userMap.keys());
const values = Array.from(userMap.values());

Map vs Object 对比

特性MapObject
键的类型任意类型字符串、Symbol
大小获取map.sizeObject.keys(obj).length
迭代直接可迭代需要 Object.keys()
性能频繁增删更优小数据量更优
原型无默认键有默认键

Map 相关的算法题目

1. LeetCode 第 1 题:两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]

看到题目后,我相信有很多人第一个想法就是通过双层 for 循环来暴力解开这道题目,但是这并不是最优解。首先使用两个循环嵌套,那么它们的时间复杂度在 O(n*n),所以它们的性能就会非常差。所以我们不能使用这个方法。我们仔细想一下,可不可以使用我们刚学习的 map 来帮助我们解决这道算法题呢?

解题思路分析:

  1. 暴力解法:双重循环,时间复杂度 O(n²),空间复杂度 O(1)
  2. 哈希表优化:使用 Map 存储已遍历的数字和索引,时间复杂度 O(n),空间复杂度 O(n)
/**
 * 两数之和 - 哈希表解法
 * @param {number[]} nums - 整数数组
 * @param {number} target - 目标值
 * @return {number[]} - 两个数的索引
 */
function twoSum(nums, target) {
  const map = new Map(); // 存储 值 -> 索引 的映射

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i]; // 计算需要的补数

    // 如果补数存在于map中,说明找到了答案
    if (map.has(complement)) {
      return [map.get(complement), i];
    }

    // 将当前数字和索引存入map
    map.set(nums[i], i);
  }

  return []; // 题目保证有解,这里不会执行到
}

// 测试用例
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]
console.log(twoSum([3, 2, 4], 6)); // [1, 2]
console.log(twoSum([3, 3], 6)); // [0, 1]

算法复杂度分析:

  • 时间复杂度:O(n) - 只需要遍历数组一次
  • 空间复杂度:O(n) - 最坏情况下需要存储 n-1 个键值对

2. LeetCode 第 49 题:字母异位词分组

/**
 * 字母异位词分组
 * @param {string[]} strs
 * @return {string[][]}
 */
function groupAnagrams(strs) {
  const map = new Map();

  for (const str of strs) {
    // 将字符串排序作为key
    const key = str.split("").sort().join("");

    if (!map.has(key)) {
      map.set(key, []);
    }
    map.get(key).push(str);
  }

  return Array.from(map.values());
}

// 测试
console.log(groupAnagrams(["eat", "tea", "tan", "ate", "nat", "bat"]));
// 输出: [["eat","tea","ate"],["tan","nat"],["bat"]]

数据结构--二叉树

什么是二叉树

二叉树(Binary Tree)是一种树形数据结构,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。二叉树是计算机科学中最重要的数据结构之一。

二叉树的特点

  • 节点度数限制:每个节点最多有两个子节点
  • 有序性:左右子树有明确的区分
  • 递归结构:每个子树也是一个二叉树
  • 应用广泛:搜索、排序、表达式解析等

二叉树的类型

  1. 满二叉树:除叶子节点外,每个节点都有两个子节点
  2. 完全二叉树:除最后一层外,其他层都被完全填满
  3. 二叉搜索树:左子树所有节点值小于根节点,右子树所有节点值大于根节点
  4. 平衡二叉树:任意节点的左右子树高度差不超过 1

二叉树的创建

在 JavaScript 中,我们可以通过对象的方式创建二叉树节点:

// 定义二叉树节点类
class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

// 方式1:使用类创建二叉树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

// 方式2:直接使用对象字面量
const tree = {
  val: 1,
  left: {
    val: 2,
    left: {
      val: 4,
      left: null,
      right: null,
    },
    right: {
      val: 5,
      left: null,
      right: null,
    },
  },
  right: {
    val: 3,
    left: null,
    right: null,
  },
};

// 方式3:从数组构建二叉树(层序遍历顺序)
function buildTreeFromArray(arr) {
  if (!arr.length) return null;

  const root = new TreeNode(arr[0]);
  const queue = [root];
  let i = 1;

  while (queue.length && i < arr.length) {
    const node = queue.shift();

    if (i < arr.length && arr[i] !== null) {
      node.left = new TreeNode(arr[i]);
      queue.push(node.left);
    }
    i++;

    if (i < arr.length && arr[i] !== null) {
      node.right = new TreeNode(arr[i]);
      queue.push(node.right);
    }
    i++;
  }

  return root;
}

// 使用示例
const treeFromArray = buildTreeFromArray([1, 2, 3, 4, 5, null, 6]);

二叉树的遍历

前序遍历(递归版)

前序遍历顺序:根 → 左 → 右

/**
 * 前序遍历 - 递归实现
 * @param {TreeNode} root - 二叉树根节点
 * @param {Array} result - 存储遍历结果的数组
 * @return {Array} - 遍历结果
 */
function preorderTraversal(root, result = []) {
  if (!root) return result;

  // 访问根节点
  result.push(root.val);
  // 递归遍历左子树
  preorderTraversal(root.left, result);
  // 递归遍历右子树
  preorderTraversal(root.right, result);

  return result;
}

// 简化版本(仅打印)
function preorderPrint(root) {
  if (!root) return;

  console.log(root.val); // 访问根节点
  preorderPrint(root.left); // 遍历左子树
  preorderPrint(root.right); // 遍历右子树
}

前序遍历(非递归版)

使用栈来模拟递归过程:

/**
 * 前序遍历 - 迭代实现
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array} - 遍历结果
 */
function preorderTraversalIterative(root) {
  if (!root) return [];

  const result = [];
  const stack = [root]; // 使用栈存储待访问节点

  while (stack.length > 0) {
    const node = stack.pop(); // 出栈
    result.push(node.val); // 访问当前节点

    // 注意:先压入右子树,再压入左子树
    // 因为栈是后进先出,这样左子树会先被访问
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }

  return result;
}

// 测试示例
const testTree = new TreeNode(
  1,
  new TreeNode(2, new TreeNode(4), new TreeNode(5)),
  new TreeNode(3)
);
console.log(preorderTraversalIterative(testTree)); // [1, 2, 4, 5, 3]

中序遍历(递归版)

中序遍历顺序:左 → 根 → 右(对于二叉搜索树,中序遍历得到有序序列)

/**
 * 中序遍历 - 递归实现
 * @param {TreeNode} root - 二叉树根节点
 * @param {Array} result - 存储遍历结果的数组
 * @return {Array} - 遍历结果
 */
function inorderTraversal(root, result = []) {
  if (!root) return result;

  // 递归遍历左子树
  inorderTraversal(root.left, result);
  // 访问根节点
  result.push(root.val);
  // 递归遍历右子树
  inorderTraversal(root.right, result);

  return result;
}

// 简化版本(仅打印)
function inorderPrint(root) {
  if (!root) return;

  inorderPrint(root.left); // 遍历左子树
  console.log(root.val); // 访问根节点
  inorderPrint(root.right); // 遍历右子树
}

中序遍历(非递归版)

使用栈和指针来实现:

/**
 * 中序遍历 - 迭代实现
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array} - 遍历结果
 */
function inorderTraversalIterative(root) {
  const result = [];
  const stack = [];
  let current = root;

  while (stack.length > 0 || current) {
    // 一直向左走,将路径上的节点入栈
    while (current) {
      stack.push(current);
      current = current.left;
    }

    // 左子树遍历完毕,访问当前节点
    current = stack.pop();
    result.push(current.val);

    // 转向右子树
    current = current.right;
  }

  return result;
}

// 测试示例
const bst = new TreeNode(
  4,
  new TreeNode(2, new TreeNode(1), new TreeNode(3)),
  new TreeNode(6, new TreeNode(5), new TreeNode(7))
);
console.log(inorderTraversalIterative(bst)); // [1, 2, 3, 4, 5, 6, 7] (有序)

后序遍历(递归版)

后序遍历顺序:左 → 右 → 根(常用于删除节点、计算目录大小等)

/**
 * 后序遍历 - 递归实现
 * @param {TreeNode} root - 二叉树根节点
 * @param {Array} result - 存储遍历结果的数组
 * @return {Array} - 遍历结果
 */
function postorderTraversal(root, result = []) {
  if (!root) return result;

  // 递归遍历左子树
  postorderTraversal(root.left, result);
  // 递归遍历右子树
  postorderTraversal(root.right, result);
  // 访问根节点
  result.push(root.val);

  return result;
}

// 简化版本(仅打印)
function postorderPrint(root) {
  if (!root) return;

  postorderPrint(root.left); // 遍历左子树
  postorderPrint(root.right); // 遍历右子树
  console.log(root.val); // 访问根节点
}

后序遍历(非递归版)

方法 1:双栈法(简单易懂)

/**
 * 后序遍历 - 双栈迭代实现
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array} - 遍历结果
 */
function postorderTraversalTwoStacks(root) {
  if (!root) return [];

  const stack1 = [root]; // 用于遍历
  const stack2 = []; // 用于存储结果
  const result = [];

  // 第一个栈按 根->右->左 的顺序处理
  while (stack1.length > 0) {
    const node = stack1.pop();
    stack2.push(node);

    // 注意:先左后右(与前序遍历相反)
    if (node.left) stack1.push(node.left);
    if (node.right) stack1.push(node.right);
  }

  // 第二个栈弹出就是后序遍历结果
  while (stack2.length > 0) {
    result.push(stack2.pop().val);
  }

  return result;
}

方法 2:单栈法(更高效)

/**
 * 后序遍历 - 单栈迭代实现
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array} - 遍历结果
 */
function postorderTraversalOneStack(root) {
  if (!root) return [];

  const result = [];
  const stack = [];
  let lastVisited = null;
  let current = root;

  while (stack.length > 0 || current) {
    if (current) {
      stack.push(current);
      current = current.left;
    } else {
      const peekNode = stack[stack.length - 1];

      // 如果右子树存在且未被访问过
      if (peekNode.right && lastVisited !== peekNode.right) {
        current = peekNode.right;
      } else {
        // 访问当前节点
        result.push(peekNode.val);
        lastVisited = stack.pop();
      }
    }
  }

  return result;
}

深度优先遍历(DFS)

深度优先遍历就是前序遍历的一种应用,优先访问深度更深的节点:

/**
 * 深度优先遍历 - 递归实现
 * @param {TreeNode} root - 二叉树根节点
 * @param {Function} visit - 访问节点的回调函数
 */
function dfsRecursive(root, visit = console.log) {
  if (!root) return;

  visit(root.val); // 访问当前节点
  dfsRecursive(root.left, visit); // 深度优先遍历左子树
  dfsRecursive(root.right, visit); // 深度优先遍历右子树
}

/**
 * 深度优先遍历 - 迭代实现
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array} - 遍历结果
 */
function dfsIterative(root) {
  if (!root) return [];

  const result = [];
  const stack = [root];

  while (stack.length > 0) {
    const node = stack.pop();
    result.push(node.val);

    // 先压入右子树,再压入左子树
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }

  return result;
}

// DFS的实际应用:查找路径
function findPath(root, target) {
  if (!root) return null;

  if (root.val === target) return [root.val];

  // 在左子树中查找
  const leftPath = findPath(root.left, target);
  if (leftPath) return [root.val, ...leftPath];

  // 在右子树中查找
  const rightPath = findPath(root.right, target);
  if (rightPath) return [root.val, ...rightPath];

  return null; // 未找到
}

层序遍历(广度优先遍历/BFS)

层序遍历按层级从上到下、从左到右访问节点,需要使用队列:

/**
 * 层序遍历 - 基础实现
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array} - 遍历结果
 */
function levelOrder(root) {
  if (!root) return [];

  const result = [];
  const queue = [root];

  while (queue.length > 0) {
    const node = queue.shift(); // 出队
    result.push(node.val); // 访问节点

    // 将子节点入队(注意null检查)
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }

  return result;
}

/**
 * 层序遍历 - 按层分组
 * @param {TreeNode} root - 二叉树根节点
 * @return {Array[]} - 按层分组的遍历结果
 */
function levelOrderGrouped(root) {
  if (!root) return [];

  const result = [];
  const queue = [root];

  while (queue.length > 0) {
    const levelSize = queue.length; // 当前层的节点数
    const currentLevel = [];

    // 处理当前层的所有节点
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      currentLevel.push(node.val);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(currentLevel);
  }

  return result;
}

// 测试示例
const tree = new TreeNode(
  3,
  new TreeNode(9),
  new TreeNode(20, new TreeNode(15), new TreeNode(7))
);
console.log(levelOrder(tree)); // [3, 9, 20, 15, 7]
console.log(levelOrderGrouped(tree)); // [[3], [9, 20], [15, 7]]

/**
 * BFS的实际应用:计算二叉树的最大宽度
 */
function maxWidth(root) {
  if (!root) return 0;

  let maxWidth = 0;
  const queue = [root];

  while (queue.length > 0) {
    const levelSize = queue.length;
    maxWidth = Math.max(maxWidth, levelSize);

    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }

  return maxWidth;
}

二叉树的常用算法

1. 计算二叉树的深度

/**
 * 计算二叉树的最大深度
 * @param {TreeNode} root
 * @return {number}
 */
function maxDepth(root) {
  if (!root) return 0;

  const leftDepth = maxDepth(root.left);
  const rightDepth = maxDepth(root.right);

  return Math.max(leftDepth, rightDepth) + 1;
}

/**
 * 计算二叉树的最小深度
 * @param {TreeNode} root
 * @return {number}
 */
function minDepth(root) {
  if (!root) return 0;
  if (!root.left && !root.right) return 1;

  let min = Infinity;
  if (root.left) min = Math.min(min, minDepth(root.left));
  if (root.right) min = Math.min(min, minDepth(root.right));

  return min + 1;
}

2. 判断二叉树的性质

/**
 * 判断是否为对称二叉树
 * @param {TreeNode} root
 * @return {boolean}
 */
function isSymmetric(root) {
  if (!root) return true;

  function isMirror(left, right) {
    if (!left && !right) return true;
    if (!left || !right) return false;

    return (
      left.val === right.val &&
      isMirror(left.left, right.right) &&
      isMirror(left.right, right.left)
    );
  }

  return isMirror(root.left, root.right);
}

/**
 * 判断是否为平衡二叉树
 * @param {TreeNode} root
 * @return {boolean}
 */
function isBalanced(root) {
  function getHeight(node) {
    if (!node) return 0;

    const leftHeight = getHeight(node.left);
    const rightHeight = getHeight(node.right);

    // 如果子树不平衡,返回-1
    if (
      leftHeight === -1 ||
      rightHeight === -1 ||
      Math.abs(leftHeight - rightHeight) > 1
    ) {
      return -1;
    }

    return Math.max(leftHeight, rightHeight) + 1;
  }

  return getHeight(root) !== -1;
}

3. 二叉搜索树操作

/**
 * 在BST中查找值
 * @param {TreeNode} root
 * @param {number} val
 * @return {TreeNode}
 */
function searchBST(root, val) {
  if (!root || root.val === val) return root;

  return val < root.val
    ? searchBST(root.left, val)
    : searchBST(root.right, val);
}

/**
 * 在BST中插入值
 * @param {TreeNode} root
 * @param {number} val
 * @return {TreeNode}
 */
function insertIntoBST(root, val) {
  if (!root) return new TreeNode(val);

  if (val < root.val) {
    root.left = insertIntoBST(root.left, val);
  } else {
    root.right = insertIntoBST(root.right, val);
  }

  return root;
}

/**
 * 验证是否为有效的BST
 * @param {TreeNode} root
 * @return {boolean}
 */
function isValidBST(root) {
  function validate(node, min, max) {
    if (!node) return true;

    if (node.val <= min || node.val >= max) return false;

    return (
      validate(node.left, min, node.val) && validate(node.right, node.val, max)
    );
  }

  return validate(root, -Infinity, Infinity);
}

总结

二叉树是一种非常重要的数据结构,掌握其遍历方法和常用算法对于算法面试和实际开发都至关重要。主要知识点包括:

  1. 四种遍历方式:前序、中序、后序、层序遍历
  2. 递归与迭代:每种遍历都有递归和迭代两种实现方式
  3. 实际应用:路径查找、深度计算、平衡判断、BST 操作等
  4. 时间复杂度:大部分操作都是 O(n),其中 n 是节点数量
  5. 空间复杂度:递归实现 O(h),迭代实现 O(h),其中 h 是树的高度

在面试中,二叉树相关题目出现频率很高,建议多加练习,熟练掌握各种遍历方法和常见算法模式。