目录
1.前言
什么是数据结构?
数据结构就是计算机存储,组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合
数据结构🆚数据库:
数据结构在内存中管理数据
数据库在磁盘中管理数据
什么是算法?
算法就是一个良好的计算过程,他取一个或一组值作为输入,并产生一个或一组的值作为输出;简单来说,算法就是一系列计算步骤,用来将输入数据转换成输出结束
我们所说的时间复杂度和空间复杂度实际上指的是算法的时间复杂度和空间复杂度
算法的时间复杂度和空间复杂度:
算法在编写出可执行程序之后,运行时需要耗费时间资源和空间(内存)资源,因此衡量一个算法的性能,一般从从时间和空间两个维度来衡量
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间;经过计算机行业的迅速发展,计算机的存储容量已经达到了一个很高的程度,所以我们现在更注重算法的时间效率而不那么重视算法的空间效率
2.时间复杂度
2.1时间复杂度的概念
时间复杂度:计算机科学中,算法的时间复杂度是一个函数(数学中带有未知数的表达式,表达式的结果为执行次数),这个函数定量描述了该算法的运行时间,运行时间和其中语句
的执行次数成正比,所以可以说算法中的基本操作的执行次数,为算法的时间复杂度
计算方法:找到某条基本语句与问题规模N之间的表达式,即为算法的时间复杂度
2.2大O的渐进表示法
大O符号:用于描述函数渐进行为的数学符号
推导大O阶方法:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数中,只保留最高阶项(最高阶对结果产生决定性的影响)
- 如果最高阶项的系数存在且不是1,则去除这个系数,得到的结果就是大O阶
另外有些算法的时间复杂度存在最好、平均和最坏情况
- 最坏情况:任意输入规模的最大运行次数(上届)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数
2.3时间复杂度计算
例1:
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 m = 10;
while (m--)
{
++count;
}
printf("%d\n", count);
}
分析:
第一个for循环:
对于内层循环:循环变量j从0到N,循环语句++count共执行了N次
对于外层循环:循环变量i从0到N,内层循环总共执行了N次
对于语句++count,总共执行次数为N*N
第二个for循环:
循环变量k从0到2*N,循环语句++count共执行了2*N次
第三个while循环:
循环变量m从10到0,循环语句++count共执行了0次
综上:++count语句执行的次数总共为N*N+2*N+10
所以这个函数的时间复杂度函数为F(N)=N*N+2*N+10
📖Note:
- 当N越大时,表达式中2*N和10这两项对结果的影响不大,可以直接省去,所以时间复杂度不一定计算精确的执行次数,计算出其量级即可
- 使用大O的渐近表示法表示时间复杂度
所以以上代码的算法时间复杂度表示为O(N^2)
例2:
void Func2(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);
}
分析:
第一个for循环:
循环变量k从0到M,循环语句++count共执行了M次
第二个for循环:
循环变量k从0到N,循环语句++count共执行了N次
综上:++count语句执行的次数总共为M+N
这种可以分为三类:
- M远大于N:时间复杂度为O(M)
- N远大于M:时间复杂度为O(N)
- M和N一样大:时间复杂度为O(M)或O(N)
例3:计算冒泡排序的时间复杂度:
void BubbleSort(int a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int flag = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], a[i]);
flag = 1;
}
}
//一趟冒泡排序结束
if (flag = 0)
{
break;
}
}
}
分析:
对于内层循环:循环变量i从1到end,循环语句Swap每次执行end-1次
对于外层循环:循环变量end从n到0,内层循环总共执行了n次
第一次循环:end=n
对于循环语句Swap,执行次数为n-1
第二次循环:end=n-1
对于循环语句Swap,执行次数为n-2
... ...
第n-1次循环:end=2
对于循环语句Swap,执行次数为1
第n次循环:end=1
对于循环语句Swap,执行次数为0
综上:循环语句Swap的执行次数为0+1+2+...+n-2+n-1 = n+n(n+1)/2
所以冒泡排序算法的时间复杂度表示为O(N^2)
例4:
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
分析:
对于for循环:
循环变量k从0到100,循环语句++count共执行了100次
所以以上代码的算法时间复杂度表示为O(1)
📖Note:
这里的1不表示1次,只能代表运行了常数次
例5:计算字符串函数strchr的时间复杂度
分析:
strchr函数是在字符串中查找字符的库函数,strstr函数是在字符串中查找子串的函数
在字符串中查找一个字符可以理解为在一个长度为N的数组中查找一个数据
最好情况:一次找到,时间复杂度为O(1)
最坏情况:找完整个数组没有找到,时间复杂度是(N)
平均情况:查找N/2次找到,时间复杂度为O(N)
在实际情况中关注的是算法的最坏运行情况,即时间复杂度是对算法性能的悲观保守的预估
综上,字符串函数strchr的时间复杂度为O(N)
例5:计算二分查找的时间复杂度
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 - 1;
}
else
{
return mid;
}
}
return -1;
}
分析:
最好情况:第一次查找a[mid]==x,时间复杂度为O(1)
最坏情况:当begin>end时,没有找到
假设有序数组有N个元素,每查找一次,查找区间的元素个数减少一半
假设最坏情况查找了x次
则有N/(2*2*2*......) = 1(分母为x个2相乘)
x = logN
所以二分查找的时间复杂度为O(logN)
例6:计算阶乘递归的时间复杂度
long long Fac(size_t N)
{
if (1 == N)
{
return 1;
}
return Fac(N - 1) * N;
}
分析:
所以使用递归计算阶乘的时间复杂度为O(N)
我们最初计算阶乘的方法是循环
long long Fac(size_t N) { long long ret = 1; for (size_t i = 1; i <= N; i++) { ret *= i; } return ret; }
这段代码的时间复杂度也为O(N)
例7:计算斐波那契数列的时间复杂度
long long Fib(size_t N)
{
if (N < 3)
{
return 1;
}
return Fib(N - 1) + Fib(N - 2);
}
分析:
斐波那契数列一次递归的时间复杂度为O(1)
计算斐波那契数列的第N项,如上图总共展开了N层递归,每层中的递归次数呈以2为公比的等比数列增长
所以总共的执行次数为2^0+2^1+...+2^(N-1) = 2^N - 1
所以斐波那契数列的时间复杂度为O(2^N)
2.4时间复杂度的比较
5201314 | O(1) | 常数阶 |
3n+1 | O(n) | 线性阶 |
3n^2+4n+1 | O(n^2) | 平方阶 |
3logn+4 | O(logn) | 对数阶 |
2n+3nlogn+5 | O(nlogn) | nlogn阶 |
n^3+2n^2+4n+4 | O(n^3) | 立方阶 |
2^n | O(2^n) | 指数阶 |
当我们可以计算一个算法的时间复杂度之后,可以进行算法之间的比较,从而选出最优算法
3.空间复杂度
空间复杂度:空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度
📖Note:
- 空间复杂度不是程序占用了多少bytes的空间,其计算的是变量的个数
- 函数运行时所需要的占空间(存储参数,局部变量,一些寄存器信息等)在编译期间就已经确定好了,因此空间复杂度主要通过函数在运行时显式申请的额外空间来确定
- 空间复杂度也使用大O渐进表示法
例1:计算BubbleSort函数的空间复杂度
void BubbleSort(int a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int flag = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], a[i]);
flag = 1;
}
}
//一趟冒泡排序结束
if (flag = 0)
{
break;
}
}
}
分析:
空间复杂度主要通过函数在运行时显式申请的额外空间来确定
冒泡排序中,我们总共申请了end,flag,i 三个额外的变量,所以冒泡排序的空间复杂度为O(1)
📖📖Note:
- 数组a不算入空间复杂内,因为数组是以形参的方式传入函数内的,不算额外开辟的空间
- 同一个栈帧中的同一块空间,在时间上是累积的,而空间上不累积(即同一个函数中的不同变量,可能占用的是同一块空间,空间进行了重复利用)
例2:计算阶乘递归的时间复杂度
long long Fac(size_t N)
{
if (1 == N)
{
return 1;
}
return Fac(N - 1) * N;
}
分析:
函数的调用需要开辟栈帧,每一次递归的调用,都会开辟一块函数栈帧
计算N的阶乘总共有F(N-1) .....F(1)共N-1层递归,每层递归的空间复杂度为O(1)
所以总共开辟的Fac函数栈帧有N个
递归计算N的阶乘的空间复杂度为O(N)
例3:计算斐波那契数列的空间复杂度
long long Fib(size_t N)
{
if (N < 3)
{
return 1;
}
return Fib(N - 1) + Fib(N - 2);
}
分析:一次斐波那契数列的递归调用的空间复杂度为O(1)