目录
1.数据结构前言
1.1 数据结构
数据结构是计算机科学中的一个重要概念,它是指在计算机中组织和存储数据的一种方式,使得数据的访问和修改变得更加高效和方便。不同的数据结构有助于以不同的方式处理数据,具体的选择往往取决于实际应用的需要和所需操作的类型。
常见的数据结构包括:
-
线性数据结构:
- 数组:一种固定大小的序列,可以通过索引访问元素。
- 链表:由一系列节点构成,每个节点包含数据部分和指向下一个节点的指针。
- 栈:遵循后进先出(LIFO)原则的线性结构。
- 队列:遵循先进先出(FIFO)原则的线性结构。
-
非线性数据结构:
- 树:一种层次结构的数据集合,包含节点和边。二叉树是最常见的树形结构。
- 图:由一组节点和连接它们的边构成的非线性结构,可以是有向图或无向图。
-
哈希表:一种通过哈希函数将键映射到值的结构,能够实现快速的数据存取。
-
集合:一种无序且唯一的数据集合,可以用于快速查找和操作。
每种数据结构都有其独特的特点和适用场景,选择合适的数据结构可以提高程序的性能和可读性。在算法设计中,数据结构与算法通常是紧密结合的,因此理解数据结构是学习计算机科学和编程的基础。
1.2 算法
算法是一个明确的、可执行的步骤或规则的集合,用于解决特定问题或完成某个任务。它可以被视为一种解决方案的方法,通常由一系列步骤构成,这些步骤可以是清晰的指令、逻辑判断或数学运算。
算法的特征包括:
- 明确性:算法中的每一步都必须是明确和清晰的。
- 可执行性:每一步都必须是可以在有限时间内完成的。
- 有限性:算法必须在有限的数据输入和步骤有限的情况下结束,不能无限循环。
- 输入和输出:算法可以有零个或多个输入,必须产生一个或多个输出结果。
算法广泛应用于计算机科学、数学、数据处理、人工智能等多个领域,常见的例子包括排序算法(如快速排序、归并排序)、搜索算法(如二分查找)、图算法(如Dijkstra算法)等。
2.时间复杂度
2.1 时间复杂度的概念
在计算机科学中,算法的时间复杂度是一个函数式T(N),它定量描述了该算法的运行时间。时间复杂度是衡量程序的时间效率。
那么算法的时间复杂度是一个函数式T(N)到底是什么呢?这个T(N)函数式计算了程序的执行次数。通过c语言编译链接章节学习,我们知道算法程序被编译后生成二进制指令,程序运行,就是cpu执行这些编译好的指令。那么我们通过程序代码或者理论思想计算出程序的执行次数的函数式T(N),假设每句指令执行时间基本一样(实际中有差别,但是微乎其微),那么执行次数和运行时间就是等比正相关,这样也脱离了具体的编译运行环境。执行次数就可以代表程序时间效率的优劣。比如解决一个问题的算法a程序T(N)=N,算法b程序T(N)=N^2,那么算法a的效率一定优于算法b。
以下内容参考 大O的渐进表示法以及最后的示例
-
时间复杂度:
- 时间复杂度描述了算法执行所需要的时间量,通常用大O符号表示。例如:
- O(1):常数时间复杂度,操作耗时与输入规模无关。
- O(log n):对数时间复杂度,操作耗时随输入规模增长缓慢。
- O(n):线性时间复杂度,操作耗时与输入规模成正比。
- O(n log n):通常出现在高效排序算法中,如归并排序和快速排序。
- O(n^2):平方时间复杂度,常见于简单的排序算法,如冒泡排序或选择排序。
- O(2^n) 和 O(n!):分别表示指数和阶乘时间复杂度,通常非常耗时,适用于小规模问题。
- 时间复杂度描述了算法执行所需要的时间量,通常用大O符号表示。例如:
数据结构的设计直接影响算法的时间和空间复杂度。例如,选择合适的数据结构可以使某些操作更高效,比如使用哈希表实现快速查找,或使用堆来实现优先队列。
理解数据结构的复杂度有助于在实际编程中做出有效的设计选择,从而提升程序的效率与性能。
2.2 大O的渐进表示法
大O表示法(Big O notation)是一种用于描述算法复杂度的数学符号,特别是在分析算法的时间复杂度和空间复杂度时。它提供了一种工具,可以帮助我们理解算法在输入规模增大时的表现。
大O表示法的基本概念是:对于给定的函数 f(n),如果存在常数 C>0 和 n0≥0,使得对所有 n≥n0,都有 ∣f(n)∣≤C⋅g(n),则称 f(n) 是 g(n) 的大O,记作 f(n)=O(g(n))。
以上的说话过于抽象,但只要我们学会代码的正确分析以及推导大O阶规则,就能顺利的求出代码的时间复杂度
2.2.1 推导大O阶规则
- 时间复杂度函数式T(N)中,只保留最高阶项,去掉那些低阶项,因为当N不断变大时,低阶项对结果影响越来越小,当N无穷大时,就可以忽略不计了。
- 如果最高阶项存在且不是1,则去除这个项目的常数系数,因为当N不断变大,这个系数对结果影响越来越小,当N无穷大时,就可以忽略不计了。
- T(N)中如果没有N相关的项目,只有常数项,用常数1取代所有加法常数。
下面我用一系列的代码分析帮助你更好的理解规则
2.3 时间复杂度计算示例
2.2.1 示例一
// 计算Func2的时间复杂度?
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);
}
计算上列代码的时间复杂度:
Func2执行的基本操作次数:T(N) = 2N + 10
根据推导规则第3条得出,Func2的时间复杂度为:O (N)
2.3.2 示例二
// 计算Func3的时间复杂度?
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);
}
Func3执行的时间复杂度:
T(N) = M + N (注意:括号中的N指代变量,并不意味着等号右边的只有N是变量)
因此:Fun2的时间复杂度为:O(N)
补充:
当m == n 时,时间复杂度为O(M) / O(N)
当m >> n 时,时间复杂度为O(M)
当m << n 时,时间复杂度为O( N)
2.3.3 示例三
// 计算strchr的时间复杂度?
const char* strchr(const char* str,char character)
{
const char* p_begin = s;
while (*p_begin != character)
{
if (*p_begin == '\0')
return NULL;
p_begin++;
}
return p_begin;
}
strchr执行的基本操作次数:
1) 若要查找的字符在字符串第一个位置,则:
T(N)=1
2) 若要查找的字符在字符串最后的一个位置,则:
T(N) = N
3) 若要查找的字符在字符串中间位置,则:
T(N)=N / 2
2
因此:strchr的时间复杂度分为:
最好情况:O(1)
最坏情况:O(N)
平均情况:O(N)
总结
通过上面我们会发现,有些算法的时间复杂度存在最好、平均和最坏情况。
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
大O的渐进表示法在实际中一般情况关注的是算法的上界,也就是最坏运行情况。
2.3.4 示例四
// 计算Bubblesort的时间复杂度?
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;
}
}
BubbleSort执行的基本操作次数:
1) 若数组有序,则:
T(N) = N
2) 若数组有序且为降序,则:
T(N) = N* (N+1)/ 2
3)若要查找的字符在字符串中间位置,则:
因此:BubbleSort的时间复杂度取最差情况为:O(N ^ 2)
计算过程:

注意:
这里只不过得到的结果刚好是O(n ^ 2),但是两个循环嵌套不一定是O(n ^ 2)
2.3.4 示例四
void func5(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}
当n=2时,执行次数为1
当n=4时,执行次数为2
当n=16时,执行次数为4
假设执行次数为x,则2^x=n
因此执行次数:
x =logn
因此:func5的时间复杂度取最差情况为:O(log2 n)
表示为O(log2 n)的原因:
1. 键盘敲不出
2. 底数不会给时间复杂度O(N)带来任何影响
2.3.5 示例五
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if (0 == N)
{
return 1;
}
return Fac(N - 1) * N;
}
调用一次Fac函数的时间复杂度为 O(1)
而在Fac函数中,存在n次递归调用Fac函数
因此:
阶乘递归的时间复杂度为:O(N)
计算过程:

3.空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中因为算法的需要额外临时开辟的空间。
空间复杂度不是程序占用了多少bytes的空间,因为常规情况每个对象大小差异不会很大,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了
因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定
示例
// 计算BubbleSort的时间复杂度?
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[il)
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
函数栈帧在编译期间已经确定好了,只需要关注函数在运行时额外申请的空间。
BubbleSort额外申请的空间有exchange等有限个局部变量,使用了
常数个额外空间
因此空间复杂度为 O(1)

1394

被折叠的 条评论
为什么被折叠?



