目录
[1.1 学习感悟](#1.1 学习感悟)
[1.2 数据结构的学习之路(初阶)](#1.2 数据结构的学习之路(初阶))
[2.1 数据结构和算法的关系](#2.1 数据结构和算法的关系)
[2.2 算法的重要性](#2.2 算法的重要性)
[2.3 如何衡量算法的好坏](#2.3 如何衡量算法的好坏)
[3.1 时间复杂度的概念](#3.1 时间复杂度的概念)
[3.2 大O的渐进表示法 O()](#3.2 大O的渐进表示法 O())
[5. 常见的时间复杂度和空间复杂度计算](#5. 常见的时间复杂度和空间复杂度计算)
[5.1 时间复杂度计算](#5.1 时间复杂度计算)
[5.2 空间复杂度计算](#5.2 空间复杂度计算)
1.前言:
1.1 学习感悟
本篇文章作为数据结构的开篇,也是对C语言部分结尾的继承,属于是承上启下,数据结构中目前使用的编译器还是Visual Studio 2022,在正式讲之前,说点篇外话,继上一篇动态内存的管理后,很久没有更新是因为博主的电脑出了问题,维修了小半个月,再者本人对于数据结构的学习周期比较长,数据结构的难度也相对于C语言高了一个层次,但是我觉得只要认真努力的学下去,武装自己,然后做对题目,那一刻,先前再怎么苦也值得!当你做对题目,在网站上提交你的代码通过的时候,肯定会有满满的成就感,不是吗?
1.2 数据结构的学习之路(初阶)
在数据结构的具体学习中,大致分为有:
- 对于时间复杂度和空间复杂度的一个基本了解
- 顺序表
- 链表
- 栈
- 队列
- 二叉树
- 排序
这7大类,数据结构基础篇主要内容就是这些,包括想要考研的朋友们,王道计算机408中的数据结构中,是线性表(顺序表,链表),栈,队列,数组,串,二叉树,查找,排序。大部分都能包含。如果想要考研的同学看到文章也有一丝丝的收获,这对我也是很大的鼓舞。在这里,我也不能说数据结构怎么学能学好,我也是在学习的过程中,但是经过了一段时间的初步学习,我认为有三点必须要做到,这样学数据结构不能说万无一失,只能说会学的很扎实,思路很顺畅,时间相对花的少很多。
- 多画图 ,对于每个内容的学习都尽量画图,它能帮助你很好的构思代码的整体流程,甚至能够潜在地培养自己的算法素质,算法能力。
- 多调试 ,在C语言学习过程中,应该都是能够学会如何基本的调试,这里就不过多赘述了,想了解基本方法请移步C语言初阶回顾-CSDN博客中有过相关介绍,在初步学习的时候可能会不重视调试,觉得我去百度一下,或者把自己的代码截图,复制给AI工具,(文心一言,deepseek)让着帮忙解析就好了,这是个方法,但不是最好的方法 ,ai会让你养成惰性,如果你学会了怎么去调试,那是自己成长的一步,如果再进一步,大部分由于疏忽导致的错误能够通过你亲手F10,F5,F11出来解决,那是一件多么舒服的事情,你可以通过自己而不是外部工具去解决,久而久之,你会爱上调试的,会出了bug第一时间不是想AI帮我一下,而是下意识F10,调试是一个基本功!在数据结构的学习中,调试是必不可少的,也是和画图同等重要的能力。
- 了解函数栈帧空间,这在二叉树的学习中,是一个门槛,如果了解了函数栈帧的相关知识,学起来将事半功倍。像博主本人在学习的过程中,把那一块知识淡忘了,就会重新去温故一下,看看曾经写过的文章,从而知新。忘记正常,记起来就好了。
进入正题
2.什么是数据结构和算法
在互联网中,各大社交平台都在讨论数据结构的时候,总会和算法紧密相连,而且我看到过很多互联网大厂和中厂,算法岗位的学历起步是要硕士。算法要求之高,那么到底什么是数据结构,什么又是算法。这两者为什么紧密相连?
2.1 数据结构和算法的关系
数据结构,在博主的看来,就是电脑在内存中对存储数据有不同的结构方式 。而算法,就是解决一个或者多个问题的一系列计算步骤,用来把输入的数据转化为自己想要结果。所以算法我认为不单单是一种解决问题的方式,更多的还是思维。
而算法和数据结构的关系可以理解为互相的关系,数据结构给算法提供发挥作用的空间,算法为数据结构进行作用。没有数据结构,算法发挥不出它的作用,没有算法,数据结构就没有问题的解决这一步,两者共生。我在网络上也搜到我认为很不错的观点,大家可以看看:
2.2 算法的重要性

2.3 如何衡量算法的好坏
算法在对问题解决的过程中,或者说在计算机中运行的时候,是要消耗内存资源和空间的。以及要花费一定时间。那这里其实已经指出了衡量算法优劣的两个标准,这也是面试官常考的点:
1.时间复杂度
2.空间复杂度
不知道你们有没有发现一个现象,或者看到这里想起来一些东西。在c语言的递归中,比如斐波那契数列,阶乘递归,一开始数字小还好,但当你输出一个较大的数字,斐波那契数列的第40,45,50,越往后计算机算的就越慢,而且打开任务管理器观察,运行该程序所占用的内存百分比会只多不少。 我以斐波那契数列来实测一下。
cpp
#include<stdio.h>
int Fib(int n)
{
if (2>=n)
{
return 1;
}
else
{
return Fib(n - 2) + Fib(n - 1);
}
}
int main()
{
int a = 0;
scanf("%d",&a);
int ret =Fib(a);
printf("%d\n",ret);
return 0;
}
输入的值为30时:速度很快,瞬间出来。
时间复杂实测
输入值为40时: 出来了,过了5秒,光标在闪动(由于博主尝试录屏,但是无法录制到cmd框,就不能放出视频演示了,手机录制效果不好,大家自行尝试)


输入的值为45时,大概过了半分钟,才出结果,数字也确实非常大。

说明用递归这个算法来解决斐波那契数列,数字大了,并不好,用for循环,会更好。计算时间就表示时间复杂度,内存资源的消耗就表示空间复杂度。那也不能每次都上机实测,所以就有了这两个概念。
3.时间复杂度
3.1 时间复杂度的概念
算法的时间复杂度是程序基本操作的执行次数,就是一个基本表达式,它在程序中运行了多少次,引入一个案例来讲:
cpp
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
count++;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
count++;
}
int A = 10;
while (A--)
{
count++;
}
上述过程中,在第一个for循环中,嵌套了一个for循环,那么在里面的执行次数,以未知数x来表示,就是,第三个for循环是单独的,是2
,第三个就是常数10了,总次数是
,但是真心要算出来吗,未知数小还好,x=1001,2345,54233这些复杂的数字,算起来未免过于繁琐。 实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数。那么这里我们使用大O的渐进表示法。
3.2 大O的渐进表示法 O()
在我的笔记中是这样的记载的:
- 用常数1取代运行时间中的加法常数
- 保留最高阶项即可
- 最高阶次方不是1的话,把这个项的相乘系数删了。
所以上面的表达式去掉常数,去掉非最高阶,就剩个了,那么规范的纠正我的错误,我刚刚是用数学表达式来写,正确的应该是把x换成N,所以是
。
通过上面大O的渐进表示法去掉了那些对结果影响不大的项,表示出了执行次数。另外有些算法的时间复杂度存在最好、平均和最坏情况:你选择哪个?
这里举个生活中的例子就明白了,你和你最好的朋友聚餐,或者工作有个重要的客户要约见,然而你手中的活还没干完,对方给你了下午5点,晚上7点和8点见面,你会选择什么时间。按理来说容错率最高的是8点吧,5点,万一还没忙完呢,7点,万一堵车呢,最迟的时间应该是最好的。你提前到了,那给人印象很好,卡点是正常,迟到就给人不好印象了。这就是最好,折中和最坏预期结果。为了心理预期,我们往往会选择最迟的点。这样很好理解了吧
在算法中,在一个长度为N数组中搜索一个数据x:
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
时间复杂度应该是。
4.空间复杂度
空间复杂度也是数学表达式,是算法在运行过程中占用内存空间大小的多少,但是说实话真去测算消耗了多少空间?不,算的是变量的个数,也是大O的渐进表示法 O(),依旧举个例子说明,用斐波那契数列来举例子:
cpp
#include<stdio.h>
int Fib(int n)
{
if (2>=n)
{
return 1;
}
else
{
return Fib(n - 2) + Fib(n - 1);
}
}
计算有几个变量,也就是开辟了几个空间,由于n是不知道的,所以开辟了n个空间,空间复杂度为。
5. 常见的时间复杂度和空间复杂度计算
5.1 时间复杂度计算
例1:
cpp
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
例2:
cpp
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
printf("%d\n", count);
}
例3:冒泡排序
cpp
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
例4:二分查找
cpp
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
while (begin < end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return -1;
}
例5:阶乘递归
cpp
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
例6:斐波那契数列
cpp
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
案例分析:
-
案例1,是2N+M,根据大O的渐进表示法,M是个常数,所以是
-
案例2,是M+N次,根据大O的渐进表示法,M,N都是未知的,所以是
-
案例3,其实是个冒泡排序,最好的情况就是它是个有序数组,比如{1,2,3,4,5,6.....,n},那么就只要排n-1次,最坏的情况,就是完全无序,第一趟排,n-1次,第二趟,n-2次,最后一趟,1次。总次数加起来就是(n-1)+(n-2)+(n-3)+.....+1,总和为
,所以按最坏的打算时间复杂度O(N^2)
-
案例4,是个二分查找,最好1次,最坏O(logN)次,时间复杂度为 O(logN) ,logN在算法分析中表示是底数为2,对数为N。ps:二分查找的原理就是缩小一半查找,理想中很好很厉害,但现实中不可靠,因为只适用于有序数组。
-
阶乘算法,最好是1次,最坏是N次,时间复杂度为O(N)。
-
斐波那契数列,操作了2^N次,所以时间复杂度为O(2^N)。图解如下:

不画图是很难看出来的,1生2,2生4,4生8,每一个诞生2个函数要执行 ,所以说画图很重要,不画图心里是很难凭空心算的。实践出真知
5.2 空间复杂度计算
例1:冒泡排序
cpp
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
例2:阶乘递归
cpp
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
例3 :斐波那契数列
cpp
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
案例分析:
-
例1冒泡排序函数内部,看看开辟了几个变量,数变量就好了,end,exchange,i,三个,是常数,所以空间复杂度为O(1);
-
例2递归调用了N次,开辟了N个栈帧,每个栈帧又会开辟另一个空间,而递归不结束,空间不归还,所以产生了FAC(N),FAC(N-1),FAC(N-2)........FAC(1),FAC(0)。这是个嵌套调用!!!,没有销毁掉,所以空间复杂度为O(N),不要以为单个栈帧空间里的变量只有一个N,没有别的变量,你要算N个空间里的变量数量。
-
例3是1分为2,然后2里面的两个递归用的同一块空间,是先调用n,然后调用n-1,一直调用到末尾2,符合if条件,返回了!注意开始归了!2的上一层是3,3下面的2已经调用了,所以会调用另一边1,1调用了,再返回3。然后发现3的1和2都计算过了,往上返回到4,依次往上,因为递归的过程是先递后归,递一边,递完返回再归另外一边,空间复杂度为O(N) ,这个案例注意调试,我是调试了很多次,看了很多次N的变化,画了递归流程图,才看明白,其实讲的再多,不如自己去好好的认真调试一遍,就很nice。

阶乘递归空间复杂度
6.时间复杂度对比图
这是我从网络上找的两张关于时间复杂度随着个数的增大的对比图,(logn理想状态是很好的)其实时间复杂度要比空间复杂度复杂很多,空间复杂度大多都是O(1),O(N),而时间复杂度会多很多,大家也可以网上找找,大部分图都是一样的。
时间复杂度
再者就是常见的时间复杂度参数了

7.结语
本篇文章作为数据结构的开篇,就先讲这么多,时间复杂度和空间复杂度是贯穿整个数据结构的,另外后面的内容顺序表,链表等都涉及到动态开辟空间,所以C语言中的动态内存管理以及函数栈帧空间的学习是很必要的,因为顺序表,和栈都和通讯录的撰写逻辑有关联 。记住一开始说的三点,画图,调试, 了解函数栈帧空间 It is imperative for you to learn!共同进步,就像gitee里说的,希望你的代码有朝一日能够像下面那张图片一样。