数据结构与算法
时间复杂度 空间复杂度
数学领域里,算法是用于解决某一类问题的公式和思想。计算机科学领域的算法,它的本质是一系列程序指令,用于解决特定的运算和逻辑问题。
我们通常需要选择时间复杂度较低的算法来处理大规模问题,使得程序的运行效率更高。同时考虑需要选择空间复杂度较低的算法,以节省内存资源。有时需要在时间空间上做取舍,空间复杂度较低的算法时间复杂度可能较高。总之,选择合适的算法,既要考虑到运行时间,也要考虑到空间需求。两者缺一不可,共同决定一个算法的性能.
-
衡量算法好坏的重要标准有两个。
时间复杂度:
是用于度量算法或函数的运行时间的一种标准。它表示算法的运行时间随问题规模的增长的方式。
主要有以下几个时间复杂度:- O(1):常数复杂度。算法运行时间不随问题规模的增加而增加。比如访问数组中的某个元素。
- O(logN):对数复杂度。算法运行时间随问题规模的增加而缓慢增加。比如二分查找算法。
- O(N):线性复杂度。算法运行时间跟问题规模成正比。比如遍历数组或链表。
- O(NlogN):线性对数复杂度。算法中包含O(logN)的循环,并且循环内还有O(N)的操作。比如快速排序的时间复杂度。
- O(N2):平方复杂度。算法运行时间随问题规模的增加而快速增加。比如简单选择排序。
- O(2^N):指数复杂度。算法运行时间随问题规模的增加而迅速增长。比如递归求解斐波那契数列。
- O(N!):阶乘复杂度。算法运行时间随问题规模的增加而爆炸性增长。这类算法基本上不可使用。
空间复杂度
是用于度量算法或函数所需的存储空间的一种标准。它表示算法的存储空间需求随问题规模的增长的方式。
主要有以下几个空间复杂度:- O(1):常数空间复杂度。算法的空间需求不随问题规模的增加而变化。比如访问数组中的某个元素。
- O(N):线性空间复杂度。空间需求随问题规模的增加而线性增长。比如一个长度为N的数组。
- O(NlogN):线性对数空间复杂度。空间需求随问题规模的增加而快速增长。比如归并排序需要的额外空间。
- O(N2):平方空间复杂度。空间需求随问题规模的增加而平方增长。某些需要生成N*N大小矩阵或表的算法。
- O(2^N):指数空间复杂度。空间需求随问题规模的增加而迅速爆炸。一些深度递归算法会达到这类空间复杂度。
- O(N!):阶乘空间复杂度。空间需求随问题规模的增加而爆炸性增长。这类算法基本上不可使用。
数据结构
- 数据结构,对应的英文单词是data structure ,是数据的组织、管理和存储格式,其使用目的是为了高效地访问和修改数据。
逻辑结构
数据的逻辑结构是对数据之间关系的描述,如顺序关系,隶属关系等,有时就把逻辑结构简称为数据结构,分为以下四种:
1、集合结构:集合结构的集合中任何两个数据元素之间都没有逻辑关系,组织形式松散。
2、线性结构:数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。
3、树状结构:树状结构是一个或多个节点的有限集合。
4、网络结构:网络结构是指通信系统的整体设计,它为网络硬件、软件、协议、存取控制和拓扑提供标准。元素存在多对多的相互关系。
存储结构
一个数据结构在计算机中的表示(又称映像)称为存储结构。
可分为顺序存储和非顺序存储(或链式存储):
- 顺序存储
顺序存储的内存地址一定是连续的;
存储密度大;
比链式节约空间;
支持随机存取,方便操作;
适用于频繁查询时使用。
- 链式存储
内存地址不一定连续;
因为链式结构每一个节点都有一个指针存储域,比较占空间;
在插入和删除上比顺序方便;
适用于频繁插入、删除、更新元素时使用。
- 散列存储
散列存储,又称哈希(Hash)存储,由节点的关键码值决定节点的存储地址。用散列函数确定数据元素的存储位置与关键码之间的对应关系
逻辑结构分类
- 结构分类
-
线性结构
-
线性结构是最简单的数据结构,包括数组、链表,以及由它们衍生出来的栈、队列、哈希表。
-
数组
内存中物理存储结构
- 数组是由有限个相同类型的变量所组成的有序集合,它的物理存储方式是顺序存储,访问方式是随机访问。利用下标查找数组元素的时间复杂度是O (1),中间插入、删除数组元素的时间复杂度是O (n )。
-
链表
内存中物理存储结构
- 链表是一种链式数据结构,由若干节点组成,每个节点包含指向下一节点的指针。链表的物理存储方式是随机存储,访问方式是顺序访问。查找链表节点的时间复杂度是O (n ),中间插入、删除节点的时间复杂度是O (1)。
-
栈
- 栈是一种
线性逻辑结构
,可以用数组实现,也可以用链表实现。栈包含入栈和出栈操作,遵循先入后出的原则(FILO)。
- 栈是一种
-
队列
- 队列也是一种
线性逻辑结构
,可以用数组实现,也可以用链表实现。队列包含入队和出队操作,遵循先入先出的原则(FIFO)。
- 队列也是一种
-
-
散列表
- 散列表也叫哈希表,
哈希表通常不是简单的线性结构,而是采用数组+链表的组合实现的。
具体来说,哈希表中的元素通常是这样存储的:
- 使用一个数组来存储元素。
- 通过哈希函数将元素映射到数组的不同位置(也称为桶)。
- 每个桶对应的数组位置上会有一个链表,用来存储哈希值相同的元素。
- 在查找时,先通过哈希函数映射到对应的桶,再在链表上顺序查找。
可以看出,哈希表利用了数组的快速查找特点,同时通过链表解决了哈希冲突问题。
哈希表是存储Key-Value映射的集合。对于某一个Key,散列表可以在接近O (1)的时间内进行读写操作。散列表通过哈希函数实现Key和数组下标的转换,通过开放寻址法和链表法来解决哈希冲突。
- 散列表也叫哈希表,
-
树结构
-
树是相对复杂的数据结构,其中比较有代表性的是二叉树,由它又衍生出了二叉堆之类的数据结构。
-
树
- 树是n 个节点的有限集,有且仅有一个特定的称为根的节点。当n >1时,其余节点可分为m个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
-
二叉树
-
二叉树是树的一种特殊形式,每一个节点最多有两个孩子节点。 满二叉树要求所有分支都是满的;而完全二叉树只需保证最后一个节点之前的节点都齐全即可。
-
满二叉树
- 一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。
-
完全二叉树
- 对一个有n 个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n 。如果这个树所有节点和同样深度的满二叉树的编号为从1到n 的节点位置相同,则这个二叉树为完全二叉树。
-
二叉树的遍历方式有几种
根据遍历节点之间的关系,可以分为前序遍历、中序遍历、后序遍历、层序遍历这4种方式;从更宏观的角度划分,可以划分为深度优先遍历和广度优先遍历两大类。
-
-
二叉堆
-
二叉堆是一种特殊的完全二叉树,分为最大堆和最小堆。
-
最大堆,任何一个父节点的值,都大于或等于它左、右孩子节点的值。
-
最小堆,任何一个父节点的值,都小于或等于它左、右孩子节点的值。
- 优先队列
- 最大优先队列
- 在最大优先队列中,无论入队顺序如何,当前最大的元素都会优先出队,这是基于最大堆实现的。
- 最小优先队列
- 在最小优先队列中,无论入队顺序如何,当前最小的元素都会优先出队,这是基于最小堆实现的。
-
-
-
图
- 图是更为复杂的数据结构,因为在图中会呈现出多对多的关联关系。
-
有了数据结构这个舞台,算法才可以尽情舞蹈。在解决问题时,不同的算法会选用不同的数据结构。例如排序算法中的堆排序,利用的就是二叉堆这样一种数据结构;
常见数据结构的时间复杂度和空间复杂度如下:
- 数组:时间复杂度O(1)用于访问元素,O(n)用于插入和删除元素;空间复杂度O(n)。
- 链表:时间复杂度O(n)用于访问、插入和删除元素;空间复杂度O(n)。
- 栈:时间复杂度O(1)用于压栈、弹栈和访问栈顶元素;空间复杂度O(n)。
- 队列:时间复杂度O(1)用于入队、出队和访问队头元素;空间复杂度O(n)。
- 二分查找树:时间复杂度O(logn)用于插入、删除和访问元素(平均),最差O(n);空间复杂度O(n)。
- AVL树:时间复杂度O(logn)用于插入、删除和访问元素(平衡后);空间复杂度O(n)。
- 红黑树:时间复杂度O(logn)用于插入、删除和访问元素;空间复杂度O(n)。
- B树:时间复杂度O(logn)用于插入、删除和访问元素;空间复杂度O(n)。
- 哈希表:平均时间复杂度O(1)用于插入、删除和访问元素,最差O(n);空间复杂度O(n)。
- 堆:时间复杂度O(logn)用于插入和删除元素,O(1)用于访问最大/小元素;空间复杂度O(n)。
从上可见,不同数据结构在时间和空间方面的性能各有优势,需要根据具体问题选择合适的数据结构。一般来说,时间复杂度较低的数据结构,空间复杂度较高;空间复杂度较低的数据结构,时间复杂度较高。
所以,在实际使用中需要权衡数据结构的时间复杂度和空间复杂度,选择最优解。这也是我们学习算法和数据结构的主要目的之一。
综上,理解不同数据结构在时间和空间效率方面的差异非常重要。这有助于我们在实际编码和问题求解中选择最优的数据结构。
根据数据量的不同数量级,最优的数据结构也不同:
- <= 10 的数据量:可以直接使用数组来存储数据,时间复杂度O(1),空间复杂度O(n)。
- 10 ~ 100 的数据量:可以使用链表来存储数据,时间复杂度O(n),空间复杂度O(n)。
- 100 ~ 1000 的数据量:可以使用二分搜索树来存储数据,平均时间复杂度O(logn)用于访问、插入和删除元素,空间复杂度O(n)。
- 1000 ~ 10000 的数据量:推荐使用AVL树或红黑树来存储数据。它们都是自平衡二叉搜索树,时间复杂度O(logn),空间复杂度O(n)。
- 大于10000 的数据量:
- 如果需要快速查找,可以使用哈希表,时间复杂度O(1),空间复杂度O(n)。
- 如果需要快速访问最大值或最小值,可以使用堆,时间复杂度O(logn)用于插入和删除,O(1)用于获取最大/小值,空间复杂度O(n)。
- 如果需要范围查询或排序,可以使用B树,时间复杂度O(logn)用于插入、删除和访问元素,空间复杂度O(n)。
- 如果是海量数据的缓存或查找,可以使用布隆过滤器,空间复杂度O(n),查询时间复杂度O(1),但有一定误判率。
数据结构的选择需要根据数据量的数量级来决定,在小数据量下选择简单的数据结构,在大数据量下选择较复杂但时间效率较高的数据结构,这是我们在实际编码中常用的思路。
- 当数据量比较小时,可以选择空间换时间的策略,使用空间复杂度较高的数组和链表。
- 当数据量逐渐增大时,需要更复杂的数据结构来保证较低的时间复杂度,这时空间复杂度也会增大。
根据数据量的不同数量级,最优的排序算法也不同:
- <= 10 的数据量:可以直接使用插入排序或选择排序,时间复杂度O(n2),空间复杂度O(1)。
- 10 ~ 100 的数据量:可以使用插入排序或希尔排序,时间复杂度O(n2)和O(nlogn),空间复杂度O(1)。
- 100 ~ 1000 的数据量:可以使用快速排序,平均时间复杂度O(nlogn),最差O(n2),空间复杂度O(logn)。
- 1000 ~ 10000 的数据量:推荐使用归并排序,时间复杂度O(nlogn),空间复杂度O(n)。
- 大于10000 的数据量:
- 如果是大数据的排序,可以使用外排序算法,比如:多路归并排序。将数据分块,内排序后归并。时间复杂度O(nlogm),m是块的数量。
- 如果是海量数据,可以使用基数排序。时间复杂度O(nk),k是整数的位数。
- 如果是需要稳定的排序,推荐归并排序。时间复杂度O(nlogn)。
- 如果内存足够,快速排序仍然是一个不错的选择。平均时间复杂度O(nlogn)。但最差时间复杂度O(n2),需要注意。
选择排序算法也需要根据数据量的数量级来决定。这是我们在编程中常用的一个思路:小数据量使用简单算法,大数据量使用复杂但更高效的算法。
- 当数据较小时,可以选择简单的排序算法,空间复杂度和时间复杂度较低。
- 当数据量较大时,需要更高效的排序算法来保证合理的时间复杂度,这时空间复杂度也会较高。
在不同的数据量下,时间复杂度和空间复杂度常常需要进行取舍。这里举几个例子:
- 当数据量较小(<=1000)时,可以选择空间复杂度较高的算法,来换取时间复杂度较低的算法。例如选择插入排序,时间复杂度O(n2),空间复杂度O(1)。而不选择归并排序,时间复杂度O(nlogn),空间复杂度O(n)。
- 当数据量中等(1000~100000)时,需要在时间复杂度和空间复杂度之间权衡。例如快速排序,平均时间复杂度O(nlogn),最差O(n2),空间复杂度O(logn);堆排序,时间复杂度O(nlogn),空间复杂度O(1)。根据具体问题选择其中一种。
- 当数据量较大(>100000)时,需要选择时间复杂度较优的算法,空间复杂度较高也在可接受范围。例如归并排序,时间复杂度O(nlogn),空间复杂度O(n);基数排序,时间复杂度O(nk),空间复杂度O(n+k)。这时候选择空间复杂度较低的插入排序或选择排序,时间复杂度会达到O(n2),不可取。
- 当数据量很大(>1000000)时,需要使用更高效的算法和数据结构。例如B树,时间复杂度O(logn),空间复杂度O(n);红黑树,时间复杂度O(logn),空间复杂度O(n)。如果此时使用链表,时间复杂度O(n),空间复杂度O(n),效率会很低。
- 超大数据量(>10000000)时,需要使用大数据技术。例如MapReduce,可处理TB级别的数据;大容量NoSQL数据库,如HBase、Redis;分布式文件系统如HDFS等。这时候空间和时间复杂度都会较高,但由于使用集群技术,单机的复杂度会较低。
所以,可以看出数据量的增大,时间复杂度和空间复杂度的取舍也在不断变化。理解这一点,有助于我们在实际问题中选择最优的算法和数据结构。这也是学习数据结构与算法的最主要的目的之一。
总之,在不同的数据量下,需要综合考虑时间复杂度和空间复杂度,选择最优解。这是我们提高编程技能的关键所在。
C++标准库中的头文件实现了常用的排序算法。主要有:
- std::sort():实现快速排序算法,对序列进行排序。平均时间复杂度O(nlogn),空间复杂度O(logn)。
- std::stable_sort():实现归并排序,对序列进行稳定排序。时间复杂度O(nlogn),空间复杂度O(n)。
- std::partial_sort():对序列的前n个元素进行排序。时间复杂度O(nlogn),空间复杂度O(logn)。
- std::nth_element():重新排列序列,使得第n个元素在序列的正确位置。时间复杂度O(n),空间复杂度O(1)。
- std::mergesort():实现归并排序,对序列进行排序。和std::stable_sort()功能相同,时间复杂度O(nlogn),空间复杂度O(n)。
- std::heapsort():实现堆排序,对序列进行排序。时间复杂度O(nlogn),空间复杂度O(1)。
- std::is_sorted():检查序列是否已排序,时间复杂度O(n),空间复杂度O(1)。
此外,STL容器如deque、list、vector等也分别重载了sort()成员函数,使得容器内元素可以通过sort()成员函数实现排序,C++标准库提供了丰富的排序算法函数,可以满足我们的排序需求。我们在编码时,可以直接调用std::sort()或其他排序算法,无需自己实现,这简化了编码难度,提高了开发效率。
例如:
#include <algorithm>
#include <vector>
int main() {
std::vector<int> nums {3, 1, 4, 2};
std::sort(nums.begin(), nums.end()); // 使用std::sort排序
// nums is now {1, 2, 3, 4}
}
红黑树既是一种数据结构,也实现了排序功能,但归并排序仅是一个排序算法,不是数据结构。
更具体地说:
- 红黑树是一种自平衡二叉搜索树,它本身就是一种数据结构,可以用于构建各种基于键值或元素的集合和映射。同时,红黑树也保证了数据的排序,使得数据按键值或元素的升序或降序排列。
- 归并排序是一种比较基于比较的排序算法,可以对数组或链表中的数据进行排序。但归并排序算法结束后,只得到一个排好序的序列,无法直接用于构建集合或映射。还需要结合数组、链表等数据结构才能组成一个容器。
所以,归并排序只负责排序功能,红黑树既实现数据结构,也保证内部数据的排序。这是两者的主要差异。
具体来说:
- 红黑树:
- 数据结构:二叉搜索树,支持快速查找、插入和删除。
- 排序方式:键值或元素自动升序或降序排列。
- 时间复杂度:O(logn)。
- 空间复杂度:O(n)。
- 归并排序:
- 数据结构:无,仅算法,需要结合数组或链表实现数据容器。
- 排序方式:稳定的、升序或降序排列。
- 时间复杂度:O(nlogn)。
- 空间复杂度:O(n)。
所以综上,我们可以看出红黑树比归并排序的功能更加强大,不仅包括排序,还包含数据结构。这也是STL选择红黑树而不是归并排序实现
算法
排序算法
主流的排序算法可以分为3大类。
排序算法还可以根据其稳定性,划分为稳定排序 和不稳定排序 。
即如果值相同的元素在排序后仍然保持着排序前的顺序,则这样的排序算法是稳定排序;如果值相同的元素在排序后打乱了排序前的顺序,则这样的排序算法是不稳定排序。
- 时间复杂度为O (n 2 )的排序算法
- 冒泡排序 (稳定)
- 选择排序
- 插入排序
- 希尔排序(性能略优于O (n 2 ),但又比不上O (n logn ))
- 时间复杂度为O (n logn )的排序算法
- 快速排序 (不稳定)
- 归并排序
- 堆排序(不稳定)
- 时间复杂度为线性的排序算法
- 计数排序 (稳定)
- 桶排序 (稳定)
- 基数排序
- 最优决策
- 0-1背包问题:
- 时间复杂度:O(n*W)。n为物品数,W为背包容量。需要两重循环遍历所有状态。
- 空间复杂度:O(n*W)。需要二维数组存储最优值。
- 编辑距离:
- 时间复杂度:O(m*n)。m和n分别为两个字符串的长度。需要两重循环遍历所有状态。
- 空间复杂度:O(m*n)。需要二维数组存储最优编辑路径。
- 最长公共子序列:
- 时间复杂度:O(m*n)。m和n分别为两个序列的长度。需要两重循环遍历所有状态。
- 空间复杂度:O(m*n)。需要二维数组存储最长公共子序列长度。
- 单源最短路径(Dijkstra算法):
- 时间复杂度:O(V^2)。V为顶点数,使用邻接矩阵存储图。
- 空间复杂度:O(V)。需要数组存储每个顶点的最短路径长度。
- 矩阵链乘问题:
- 时间复杂度:O(n^3)。n为矩阵链中矩阵个数。需要三重循环遍历所有括号匹配方案。
- 空间复杂度:O(n^2)。需要二维数组存储最优括号匹配方案。
综上,最优决策算法的时间复杂度和空间复杂度较高,一般在O(n2)到O(n3)级别。这是由于最优决策算法需要遍历问题的全部或较多状态,以得出最优解。
所以总体来说,最优决策算法仅适用于小规模和中等规模问题,时间复杂度和空间复杂度较高。对于大规模问题,最优决策算法的性能会较低,这时需要对算法进行改进。
最优决策算法可以通过各种策略得到问题的最优解,这也是该算法的优点。但是算法性能较差,这限制了其适用范围。这也是最优决策算法最大的局限性。
在使用最优决策算法时,需要根据问题的规模选择恰当的解决策略。对于规模较大的问题,可以采取动态规划算法的空间优化技巧,降低空间复杂度。也可以尝试贪心算法或者启发式算法得到近似最优解,以降低时间复杂度。
总之,选择和设计最优决策算法时,需要根据问题的具体要求选择算法策略。只有在问题规模较小且需要精确最优解的情况下,才会选择最优决策算法。过度依赖最优决策算法会造成性能瓶颈,这需要程序员在设计时多加权衡。
运算算法
-
数组算法:主要用于数组元素的排序、搜索、遍历等操作。常见的有冒泡排序、选择排序、插入排序、归并排序、快速排序、二分搜索、线性搜索等。
常见数组算法的时间复杂度和空间复杂度如下:
- 冒泡排序:
- 时间复杂度:O(n^2)。需要n轮比较,每轮n-1次比较。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 选择排序:
- 时间复杂度:O(n^2)。需要n轮选择,每轮遍历n-1次数。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 插入排序:
- 时间复杂度:O(n^2)。需要n轮插入,每轮的插入操作需要遍历插入位置之前的元素。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 归并排序:
- 时间复杂度:O(nlogn)。需要logn轮归并,每轮归并的时间复杂度为O(n)。
- 空间复杂度:O(n)。需要额外空间存储归并中间结果。
- 快速排序:
- 平均时间复杂度:O(nlogn)。pivot的选择导致最差最好情况不同。
- 最差时间复杂度:O(n^2)。极端情况下,每轮只划分出1个元素。
- 空间复杂度:O(logn)。递归调用栈的深度最大为logn。
- 二分搜索:
- 时间复杂度:O(logn)。每轮搜索后,搜索范围缩小一半。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 线性搜索:
- 时间复杂度:O(n)。需要遍历数组一次。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
综上,数组算法的时间复杂度主要集中在O(n),O(nlogn)和O(n^2),其中 O(nlogn)的算法适用于大规模数组,O(n^2)的算法较慢,适用于小规模数组。空间复杂度方面,大多数数组算法只需要O(1)的额外空间,部分算法如归并排序需要O(n)的额外空间。
-
链表算法:主要用于链表元素的排序、搜索、遍历、插入、删除等操作。常见的有链表的插入、删除、遍历等算法。
常见链表算法的时间复杂度和空间复杂度如下:
- 链表插入:
- 时间复杂度:O(1)。只需要修改几个指针,时间复杂度不依赖于链表长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 链表删除:
- 时间复杂度:O(1)。只需要修改几个指针,时间复杂度不依赖于链表长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 链表查询:
- 时间复杂度:O(n)。需要从头节点开始遍历链表,时间复杂度线性依赖于链表长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 链表遍历:
- 时间复杂度:O(n)。需要从头节点开始遍历链表,时间复杂度线性依赖于链表长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 链表反转:
- 时间复杂度:O(n)。需要从头节点开始遍历并反转链表,时间复杂度线性依赖于链表长度。
- 空间复杂度:O(1)。可以就地反转,只需要常数级别的额外空间。
- 链表排序:
- 时间复杂度:O(nlogn)。使用归并排序,需要logn层归并,每层的时间复杂度为O(n)。
- 空间复杂度:O(1)。可以就地排序,只需要常数级别的额外空间。
综上,链表算法的时间复杂度主要集中在O(1),O(n)和O(nlogn),其中插入和删除操作的时间复杂度为O(1),不依赖于链表长度。查询、遍历和反转操作的时间复杂度为O(n),依赖于链表长度。链表排序使用归并排序,时间复杂度为O(nlogn)。空间复杂度方面,大多数链表算法只需要O(1)的额外空间。
所以总体来说,链表插入和删除这类操作比较高效,时间复杂度最低,而查询、遍历和排序等操作的时间复杂度则较高,需要遍历链表,性能较低。这也体现了链表的操作性能特点。
-
字符串算法:主要用于字符串的比较、查找、替换、排序等操作。常见的有KMP算法、Rabin-Karp算法、Levenshtein 距离算法等。
常见字符串算法的时间复杂度和空间复杂度如下:
- KMP算法:
- 时间复杂度:O(n)。需要遍历文本串一次,时间复杂度线性依赖于文本串长度。
- 空间复杂度:O(k)。需要额外空间存储长度为k的next数组,k为模式串长度。
- Rabin-Karp算法:
- 平均时间复杂度:O(n)。使用hash函数,每次只需计算窗口的hash值。
- 最差时间复杂度:O(nm)。当所有的窗口的hash值都匹配时需要逐字符匹配。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- BF算法:
- 时间复杂度:O(nm)。需要遍历文本串和模式串,时间复杂度等于两者长度之积。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- BM算法:
- 时间复杂度:O(n)。需要遍历文本串一次,跳过不匹配字符,时间复杂度线性依赖于文本串长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 字符串匹配:
- 朴素算法时间复杂度:O(nm)。需要双重循环,遍历文本串和模式串。
- KMP算法时间复杂度:O(n)。需要单次遍历文本串。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 字符串替换:
- 时间复杂度:O(n)。需要遍历文本串一次,时间复杂度线性依赖于文本串长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
综上,字符串算法的时间复杂度较广,从O(n)到O(nm),其中KMP、BM和Rabin-Karp算法的平均时间复杂度为O(n),为最优字符串匹配算法。朴素字符串匹配算法和BF算法的时间复杂度较高,为O(nm)。空间复杂度方面,大多数字符串算法只需要O(1)的额外空间,KMP算法需要O(k)的额外空间存储next数组。
所以总体来说,高效的字符串算法应优先选择KMP、BM和Rabin-Karp算法,这三种算法的时间复杂度较低,且适用于大规模数据集,而朴素和BF算法的时间复杂度过高,不太实用。空间复杂度方面,应优先选择常数空间复杂度的算法,只有在无法避免的情况下才选择需要额外空间的算法。
-
查找算法:主要用于在一组元素中搜索特定元素或最大/最小元素。常见的有二分搜索、Hash查找、BFS、DFS等。
常见查找算法的时间复杂度和空间复杂度如下:- 二分查找:
- 时间复杂度:O(logn)。每次搜索后,搜索范围缩小一半,时间复杂度对数依赖于数组长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 线性查找:
- 时间复杂度:O(n)。需要遍历数组一次,时间复杂度线性依赖于数组长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- Hash查找:
- 平均时间复杂度:O(1)。使用hash函数映射到桶中,每次只需计算hash值。
- 最差时间复杂度:O(n)。当hash函数映射全部数据到同一桶时,需遍历桶。
- 空间复杂度:O(n)。需要额外空间存储hash表。
- BFS:
- 时间复杂度:O(V+E),V为顶点数,E为边数。需要遍历所有顶点和边。
- 空间复杂度:O(V)。需要队列额外空间存储顶点。
- DFS:
- 时间复杂度:O(V+E),V为顶点数,E为边数。需要遍历所有顶点和边。
- 空间复杂度:O(V)。需要栈额外空间存储顶点。
- 最大值查找:
- 时间复杂度:O(n)。需要遍历数组一次,时间复杂度线性依赖于数组长度。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
综上,查找算法的时间复杂度从O(1)到O(n),其中二分查找和Hash查找的平均时间复杂度最低,为O(logn)和O(1),适用于大规模数据集。而线性查找的时间复杂度最高,为O(n),仅适用于小规模数据集。空间复杂度方面,大多数查找算法需要O(1)的额外空间,Hash查找和DFS、BFS需要O(n)的额外空间进行查找。
所以总体来说,高效的查找算法应选择二分查找、Hash查找和BFS、DFS。这些算法的平均时间复杂度较低,且空间复杂度也可控,适用于大规模数据集。而线性查找的时间复杂度过高,不太实用。空间复杂度方面,应根据实际情况选择合适的算法,平衡时间复杂度和空间复杂度。
-
图算法:主要用于图结构中节点和路径的搜索、遍历等操作。常见的有DFS、BFS、Dijkstra算法、Bellman-Ford算法、Floyd算法等。
常见图算法的时间复杂度和空间复杂度如下:
- DFS:
- 时间复杂度:O(V+E),V为顶点数,E为边数。需要遍历所有顶点和边。
- 空间复杂度:O(V)。需要栈额外空间存储顶点。
- BFS:
- 时间复杂度:O(V+E),V为顶点数,E为边数。需要遍历所有顶点和边。
- 空间复杂度:O(V)。需要队列额外空间存储顶点。
- Prim算法:
- 时间复杂度:O(V^2)。使用邻接矩阵,需遍历所有顶点和边。
- 空间复杂度:O(V)。需要队列额外空间存储顶点。
- Kruskal算法:
- 时间复杂度:O(ElogE)。需要排序所有边,E为边数。
- 空间复杂度:O(E)。需要额外空间存储所有边。
- Dijkstra算法:
- 时间复杂度:O(V^2)。使用邻接矩阵,需遍历所有顶点和边。
- 空间复杂度:O(V)。需要优先队列额外空间存储顶点。
- Bellman-Ford算法:
- 时间复杂度:O(V*E)。需遍历所有顶点V次,每次遍历所有边。
- 空间复杂度:O(V)。需要额外空间存储顶点的数据。
- Floyd算法:
- 时间复杂度:O(V^3)。需要遍历邻接矩阵V次,每次遍历V*V个元素。
- 空间复杂度:O(V^2)。需要额外空间存储邻接矩阵。
综上,图算法的时间复杂度较广,从O(V+E)到O(V3),其中DFS和BFS时间复杂度较低,为O(V+E),适用于稠密图。Dijkstra和Bellman-Ford算法时间复杂度为O(V2),适用于稀疏图。Floyd算法时间复杂度最高,为O(V3),仅适用于小规模图。空间复杂度主要集中在O(V)和O(E),部分算法需要O(V2)的额外空间。
所以总体来说,高效的图算法应选择DFS、BFS、Dijkstra和Bellman-Ford算法。这些算法时间复杂度较低,且空间复杂度可控,适用于大规模图。而Floyd算法时间复杂度过高,仅适用于小规模图。空间复杂度方面,应根据算法和输入图的性质选择合适的算法,平衡时间复杂度和空间复杂度。
-
动态规划:主要用于通过组合提前计算出的子问题的解来解决复杂问题。常见的有斐波那契数列、0-1背包问题、最长公共子序列等。
动态规划算法的时间复杂度和空间复杂度与具体问题相关,无法给出统一的复杂度。这里举几个典型例子:- 斐波那契数列:
- 时间复杂度:O(n)。需要计算前n个斐波那契数。
- 空间复杂度:O(n)。需要存储n个斐波那契数。
- 0-1背包问题:
- 时间复杂度:O(n*W)。n为物品数,W为背包容量。需要两重循环遍历所有状态。
- 空间复杂度:O(n*W)。需要二维数组存储所有状态的值。
- 最长公共子序列:
- 时间复杂度:O(m*n)。m和n分别为两个序列的长度。需要两重循环遍历所有状态。
- 空间复杂度:O(m*n)。需要二维数组存储所有状态的值。
- 编辑距离:
- 时间复杂度:O(m*n)。m和n分别为两个字符串的长度。需要两重循环遍历所有状态。
- 空间复杂度:O(m*n)。需要二维数组存储所有状态的值。
- 最大子数组和:
- 时间复杂度:O(n)。需要遍历数组一次。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
综上,动态规划算法的时间复杂度和空间复杂度与问题的规模和状态数相关。对于二维状态空间的问题,时间复杂度和空间复杂度一般为O(n^2)级别。对于一维状态空间的问题,时间复杂度和空间复杂度可以达到O(n)级别。
所以总体来说,动态规划算法适用于规模较小的问题,时间复杂度和空间复杂度较高。对于大规模数据集,动态规划算法的性能会较低,这时可以考虑使用其他算法,或在动态规划算法的基础上进行优化,降低时间复杂度和空间复杂度。
空间复杂度方面,应尽量选择只需要常数级或线性空间的动态规划算法。二维空间的动态规划算法空间复杂度较高,仅适用于小规模问题。可以考虑使用一维数组进行空间优化。
总之,选择和设计动态规划算法时,需要根据时间复杂度和空间复杂度的要求选择恰当的解决方案。平衡算法性能和问题的规模,这是动态规划算法的重要设计思想。
-
分治算法:主要用于通过递归地将问题分成子问题来解决问题。常见的有快速排序、归并排序、二叉搜索树等。
分治算法的时间复杂度和空间复杂度与具体问题相关,无法给出统一的复杂度。这里举几个典型例子:- 二分查找:
- 时间复杂度:O(logn)。每次搜索后,搜索范围缩小一半。
- 空间复杂度:O(logn)。需要递归调用栈空间。
- 快速排序:
- 平均时间复杂度:O(nlogn)。pivot的选择会影响最差最好情况。
- 最差时间复杂度:O(n^2)。极端情况下,每轮只划分出1个元素。
- 空间复杂度:O(logn)。需要递归调用栈空间。
- 合并排序:
- 时间复杂度:O(nlogn)。需要logn轮归并,每轮归并时间复杂度为O(n)。
- 空间复杂度:O(n)。需要额外空间存储归并过程中的临时结果。
- 斐波那契数列:
- 时间复杂度:O(2^n)。存在重叠子问题,使用记忆化搜索可以优化到O(n)。
- 空间复杂度:O(n)。需要递归调用栈空间,使用记忆化搜索可以优化到O(1)。
- 汉诺塔问题:
- 时间复杂度:O(2n)。需要移动2n-1个盘子。
- 空间复杂度:O(n)。需要递归调用栈空间。
综上,分治算法的时间复杂度和空间复杂度与问题的规模相关。对于存在重叠子问题的问题,如果没有优化,时间复杂度和空间复杂度较高,为O(2^n)级别。对于不存在重叠子问题的问题,时间复杂度可以达到O(nlogn)级别,空间复杂度可以达到O(logn)级别。
所以总体来说,分治算法适用于规模较小的问题,时间复杂度和空间复杂度较高。对于存在重叠子问题的问题,需要使用记忆化搜索等技术进行优化,以降低时间复杂度和空间复杂度。
空间复杂度方面,应选择只需要常数级或O(logn)空间的分治算法。O(n)空间的分治算法空间复杂度较高,仅适用于小规模问题。
总之,选择和设计分治算法时,需要根据时间复杂度和空间复杂度的要求选择恰当的解决方案。对存在重叠子问题的问题,一定要使用记忆化技术之类的优化方法。这是分治算法的重要设计思想。
-
回溯算法:主要用于通过探索所有可能的候选解,找到最优解。常见的有八皇后问题、骑士周游问题、图的着色问题等。
回溯算法的时间复杂度和空间复杂度与具体问题相关,无法给出统一的复杂度。这里举几个典型例子:
- 八皇后问题:
- 时间复杂度:O(n!)。需要尝试所有的棋盘布局。
- 空间复杂度:O(n)。需要递归调用栈空间。
- 0-1背包问题:
- 时间复杂度:O(2^n)。需要尝试所有的物品布局。
- 空间复杂度:O(n)。需要递归调用栈空间。
- 全排列:
- 时间复杂度:O(n*k)。n为数组长度,k为数组中数字的最大值。需要尝试所有排列。
- 空间复杂度:O(n)。需要递归调用栈空间。
- 字符串的所有排列:
- 时间复杂度:O(n!)。需要尝试所有的字符串排列。
- 空间复杂度:O(n)。需要递归调用栈空间。
- N皇后问题:
- 时间复杂度:O(n!)。需要尝试所有的棋盘布局。
- 空间复杂度:O(n)。需要递归调用栈空间。
综上,回溯算法的时间复杂度较高,一般为O(n!)或O(2^n)级别。空间复杂度较低,为O(n)级别。
所以总体来说,回溯算法仅适用于小规模问题,时间复杂度较高。对于较大规模问题,回溯算法的性能较低,这时应选择其他算法。
回溯算法的优点是简单易实现,可以通过枚举的方式得到问题的所有解。但是当问题规模较大时,会产生较长的递归调用链,空间开销较大。这时可以采用迭代的方式代替递归调用,以节省空间。
总之,选择和设计回溯算法时,需要根据问题的时间复杂度和空间复杂度的要求选择恰当的解决方案。回溯算法仅适用于可以枚举求解的小规模问题,在较大规模问题上性能较差。这需要在设计时进行权衡。
时间复杂度较高也是回溯算法的重要局限,这需要算法设计者在使用回溯算法时要多加留意。可以采取一定的剪枝措施,尽量避免不必要的递归调用,以提高算法性能。这也是运用回溯算法的一个关键技巧。
-
贪心算法:主要用于通过每一步选择最优的操作来解决问题。常见的有最小生成树的Prim算法和Kruskal算法等。
贪心算法的时间复杂度和空间复杂度与具体问题相关,无法给出统一的复杂度。这里举几个典型例子:- 背包问题:
- 时间复杂度:O(nlogn)。需要对物品进行排序,n为物品数。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 最小生成树问题(普里姆算法):
- 时间复杂度:O(V^2)。V为顶点数,使用邻接矩阵存储图。
- 空间复杂度:O(V)。需要存储已选择的顶点集合。
- 区间调度问题:
- 时间复杂度:O(nlogn)。需要对区间进行排序,n为区间数。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- 集合覆盖问题:
- 时间复杂度:O(n)。n为元素数,需要遍历所有元素。
- 空间复杂度:O(1)。只需要常数级别的额外空间。
- Huffman编码:
- 时间复杂度:O(nlogn)。需要对字符出现频率进行排序,n为字符数。
- 空间复杂度:O(n)。需要存储Huffman树。
综上,贪心算法的时间复杂度一般为O(nlogn)或O(n)级别,主要取决于是否需要排序。如果需要进行排序,时间复杂度为O(nlogn)级别;如果不需要排序,时间复杂度为O(n)级别。空间复杂度较低,一般为O(1)级别,部分算法需要O(n)的额外空间。
所以总体来说,贪心算法适用于各类规模的问题,时间复杂度较低。空间复杂度也较低,大多数贪心算法只需要常数级别的额外空间,适用性较广。
贪心算法的思想简单,易于实现,运行高效。但是贪心算法得到的结果不一定是最优解,这需要在算法设计时进行论证。这也是设计贪心算法的关键所在。
综上,选择和设计贪心算法时,需要根据问题的时间复杂度和空间复杂度的要求选择恰当的解决方案。同时需要对贪心算法的最优性进行证明,这是贪心算法设计的关键步骤。只有在保证得到最优解的情况下,贪心算法才能够运用。这一点需要算法设计者多加留意。