菜单

数据结构

相关源文件

本文全面概述了编码面试中必不可少的基本数据结构。数据结构是组织和存储数据的特定方式,能够实现高效操作。理解数据结构对于编写高效代码和在Google、Amazon、Microsoft等公司的技术面试中取得成功都至关重要。

有关与这些数据结构配对的算法复杂度分析,请参阅算法复杂度 / 大O表示法 / 渐近分析

有关利用这些数据结构的算法,请参阅算法

数据结构概述

数据结构是高效算法的基石。为特定问题选择正确的数据结构可以显著提高性能并简化代码。

来源:README.md599-720

线性数据结构

数组

数组是最基本的数据结构,由存储在连续内存位置的元素组成。

主要特点

  • 固定大小(在大多数语言中)或动态(如 Java 中的 ArrayList,C++ 中的 vector,Python 中的 list)
  • 随机访问 - 元素可以使用索引在 O(1) 时间内直接访问
  • 对于基本数据类型来说内存效率高
  • 由于内存局部性,缓存友好

操作和时间复杂度

操作时间复杂度注意
访问O(1)直接索引访问
搜索(未排序)O(n)线性搜索
搜索(已排序)O(log n)二分查找
在末尾插入(分摊)O(1)使用动态数组时
在任意位置插入O(n)需要移动元素
在末尾删除O(1)无需移动
在任意位置删除O(n)需要移动元素

动态数组

动态数组(如 Java 中的 ArrayList 或 C++ 中的 vector)在达到容量时会自动调整大小,通常会将其大小加倍。

常见实现任务

  • 向量(可变大小数组)的实现应包括
    • size() - 项目数量
    • capacity() - 最大项目数量
    • is_empty()
    • at(index) - 返回给定索引处的项目
    • push(item) - 在末尾添加项目
    • insert(index, item) - 在索引处插入,并移动后面的元素
    • prepend(item) - 在开头插入
    • pop() - 移除并返回最后一个项目
    • delete(index) - 移除索引处的项目
    • remove(item) - 如果找到则移除项目
    • find(item) - 返回项目的第一个索引
    • resize(new_capacity) - 调整数组大小的私有函数

来源:README.md602-632

链表

链表是线性数据结构,元素存储在节点中,每个节点包含数据和指向下一个节点的引用。

链表的类型

  • 单向链表:每个节点指向下一个节点
  • 双向链表:每个节点同时指向下一个和前一个节点
  • 循环链表:最后一个节点指回第一个节点

操作和时间复杂度

操作时间复杂度注意
访问O(n)必须从头节点遍历
搜索O(n)线性遍历
在开头插入O(1)更改头节点引用
在末尾插入O(1)带有尾指针
在指定位置插入O(n)必须遍历到指定位置
在开头删除O(1)更改头节点引用
在末尾删除O(n) 或 O(1)使用双向链表时为 O(1)
在指定位置删除O(n)必须遍历到指定位置

链表与数组的对比

链表的优点

  • 动态大小
  • 在开头高效插入/删除
  • 无需连续内存

链表的缺点

  • 无法随机访问
  • 指针需要额外内存
  • 不利于缓存
  • 只能顺序访问

常见实现任务

链表实现应包括以下操作:

  • size() - 返回数据元素的数量
  • empty() - 如果为空则返回 true
  • value_at(index) - 返回第 n 个位置的值
  • push_front(value) - 添加到开头
  • pop_front() - 移除开头项目
  • push_back(value) - 添加到末尾
  • pop_back() - 移除末尾项目
  • front() - 获取开头项目的值
  • back() - 获取末尾项目的值
  • insert(index, value) - 在索引处插入值
  • erase(index) - 移除索引处的节点
  • value_n_from_end(n) - 返回从末尾算起第 n 个位置的节点值
  • reverse() - 反转列表
  • remove_value(value) - 移除值的首次出现

来源:README.md634-669

栈是一种遵循后进先出(LIFO)原则的线性数据结构。

主要特点

  • 元素从同一端(栈顶)添加和移除
  • 在任何时候只有栈顶元素可访问
  • 遵循 LIFO (后进先出) 原则

操作和时间复杂度

操作时间复杂度
推送O(1)
弹栈(Pop)O(1)
查看栈顶/栈顶(Peek/Top)O(1)
搜索O(n)
判断是否为空(IsEmpty)O(1)

实现方法

  • 使用数组
    • 简单但可能需要调整大小
  • 使用链表
    • 动态大小但稍微复杂

常见应用

  • 函数调用管理(调用栈)
  • 表达式求值和语法解析
  • 应用程序中的撤销机制
  • 回溯算法
  • 深度优先搜索实现

来源:README.md670-673

队列

队列是一种遵循先进先出(FIFO)原则的线性数据结构。

主要特点

  • 元素从一端(队尾)添加,从另一端(队头)移除
  • 遵循 FIFO (先进先出) 原则
  • 可以使用数组或链表实现

操作和时间复杂度

操作时间复杂度
入队(Enqueue)O(1)
出队(Dequeue)O(1)
队头(Front)O(1)
队尾(Rear)O(1)
判断是否为空(IsEmpty)O(1)

实现方法

  • 使用数组(循环缓冲区)
    • 高效利用空间
    • 需要跟踪队头和队尾索引
  • 使用链表
    • 动态大小
    • 需要跟踪头和尾

循环队列/缓冲区

循环缓冲区通过在元素出队后重用数组元素来优化空间。

常见实现任务

  • 使用带有尾指针的链表
    • enqueue(value) - 在队尾添加值
    • dequeue() - 返回值并移除最近添加的元素
    • empty()
  • 使用固定大小数组
    • enqueue(value) - 在末尾添加项目
    • dequeue() - 返回值并移除最近添加的元素
    • empty()
    • full()

来源:README.md676-694

基于哈希的结构

哈希表

哈希表(也称为哈希映射)是一种实现关联数组的数据结构,它将键映射到值。

主要特点

  • 使用哈希函数将键映射到桶位置
  • 提供插入、删除和查找的平均 O(1) 操作
  • 当不同的键哈希到同一个桶时,必须处理冲突

冲突解决技术

  1. 链式法 - 每个桶包含一个条目链表
  2. 开放寻址法 - 探测下一个可用槽位
    • 线性探测
    • 二次探测
    • 双重哈希

操作和时间复杂度

操作平均时间最坏时间
插入O(1)O(n)
删除O(1)O(n)
搜索O(1)O(n)

哈希函数

一个好的哈希函数应该

  • 均匀分布键
  • 计算速度快
  • 最小化冲突
  • 提供确定性结果

负载因子和重新调整大小

  • 负载因子 = 条目数 / 桶数
  • 当负载因子超过阈值(通常为 0.7)时,哈希表会重新调整大小
  • 重新调整大小通常涉及将容量加倍并重新哈希所有条目

常见实现任务

  • 使用线性探测
    • hash(k, m) - m 是哈希表的大小
    • add(key, value) - 如果键存在,则更新值
    • exists(key)
    • get(key)
    • remove(key)

来源:README.md695-719

树和类树结构

虽然树将在单独的章节中详细介绍,但它们是值得在此提及的基本数据结构。

二叉搜索树(BST)

一种二叉树,其中

  • 每个节点最多有两个子节点
  • 左子节点 < 父节点 < 右子节点
  • 允许高效的搜索、插入和删除

一种专门的基于树的数据结构

  • 完全二叉树
  • 堆属性:父节点总是大于或等于(最大堆)或小于或等于(最小堆)其子节点

Trie 树

一种用于存储字符串的类树数据结构

  • 每个节点代表一个字符
  • 从根到叶的路径代表完整的字符串
  • 高效地进行前缀匹配和自动补全功能

有关树的更多详细信息,请参阅学习计划的树部分。

来源:README.md100-111

图是由节点(顶点)和连接这些节点的边组成的非线性数据结构。

邻接矩阵和邻接表

表示图的两种常用方式

邻接矩阵:

  • 一个二维数组,如果从节点 i 到节点 j 有边,则matrix[i][j] = 1
  • 空间复杂度:O(V²),其中 V 是顶点数量
  • 快速边缘查找:O(1)
  • 稠密图

邻接表:

  • 一个列表数组,每个列表存储一个顶点的相邻顶点
  • 空间复杂度:O(V + E),其中 E 是边的数量
  • 更适用于稀疏图
  • 边缘查找稍慢:O(度)

有关图的更多详细信息,请参阅学习计划的图部分。

来源:README.md119-124

内存和时间复杂度比较

了解数据结构操作的时间和空间复杂度对于技术面试至关重要。

数据结构访问搜索插入删除Space键
数组O(1)O(n)O(n)O(n)O(n)
动态数组O(1)O(n)O(1)/O(n)*O(1)/O(n)*O(n)
单向链表O(n)O(n)O(1)**O(1)**O(n)
双向链表O(n)O(n)O(1)**O(1)**O(n)
O(n)O(n)O(1)O(1)O(n)
队列O(n)O(n)O(1)O(1)O(n)
哈希表不适用O(1)***O(1)***O(1)***O(n)
二叉搜索树O(log n)****O(log n)****O(log n)****O(log n)****O(n)
O(1)†O(n)O(log n)O(log n)O(n)

*末尾操作为均摊时间,任意位置为最坏情况
**当位置已知时(例如,在头/尾部)
***平均情况,最坏情况为 O(n)
****平衡树,非平衡树的最坏情况为 O(n)
†仅限最大/最小元素

来源:README.md627-632 README.md642-647 README.md689-693

面试中的实现注意事项

在编程面试中,你通常需要从头开始实现数据结构或理解其底层原理。

内存管理

  • 在 C/C++ 等语言中,了解如何正确分配和释放内存
  • 对于动态数据结构,高效处理大小调整
  • 注意手动内存管理语言中的内存泄漏

边界情况

  • 空数据结构
  • 单元素数据结构
  • 边界操作(第一个/最后一个元素)
  • 满数据结构(对于固定大小的实现)

常见面试任务

  • 从头实现数据结构
  • 为特定用例修改数据结构
  • 为给定问题选择最优数据结构
  • 分析操作的时间和空间复杂度

面试策略

  1. 清楚理解问题需求
  2. 考虑哪种数据结构最适合该问题
  3. 讨论不同数据结构之间的权衡
  4. 清晰地实现所选解决方案
  5. 使用示例(包括边界情况)进行测试
  6. 分析时间和空间复杂度

来源:README.md518-532 README.md602-694

实际应用模式

不同的问题需要不同的数据结构。以下是一些常见模式:

问题类型推荐数据结构
按键快速查找哈希表
先进先出处理队列
后进先出处理
频繁插入/删除的有序数据平衡二叉搜索树
字符串前缀匹配Trie树
动态连通性并查集
图遍历邻接表/矩阵
优先级调度
维护带插入的排序数据二叉搜索树或堆
缓存哈希表 + 链表(LRU 缓存)

来源:README.md599-720

结论

掌握基本数据结构对于编程面试至关重要。理解何时以及如何使用每种数据结构,以及它们各自的权衡,将帮助你高效解决问题。

练习从头开始实现这些结构,并解决利用它们的问题。请记住,面试问题通常会结合多种数据结构来解决复杂问题。

如需更多练习,请参阅学习计划的编码问题练习部分。