数据结构 | 时间复杂度与空间复杂度

news2024/11/20 4:25:02

🌳🌲🌱本文已收录至:数据结构 | C语言
更多知识尽在此专栏中!
道阻且长,行则将至
🎉🎉🎉欢迎点赞、收藏、关注 🎉🎉🎉

文章目录

  • 🌳前言
  • 🌳正文
    • 🌲时间复杂度
      • 🌱先说概念
      • 🌱大O渐进表示法
      • 🌱示例
        • 🪴题目一
        • 🪴题目二
        • 🪴题目三
        • 🪴题目四
        • 🪴题目五
        • 🪴题目六(递归)
    • 🌲空间复杂度
      • 🌱照例,先说概念
      • 🌱示例
        • 🪴题目一
        • 🪴题目二(递归)
    • 🌲各种复杂度量级展示
    • 🌲相关题目推荐
  • 🌳总结


🌳前言

复杂度是衡量一个算法好坏的标准,可以从 时间空间 两个维度进行比较。可能你之前听说某个算法的时间复杂度是O(N),空间复杂度是O(1),知道这是一个还不错的算法,那么你知道这些复杂度是如何计算出来的吗?本文将会揭开它们神秘的面纱,让你拥有一把衡量算法好坏的度量衡。

🌳正文

先说结论

  • 时间复杂度主要是衡量一个算法的运行快慢,通常由循环决定
  • 空间复杂度主要是衡量一个算法运行所需的额外空间,通常由开辟的空间大小决定

常见错误理解

  • 时间复杂度是就是一段代码实际运行运行所花的时间。这种理解是错误的,因为环境的不同会导致代码运行的快慢,比如将一个大型程序放在你电脑上运行,和放在神威·太湖之光上运行所花的时间是肯定不同的,为了统一评判,我们将算法中基本操作的执行次数,称为算法的时间复杂度
  • 空间复杂度和代码长度有关,代码码越长越复杂。错误,假如我们只创建了常数个变量,纵使代码写的再长,这个算法的空间复杂度也是O(1),在程序中创建的变量个数(在内存中申请的空间大小),称为空间复杂度,创建的变量数越多,算法所占空间就越复杂

当然这只是最基本的知识,关于时间&空间复杂度的更多知识可以往下看


🌲时间复杂度

🌱先说概念

在计算机科学中,算法的时间复杂度是一个函数,它定量地描述了该算法的运行时间

同大多数读者一样,我也不喜欢冗长复杂的官方解释,通俗来说,算法中基本操作的执行次数(循环部分),就是代表了该算法的时间复杂度,比如下面这段代码

//请问这段代码的时间复杂度是多少?
int main()
{
    int N = 0;
    scanf("%d", &N);
    int count = 0;
    int i = 0;
    for (i = 0; i < N; i++)
    {
        count++;
    }
    return 0;
}

直接看循环部分,可以看到这个算法会循环N次,N是可变的,因此这个算法的时间复杂度就是N,简单吧,当然这只是一个最简单的例子,真实的程序循环比这复杂得多,此时就需要一个工具:大O渐进表示法,来帮助我们计算出算法的时间复杂度

🌱大O渐进表示法

O符号:是用来描述函数渐进行为的数学符号,这个符号有点像数学中取极限
大O渐进表示法 的推导步骤:

  1. 去掉已求出时间中的常数项。如果都是常数项,就用常数1代表时间复杂度
    • 比如时间为 2N ^ 2 + 2N + 100,需要去掉常数项100
  2. 取出其中的最高阶项。比如 N、2N与N ^ 2,最高阶项为N^2
    • 接着上面的推导 2N ^ 2 + 2N,显而易见 2N ^ 2 要大于 2N2N ^ 2就是这里的最高阶项
  3. 如果存在常数项 * 最高阶项的情况,就要去除常数项。比如2N,最终复杂度为N
    • 最后在对最高阶项进行处理 2N ^ 2常数项 2 对整体时间复杂度影响是不大的,应该去除

以上就是通过 大O渐进表示法时间复杂度的步骤,当然示例中的时间复杂度最终为O(N ^ 2)

大O渐进表示法 这样表示,是否合理呢?
答:很合理,尤其是放在计算机上使用

首先假设存在这么一个时间复杂度(不用 大O渐进表示法 版): F(N) = N^2 + 2 * N + 10
经过 大O渐进表示法 处理后,变成 F(N) = O(N^2),让我们来测试一组数据:

NF(N) = N^2+2*N+10F(N) = O(N^2)相差率
10100+20+10 = 13010023%
10010000+200+10 = 10210100002%
100001000200101000000000.02%

显然,随着数据的不断增大,二者间的差距会越来越小,而经过 大O渐进表示法 计算后的时间复杂度,是更容易计算的,除非追求精确的数据,否则用 大O渐进表示法 是很合理的~
大O渐进表示法 的核心作用就是去除那些对结果影响不大的项

🌱示例

时间复杂度这一块有几个比较经典的题目需要掌握一下,学会使用 大O渐进表示法 求出时间复杂度

🪴题目一

// 计算Func1的时间复杂度?
void Func1(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);
}

答案:O(N + M)
因为这里有两次循环,并且 NM 都是未知数,无法区分出谁是最高阶项,因此两个都取出,都没有带常数项,不做去除操作。综上 Func1 的时间复杂度就是 O(N + M)

🪴题目二

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

答案: O(1)
显然,这里需要循环100次,都是常数项,直接遵循推导步骤一,时间复杂度O(1)
这里只循环了100次,即使循环1k次、1w次,也都是常数项,一样是 O(1)

🪴题目三

//计算strchr的时间复杂度?
const char* strchr(const char* str, int character);

答案: O(N)
说明:strchr 是一个字符串寻找函数,作用是在字符串str中查找目标字符character
有三种情况:

  1. 最好的情况,只找一次,此时的时间复杂度O(1)
  2. 最坏的情况,没有目标字符,需要把整个字符串找一遍,时间复杂度为 O(N)
  3. 平均的情况,在中间就找到了,时间复杂度O(N / 2)

面对这种多分支情况,我们要做预期管理用最悲观的态度来判断程序,这样做的好处是预期值低,结果出来时不会有很大落差,生活中也可以像这样,做好准备。言归正传,这里选择最坏的情况,即 O(N),当然这种情况比较特殊,值得注意一下

🪴题目四

//计算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;
    }
}

答案: O(N ^ 2)
冒泡排序是一个神奇的算法,每次冒泡比较的趟数都不同,可以这样推导

  • 第一趟:比较 N - 1
  • 第二趟:比较 N - 2
  • 第三趟:比较 N - 3
  • …………
  • 最后一趟: 比较 1

总共比较的次数,就是时间复杂度,即 (N - 1) + (N - 2) + …… + 1,显然这是一个首项为 N - 1,尾项为 1 的等差数列,并且共有 N - 1 项,把高中学的知识用起来,N - 1 项和为 (N - 1) * N / 2 ,通过 大O渐进表示法 进行计算,最终结果为 O(N ^ 2)

🪴题目五

//计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n - 1;
    // [begin, end]:begin和end是左闭右闭区间,因此有=号
    while (begin <= end)
    {
        int mid = begin + ((end - begin) >> 1);
        if (a[mid] < x)
            begin = mid + 1;
        else if (a[mid] > x)
            end = mid - 1;
        else
            return mid;
    }
    return -1;
}

答案: O(logN)
折半查找,一个站在巨人肩膀上的算法,假如我们想在世界范围内找一个人(假设有70亿人,且数据已排序),通过二分查找,最多只需要查找33次,就能找出这个人,厉害吧?下面是二分查找的推导过程
假设需要查找的次数为 k 次,那么可以这样写

  1. N / 2 -> N / 2 ^ 1
  2. N / 4 -> N / 2 ^ 2
  3. N / 8 -> N / 2 ^ 3
  • …………

其中,左边的序号就是查找的次数 k ,可得出式子 N = 2 ^ k ,稍微变换下,得到 k = logN,其中第二个式子就是二分查找的时间复杂度,可能细心的人能发现,我没有写底数 2,不是不写,而是不好写,除非文本编辑器支持插入数学式,否则这个是很难表示的,鉴于这个东西应用的广泛性,有这样一个规定:在底数为 2 时,可以不写底数;如果底数为其他数,就需要写出来,其他底数都很少见的。 在有的地方,会把 lgN 看作 logN,第一种方法是有歧义的,和以 10 为底的对数表示形式一致,这是不太好的,但如果我们看到了,要明白 lgN 也是一个以 2 为底的对数
二分查找为何厉害?因为二分查找在计算时,每次都是对半查找,即 2 ^ k,是一个指数爆炸级查找,因此很快就能找到目标数,听说过一张纸对折64次就能碰到月球的故事吧?指数爆炸是个很庞大的数据

🪴题目六(递归)

//计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
    if (N < 3)
        return 1;
    return Fib(N - 1) + Fib(N - 2);
}

答案: O(2 ^ N)
递归本来就是一个很麻烦的东西,更何况这是计算斐波那契数列

  • 递归中的时间复杂度,计算的是每次递归中执行次数的累加

我们可以将递归斐波那契数列水平展开,即 1+2+4+8+16+32+……+2^N
根据 大O渐进表示法 ,去除影响小的常数项,最终结果为 O(2 ^ N)

相信你看完这些经典例题后,能对 大O渐进表示法 有一个新的认识,加油,你会越来越强的!


🌲空间复杂度

🌱照例,先说概念

算法的空间复杂度是指临时占用储存空间大小的量度

简单理解,空间复杂度是算法中变量的个数(申请的空间大小)。因为变量在使用前,要先声明,而声明会在内存中开辟空间,无论是在堆上还是栈上,都会造成内存损耗,损耗越大,空间复杂度就越高 ,先看代码:

//空间复杂度
int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	return 0;
}

看变量个数,有a、b、c三个变量,属于常数次,所以这个程序的空间复杂度O(1)空间复杂度也遵循大O渐进表示法,这里就不再介绍了,忘记了的同学可以往上翻翻

  • 当出现函数调用时,形参部分空间不计入空间复杂度的计算,递归除外,递归会建立额外的函数栈帧
    • 函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定
  • 大多数情况下,算法的空间复杂度是 O(1)O(N)

眼看千遍,不如手过一遍,下面跟着我一起来看看试题,练练手吧!

🌱示例

🪴题目一

//计算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;
}

答案: O(N)
先看题目,这是一个迭代实现的斐波那契数列,没有使用递归,直接看看创建的变量个数:

  1. fibArray
  2. (n+1) * sizeof(long long)
  3. i

共计申请了三块空间,其中第二块空间最大,为最高阶项,即 n + 1,去除常数项,最终结果为 O(N)

🪴题目二(递归)

//计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
    if (N == 0)
        return 1;
    return Fac(N - 1) * N;
}

答案: O(N)
递归的规则是 先递出,再回归,如果中途遇到递归,继续递出,因此在计算递归空间复杂度时,计算的是每次递归调用的变量个数相加(所开辟的空间),也可以看作递归的深度
显然这里的递归深度是 N,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度自然就是 O(N)

你学会了吗?是不是感觉空间复杂度要比时间复杂度简单些?毕竟现在是大容量时代,内存都变得不值钱了,于是对空间的要求自然而然的变低了。


🌲各种复杂度量级展示

一般算法的常见复杂度类型如图所示

  • 常见复杂度
    这是各种复杂度的关系图
    可以看到二分查找 logN 是真的强!
  • 大O渐进法

🌲相关题目推荐

有些题目中,对时间复杂度和空间复杂度是有要求的,比如下面这两个简单题,用来练手就很不错,动起来吧!关于这两题的题解,后续会发布的

  • 题目一:消失的数字 1

    • 消失的数字
  • 题目二:旋转数组 2

    • 轮转数组

🌳总结

以上就是本次关于时间复杂度空间复杂度的全部内容了,作为数据结构中的第一课,算是比较偏向于理论的部分,学起来也还比较简单,开胃菜嘛,等后面手撕顺序表、链表、二叉树就爽了

如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正
星辰大海

相关文章推荐
你真的了解时间复杂度吗
如何计算时间复杂度
时间复杂度计算 例题合集

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++初阶】类和对象(二)

大家好我是沐曦希&#x1f495; 类和对象1.类的6个默认成员函数2.构造函数2.1 概念2.2 特性3.析构函数3.1 概念3.2 特性4.拷贝构造函数4.1 概念4.2 特征1.类的6个默认成员函数 空类&#xff1a;类中一个成员都没有 可是空类真的什么都没有吗&#xff1f; 并不是&#xff0c;任…

STM32关于UART的接收方式

STM32的 UART 一般分为定长接收和不定长接收 定长接收&#xff1a; HAL_UART_Receive():只能接收固定长度的数据&#xff0c;如果超过固定长度的数据只能接收对应长度&#xff0c;如果小于固定长度则不会接收 HAL_UART_Receive_IT():中断方式接收&#xff0c;每接收一个字节…

CSS 2 CSS 选择器 - 5 2.8 伪选择器 2.8.1 伪类选择器【根据特定状态选取元素】

CSS 文章目录CSS2 CSS 选择器 - 52.8 伪选择器2.8.1 伪类选择器【根据特定状态选取元素】2 CSS 选择器 - 5 2.8 伪选择器 2.8.1 伪类选择器【根据特定状态选取元素】 【什么是伪类】 伪类用于定义元素的特殊状态。 例如&#xff0c;它可以用于&#xff1a; 设置鼠标悬停在…

如何删除ZIP压缩包的密码?

ZIP是比较常用的压缩文件格式&#xff0c;有时候因为工作需要很多人还会给压缩包设置打开密码。那如果后续不需要密码保护了要如何删除密码呢&#xff1f;密码忘记了还能删除吗&#xff1f; 首先来说说第一种情况&#xff0c;也就是知道密码但后续不需要密码保护&#xff0c;只…

1. 初识Python

1. Pythond 简介 Python 语言由荷兰的 Guido Van Rossum (吉多范罗苏姆, 江湖人称龟叔) 在1989年圣诞节期间为了打发圣诞节的无趣而开发的一个脚本解释语言.Python 源代码遵循 GPL(GNU General Public License)开源协议, 也就是说你可以免费使用和传播它, 而不用担心版权的问…

libusb系列-005-部分API简介

libusb系列-005-部分API简介 文章目录libusb系列-005-部分API简介摘要libusb_initlibusb_open_device_with_vid_pidlibusb_kernel_driver_activelibusb_detach_kernel_driverlibusb_claim_interfacelibusb_release_interfacelibusb_attach_kernel_driverlibusb_closelibusb_exi…

【论文翻译】分布式并发控制中时间戳排序算法与本地计数器同步的改进方法

An Advanced Approach of Local Counter Synchronization to Timestamp Ordering Algorithm in Distributed Concurrency Control DOI目录1 介绍2 时间戳排序算法3 本地计数器同步的一种高级方法3.1 改进更新本地计数器的广播消息方式3.2 减少广播消息中的数据传输费用4 结论参…

时间复杂度与空间复杂度

文章目录1.什么是数据结构2.什么是算法3.如何学好数据结构呢3.1写代码3.2 多去动手画图4.算法效率4.1如何评判一个算法的好与坏呢4.2算法的复杂度5.时间复杂度5.1 概念5.2大O渐进法6常见的时间复杂度6.1常数阶6.2线性阶6.3 对数阶6.4平方阶6.5函数调用6.5.1普通调用6.5.2递归调…

1024程序节|Android框架之一 BRVAH【BaseRecyclerViewAdapterHelper】使用demo

文章目录&#x1f353;&#x1f353;BRVAH 上部&#x1f344;&#x1f353;动态图结果展示&#x1f344;&#x1f344;myAdapter.java【第一个布局适配器】&#x1f344;&#x1f344;youAdapter.java【第二个布局适配器】&#x1f344;&#x1f344;MainActivity.java【主活动…

【Android】自制静音App,解决他人手机外放问题

契源 看到一个粉丝留言&#xff0c;吐槽舍友深夜手机外放&#xff0c;打扰别人休息&#xff0c;想设计一款软件阻止舍友行径。于是我就来简单设计一下。 需求实现分析 实际上&#xff0c;我之前有篇博文提到过一个类似的Android APP&#xff0c;主要功能是将手机声音强制开到…

内存函数 memcpy、memmove 的简单模拟实现

一、memcpy 函数 数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。注意是以字节为单位进行拷贝。函数声明如下&#xff1a; 1、参数返回值解析 第二个参数 src&#xff1a;源地址&#xff0c;即你要从哪开始拷贝。 第三个参数 count&#xff1a…

Qt 物联网系统界面开发 “ 2022湖南省大学生物联网应用创新设计竞赛技能赛 ——应用物联网的共享电动自行车 ”

文章目录前言一、实现效果二、程序设计1. 界面背景图设计2. 信号槽设计3. 定时器设计4. 动态曲/折线图的设计5. 摄像头扫码6. 注册设计7. 登录设计8. 巡检人员设计三、综合分析前言 本篇源于 “ 2022 湖南省大学生物联网应用创新设计竞赛技能赛参考样题 ” ——应用物联网的共享…

【git】git ssh 公钥私钥 在 windows和mac 双系统分别如何生成 以及对接各个平台说明

win和mac 双系统分别如何生成 git ssh 一、windows 生成 ssh 公钥私钥 windows版本需要下载git bash&#xff1a;https://gitforwindows.org/ 在 git bash 中输入如下指令&#xff1a; # 创建全局名称&#xff08;将会在你的git提交作者中显示&#xff09;git config --glo…

【allegro 17.4软件操作保姆级教程三】布局操作基础二

4精准定位与坐标定位 在设计中经常会有一些器件或结构孔要摆放在指定位置&#xff0c;如果用move命令用鼠标去移则很难定位完全&#xff0c;这时候就需要精准定位。 操作步骤为&#xff1a; 1、点击move命令&#xff0c;在option面板选择器件原点&#xff0c;这时器件就会悬停在…

策略分析中缺失值的处理方法

在日常的策略分析中&#xff0c;经常会碰到分析的变量出现缺失值的情况&#xff0c;如果对这些缺失值视而不见&#xff0c;则会对策略分析的结果造成一定的影响。那么我们如何处理缺失值呢&#xff1f;关注“金科应用研院”&#xff0c;回复“CSDN”领取“风控资料合集” 首先…

本地数据库IndexedDB - 学员管理系统之登录(一)

IndexedDB是浏览器提供的本地数据库&#xff0c;它可以被网页脚本创建和操作。IndexedDB允许存储大量数据&#xff0c;提供查找接口&#xff0c;还能建立索引。这些都是LocalStorage或Cookie不具备的。就数据库类型而言&#xff0c;IndexedDB不属于关系型数据库&#xff08;不支…

插入排序图解

七大排序之插入排序 文章目录七大排序之插入排序前言一、直接插入排序1.1 算法图解1.2 算法稳定性1.3 插入排序和选择排序相比到底优在哪&#xff1f;二、折半插入排序总结前言 博主个人社区&#xff1a;开发与算法学习社区 博主个人主页&#xff1a;Killing Vibe的博客 欢迎大…

springboot:实现文件上传下载实时进度条功能【附带源码】

0. 引言 记得刚入行的时候&#xff0c;做了一个文件上传的功能&#xff0c;因为上传时间较久&#xff0c;为了用户友好性&#xff0c;想要添加一个实时进度条&#xff0c;显示进度。奈何当时技术有限&#xff0c;查了许久也没用找到解决方案&#xff0c;最后不了了之。 近来偶…

全网最全面的pytest测试框架进阶-conftest文件重写采集和运行测试用例的hook函数

【文章末尾有.......】 使用pytest不仅仅局限于进行单元测试&#xff0c;作为底层模块可扩展性强&#xff0c;有必要理解其运行机制&#xff0c;便于进行二次开发扩展&#xff0c;通过文档的学习很容易理解。 构建一个简单的测试脚本 import pytest import requestsdef add(…

Hive数据倾斜常见场景及解决方案(超全!!!)

Hive数据倾斜常见问题和解决方案 文章目录 前言、一、Explain二、数据倾斜&#xff08;常见优化&#xff09;前言 Hive数据倾斜是面试中常问的问题&#xff0c;这里我们需要很熟练地能举出常见的数据倾斜的例子并且给出解决方案。 一、Explain 我们可以通过sql语句前面加expa…