文章目录
1. 复杂度计算
复杂度计算用于评估算法的效率,通常关注两个方面:时间复杂度和空间复杂度。
1.1 时间复杂度
时间复杂度衡量算法运行所需的时间,通常用大 O 表示法来表示:
- O(1):常数时间,算法执行时间不随输入规模的变化而变化。
- O(log n):对数时间,执行时间随输入规模的对数增长,例如二分查找。
- O(n):线性时间,执行时间与输入规模成正比,例如线性搜索。
- O(n log n):线性对数时间,通常见于高效排序算法,如快速排序。
- O(n^2):平方时间,执行时间与输入规模的平方成正比,例如冒泡排序。
- O(2^n):指数时间,执行时间随输入规模指数增长,例如某些递归算法。
- O(n!):阶乘时间,执行时间随输入规模阶乘增长,例如某些组合问题的算法。
1.2 空间复杂度
空间复杂度衡量算法运行时所需的内存,通常也用大 O 表示法:
- O(1):常数空间,所需内存与输入规模无关。
- O(n):线性空间,所需内存与输入规模成正比,例如存储输入数据的数组。
- O(n^2):平方空间,所需内存与输入规模的平方成正比,例如二维矩阵。
1.3 如何计算
- 确定基本操作:找出算法中最频繁执行的操作。
- 分析循环结构:嵌套循环通常会增加复杂度的阶数。
- 简化表达式:忽略低阶项和常数系数,专注于主要增长趋势。
示例:
假设有一个简单的算法:
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 常数时间操作
}
}
这个算法的时间复杂度是 O(n^2),因为有两个嵌套的循环,每个循环都运行 n 次。
2. 常见线性结构
2.1 顺序表 和 链表
2.1.1 顺序表 (Array List)
- 原理:使用连续内存空间,支持通过索引直接访问。
- 优点:随机访问速度快 (O(1))。
- 缺点:插入和删除操作较慢 (O(n)),可能需要数组扩展或缩减。
操作:
- 查找:O(1)
- 插入:O(n)
- 删除:O(n)
- 修改:O(1)
2.1.2 链表 (Linked List)
- 原理:节点不连续存储,通过指针链接。
- 优点:插入和删除操作快 (O(1)),动态大小。
- 缺点:随机访问较慢 (O(n)),额外空间用于指针。
操作:
- 查找:O(n)
- 插入:O(1)(已知位置),O(n)(查找位置)
- 删除:O(1)(已知位置),O(n)(查找位置)
- 修改:O(n)(查找位置)
2.2 栈 和 队列
2.2.1 什么是栈?什么是队列?关系是什么?
栈 (Stack)
- 定义:遵循“后进先出”(LIFO)原则。最后插入的元素最先被取出。
- 主要操作:入栈、出栈、查看栈顶、判断空栈。
- 应用:函数调用、浏览器历史记录。
队列 (Queue)
- 定义:遵循“先进先出”(FIFO)原则。最早插入的元素最先被取出。
- 主要操作:入队、出队、查看队头、判断空队列。
- 应用:任务调度、数据缓冲。
关系
- 操作方式:栈在一端操作(栈顶),队列在两端操作(队头和队尾)。
- 实现:栈和队列可以通过数组或链表实现,但操作规则不同。
2.2.2 如何实现?数组链表原生实现?适配器?
栈 的实现:
- 数组实现:
- 入栈:添加到数组末尾。
- 出栈:从数组末尾移除元素。
- 查看栈顶:访问数组末尾元素。
- 链表实现:
- 入栈:在链表头部插入元素。
- 出栈:从链表头部移除元素。
- 查看栈顶:访问链表头部元素。
队列 的实现:
- 数组实现:
- 入队:添加到数组末尾。
- 出队:从数组开头移除元素。
- 循环数组:使用环形缓冲区优化性能。
- 链表实现:
- 入队:在链表尾部插入元素。
- 出队:从链表头部移除元素。
- 查看队头:访问链表头部元素。
适配器实现
- 栈基于队列:
- 单队列:通过旋转队列模拟栈。
- 双队列:使用两个队列交替操作。
- 队列基于栈:
- 双栈:使用两个栈交替操作实现队列。
3. 常见非线性结构
3.1 二叉树
3.2 堆
3.2.1 性质和概念
堆是一种完全二叉树数据结构,分为最大堆和最小堆:
- 最大堆:每个节点的值大于或等于其子节点的值,根节点是最大值。
- 最小堆:每个节点的值小于或等于其子节点的值,根节点是最小值。
3.2.2 实现
数组表示:
- 左子节点索引:
2i
- 右子节点索引:
2i + 1
- 父节点索引:
i / 2
操作:
- 插入:将新元素添加到末尾,然后上浮调整。
- 删除:删除根节点(最大或最小值),用末尾元素替换,然后下沉调整。
- 堆化:将数组调整为堆。
3.2.3 topk
Top-K 问题:
- 使用最小堆保持
k
个最大元素,时间复杂度O(nlogk)
3.2.4 堆排序
堆排序:
- 构建最大堆。
- 排序:将根节点(最大值)与最后一个元素交换,调整堆。
- 时间复杂度:
O(n log n)
,空间复杂度:O(1)
。
3.3 二叉树常见问题
3.4 搜索树
搜索树是一种用于存储有序数据并支持高效查找、插入和删除操作的数据结构。主要包括二叉搜索树(BST)、AVL树和红黑树。
3.4.1 概念性质
- 二叉搜索树(BST):
- 性质:对于每个节点,其左子树包含所有小于该节点的值,右子树包含所有大于该节点的值。
- 查找:平均时间复杂度
O(log n)
,最坏情况O(n)
(如链表)。
3.4.2 增删查改
- 查找:从根节点开始,根据键值向左或右子树递归查找。
- 插入:插入新节点后,维护 BST 的性质。
- 删除:删除节点后,可能需要重构树以保持 BST 的性质。三种情况:
- 删除叶节点。
- 删除只有一个子节点的节点。
- 删除有两个子节点的节点(用右子树的最小值或左子树的最大值替换)。
3.4.3 缺陷
- 缺陷:BST 在最坏情况下退化为链表,导致操作效率降低。
3.4.4 缺陷解决方案:平衡树
平衡树通过自平衡机制保持树的高度尽可能低,保证操作效率。主要包括 AVL 树和红黑树。
- AVL树:
- 性质:每个节点的左子树和右子树的高度差(平衡因子)至多为 1。
- 效率:查找、插入、删除操作的时间复杂度均为 O(log n)。
- 实现逻辑:
- 旋转操作:单旋转(左旋、右旋)和双旋转(左-右旋、右-左旋),用于恢复平衡。
- 插入:插入后进行旋转调整。
- 删除:删除后进行旋转调整以恢复平衡。
- 红黑树:
- 性质:每个节点是红色或黑色,满足以下条件:
- 根节点是黑色。
- 个叶子节点(NIL)是黑色。
- 如果一个节点是红色,则它的两个子节点都是黑色。
- 从每个节点到其每个叶子节点的路径包含相同数量的黑色节点。
- 效率:查找、插入、删除操作的时间复杂度均为 O(log n)。
- 实现逻辑:
- 旋转操作:类似 AVL 树。
- 颜色调整:插入或删除后调整节点颜色和进行旋转以维护红黑树性质。
- AVL树 与 红黑树 的区别:
- 平衡性:
- AVL 树:更严格的平衡,确保树高度差不超过 1,查找操作更快,但插入和删除需要更多的旋转操作。
- 红黑树:较宽松的平衡,允许更多的高度差,插入和删除操作相对较快,但查找操作可能稍慢。
- 实现复杂性:
- AVL 树:旋转操作较多,复杂度高。
- 红黑树:相对较少的旋转操作和颜色调整,易于实现。
- 应用场景:
- AVL 树:适用于查找频繁的场景。
- 红黑树:适用于插入和删除操作频繁的场景,如 C++ STL 的
map
和set
使用红黑树。
3.5 哈希
3.5.1 什么是哈希?
哈希是一种将数据映射到固定大小的值(哈希值)的过程,用于快速查找和存储数据。
3.5.2 如何解决哈希冲突
闭散列(开放地址法)是处理哈希冲突的一种方法。主要有线性探测和二次探测两种方式:
- 线性探测(Linear Probing)
- 方法:发生冲突时,顺序查找下一个位置。
- 缺陷:
- 主聚集:连续的被占用区域,导致性能下降。
- 性能退化:表满时,查找和插入效率降低。
- 二次探测(Quadratic Probing)
- 方法:冲突后,使用二次方程计算下一个位置。
- 缺陷:
- 二次聚集:探测序列可能会形成聚集。
- 表大小要求:通常要求表大小为质数,增加实现复杂性。
- 总结:线性探测简单但易聚集,二次探测减少了聚集但复杂度更高。选择适当方法需根据实际需求。
开散列(拉链法)是一种解决哈希表冲突的方法,通过在每个哈希槽上维护一个链表来存储冲突的元素。它的优点包括:
- 简单易实现:链表操作简单,易于处理动态增长的元素。
- 性能稳定:负载因子增加时,性能相对稳定。
- 灵活的内存使用:链表可以动态增长,无需重新分配整个哈希表。
- 删除操作简单:删除元素只需从链表中移除即可。
应对冲突严重的方法:
- 优化哈希函数:设计良好的哈希函数以减少冲突。
- 调整表的大小:通过扩展哈希表和重新哈希减少链表长度。
- 使用其他数据结构:长链表可用平衡树替代,以提高效率。
- 合理设置负载因子:动态调整负载因子,定期扩展哈希表。
- 监控性能:定期检查哈希表性能,以便进行优化。
3.5.3 位图
位图是一种用于高效存储和快速查询的二进制数据结构,主要用于表示集合中的元素是否存在。以下是位图的主要特点:
- 存储方式:位图使用一个数组,每个元素用一个比特位表示。如果元素存在,则对应的位设置为1,不存在则设置为0。
- 直接定址:元素的值直接作为数组索引,通过该索引访问或修改对应的位,因此没有哈希冲突。
- 节省空间:位图每个元素只用一个比特位,相比其他数据结构(如整型数组),能够显著减少内存使用,尤其适合处理大量数据的存在性检测。
- 查询效率高:由于位图的查询和更新操作都可以在常数时间内完成,适合需要快速判断元素是否存在的场景。
总结:位图是一种高效的空间利用和查询的数据结构,用于表示和操作大量元素的存在性。
3.5.4 布隆过滤器
布隆过滤器是一种高效的空间优化数据结构,用于判断一个元素是否在一个集合中。其特点包括:
- 空间节省:使用多个哈希函数和一个位图来表示集合,显著减少存储需求,适合处理大规模数据。
- 误判:布隆过滤器可以准确地确定元素不在集合中,但不能100%保证元素在集合中,可能存在误判(即误报),因为不同的元素可能会映射到相同的位。
- 操作效率:元素的加入和查询操作都很快,时间复杂度为常数时间 O(k),其中 k 是哈希函数的数量。
- 删除问题:布隆过滤器无法直接删除元素,因为其位图中的位可能被多个元素共享,解决方案包括使用计数型布隆过滤器或周期性重新生成过滤器。
总结:布隆过滤器通过使用多个哈希函数和位图,能够高效地存储和查询大规模数据,但存在一定的误判概率。
3.5.5 海量数据处理的问题
在海量数据处理问题中,以下三种技术可以有效解决数据存储和查询问题:
- 位图:
- 解决方法:使用一个大位数组(或位图)来记录数据的存在性,每一位表示一个数据项的状态(存在或不存在)。适用于数据范- 围较小且可以用位表示的场景。
- 优点:查询效率高,适合存储和处理布尔值数据。
- 缺点:对于范围非常大的数据(例如大范围的整数),位图可能占用过多内存。
- 布隆过滤器:
- 解决方法:使用多个哈希函数和一个位图来高效地检测某个元素是否在集合中。适用于需要处理大量数据的情况。
- 优点:节省存储空间,查询速度快。
- 缺点:可能产生误判(误报),不能直接删除元素。
- 哈希切分(Hash Partitioning):
- 解决方法:通过哈希函数将数据分散到多个存储分区中,以便更均匀地分配存储负载和提高并行处理能力。
- 优点:能处理超大规模数据,通过分区管理避免单个节点的负载过重。
- 缺点:需要设计合理的哈希函数和分区策略,以避免数据倾斜和负载不均。
这些技术可以根据具体的数据处理需求和应用场景,单独或组合使用,以优化数据存储和查询性能。
4. 算法
4.1 排序
- 冒泡排序(Bubble Sort):
- 实现:重复遍历待排序列,比较相邻元素并交换,使得较大的元素逐渐“冒泡”到末尾。
- 复杂度:最坏和平均情况 O(n²),最佳情况 O(n)(当已排序时)。
- 稳定性:稳定。
- 适用场景:数据量较小或对稳定性有高要求时使用。
- 选择排序(Selection Sort):
- 实现:每次选择未排序部分的最小(或最大)元素,将其放到已排序部分的末尾。
- 复杂度:O(n²)。
- 稳定性:不稳定。
- 适用场景:对内存占用有严格要求且数据量小的情况。
- 插入排序(Insertion Sort):
- 实现:将待排序元素插入到已排序部分的适当位置,像手工排序扑克牌一样。
- 复杂度:最坏和平均情况 O(n²),最佳情况 O(n)(当已排序时)。
- 稳定性:稳定。
- 适用场景:数据量较小或部分已排序时使用。
- 归并排序(Merge Sort):
- 实现:将数据分割成更小的部分,递归地排序这些部分,然后合并已排序的部分。
- 复杂度:O(n log n)。
- 稳定性:稳定。
- 适用场景:大数据量,且需要稳定排序时。
- 快速排序(Quick Sort):
- 实现:选择一个“基准”元素,将数据分为两个部分,递归地对这两部分进行排序。
- 复杂度:平均情况 O(n log n),最坏情况 O(n²)(例如基准选择不当)。
- 稳定性:不稳定。
- 适用场景:大数据量时,快速排序通常表现优越。
- 堆排序(Heap Sort):
- 实现:将数据构建成一个堆,每次取出堆顶元素,并重建堆。
- 复杂度:O(n log n)。
- 稳定性:不稳定。
- 适用场景:需要优良时间复杂度且不要求稳定性时。
- 计数排序(Counting Sort):
- 实现:统计每个元素的出现次数,然后按顺序重建排序后的数据。
- 复杂度:O(n + k),其中 k 是元素范围。
- 稳定性:稳定。
- 适用场景:元素范围有限且数据量大的情况。
- 基数排序(Radix Sort):
- 实现:按每一位的值进行多轮排序(通常结合计数排序作为稳定的子排序)。
- 复杂度:O(nk),其中 k 是位数。
- 稳定性:稳定。
- 适用场景:处理整数数据,尤其是位数较少时效果良好。
- 希尔排序(Shell Sort):
- 实现:通过多轮插入排序,逐步减少间隔(增量),最终进行标准插入排序。
- 复杂度:最坏情况 O(n²),增量序列优化后可达 O(n log n)。
- 稳定性:不稳定。
- 适用场景:中等规模的数据集,内存有限时。
4.2 二分查找
二分查找是一种高效的查找算法,适用于已排序的数组。其基本思路是将数组分成两半,通过比较中间元素与目标值,决定继续在哪一半进行查找。算法步骤如下:
- 初始化:设定两个指针,
left
指向数组起始位置,right
指向数组末尾位置。 - 循环:计算中间位置
mid
,比较mid
位置的元素与目标值。
- 如果相等,返回
mid
位置。- 如果目标值小于
mid
位置的元素,将right
指向mid - 1
。- 如果目标值大于
mid
位置的元素,将left
指向mid + 1
。
- 结束:当
left
超过right
时,查找结束,如果未找到目标值,返回-1
或表示未找到的标识。
时间复杂度:O(log n),空间复杂度:O(1)。