数据结构--字典
什么是字典
字典(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 对比
| 特性 | Map | Object |
|---|---|---|
| 键的类型 | 任意类型 | 字符串、Symbol |
| 大小获取 | map.size | Object.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 来帮助我们解决这道算法题呢?
解题思路分析:
- 暴力解法:双重循环,时间复杂度 O(n²),空间复杂度 O(1)
- 哈希表优化:使用 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
二叉树的创建
在 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);
}
总结
二叉树是一种非常重要的数据结构,掌握其遍历方法和常用算法对于算法面试和实际开发都至关重要。主要知识点包括:
- 四种遍历方式:前序、中序、后序、层序遍历
- 递归与迭代:每种遍历都有递归和迭代两种实现方式
- 实际应用:路径查找、深度计算、平衡判断、BST 操作等
- 时间复杂度:大部分操作都是 O(n),其中 n 是节点数量
- 空间复杂度:递归实现 O(h),迭代实现 O(h),其中 h 是树的高度
在面试中,二叉树相关题目出现频率很高,建议多加练习,熟练掌握各种遍历方法和常见算法模式。