前言:本专栏适用于对C语言,或者其他高级语言有一定的基础的人学习,从0开始详解的介绍有关数据结构的知识,从思路讲解到代码实现。
数据结构对于一名程序员来说是极其极其重要的,大家千万不能忽视对这方面的学习,无论你学的是c++还是c#,Java,python等,数据结构是体现一个人功底的最好方式。
环境:VS2019
语言:C语言
开场白
想必大家都知道高斯求和的故事——计算1+2+3+4…+100
这个问题对大家来说已经很简单了,学完一门高级语言之后我们可以有多种方法来实现这个算法。
第一种想到的方法可能是从循环1到100,依次相加,既能得出结果
int sum = 0;
for(int i = 0; i <= 100; ++i)
{
sum += i;
}
第二种方法就是高斯想出来的方法
看到这里我惊呼到!不愧是高斯啊,牛逼!
int sum = 0;
int n = 100;
sum = (1+n) * n / 2;
那么看到这里,我们思考一个问题,上面的两种方法中,要你来选的话,你选择哪一种呢?
在我们没学数据结构这门课之前,我们可能不会思考这个问题,我们更关注的是如何实现一个算法,而数据结构其实更讲究的是效率!
数据结构是干嘛的?
数据结构是干嘛的?
官方一点讲就是:数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素集合。 说的更直白一点就是更有效的对数据进行处理,不再单一去关注如何实现一个算法,而更考究算法的高效性
哪么我们怎么去衡量一个算法的好坏(即效率的高低)呢?,计算机的前辈们就给出了两种方法,时间复杂度,空间复杂度。
时间复杂度
关于时间复杂度的定义,基本上可以这样说:算法中的基本操作的执行次数,为算法的时间复杂度。
大家注意,这里讲的时间复杂度并不是代码实际在机器上的运行时间,因为同一段代码在不同的机器上可能所需要的时间并不相同,这取决于计算机的硬件系统的物理工艺,所以我们在说时间复杂度的时候,一般是这代码的基本操作次数,举个列子来看。
int sum = 0;
for(int i = 0; i <= n; ++i)
{
}
这里int sum = 0;即是基本的操作次数,这行代码只需要执行一次。
for循环的int i = 0 也只执行了一次,i<=100 和 ++i都是执行了n次,所以这行代码执行了n+1次
循环体内部的加法运算执行了n次
所以这个算法具体的时间复杂度就是1+n+1次
但是我们在计算时间复杂度的时候,并不需要那么具体时间复杂度计算,因为像循环体之外的这种只执行一次的基本操作我们是可以忽略不记的。想一下也知道,假如有一个循环循环了1000次,那么像int sum这种基本的操作次数对整个算法的时间影响并不大。所以我们可以忽略加法常数。
假如我们给出几种算法的时间复杂度
n | 算法A(n) | 算法B(3n) | 算法C(3n+10) |
---|---|---|---|
n = 10 | 10 | 30 | 40 |
n = 100 | 100 | 300 | 310 |
n = 10000 | 10000 | 30000 | 30010 |
n = 1000000 | 1000000 | 3000000 | 3000010 |
从这个表格可以看出加法常数和n最高次项的系数对算法的时间的影响很小(现代计算机计算速度是很快的,执行一一万次循环的时间可能0毫秒都不到),所以我们在计算时间复杂度的时候,可以忽略加法常数和最高次项的系数
我们再来看一下下面的这段代码
int sum = 0;
for(int i = 0; i <= n; ++i)
{
for(int j = 0; j < n; ++j)
{
}
}
for(int j = 0; j < n; ++j)
{
}
n次循环里面又嵌套了一层n次循环,再去掉加法常数,时间复杂度就是n*n + n=n^2+n。
那么我们拿n和n^2+n再来做个比较
n | n^2+n | n |
---|---|---|
100 | 300 | 100 |
100000 | 10000100000 | 100000 |
我们同样可以看出在一个算法当中+n对整个结果的影响并不是很大,所以我们还可以忽略掉
除最高次以外的项
由此我们总结出关于时间复杂度的计算规则:
我们使用大O阶的渐进表示法来表示时间复杂度(空间复杂度也是)
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
即上面的n^ 2 和n可以表示为 O( n^2 )和O(n)。
这里给出常见的时间复杂度的大小关系
这里我们再额外介绍一下O(logn) 是怎么得来的,这个还是有必要说一下的。
我们来看下面的这段代码
int num = 1;
while(num < n)
{
num = num * 2;
}
这里由于num*2之后,就距离n更近了一步。也就是说,有多少个2相乘之后大于n。
2可以省略掉,所以上面的算法即为对数阶O(logn)
空间复杂度
空间复杂度的计算同样也是遵循大O阶的表示法的。
由于我们现在的计算机内存不再像以前那样小,所以我们在处理数据的时候,更多的是去关注时间复杂度的优化,所以我们在这里就不着重介绍空间复杂度了,大家知道其原理即可。
空间复杂度:是对一个算法在运行过程中临时占用存储空间大小的量度 。同样和时间复杂度的规则一样,我们只在乎额外的空间开销。
一般情况下,一个程序在机器上的执行时,除了除了需要存储程序本身的指令,常数,变量和输入数之外还需要存储对数据操作的存储单元。
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n ; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
}
return fibArray;
}
这个算法除去常数项之后,空间复杂度为O(n)
总结
本篇文章并不是我们数据结构的重点内容,一时无法掌握没关系,在后续的介绍中,我们都会对实现的算法评判其复杂度的大O阶表达式,大家复杂度计算的功底也就潜移默化的增长了。
大家可以先稍微的记一下这个表(可以自己画出函数图像,有利于记忆)
最后推荐小白一本书,程杰老师的<大话数据结构>,好书是良师,亦是好友,身为一名程序员不看书怎么能行呢!!