数据结构初识,与算法复杂度
前言
哎呀,前前后后,里里外外,我拖拖沓沓了快四个月,终于学完了C语言。
我只明白了一件事:学习,一定要多敲代码,一定要找一个安静(最好无人)的地方,一定要排除万难,一切为学习让步。
现在是2025年10月25日,16时6分,不说了,开干。
1、数据结构与算法
数据结构,是计算机存储、组织数据的方式,方式包括数据的增、删、查、改。
数据结构,指相互之间存在一种或多种特定关系的数据元素的集合。
算法,指的是一系列计算过程,用于将输入的一个(或一组)数据,经这个计算过程,输出一个(或一组)数据(结果)。
2、数据结构与算法的重要性
非常重要!非常重要!非常重要!
所以要好好学。
3、复杂度
3.1、复杂度的概念
我们知道,衡量一个算法的好坏,一般是从时间和空间两个维度出发。
即时间复杂度,和空间复杂度。
| 时间复杂度 | 空间复杂度 |
|---|---|
| 衡量一个算法的运行快慢 | 衡量一个算法所需要的额外空间的多少 |
3.2、复杂度的重要性
一句话:节约时间、节省空间、降低成本。
对于笔试面试,和实际开发,都有帮助。
3.3、时间复杂度
算法的时间复杂度,是一个函数T(N),用于定量计算算法的运行时间。
那为什么,不直接跑一跑这个算法,然后记录从开始运行,到结束,这个真实时间,并拿来分析呢?有三点原因:
- 编译环境(编译器)的更新迭代,会导致同一段代码,实际运行时间不同。
- 机器的新旧,会导致同一段代码,实际运行时间不同。
- 要知道这个真实时间,必须先运行代码,而不能通过一种理论,来提前判别。
话说回来。我们知道,写出来的代码,被编译为二进制可执行指令,而程序执行的过程,就是CPU执行这些指令。其中,某一条指令可能被执行一次,也可能被执行多次。所以:
程序的执行时间 = 二进制指令执行时间 ∗ 执行次数 程序的执行时间=二进制指令执行时间*执行次数 程序的执行时间=二进制指令执行时间∗执行次数
举个例子:
c
//计算++count被执行的次数
void Func(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
++count;
}
for (int i = 0; i < 2 * k; ++i)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
}
不难得出:
T ( N ) = N 2 + 2 ∗ N + 10 T(N)=N^2+2*N+10 T(N)=N2+2∗N+10
其中,N^2对==T(N)==的影响最大。
但实际上,如果单单是执行一条简单的指令,那运行时间就微乎其微了。而有些语句,随着输入参数的变化,它们执行的次数也会随之发生相应有规律的变化。因此,我们计算算法复杂度,只是想计算算法程序的增长量级,也就是当N不断变化(一般是增大)时T(N)的差别。
上面的例子中,我们不难看出,低阶项与常数项,对结果的影响很小,所以我们只需计算一个大概的执行次数。这时可以用大O的渐进表示法。
3.3.1、大O的渐进表示法
规则:
- 时间复杂度函数式
T(n)中,保留最高阶项。比如O(N^2) - 去掉最高阶项的常数系数。
- 如果函数式中只有常数项,为
O(1)。
3.3.2、几个例子
举几个我觉得不太好理解的例子。
例一:
c
void Func1(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);
}
在这里:
- T(N) = M + N
- 时间复杂度:O(M + N)
- 当
M == N,时间复杂度可以为O(M) O(N) - 当
M >> N,时间复杂度O(M) - 当
M << N,时间复杂度O(N)
- 当
例二:
c
const char* strchr (const char * str, char char)
{
const char* p_begin = str;
while (*p_begin != char)
{
if (*p_begin == '\0')
return NULL;
p_begin++;
}
return p_begin;
}
在这里:
- 当要查找的字符串在第一位,则
T(N) = 1 - 当要查找的字符串在中间位置,则
T(N) = N/2 - 当要查找的字符串在末尾,则
T(N) = N
所以:
- 最好情况(下界):
O(1) - 平均情况:
O(N) - 最坏情况(上界):
O(N)
而大O的渐进表示法,一般关注上界,即最坏情况。
例三:
c
//冒泡排序
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;
}
}
现在是2025年10月28日12时59分
在这里:
- 当序列升序,
O(N) = N - 当序列降序:
| n 的取值 | 对应交换操作执行的次数 |
|---|---|
| 1 | 0 |
| 2 | 1 |
| 3 | 2 |
| 4 | 3 |
| ... | ... |
| n | n - 1 |
不难得出,执行次数为N(N - 1)/2,时间复杂度为O(N^2)。
例四:
c
void func2(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}
可以看出,
当n = 2,执行次数为1;
当n = 4,执行次数为2;
当n = 8,执行次数为4;
......
所以此时的T(N) = logN (这里底数不重要,可由换底公式换成任意底),时间复杂度为O(logN)。
例五:
也就是函数递归算法的复杂度。
递归的复杂度 = 递归的次数 * 每次递归的时间复杂度
3.4、空间复杂度
3.4.1、概念浅析
由于函数运行所需的栈空间,在编译期间就已经开辟好了,所以空间复杂度主要讨论的是函数在运行时,申请的额外内存空间。
3.4.2、示例
例一:
c
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;
}
}
还是这个冒泡排序。不难看出,使用了常数个空间,空间复杂度为O(1)。
例二:
c
//计算阶乘的递归
long long Fac(int n)
{
if (n == 0)
return 1;
else
return n*Fac(n - 1);
}
函数进行了n次递归,而每次递归申请了常数个空间,所以空间复杂度为O(N)。