算法效率的计算
目录
- 一、如何衡量一个算法的好坏?
- 二、时间复杂度
- 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记录这些片段…...

简单的kafkaredis学习之redis
简单的kafka&redis学习之redis 2. Redis 2.1 什么是Redis Redis是一种面向 “Key-Value” 数据类型的内存数据库,可以满足我们对海量数据的快速读写需求,Redis是一个 NoSQL 数据库,NoSQL的全称是not only sql,不仅仅是SQL&…...

前端性能优化全攻略:提升用户体验,加速页面加载
在当今互联网时代,用户对于网页的加载速度和性能要求越来越高。快速响应的网页不仅能提升用户体验,还能提高网站的搜索引擎排名和转化率。因此,前端性能优化成为了前端开发中至关重要的一环。本文将深入探讨前端性能优化的原则、方法以及如何…...

手机玩亚托莉:我挚爱的时光!手机推gal、躺床玩漫改gal教程
亚托莉:我挚爱的时光是一款视觉与情感交织的好游戏 。游戏背景设定在因为不明原因导致全球海平面上升之后的未来,在全球大多数地方都被海洋淹没城市才是相对环境的情况下,在一场事故失去了一条腿的男主斑鸠夏生却选择了放弃城市,转…...

metasploit/modules/evasion 有哪些模块,以及具体使用案例
Metasploit框架的evasion模块用于生成绕过安全检测的有效载荷。以下是一些常见的evasion模块及其使用案例: 1. 通用Evasion模块 windows/meterpreter/reverse_tcp_rc4:使用RC4加密的反向TCP Meterpreter会话。 set PAYLOAD windows/meterpreter/reverse…...

网络安全入门文档-虚拟机配置篇
前言 虚拟机作为网络安全渗透测试中常见的工具。通常被用来安装kali系统 简单解释一下,目前操作系统分为三类 windows、linux、mac linux又有两个小类,分别是RedHat、Debian 而我们要安装的kali就是基于Debian的操作系统。 简单来说。虚拟机和系统是两个…...

class 041 最大公约数、同余原理
1. 辗转相除法 对下面的证明过程有什么问题和怀疑的直接随便找两个数字自己写一遍就行了. 1.1 利用辗转相除法计算最大公约数 直接记忆这段代码公式就行了(具体的证明过程直接去看左程云老师写的就行了). public static long gcd(long a, long b) { // Greatest Common Di…...

token的创建与解析,并配合拦截器使用
场景: 进行web应用开发时,往往需要对当前用户进行身份判断,此时可以使用token技术 1.导入依赖 <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><scope>runtime<…...

Oracle 数据库历史备份数据恢复验证
Oracle ASM 管理的数据库历史备份数据恢复至单实例数据库 简介: 验证 ASM 管理的数据库的历史备份恢复至单实例数据库(主要目的在于验证历史备份是否可用的一次恢复演练) 一、恢复演练系统选择 根据数据库情况选择恢复测试的环境。 此次恢…...

【网络面积篇】TCP断开连接(笔记)
目录 一. 四次挥手 (1)过程描述 (2)为什么是四次挥手? 二、相关问题 1. 第一次挥手丢失了,会发生什么? 2. 第二次挥手丢失了,会发生什么? 补充:close …...

下跌多少才能涨回来?
文章目录 上涨下跌函数关系函数图形数学分析 上涨下跌函数关系 最近炒股很热,对于股票来说,有个很重要的参数涨跌幅,那么下跌多少才能涨回来?这个不需要太深的知识就可以计算出来,下跌和上涨不是等价的,下跌…...