前言
最近打算系统性的学习数据结构与算法这一块的内容。在此之前其实对这一块的内容并不陌生,大学中开过这方面的课,但是当时不知道学了有什么用、不知道在哪用、不知道怎么用?也就只是学的勉勉强强,哈哈😄。做了几年开发之后才知道数据结构与算法的重要性,因此打算再系统性的学习(复习)一遍。
数据结构
首先借鉴(抄)一段百度百科上对数据结构的定义。
数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。
从定义上可以看到,数据结构研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系。那么下面就来看看什么是逻辑结构,什么是物理结构。
逻辑结构
数据的逻辑结构,重点在这逻辑二字上。
逻辑结构,顾名思义,数据真正存储的结构可能不是这样的,但是我们可以认为数据就是这样存储的。这种结构是我们人为赋予数据的,至于意图,当然就是为了让我们更加容易理解数据的存储和使用。
有哪些逻辑结构
- 集合结构。集合中任何两个数据元素之间都没有逻辑关系,互不干扰。1对0。
举例:int a = 2, b = 3;那么a和b之间没有任何关系,它们之间就属于集合结构。 - 线性结构。数据元素按照顺序连接成一大串。1对1。
举例:数组、链表、队列等这样线性排列的数据集合。 - 树形结构。树形结构具有分支、层次特性,其形态像现实中的树,因此叫树形结构。1对n,即1对多。
举例:二叉树、B树、B+树等。 - 图形结构。图形结构中的节点按逻辑关系互相缠绕,任何两个节点之间都可以相互连接。m对n,即多对多。
举例:互联网。
物理结构
物理结构。物理就是指现实中的物体,在这里指的是内存或磁盘。
数据的物理结构代表着数据在内存中的结构。物理结构有两种:
- 顺序结构。即数据在内存中占用的内存空间是连续的。例如数组
int a[10];
- 链式结构。即数据在内存中占用的内存空间是不连续的。例如链表。
算法复杂度
算法复杂度分为时间复杂度和空间复杂度
。时间复杂度
是指执行算法所需要的计算工作量;而空间复杂度
是指执行这个算法所需要的内存空间。
时间复杂度
算法的时间复杂度是一个函数,它定性描述算法的运行时间。这是一个代表
算法输入值
的字符串的长度的函数。时间复杂度常用大O符号表述,不包括
这个函数的低阶项
和首项系数
。使用这种方式时,时间复杂度可被称为是渐近的,即考察输入值大小趋近无穷时
的情况。
只说概念可能会比较困惑,下面看几个例子就能理解了。
- 这个算法中,
printf("------%d-------\n",i);
语句一共会执行n次,且n是输入的变量,因此这个算法的时间复杂度就可以表示为O(n)。
void print1(int n) {
// 因为n是变量,所以这个算法的时间复杂度是O(n)
for (int i = 0; i < n; i++) {
printf("------%d-------\n", i);
}
}
- 这个算法中,因为n是一个常量100,因此
printf("++++++%d+++++++\n",i);
语句执行的次数就是固定的100次,100是个常数,因此这个算法的时间复杂度被称为是常数阶,用O(1)表示。
void print2() {
// 因为n是一个常量100,所以这个算法的时间复杂度是O(1)
int n = 100;
for (int i = 0; i < n; i++) {
printf("++++++%d+++++++\n", i);
}
}
- 这个算法中,m和n都是变量 而且在内层for循环 中 执行了两句打印,因此
printf
语句执行的总次数为次。回到上面的概念中看,时间复杂度
不包括
函数的首项系数
。在这里,2就属于首项系数,所以这个算法的时间复杂度就可以表示为O(m*n)。
void print3(int m, int n) {
// m和n都是变量,所以时间复杂度是O(m*n)
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
printf("-----------\n");
printf("+++++++++++\n");
}
}
}
- 这个算法中,我们一眼可能看不出
x *= 2;
一共执行几次,于是我们只好来进行一些简单的计算了。
首先假设一共执行了i
次终于不满足x < n
这个条件了,那么我们就可以得到,当循环结束时,有,取临界条件可得
,进而可得
。即这个算法的时间复杂度是
。不包括
低阶项
,所以-1那个常数阶就不要了。
void print4(int n) {
/*
设循环i次
则到最后一次 2的(i+1)次方 >= n 所以 i = log2 n - 1
*/
int x = 2; // 1次
while (x < n) {
x *= 2; // log以2为底n的对数 - 1次
}
// 所以时间复杂度是 O(log2 n)
}
- 这个算法中,
printf("xxxxxxx\n");
语句一共执行了次,即
次,忽略
低阶项
得到了,再忽略
首项系数
最终得到,所以最终这个算法的时间复杂度就表示为
。
void print5(int n) {
for (int i = 0; i < 2n; i++) { // 2n次
for (int j = 0; j < n - 1; j++) { // n - 1次
printf("xxxxxxx\n"); // 2n(n - 1)次
}
}
// 所以时间复杂度是 O(n2)
}
通过上面这几个例子,我相信你已经能够理解时间复杂度是个什么东西了。但是还是需要提醒一句,并不是时间复杂度大的算法执行的语句的次数就一定多
,具体执行多少次是要和算法输入n
有关的。而算法的时间复杂度
只是用来估算
当输入的n趋于无穷时
算法执行需要时间的多少
。
看到这“估算”二字是不是也就理解了上面例子中为什么要忽略低阶项
和首项系数
,以及第4个例子中 忽略大于直接去等了?
再借鉴一张图来总结一下常见的时间复杂度及其专业术语吧。

下面的a表示常数。
空间复杂度
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。
算法占用的空间的计算方法
分析一个算法所占用的存储空间要从各方面综合考虑。如对于递归算法来说,一般都比较简短,算法本身所占用的存储空间较少,但运行时需要一个附加堆栈,从而占用较多的临时工作单元;若写成非递归算法,一般可能比较长,算法本身占用的存储空间较多,但运行时将可能需要较少的存储单元。
空间复杂度的计算方法
一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。
从上面概念可以看出,计算空间复杂度只考虑算法在运行过程中分配的存储空间的大小,而不考虑算法本身所占的内存空间。
概念看的头疼,还是来用代码解释怎么计算空间复杂度吧。
- 这个算法中,函数的参数有a b c,局部变量有a1 b1 c1,所以总共占用了6块内存空间。但不管几块,是个常数就行,空间复杂度O(1)。
void print1(int a, float b, char c)
{
int a1 = a;
float b1 = b;
char c1 = c;
printf("%d\n%f\n%c\n", a1, b1, c1);
}
- 这个算法中,每次递归都会开辟一块空间,并且在递归期间这一块空间并不会释放,因此这个算法的空间复杂度是O(n)。
void print2(int n) {
if (n > 0) {
int a = n - 1;
print2(a);
printf("%d\n", a);
}
else {
printf("%d\n", 0);
}
}
总结
本篇文章主要记录了两大部分
- 数据的结构。包括
四大逻辑结构
和两大物理结构
。 - 算法的复杂度。包括
时间复杂度
和空间复杂度
及对应的计算方法
。
本文地址https://juejin.cn/post/6844904117798715406