1. 数据结构前言
1.1 数据结构
数据结构是计算机存储、组织数据的形式,指相互之间存在一种或多种特定关系的数据元素的集合
1.2 算法
算法:良好的计算过程,它取一个或一组的值为输入,并产生出一个或一组的值作为输出。即算法经过一系列的计算步骤,用来将输入数据转化成输出结果。
2. 总结
数据结构与算法在校园招聘笔试和面试都具有重要的地位,因此学会它是必要的。
2. 算法效率
2.1 复杂度概念
算法在编写成可执行程序后,运行时需要耗费时间资源和空间资源。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。随着时代的发展,计算机的容量得到了较快的发展,因此一个算法不在特别关注空间复杂度。
3. 时间复杂度
定义:算法的时间复杂度是一个函数式T(N),它定量描述该算法的运行时间。时间复杂度是衡量的时间效率,为什么不计算程序运行时间。
那么算法的时间复杂度是⼀个函数式T(N)到底是什么呢?这个T(N)函数式计算了程序的执⾏次数。通 过c语⾔编译链接章节学习,我们知道算法程序被编译后⽣成⼆进制指令,程序运⾏,就是cpu执⾏这 些编译好的指令。那么我们通过程序代码或者理论思想计算出程序的执⾏次数的函数式T(N),假设每 句指令执⾏时间基本⼀样(实际中有差别,但是微乎其微),那么执⾏次数和运⾏时间就是等⽐正相关, 这样也脱离了具体的编译运⾏环境。执⾏次数就可以代表程序时间效率的优劣。⽐如解决⼀个问题的 算法a程序T(N) = N,算法b程序T(N) = N^2,那么算法a的效率⼀定优于算法b。
案例
// 请计算⼀下 Func1 中 ++count 语句总共执⾏了多少次?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;}}
实际中我们计算时间复杂度时,计算的也不是程序的精确的执⾏次数,精确执⾏次数计算起来还是很 ⿇烦的(不同的⼀句程序代码,编译出的指令条数都是不⼀样的),计算出精确的执⾏次数意义也不⼤, 因为我们计算时间复杂度只是想⽐较算法程序的增⻓量级,也就是当N不断变⼤时T(N)的差别,上⾯我 们已经看到了当N不断变⼤时常数和低阶项对结果的影响很⼩,所以我们只需要计算程序能代表增⻓量 级的⼤概执⾏次数,复杂度的表⽰通常使⽤⼤O的渐进表⽰法。
3.1 大O渐进表示法
大O符号:用于描述函数渐进行为的数学符号。
通过以上⽅法,可以得到
Func1
的时间复杂度为:
O
(
N
2
)
3.2 时间复杂度计算实例
3.2.1 实例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
根据推导规则第二条得出
Func2的时间复杂度为:O(N)
3.2.2 实例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)=N+M
Func2的时间复杂度为:O(N +M)
3.2.3 实例3
// 计算 Func4 的时间复杂度?void Func4(int N){int count = 0;for (int k = 0; k < 100; ++ k){++count;}printf("%d\n", count);}
Func4执行的基本操作次数:
T(N)=100
根据推导规则第三条得出
Func2的时间复杂度为:O(1)
3.2.4 实例4
// 计算 strchr 的时间复杂度?const char * strchr ( const char* str, int 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
) =
2
N
因此:strchr的时间复杂度分为:
最好情况:
O
(1)
最坏情况:
O
(
N
)
平均情况:
O
(
N
)
3.2.5 实例5
// 计算 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;}}
1)若数组有序,则:
T
(
N
) =
N
2)若数组有序且为降序,则:
T
(
N
) =
N
∗ (
N
+ 1)/2
3)若要查找的字符在字符串中间位
置,则:
因此:BubbleSort的时间复杂度取最
差情况为:
O
(
N
2
)
3.2.6 实例6
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
= log
n
因此:func5的时间复杂度取最差情况为:
O
(log
2
n
)
3.2.7 实例7
// 计算阶乘递归 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
)
4. 空间复杂度
空间复杂度也是⼀个数学表达式,是对⼀个算法在运⾏过程中因为算法的需要额外临时开辟的空间。空间复杂度不是程序占⽤了多少bytes的空间,因为常规情况每个对象⼤⼩差异不会很⼤,以空间复 杂度算的是变量的个数。 空间复杂度计算规则基本跟实践复杂度类似,也使⽤⼤O渐进表⽰法。 注意:函数运⾏时所需要的栈空间(存储参数、局部变量、⼀些寄存器信息等)在编译期间已经确定好 了,因此空间复杂度主要通过函数在运⾏时候显式申请的额外空间来确定。
4.1 空间复杂度示例
4.1.1 实例1
// 计算 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额外申请的空间有 exchange等有限个局部变量,使⽤了常数个额外空间 因此空间复杂度为 O
(1)
4.1.2 实例2
// 计算阶乘递归 Fac 的空间复杂度?long long Fac(size_t N){if(N == 0)return 1;return Fac(N-1)*N;}
Fac递归调⽤了N次,额外开辟了N个函数栈帧, 每个栈帧使⽤了常数个空间 ,因此空间复杂度为: O
(
N
)