算法效率的计算
目录
- 一、如何衡量一个算法的好坏?
- 二、时间复杂度
- 1. 时间复杂度的计算方法
- 2. 时间复杂度习题
- 三、空间复杂度
- 1. 空间复杂度的计算方法
- 2. 空间复杂度习题
- 四、常见复杂度对比
- 五、复杂度oj题
- 1. 消失的数字
- 2. 轮转数组
一、如何衡量一个算法的好坏?
如果一个算法运行速度快且消耗内存少,那么该算法一定是一个效率高的好算法。那么如何计算一个算法的速度和消耗的内存呢?通常我们使用时间复杂度和空间复杂度来衡量一个算法的好坏,并且使用大O表示法来表示。
二、时间复杂度
1. 时间复杂度的计算方法
一、确定基本操作
首先,明确算法中的基本操作。基本操作通常是指算法中执行次数最多或者对时间影响最大的操作。例如,在一个循环中,循环体的执行次数通常是基本操作;在排序算法中,比较操作和交换操作可能是基本操作。
二、分析基本操作的执行次数与问题规模的关系
- 用问题规模的变量来表示基本操作的执行次数。例如,如果算法是对一个长度为 n 的数组进行操作,那么问题规模通常用 n 表示。
- 分析算法的结构,确定基本操作的执行次数与问题规模之间的关系。这可能需要对算法进行逐步分析,考虑不同的情况和分支。
三、用大 O 记号表示时间复杂度
- 忽略低阶项和系数。在表示时间复杂度时,只关注最高阶项,因为当问题规模足够大时,低阶项和系数对时间的影响相对较小。
- 根据基本操作的执行次数与问题规模的关系,常见的时间复杂度级别有常数阶 O (1)、对数阶 O (log n)、线性阶 O (n)、线性对数阶 O (n log n)、平方阶 O (n²) 等。
用作者的话来说,就是有循环就计算整个算法中循环语句的执行次数,没有循环就计算整个算法所执行的所有语句数。然后用问题的规模的变量把这个计算结果表示出来,最后使用大O表示法去掉最高项的系数,只保留最高项的阶数,结果就是该算法的时间复杂的大O表示法。如果该算法中有函数调用或者递归也是同样的计算方法。
2. 时间复杂度习题
例题1
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;}
}
首先,有循环就计算循环语句的执行次数,n2 + 2n + 10
其次,从该函数的参数判断该算法的规模为 n
接着,使用规模变量表示前面的执行次数,n2 + 2n + 10
最后,使用大O表示法,去掉最高阶系数,只保留最高阶,O(n2)
例题2
下面是一个计算前 n 项正整数之和的算法:
// 计算前 n 项和
long long sum_first_n(int n)
{int i;long long sum = 0;for (i = 1; i <= n; ++i)sum += i;// 返回return sum;
}
首先,有循环,那么就计算循环语句的执行次数,n。
然后,该算法是计算前 n 项正整数的和,规模变量为 n。
接着用规模变量表示前面计算的执行次数,n。
最后使用大O表示法,去掉最高阶系数,只保留最高阶,O(n)。
例题3
// 计算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);
}
有了上面两个练习,下面的题目就写得简洁一些了。
循环语句执行次数:M+N
规模变量表示:M+N
如果题目说明 M 远大于 N,则 O(M),如果 N 远大于 M,则O(N),如果没有说明则 O(M+N)。
例题4
// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++ k){++count;}printf("%d\n", count);
}
循环语句执行次数:100
问题规模变量表示:100
结果为常数,那就是常数阶O(1),不管这个常数多大,只要是一个常数那都是O(1)
例题5
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
库函数 strchr() 的功能是在一个字符串中查找指定字符,如果找到了就返回该字符的地址,如果没找到就返回空指针。
那么通过计算,该算法的时间复杂度并不唯一,如果带查找的字符刚好在第一个,那么只需要查找一次,时间复杂度也就是O(1);如果带查找的字符刚好在最后一个,那么需要查找 n 次,时间复杂度也就是O(n)。
在这种存在最好和最坏的情况的算法中,通常是取最坏的情况。那么该算法的时间复杂度就是 O(n)。
例题6
下面是一个优化过后的冒泡排序:
// 计算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)当待排序数组完全有序,那么第一轮比较之后就会跳出循环。那么循环中 if 条件语句执行的次数为 n-1,其时间复杂度也就是 O(n)
(2)当待排序数组完全逆序,那么冒泡排序就会一直执行到结束。那么循环中 if 条件语句的执行次数为 (n-1)+(n-2)+…+1,其时间复杂度为 O(n2)
例题7
下面是一个二分查找的算法:
// 计算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)>>1);// 判断if (a[mid] < x)begin = mid+1;else if (a[mid] > x)end = mid;elsereturn mid;}return -1;
}
二分查找是在 n 个有序的数中查找一个数,而且每次查找都可以去掉一半的数,也就是剩下 n/2 个数。最坏的情况就是查找到最后一个数,也就是 n/2/2/2…/2 = 1,即 2k = n,k = log2n,这个 k 也就是要查找的次数,且每次查找所执行的语句也是常数,所以该算法的时间复杂度就是 O(log2n),也就是上面说的对数阶。
例题8
下面是计算正整数 n 的阶乘的递归算法:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{if(0 == N)return 1;return Fac(N-1)*N;
}
通过计算,该函数加上递归一共需要被调用 N+1 次,每次调用该函数执行常数条语句,所以该算法的时间复杂度为 O(n)
例题9
下面是第 n 项斐波那契数列的递归算法:
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}
首先,递归调用一次执行常数条语句,那么一共递归调用多少次?
如上图所示,我们把缺失的项设置为 k,那么递归调用的次数为:20 + 21 + 22 +…+ 2n-1 - k = 2n - 1 - k,相比于 2n 来说 1+k 显得微不足道,所以该求斐波那契第 n 项的递归算法的时间复杂度为 O(2n)
三、空间复杂度
1. 空间复杂度的计算方法
有了前面时间复杂度的计算方法,那么空间复杂度的计算就要简单很多了。
(1)统计创建变量的个数(不管是什么类型)
(2)用问题的规模变量表示变量的总个数
(3)大O表示法,去掉最高阶的系数,只保留最高阶
2. 空间复杂度习题
习题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;}
}
由于在执行算法的过程中只开辟了常数个变量(end、i 和 exchange),所以该算法的空间复杂度为 O(1)
习题2
下面是阶乘的递归算法:
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if(N == 0)return 1;return Fac(N-1)*N;
}
虽然在递归的过程中没有开辟新的变量,但是每次递归都需要开辟栈帧空间,一共递归 n+1 次,开辟了 n+1 个栈帧空间,所以该算法的空间复杂度为 O(n)
习题3
下面是第 n 项斐波那契数列的递归算法:
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}
前面通过画图的方式的出,该递归需要调用 2n 层级的次数,那么是否需要开辟 2n 层级的函数栈桢?
答案肯定不是,仔细阅读代码,你会发现,每次递归调用函数 Fib 时,它都会先调用左边的 Fib(N-1),然后再在里面调用他自己的 Fib(N-1),直到最后一次递归时它的 N-1 < 3 时才会返回上一层递归,然后调用该层递归的 Fib(N-2)。如下图:
所以,该算法运行时函数栈帧消耗的最大层数为 n,所以该算法的空间复杂度为 O(n)
四、常见复杂度对比
上述图片均来自比特的课件哈,作者比较懒也不会画。
五、复杂度oj题
1. 消失的数字
题目描述: 数组nums包含从 0 到 n 的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在 O(n) 的时间复杂度内完成吗?
注意:本题相对书上原题稍作改动
示例 1:
输入:[3,0,1]
输出:2
示例 2:
输入:[9,6,4,2,3,5,7,0,1]
输出:8
题目OJ链接: 链接: link
题目解析:
(1)题目要求时间复杂度需要 <= O(n)
(2)三种算法:
a. 计算 0 - n 的和然后减去数组的每个元素,结果就是缺失的数
// 计算 0 - n 的和,然后减去数组的每个元素
int find_missing_num(int *arr, int sz)
{// 计算 0-n 之和int sum = (sz + 1) * sz / 2;// 减去数组的每个元素int i;for (i = 0; i < sz; ++i){sum -= arr[i];}// 返回return sum;
}
时间复杂度: O(n)
空间复杂度: O(1)
b. 用 0 - n 与数组的每个元素异或,结果就是缺失的数
int find_missing_num(int* arr, int sz)
{int result = sz;int i;for (i = 0; i < sz; ++i){result ^= i ^ arr[i];}// 返回return result;
}
由于循环是 0 - n-1,所以 result 的初值为 sz。
时间复杂度: O(n)
空间复杂度: O(1)
c. 使用 calloc() 函数动态开辟一个大小为 n+1 的 int 数组,该数组用来统计 0 - n 的出现次数,遍历原数组,记录出现次数,然后遍历次数数组,出现次数为 0 的就是缺失的数。
int find_missing_num(int* arr, int sz)
{// 动态开辟次数数组int* times = (int*)calloc((sz + 1), sizeof(int));// 遍历原数组统计次数int i;for (i = 0; i < sz; ++i)++times[arr[i]];// 遍历次数数组找缺失数for (i = 0; i <= sz; ++i){if (0 == times[i])break;}// 释放free(times);times = NULL;// 返回return i;
}
时间复杂度: O(n)
空间复杂度: O(n)
2. 轮转数组
题目描述: 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105
题目OJ链接: 链接: link
题目解析:
(1)像这种旋转数组或者字符串的问题,不管是右旋转还是坐旋转,当旋转次数为其长度的整数倍时,就会复原。也就是旋转的周期是其长度,如:1,2,3,4,右旋转或者左旋转 4n 次后复原。所以,实际的旋转次数需要对其长度取余。
(2)三种解法:
a. 暴力求解法
就是直接一步一步旋转,没有任何技巧可言
// 暴力求解法
void rotate_arr_i(int* arr, int sz, int k)
{// 实际旋转次数int n = k % sz;// 旋转while (n--){// 存储尾元素int tmp = arr[sz - 1];// 前面元素往后移int i;for (i = sz - 1; i > 0; ++i)arr[i] = arr[i - 1];// 尾元素放开头arr[0] = tmp;}
}
时间复杂度: O(n2)
空间复杂度: O(1)
b. 三步逆序法(三种里面最优)
先计算实际旋转次数 k,然后把后 k 项逆序,再把前 sz-k 项逆序,最后整个数组逆序,就是想要的结果。
// 三步逆序法// 逆序
void reverse(int* left, int* right)
{while (left < right){// 交换int tmp = *left;*left = *right;*right = tmp;// 下一组++left;--right;}
}// 三步
void rotate_arr_i(int* arr, int sz, int k)
{// 实际旋转次数k %= sz;// 三步逆序reverse(arr + sz - k, arr + sz - 1);reverse(arr, arr + sz - k - 1);reverse(arr, arr + sz - 1);
}
时间复杂度: O(n)
空间复杂度: O(1)
c. 额外开辟数组法
这是一个空间换时间的方法,额外开辟一个大小相同的动态数组,然后算出实际旋转次数 k,把后 k 项放在新数组前面,再把前 sz-k 项放在新数组后面,最后把新数组拷贝到原数组。
// 额外开辟数组法
void rotate_arr_i(int* arr, int sz, int k)
{// 实际旋转次数k %= sz;// 开辟额外数组int* tmp = (int*)malloc(sizeof(int) * sz);// 把原数组旋转后的顺序拷贝到新数组int i;for (i = 0; i < k; ++i)tmp[i] = arr[sz - k + i];for (i = 0; i < sz - k; ++i)tmp[k - 1 + i] = arr[i];// 把新数组拷贝到原数组for (i = 0; i < sz; ++i)arr[i] = tmp[i];// 释放额外数组free(tmp);
}
时间复杂度: O(n)
空间复杂度: O(n)
相关文章:

算法效率的计算
目录 一、如何衡量一个算法的好坏?二、时间复杂度1. 时间复杂度的计算方法2. 时间复杂度习题 三、空间复杂度1. 空间复杂度的计算方法2. 空间复杂度习题 四、常见复杂度对比五、复杂度oj题1. 消失的数字2. 轮转数组 一、如何衡量一个算法的好坏? 如果一…...
迷茫内耗的一天
迷茫的一天 今天看了看动态规划,不知不觉看了三四个小时,英语也没背,项目也已经停止了一个星期就看了几个小时的xml文件(不停ctrlB),好累,感觉要学的好多。这难道是必经之路吗? 一个星期算法已经刷了40道题…...

【android12】【AHandler】【4.AHandler原理篇ALooper类方法全解】
AHandler系列 【android12】【AHandler】【1.AHandler异步无回复消息原理篇】-CSDN博客 【android12】【AHandler】【2.AHandler异步回复消息原理篇】-CSDN博客 【android12】【AHandler】【3.AHandler原理篇AHandler类方法全解】-CSDN博客 其他系列 本人系列文章-CSDN博客…...
在canon的生活
街道地址 朝阳区针织路23号中国人寿金融中心33层 大家好!【ji建军】 今天是在我佳能工作的最后一天,1989毕业后入公司,从一而终,三十五年整。 感谢宫里总经理和历届领导对我的信任和教导; (唐晓阳老师、内…...

萤石设备视频接入平台EasyCVR私有化部署视频平台高速公路视频上云的高效解决方案
经济的迅猛发展带来了高速公路使用频率的激增,其封闭、立交和高速的特性变得更加显著。然而,传统的人工巡查方式已不足以应对当前高速公路的监控挑战,监控盲点和响应速度慢成为突出问题。比如,非法占用紧急车道的情况屡见不鲜&…...

如何解决docker镜像下载失败问题
经常用docker的朋友都知道,docker hub的镜像仓库经常访问不通 rootiZwz97kfjnf78copv1ae65Z:~# docker pull ubuntu:18.04 Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.…...

Python_PyCharm无法打开终端命令行最终解决方案(实测)
关于PyCharm在加载库时出现无法打开终端的问题,相信大家已见到网上众多的添加变量的方式,但也有很多童鞋无法解决,那是因为我们忽略了我们测试虚拟化本身的环境因素,不多赘述,请看以下: 环境:V…...

若依-侧边栏开关按钮禁用,侧边栏始终保持展开
若依框架,当首页为echarts图时,侧边栏展开关闭echarts会超出 解决思路: 当菜单为首页时,侧边栏开关按钮禁用,侧边栏始终保持展开 \src\store\modules\app.jstoggleSideBar(withoutAnimation, typeVal) {if (typeVal …...

洛雪音乐 1.6.1| 全网音乐免费听,附加音源
洛雪音乐汇集了多个平台的音乐资源,让你可以免费播放各种热门音乐。有经典怀旧的老歌,有最近火爆网络的热曲,还有很多原创音乐人发布的最新作品。因触动资本利益,现已转为空壳软件,需要导入音源来使用。功能特点包括&a…...
进程(Process)、线程(Thread)和协程(Coroutine)
进程(Process)、线程(Thread)和协程(Coroutine)都是计算机中实现并发的重要概念,它们有以下区别: 进程是操作系统资源分配的最小单位,也是程序的一次执行过程。进程拥有独…...

蓝牙 BLE 详解
参考链接 BLE博客书籍推荐:Intro to Bluetooth Low Energy: The easiest way to learn BLE...

Spring 获取Header
Spring 获取Header 传统方法获取使用 Spring 获取 Header 传统方法获取 尝试获取一个 User-Agent,(表示的是哪个客户端在访问) // 传统方法获取 HeaderRequestMapping("/getHeader")public String getHeader(HttpServletRequest request) {String userAgent reques…...

第8课 字符串
一、字符串的创建 字符串(string)是Python中最常用的数据类型,是不可变序列的一种,序列的通用操作也适用于字符串。字符串的标志性符号是引号,单引号或者双引号都可以(注意:是英文输入法下的引号,必须成对…...

告别繁琐统计,一键掌握微信数据
微信数据管理的挑战在数字时代,微信已成为我们日常沟通和商业活动的重要工具。然而,随着微信号数量的增加,手动统计每个账号的数据变得越来越繁琐。从好友数量到会话记录,再到转账和红包,每一项都需要耗费大量的时间和…...

企业出海网络:SD-WAN与专线混合组网方案
随着越来越多的国内企业进入海外市场,包括出海电商、游戏、社交网络和区块链等领域,它们通常需要使用海外服务器。同时,这些企业在国内也会拥有自己的机房、IDC或依赖其他云服务提供商的机房。在这种情况下,如何实现国内外之间的高…...

胡壮麟《语言学教程》第五版PDF英文版+中文版翻译
胡壮麟《语言学教程》中文版:https://pan.quark.cn/s/9491130ec572 《语言学教程》(英文版)是一部经典的语言学教材,自 1988 年面世以来,被众多高校广泛采用,长销不衰。该教材自出版以来不断修订ÿ…...

DriftingBlues: 1渗透测试
靶机:DriftingBlues: 1 DriftingBlues: 1 ~ VulnHubhttps://www.vulnhub.com/entry/driftingblues-1,625/ 攻击机:kail linux 2024 1,将两台虚拟机网络连接都改为NAT模式,并查看靶机的MAC地址 2,攻击机上做主机扫描发现靶机 靶机I…...
分类算法——决策树 详解
决策树的底层原理 决策树是一种常用的分类和回归算法,其基本原理是通过一系列的简单决策,将数据集划分为多个子集,从而实现分类。决策树的核心思想是通过树形结构表示决策过程,节点代表特征,边代表决策,叶子…...
C# 编程基础:深入解析构造函数与析构函数
在C#中,构造函数和析构函数是特殊的成员函数,它们分别在对象创建和销毁时自动调用。 构造函数 构造函数是一个在创建对象时自动调用的特殊方法,用于初始化对象的状态。它可以有参数,也可以没有参数。一个类可以有一个或多个构造…...

中国大学慕课视频资源分析
右键查看视频信息 关注点在 urls 这个参数,仔细分析就会发现其实是由若干个.ts拓展名和一个.m3u8拓展名的视频文件,每一个.ts视频文件的时长在10秒钟左右。 中国大学MOOC将课程的视频文件拆分成若干个这样的.ts片段,并且用.m3u8记录这些片段…...

Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

云原生安全实战:API网关Kong的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关(API Gateway) API网关是微服务架构中的核心组件,负责统一管理所有API的流量入口。它像一座…...