【数据结构与算法】:关于时间复杂度与空间复杂度的计算(C/C++篇)——含Leetcode刷题

文章目录

      • 一、什么是时间复杂度和空间复杂度?
        • 1.1 算法效率
        • 1.2 时间复杂度的概念
        • 1.3 空间复杂度的概念
        • 1.4 复杂度计算在算法中的意义
      • 二、时间复杂度的计算
        • 2.1 大O渐进表示法
        • 2.2 常见时间复杂度计算举例
      • 三、空间复杂度的计算
      • 四、Leetcode刷题
        • 1. 消失的数
        • 2. 旋转数组

一、什么是时间复杂度和空间复杂度?

1.1 算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

1.2 时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有在电脑上跑起来之后才知道,而且根据电脑硬件配置的不同,同一个程序跑的效率可能是不一样的,所以时间复杂度不是计算一个程序跑的时间长短。而是一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度,时间复杂度通常用大O渐进表示法

1.3 空间复杂度的概念

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法

1.4 复杂度计算在算法中的意义

一张图告诉你复杂度计算的意义:
在这里插入图片描述

二、时间复杂度的计算

2.1 大O渐进表示法
// 请计算一下Func1基本操作执行了多少次?
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);
}

在这里插入图片描述
Func1 执行的操作次数 :
F ( N ) = N 2 + 2 N + 10 F(N)= N^2 + 2N + 10 FN=N2+2N+10

当N = 10, F(N)= 130
当N = 100,F(N)= 10210
当N = 1000,F(N)= 1002010

我们会发现,随着N的增大,这个表达式中N^2对结果的影响是最大的。时间复杂度其实是一个估算,是去看表达式中影响最大的那一项,后面的可以直接忽略掉,类似于数学中的极限。时间复杂度我们用大O的渐进表示法。

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法

1、用常数1取代运行时间中的所有加法常数
2、在修改后的运行次数函数中,只保留最高阶项
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

使用大O的渐进表示法以后,Func1的时间复杂度为:
O ( N 2 ) O(N^2) ON2
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)

例如:在一个长度为N数组中搜索一个数据x

最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到

在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

2.2 常见时间复杂度计算举例

【示例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的执行操作次数:
F ( N ) = 2 N + 10 F(N)= 2N + 10 FN=2N+10
根据上面的大O渐进表示法,最高阶的系数不为1,就去除最高项的系数,即Func2的时间复杂度为:
O ( N ) O(N) ON

【示例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的执行操作次数:
F ( N ) = N + M F(N)= N + M FN=N+M
时间复杂度为:
O ( M + N ) O(M+N) O(M+N)
由于不确定M和N的大小,所以这里都不能忽略掉。
假设给了条件:

M远大于N,那么其时间复杂度就是O(M)
M和N差不多大,那么其时间复杂度就是O(M)或则O(N),相当于两倍的M或则N。

【示例3】

// 计算Func4的时间复杂度?
void Func4(int N)
{
    int count = 0;
    for (int k = 0; k < 100; ++ k)
    {
        ++count;
    }
    printf("%d\n", count);
}

在这里插入图片描述
像这种可以直接知道具体的执行次数的那么那么他的时间复杂度就是:
O ( 1 ) O(1) O1
注意:如果一个算法的时间复杂度为O(1)并不是他执行一次,而是执行了常数次,这个常数不一定是1,可能是10,可能是100,也有可能是1000,反正是一个具体的数。

【示例4】

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, char character )
{
    while(*str != '\0')
    {
        if(*str == character)
            return str;
        ++str;
    }
    return NULL;
}

在这里插入图片描述
对于这个算法要分情况(假设字符串长度为N):

最好情况:只执行一次就找到了所需字符,时间复杂度为O(1)
平均情况:执行到N/2的时候找到所需字符,时间复杂度为O(N / 2)
最坏情况:执行到N次才找到所需字符,时间复杂度为O(N)

像这种需要分情况的算法,我们一般都会采取最坏的打算,毕竟具体的执行次数是不确定的,取最坏情况也就意味着不会出现更差的情况,更加合理。
所以这个算法的时间复杂度就是:
O ( N ) O(N) ON

【示例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;
    }
}

冒泡排序的时间复杂度的计算,假设数组的长度为N:
比较次数:

第一趟冒泡:N
第二趟冒泡:N - 1
第三趟冒泡:N - 2

第N趟冒泡:1

具体的执行次数:
F ( N ) = ( N + 1 ) ∗ N / 2 F(N)= (N + 1)* N / 2 FN=N+1N/2
展开之后得到的时间复杂度就是:
O ( N 2 ) O(N^2) ON2

【示例6】

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n;
    while (begin < end)
    {
        int mid = begin + ((end-begin)>>1);
        if (a[mid] < x)
            begin = mid+1;
        else if (a[mid] > x)
            end = mid;
        else
            return mid;
    }
    return -1;
}

在这里插入图片描述
二分查找的时间复杂度计算,假设数组长度为N:
在这里插入图片描述
使用二分查找首先要确保这个数组是有序的,选定一个中间值,如果所找的值比中间值要大,就可以利用left来缩放空间(mid的取值范围在left和right之间,一般取left和right的中间值),每次查找都能折半,直到找到所需的值。
这种算法也需要分情况:
我们假设找了X次,数组长度为N:

最好情况(X = 1):只找了一次,时间复杂度为O(1)
找的次数:1 * 2 * 2 * 2 … * 2 = N --> 2^X = N
最坏情况:找的次数为

X = log ⁡ 2 N X = \log_2 N X=log2N
在算法的复杂度计算中,习惯省略对数的底数,即这个算法的时间复杂度为:
O ( N ) = l o g N O(N) = logN ON=logN

【示例7】

// 计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{
    return N < 2 ? N : Factorial(N-1)*N;
}

在这里插入图片描述
求10的阶乘:
在这里插入图片描述
递归调用了N次,每次递归运算了 --> O(1)
即这个算法的时间复杂度为:
O ( N ) O(N) ON

常见的复杂度对比
在这里插入图片描述

三、空间复杂度的计算

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少Byte的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大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;
    }
}

在这里插入图片描述
记住一个点:时间是累计的,空间是不累计的,空间是可以重复利用的,for循环走了N次,重复利用的是一个空间。
即这个算法的空间复杂度为:
O ( 1 ) O(1) O1

【示例2】

// 计算Fibonacci的空间复杂度?
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 ;
}

在这里插入图片描述
空间复杂度为:
O ( N ) O(N) ON

【示例3】

// 计算阶乘递归Factorial的空间复杂度?
long long Factorial(size_t N)
{
    return N < 2 ? N : Factorial(N-1)*N;
}

在这里插入图片描述

在这里插入图片描述
每次调用都会创建栈帧,调用了N次,每个栈帧使用了常数个空间O(1),其整体的空间复杂度为:
O ( N ) O(N) ON

四、Leetcode刷题

1. 消失的数

消失的数
在这里插入图片描述
思路一:排序 --> 对于示例输入:0 1 3,后一个数比前一个数大一就说明找到了
这个思路可行,但不符合提议为O(n)
排序 --> 最快排序O(N * logN),不符合。

思路2:把0到n的所有整数加到一起,结果为ret1,把输入示例中数组的数加到一起,结果为ret2,用ret1减去ret2,得到的结果就是所缺失的数。

int missingNumber(int* nums, int numsSize){
    int ret1 = 0;
    // 缺失一个数,那么0到n的所有数的个数就是numsSize的个数加1
    for(int i = 0; i < numsSize + 1; i++)
    {
        ret1 += i;  // 计算0到n之间所有的数的和
    }
    int ret2 = 0;
    for(int j = 0; j < numsSize; j++)
    {
        ret2 += nums[j];  // 计算数组nums中所有数的和
    }
    return ret1 - ret2;
}

在这里插入图片描述

思路3:按位异或,两个数按位异或(二进制),相同为0,相异为1,两个相同的数按位异或得到的就是0,另外,异或是支持交换律的,这意味着不需要排序直接依次异或即可。我们把从0到n之间的所有数与数组中的数依次按位异或,相同的数按位异或直接就等于0,最后得到的结果就是缺失的数。

int missingNumber(int* nums, int numsSize){
    int n = 0;
    for(int i = 0; i < numsSize; i++)
    {
    	// 先跟数组中的数异或
        n ^= nums[i]; // 0异或任何数还是原来那个数 
    }
    for(int j = 0; j < numsSize + 1; j++)
    {
    	// 在跟[0,n]之间所有的数异或
        n ^= j;
    }
    return n;
}

在这里插入图片描述

2. 旋转数组

旋转数组
在这里插入图片描述
题意:输入一个数k,将数组中的每个元素向右移动k位,数组的最后一个元素向右移动移位后就成了数组的第一个元素。

思路一:旋转k次,给一个变量tmp用于存数组的最后一个元素,从数组的最后一个元素开始,与他的前面一个元素互换,然后将tmp赋值给数组的首元素,这是旋转一次的过程,最后循环k次就可以了。
缺陷:Leetcode中有些测试样例将数组给的特别大,跑不过。
在这里插入图片描述
这种算法的时间复杂度为O(N * K)

思路二:以空间换时间,创建一个和nums同样大的数组,将nums数组的后k位元素与前k位元素进行互换,然后在将新数组中的元素拷贝到nums中。
缺陷:时间复杂度为O(N),空间复杂度为O(N),与题意不相符。

思路三:后k个逆置,前n - k个逆置,最后在整体逆置。假设给定一个数组:[1,2,3,4,5,6,7],k = 3,前k个逆置之后变成[1,2,3,4,7,6,5],前n - k个逆置后变成[4,3,2,1,7,6,5],最后在整体逆置后变成[5,6,7,1,2,3,4],最后得到的结果就和测试样例中的一样啦。

样例中可能会出现k大于数组元素的个数,对k取数组大小的余数即可。

// 逆置操作
void Reverse(int *nums, int left, int right)
{
    while(left < right)
    {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }
}
void rotate(int* nums, int numsSize, int k) {
    if(k >= numsSize)
    {
        k %= numsSize;  // 如果k大于数组, 对k进行取模操作
    }
    // 数组后k个逆置
    Reverse(nums, numsSize - k, numsSize - 1);
    // 数组前n - k个逆置
    Reverse(nums, 0, numsSize - k - 1);
    // 数组整体逆置
    Reverse(nums, 0, numsSize - 1);
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/552169.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索设计模式的魅力:融合AI大模型与函数式编程、开启智能编程新纪元

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 ✨欢迎加入探索AI大模型与函数式编程模式融合之旅✨ 在编程世界的广阔疆域里&#xff0c;两大…

组织机构代码是哪几位?营业执照怎么看组织机构代码?

组织机构代码是哪几位? 组织机构代码通常指的是组织机构代码证上的一组特定数字&#xff0c;它用于唯一标识一个组织或机构。在中国&#xff0c;组织机构代码由9位数字组成&#xff0c;前8位是本体代码&#xff0c;最后1位是校验码。这组代码是按照国家有关标准编制的&#x…

第一部分-基础入门-学习导航

专题地址:MacOS一站式程序开发系列专题 第一部分:基础入门学习导航 OSX-01-Mac OS应用开发概述:简单介绍下MacOS生态、Xcode使用以及使用Xcode创建app的方法OSX-02-Mac OS应用开发系列课程大纲和章节内容设计:介绍下此系列专题的文章内容组织形式以及此系列专题的覆盖内容…

帮助中心最核心的内容,你都知道吗?

帮助中心&#xff0c;其实就是个解决问题的“百事通”。当你在使用某产品时&#xff0c;遇到了一些问题&#xff0c;就可以到帮助中心去查询相关的信息以解决问题。很多公司都会搭建帮助中心&#xff0c;那么&#xff0c;帮助中心的核心内容都有哪些呢&#xff1f;这就是今天我…

K8s拉取habor镜像

目录 在daemon.json中添加仓库地址 重新加载daemon.json并重启docker 在目标node节点添加域名 验证目标node是否能正常登录镜像仓库 创建pod资源 加载yml文件 验证 查看pod的ip与端口号 在daemon.json中添加仓库地址 此处需要在创建资源对象所在的节点进行添加 路径&a…

c++的学习之路:24、 二叉搜索树概念

摘要 本章主要是讲一下二叉搜索树的实现 目录 摘要 一、二叉搜索树概念 二、 二叉搜索树操作 1、二叉搜索树的查找 2、二叉搜索树的插入 3、二叉搜索树的删除 三、二叉搜索树的实现 1、插入 2、中序遍历 3、删除 4、查找 四、二叉搜索树的递归实现 1、插入 2、删…

Web前端-Vue组件库Element

黑马程序员JavaWeb开发教程 文章目录 一、快速入门&#xff08;1&#xff09;什么是Element&#xff08;2&#xff09;快速入门 二、常见组件1、表格2、分页&#xff08;Pagination&#xff09;3、表单 三、案例&#xff08;1&#xff09;根据页面原型完成员工管理页面开发&…

vue实现文字转语音的组件,class类封装,实现项目介绍文字播放,不需安装任何包和插件(2024-04-17)

1、项目界面截图 2、封装class类方法&#xff08;实例化调用&#xff09; // 语音播报的函数 export default class SpeakVoice {constructor(vm, config) {let that thisthat._vm vmthat.config {text: 春江潮水连海平&#xff0c;海上明月共潮生。滟滟随波千万里&#xf…

MySQL高级(性能分析-查看执行频次、慢查询日志)

目录 1、SQL性能分析 1.1、SQL执行频率 1.2、慢查询日志 1、SQL性能分析 1.1、SQL执行频率 MySQL 客户端连接成功后&#xff0c;通过 show [ session | global ] status 命令可以提供服务器状态信息。通过如下指令&#xff0c;可以查看当前数据库的 insert、update、delete、…

[CSS]样式属性+元素设置

哎呀&#xff0c;好多东西&#xff0c;根本记不住&#xff0c;更多的还是边用边记吧&#xff0c;这里的代码就当使用范例&#xff0c;但其实如果可以让gpt应该会更好&#xff0c;哎学吧&#xff0c;反正记得住当然更好 文本 属性名描述word-break单词换行。取值如下&#xff1…

海康威视IPC配置NAS

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、安装Samba1.Windows102.Ubuntu-22.04 二、配置IPC总结 前言 简而言之&#xff0c;我手上几个海康威视的IPC都是比较老的设备吧&#xff0c;经过测试不支持…

【C语言】贪吃蛇项目(1) - 部分Win32 API详解 及 贪吃蛇项目思路

文章目录 一、贪吃蛇项目需要实现的基本功能二、Win32 API介绍2.1 控制台2.2 部分控制台命令及调用函数mode 和 title 命令COORD 命令GetStdHandle&#xff08;获取数据&#xff09;GetConsoleCursorInfo&#xff08;获取光标数据&#xff09;SetConsoleCursorInfo &#xff08…

Free day2

1.总结串口的发送和接收功能使用到的函数 发送函数&#xff1a;HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) 参数1&#xff1a;指定要使用的串口 参数2&#xff1a;要发送的数据字节数&#xff…

VIT论文阅读

论文地址&#xff1a;https://arxiv.org/pdf/2010.11929.pdf VIT论文阅读 摘要INTRODUCTION结论RELATEDWORKMETHOD1.VISIONTRANSFORMER(VIT)整体流程消融实验HEAD TYPE AND CLASSTOKENpoisitional embedding 整体过程公式Inductive biasHybrid Architecture 2.FINE-TUNINGANDH…

阿里面试:DDD中的实体、值对象有什么区别?

在领域驱动设计&#xff08;DDD&#xff09;中&#xff0c;有两个基础概念&#xff1a;实体&#xff08;Entity&#xff09;和值对象&#xff08;Value Object&#xff09;。 使用这些概念&#xff0c;我们可以把复杂的业务需求映射成简单、明确的数据模型。正确使用实体和值对…

AI大模型日报#0417:国产音乐SOTA、AI评标师上岗、北大ASC全球超算冠军

导读&#xff1a; 欢迎阅读《AI大模型日报》&#xff0c;内容基于Python爬虫和LLM自动生成。目前采用“文心一言”生成了每条资讯的摘要。 标题: 首个国产音乐SOTA模型来了&#xff01;专为中文优化&#xff0c;免费用&#xff0c;不限曲风 摘要: 昆仑万维发布「天工3.0」和…

华为 2024 届实习招聘——硬件-电源机试题(四套)

华为 2024 届实习招聘——硬件-电源机试题&#xff08;四套&#xff09; 部分题目分享&#xff0c;完整版带答案(有答案&#xff0c;答案非官方&#xff0c;未仔细校正&#xff0c;仅供参考&#xff09;&#xff08;共四套&#xff09; 获取&#xff08;WX:didadidadidida313&…

LeetCode———100——相同的树

目录 ​编辑 1.题目 2.解答 1.题目 . - 力扣&#xff08;LeetCode&#xff09; 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&…

两台服务器如何超快速互传文件/文件夹【xshell详细版 速度真的超快!】

如果您需要将一台服务器上的资料传到另一台服务器上&#xff0c;您如果老实地先下载文件到本地或者另外一个地方&#xff0c;再上传到另外一台服务器上&#xff0c;那这样也太浪费您宝贵的时间了吧&#xff01;在这里&#xff0c;只需要使用一个简单的命令&#xff0c;即可实现…

Python 物联网入门指南(八)

原文&#xff1a;zh.annas-archive.org/md5/4fe4273add75ed738e70f3d05e428b06 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第三十章&#xff1a;制作机械臂 最后&#xff0c;我们终于到达了大多数人自本书开始以来就想要到达的地方。制作一个机械臂&#xff01;在…