【数据结构入门】算法的时间复杂度和空间复杂度详解
文章目录
- (1)算法效率
- (2)时间复杂度的计算
- 1)什么是时间复杂度
- 2)大O渐进表示法(估算)
- 3)时间复杂度计算实例
- 4)总结
- 5)一些思考
- (3)空间复杂度的计算
- (4)常见复杂度对比
本篇前言
在C语言阶段,我们学过了一些排序和查找算法,冒泡排序,快速排序,二分查找等等,哪种算法更好呢,我们如何衡量一个算法的好坏呢?本篇来学习算法的时间复杂度和空间复杂度,相信学完后你就会明白了。
(1)算法效率
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
随着计算机行业的高速发展,计算机储存容量已经达到了很高的程序,已经不需要特别关注一个算法的空间效率了,重点关注其时间复杂度。
(2)时间复杂度的计算
1)什么是时间复杂度
算法中的基本操作的==执行次数==,为算法的时间复杂度。
直接上实例来讲解具体的计算方法吧
//计算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;}printf("%d\n", count);
}
算法执行次数函数表达式:F(N) = N2 + 2 * N + 10
N = 10,F(N) = 130;N = 100,F(N) = 10210;N = 1000,F(N) = 1002010;
通过计算发现,N越大,对结果影响越小,所以在实际计算时间复杂度时,我们并不需要计算精确的执行次数,而只需要计算大概执行次数,使用大O渐进表示法(估算),只保留对结果影响最大的一项。
2)大O渐进表示法(估算)
1、推导大O阶方法:
用常数1取代运行时间中的所有加法常数。
在修改后的运行次数函数中,只保留最高阶项。
如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
这些方法看起来有些许摸不着头脑哈,我们来举几个例子说明,比如:
- 执行次数函数 F(N) = 10,使用大O渐进表示法后,时间复杂度为:O(1)
- 执行次数函数 F(N) = N2 + 2 * N + 10,使用大O渐进表示法后,时间复杂度为:O(N2)
- 执行次数函数 F(N) = 2 * N + 10,使用大O渐进表示法后,时间复杂度为:O(N)
2、有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为 N 数组中搜索一个数据 X
最好情况:1次就找到
最坏情况:N次才找到(一般以最坏情况为准)
平均情况:N/2次找到
而在实际中一般情况关注的是算法的最坏运行情况,是一种保底思维,没有比这更差了哈哈,所以数组中搜索数据时间复杂度为O(N)
3)时间复杂度计算实例
实例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);
}
执行次数函数 F(N) = 2 * N + 10,使用大O渐进表示法后,时间复杂度为:O(N)
(保留影响最大的一项,去掉系数)
实例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);
}
执行次数函数 F(M,N) = M + N,有两个未知数 M 和 N,我们不知道谁大谁小,
所以时间复杂度可以写成:O(M + N) 或 O(max(M,N))
如果有条件限定:
如果能说明 M 远大于 N,则 O(M)
如果能说明 N 远大于 M,则 O(N)
如果能说明 M 和 N 差不多大,则 O(M) 或 O(N)
(这道题要看具体的场景,看有没有具体限定)
实例3:
// 计算Func4的时间复杂度
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++k){++count;}printf("%d\n", count);
}
执行次数函数 F(N) = 100,使用大O渐进表示法后,时间复杂度为:O(1)
(时间复杂度 O(1) 中的 1 不是代表 1 次,而是常数次)
实例4:
// 计算strchr的时间复杂度
// strchr - 定位字符串中第一个出现的字符
const char* strchr(const char* str, int character);
// 函数内部大致逻辑
while(*str)
{if(*str == character)return str;else++str;
}
循环中的 if 语句要比较 N 次,时间复杂度为:O(N)
实例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; //发生了交换,赋值为1}}if (exchange == 0) //没有发生交换,说明已经有序了break;}
}
若初始文件是正序的,一趟扫描即可完成排序。所以冒泡排序最好的时间复杂度为:O(n)
若初始文件是反序的,n 个数,需要进行 n-1 趟排序 ,每一趟重复地走访过要排序的元素列,依次比较两个相邻的元素,在这种情况下,比较和交换次数均达到最大值:
第 1 趟比较 n-1 次,
第 2 趟比较 n-2 次,
第 3 趟比较 n-3 次,
…………,
第 n-2 趟比较 2 次,
第 n-1 趟比较 1 次。
共 n-1 趟,每一趟 if 语句执行次数累加为:
n-1 + n-2 + n-3 + n-4 + …… + 3 + 2 + 1 = (n - 1)(1 + n - 1) / 2 = n(n-1) / 2
等差数列前 n 项和公式为:
Sn = n * a1 + n (n - 1)d / 2
Sn = n(a1 + an) / 2
所以冒泡排序最差的时间复杂度为:O(n2)
实例6:
// 计算二分查找BinarySearch的时间复杂度(二分查找前提是排序数组)
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) / 2;int mid = begin + ((end - begin) >> 1); // mid时排序数组a中间元素的下标if (x > a[mid]) // 大于中间元素begin = mid + 1;else if (x < a[mid]) // 小于中间元素end = mid - 1;elsereturn mid;}return -1;
}
二分(折半)查找,从 n 个数中找,计算一共折半了多少次才找到该数,那么时间复杂度就是折半的次数
图解说明:
注:对数时间复杂度记法,O(log2N),可以简化写成O(logN)
实例7:
// 计算阶乘递归Fac的时间复杂度
long long Fac(size_t N)
{if (1 == N)return 1;return Fac(N - 1) * N;
}
递归次数为 N 次,每一次递归中运行次数为 1,所以时间复杂度为:O(N)
实例8:
// 计算斐波那契递归Fib的时间复杂度
long long Fib(size_t N)
{if (N < 3)return 1;return Fib(N - 1) + Fib(N - 2);
}
图解说明:
F(N) = 1 + 2 + 4 + 8 + …… + 2N-3 + 2N-2 = (1 - 2N-2 * 2)/ (1 - 2) = 2N-1 - 1
补充:
这个二叉树的右边其实是有缺失的,右边没有N - 1层,所以算出来的 F(N) 还要减去右边缺的一些调用(常数)
使用大O渐进表示法后,所以时间复杂度为:O(2N)
4)总结
计算时间复杂度,
就是计算出该算法中的基本操作的执行次数,
一般以算法的最坏运行情况为准,
最终结果用大O渐进表示法(估算)书写
5)一些思考
- 其实这个斐波那契数列递归的算法是非常慢的,重复计算特别多,时间复杂度 O(2N),成指数增长的
N = 10,2N = 1024
N = 20,2N = 100万+
N = 30,2N = 10亿+
N = 40,2N = 10000亿+
N = 50,2N = 1000万亿+
所以尽量用非递归的方式去算斐波那契数,用数组,或者用三个变量倒的方法
三个变量f1、f2、f3,
f1 和 f2 存头两个斐波数Fib1和Fib2,
第三个变量 f3 算斐波数Fib3 = Fib1 + Fib2,
然后往前移动一下,f1 存 Fib2,f2 存 Fib3,然后再算出 Fib4 存到 f3 中
不断循环上述过程
- 二分(折半)查找时间复杂度是 O(log2N),效率是非常高的
N = 1000,log2N ≈ 10(210 = 1024)
N = 100万,log2N ≈ 20(220 = 100万+)
N = 10亿,log2N ≈ 30(230 = 10亿+)
N = 10000亿,log2N ≈ 40(240 = 10000亿+)
从这个角度分析,我们就能知道二分查找算法是一个非常好的算法,假设所有中国人的身份证号码已排好序存起来(14亿),查找一个人最多要找几次?
log214亿 ≈ 31次(因为230 = 10亿+,231 = 20亿+)
但二分查找也有一个致命的问题,就是必须要数据有序才行,而排序也是一个挺大的工程,所以实际中查找用得并没有那么多,查找更多的是用后面回讲到的一种数据结构:搜索树
(3)空间复杂度的计算
空间复杂度是对一个**算法在运行过程中临时占用存储空间大小**的量度 。
空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候向外申请的额外空间来确定。
实例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;}
}
数组 a 不是因为这个算法而开辟的,所以它是不参与计算的,该算法在运行时,定义了变量(end、exchange、i),临时占用了常数个储存空间的,所以空间复杂度是:O(1)
这时可能有人会有疑问:每次循环中 exchange 都被重新定义了,为啥只占用了常数个空间呀
因为空间是可以重复利用的,每个函数调用,都会开辟一个栈帧,里面存放函数运行时所需的局部变量、参数等
实例2:
// 计算Fibonacci的空间复杂度
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t 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;
}
算法在运行时,使用了 malloc 动态开辟并临时占用了 N 个储存空间,空间复杂度为:O(N)
实例3:
// 计算阶乘递归Fac的空间复杂度
long long Fac(size_t N)
{if (N == 1)return 1;return Fac(N - 1) * N;
}
递归调用了N次,最多递归了N - 1层,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度为:O(N)
思考这道题:
// 计算斐波那契递归Fib的空间复杂度
long long Fib(size_t N)
{if (N < 3)return 1;return Fib(N - 1) + Fib(N - 2);
}
空间是可以重复利用的,栈帧被开辟,返回时又会被销毁,最多递归了N - 1层,开辟了N - 1个栈帧,每个栈帧使用了常数个空间,空间复杂度为:O(N)
(4)常见复杂度对比
时间复杂度大小比较:O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n)
复杂度介绍完啦,下次再见!
【数据结构入门】算法的时间复杂度和空间复杂度详解相关推荐
- c语言将AOE网络的数据写入TXT文档中,数据结构与算法学习辅导及习题详解.张乃孝版-C/C++文档类资源...
数据结构与算法学习辅导及习题详解.张乃孝版.04年10月 经过几年的努力,我深深体会到,编写这种辅导书要比编写一本湝通教材困难得多. 但愿我的上述理想,在本书中能够得以体现. 本书的组织 本书继承了& ...
- 算法的时间与空间复杂度详解
算法(Algorithm)是指用来操作数据.解决程序问题的一组方法.对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别. 那么我们应该如何去衡量不同 ...
- 数据结构与算法之时间复杂度与空间复杂度
⭐️前面的话⭐️ 本篇文章带大家认识数据结构与算法基础,时间复杂度与空间复杂度.算法效率分析分为两种:第一种是时间效率,第二种是空间效率.时间效率被称为时间复杂度,而空间效率被称作空间复杂度. 时间复 ...
- 【数据结构与算法】时间复杂度和空间复杂度
文章目录 前言 数据结构与算法 如何学好数据结构和算法 算法效率 算法的复杂度 时间复杂度 大O的渐进表示法 空间复杂度 常见复杂度对比 OJ练习题 消失的数字 轮转数组 结语 前言 大家好哦,< ...
- 【数据结构和算法】时间复杂度和空间复杂度
目录 一.前言 二.时间复杂度 2.1时间复杂度表示形式 2.1.1规则: 3.1如何计算时间复杂度 3.1.1线性阶 3.1.2平方阶 3.1.3对数阶 常见的时间复杂度排序: 三.空间复杂度 3. ...
- 数据结构和算法———P3 时间复杂度和空间复杂度
算法效率的度量方法 算法时间复杂度的定义: 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级.算法的时间复杂度,也就是算法的时间度 ...
- 【数据结构】算法的时间复杂度和空间复杂度解析
目录 1. 算法的效率 2 时间复杂度 2.1 时间复杂度的概念 2.2 大O渐进表示法 2.3 常见算法的时间复杂度分析 3. 空间复杂度 3.1 常见空间复杂度分析 1. 算法的效率 我们在写一个 ...
- 数据结构: 算法的时间复杂度和空间复杂度
1.算法的概念: 算法 (Algorithm),是对特定问题求解步骤的一种描述. 解决一个问题往往有不止一种方法,算法也是如此.那么解决特定问题的多个算法之间如何衡量它们的优劣呢?有如下的指标: 2. ...
- 算法时间复杂度和空间复杂度详解
1.算法的概念: 算法 (Algorithm),是对特定问题求解步骤的一种描述. 解决一个问题往往有不止一种方法,算法也是如此.那么解决特定问题的多个算法之间如何衡量它们的优劣呢?有如下的指标: 2. ...
最新文章
- shell下的seq用法
- Asp.net Web API实战
- 随手小记:快速适应未必是个好策略
- php判断几维数组的方法,php如何判断数组是几维
- [转载]智能科普:VR、AR、MR的区别
- Java每次输入一个字符+高精度取整计算(记洛谷P2394题WA+TLE+RE的经历,Java语言描述)
- 中国金融家俱乐部秋季论坛聚焦“自贸区将带来什么”展开论道
- 项目中的textarea遇到的小问题
- iPads和iPhones的Media Queries(转载)
- mac vi快速删除_Mac下Vim编辑快捷键小结
- 福州万宝产业园的远程预付费电能管理系统
- GO程序设计语言 PDF 黑色的书 百度云
- 【原创】微信最新表情js代码
- Linux之rz和sz命令用法详解
- 电子计算机硬件是由哪五个部分构成,计算机硬件系统由哪五个基本部分组成?...
- python是面向对象开发_Python开发之路-面向对象
- C++中#define宏定义的min与max函数
- html投影电脑,无线投影小PC 联想投影电脑610S评测
- Excel如何利用时间差操作,求得员工的工龄
- pytest中参数化方法,并且根据执行的命令进行动态参数化