1数据结构
1.1 什么是数据结构?
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
1.2 什么是算法?
算法(Algorithm):就是定义良好的计算过程,取一个或一组的值作为输入,并产生一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
2时间复杂度与空间复杂度
2.1 算法效率
算法效率分为两种:一种是时间效率,一种是空间效率。时间效率被称为时间复杂度,空间效率被称为空间复杂度。时间复杂度主要衡量的是一个算法的运行速度,空间复杂度主要衡量的是一个算法所需要的额外空间。
在计算机的发展早期,计算机的存储容量很小,所以对空间复杂度很在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以,目前我们更加关注计算机的时间复杂度。
2.2 时间复杂度
2.2.1 时间复杂度的概念
一个算法执行所消耗的时间,从理论上是很难计算出来的,且意义不大。因为一个算法的真正运行时间取决于cpu,运行环境等等条件,在不同计算机上算法的运行时间是不同的,甚至在不同时刻算法的运行时间也不同。
所以,时间复杂度定义为:算法中的基本操作的执行次数。
观察下面代码,计算一下fun的基本操作执行了多少次?
void fun(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);
}
对于fun函数来说,基本操作的执行次数为:
F
(
n
)
=
n
2
+
2
∗
n
+
10
F(n)=n^{2}+2*n + 10
F(n)=n2+2∗n+10
则fun函数的时间复杂度为:
F
(
n
)
=
n
2
+
2
∗
n
+
10
F(n)=n^{2}+2*n + 10
F(n)=n2+2∗n+10
2.2.2 大O的渐进表示法
实际开发中,我们并不需要准确的知道每个算法中基本操作的具体执行次数,我们只需要了解一个大致的执行次数即可,换句话说我们只需要了解执行次数的数量级即可,那么这里使用的方法称为大O的渐进表示法。
- 数量级
对于数字14来说,它的数量级为10
对于数字140来说,它的数量级为100
对于数字1400来说,它的数量级为1000
数量级是指数量的尺寸和大小的级别。
- 大O符号
用来描述函数渐进行为的数学符号
- 推导大O阶方法
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O 的渐进表示法来描述fun的时间复杂度:
O
(
n
2
)
O(n^{2})
O(n2)
显然,大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
- 最好、最坏和平均情况
有些算法会存在最好、最坏和平均情况,如在数组中查找某个元素并返回其数组下标。
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
如在一个长度为N的数组中搜索一个数据x:
最坏情况:N次找到,即最后找到
平均情况:N/2次找到
最好情况:1次找到
实际中,我们一般关注的是算法的最坏运行情况,所以在数组中搜索数据的时间复杂度为O(N)。
2.2.3 常见时间复杂度的计算
实例1:
void fun1(int N) {
int count = 0;
for (int k = 0; k < 2 * N; k++) {
++count;
}
int M = 10;
while (M--) {
++count;
}
printf("%d\n", count);
}
fun1的基本操作的执行了2N+10次,通过推导大O阶方法知道,时间复杂度为O(N)。
实例2:
void fun2(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);
}
fun2的基本操作的执行了N+M次,通过推导大O阶方法知道,时间复杂度为O(N+M)。
实例3:
void fun4(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
++count;
}
printf("%d\n", count);
}
fun3的基本操作的执行了100次,通过推导大O阶方法知道,时间复杂度为O(1)。
实例4:
//strchr函数功能为在一个串str中查找给定字符character的第一个匹配之处
const char* strchr(const char* str, int character);
该函数的功能为在一个字符串查找字符character并返回该字符的位置。这个查找函数的基本操作执行次数分为:
最好情况:1次
最坏情况:N次
平均情况:N/2次
时间复杂度一般依据最坏情况,所以该函数的时间复杂度为O(N)。
实例5:
void Swap(int* a, int* b) {
int c = *a;
*a = *b;
*b = c;
}
//冒泡排序 --从小到大
void BubbleSort(int* a, int n) {
assert(a);//异常处理
for (int end = n; end > 0; --end) {
int exchange = 0;
for (int i = 1; i < end; ++i) {
if (a[i - 1] > a[i]) {
//两个数据进行比较,前面一个数据大于后一个数据
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
//如果遍历整个数组,发现没有数据进行交换,即每个元素均小于等于后一个元素
//则无须在进行排序,直接结束循环即可
if (exchange == 0)
break;
}
}
对于BubbleSort函数来说,它存在
最好情况:数组为顺序,执行N次
最坏情况:数组为逆序,执行N*(N+1)/2次
平均情况
这里我们考虑最坏情况,即数组为逆序,时间复杂度为:O(N^2)。分析过程如下:
实例6:
//二分查找法
int BinarySearch(int* a, int n, int x) {
assert(a);
int begin = 0;
int end = n - 1;
while (begin < end) {
int mid = ((end - begin) >> 1) + begin; //计算end与begin的中间值,右移1位相当于除以2
if (a[mid] < x) {
begin = mid - 1;
}
else if(a[mid]>x){
end = mid;
}
else {
return mid;
}
}
return -1;
}
对于BinarySearch函数来说,它存在
最好情况:执行1次
最坏情况:约执行logN次,这里的logN是以2为底,以N为对数。
平均情况
这里我们考虑最坏情况,时间复杂度为:O(logN)。分析如下:
第一次查找:在长度为N的数组中查找值,取中间值进行比较
第二次查找:在长度为N/2的数组中查找值,取中间值进行比较
第三次查找:在长度为N/(2^2)的数组中查找值,取中间值进行比较
…
第logN次查找:在长度为N/(2^logN)的数组中查找值,即在长度为1的数组中查找,无论是否找到均跳出循环,结束查找。
实例7:
//求阶乘
long long Factorial(int N) {
return N < 2 ? N : Factorial(N - 1) * N;
}
Fibonacci中函数调用操作为N次,时间复杂度为O(N),分析过程如下:
实例8:
//斐波那契函数
long long Fibonacci(int N) {
return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}
Fibonacci函数的时间复杂度为O(2^N),分析过程如下:
2.3 空间复杂度
2.3.1 空间复杂度的概念
一个算法中所有变量实际占的内存空间很难估量,且意义不大。因为数据类型在内存中所占的内存数与计算机的系统有关系,在不同计算机上算法所占的内存是不同的。
所以,空间复杂度定义为:算法中变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
2.3.2 常见空间复杂度的计算
实例1:
void Swap(int* a, int* b) {
int c = *a;
*a = *b;
*b = c;
}
//冒泡排序 --从小到大
void BubbleSort(int* a, int n) {
assert(a);//异常处理
for (int end = n; end > 0; --end) {
int exchange = 0;
for (int i = 1; i < end; ++i) {
if (a[i - 1] > a[i]) {
//两个数据进行比较,前面一个数据大于后一个数据
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
//如果遍历整个数组,发现没有数据进行交换,即每个元素均小于等于后一个元素
//则无须在进行排序,直接结束循环即可
if (exchange == 0)
break;
}
}
BubbleSort中变量为a、n、end、exchange、i,变量个数为5,故该算法的空间复杂度为O(1)。
实例2:
//斐波那契函数
long long* Fibonacci(int 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;
}
Fibonacci函数中的变量有:n、i、fibArray、以及fibArray所指向的n+1个空间,所有该算法中共有n+4个变量,故空间复杂度为O(n)。
实例3:
//求阶乘
long long Factorial(int N) {
return N < 2 ? N : Factorial(N - 1) * N;
}
Factorial递归调用了N次,开辟了N个栈帧,每个栈帧使用了1个空间,故空间复杂度为O(N)。内存存储如下:
实例4:
//斐波那契函数
long long Fibonacci(int N) {
return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}
Fibonacci函数的空间复杂度为O(N),分析过程如下:
注:本文章所用代码的头文件为:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>