【数据结构】复杂度

news2024/11/15 0:27:51

在这里插入图片描述

目录

  • 📖什么是数据结构?
  • 📖什么是算法?
  • 📖算法效率
  • 📖时间复杂度
    • 🔖大O的渐进表示法
    • 🔖常见时间复杂度计算举例
    • 🔖面试题:消失的数字
  • 📖空间复杂度
    • 🔖递归的空间复杂度
    • 🔖面试题:轮转数组

📖什么是数据结构?

 数据结构(Data Structure)是计算机存储组织数据的方式,指相互之间存在一种或多种特定关系的数据元素集合。

📖什么是算法?

 算法(Algorithm)就是定义良好的计算过程,它取一个或一组良好的值作为输入,并产生出一个或者一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

📖算法效率

 通常我们会用复杂度去衡量一个算法的好坏。算法在编写成可执行程序后,运行时需要消耗时间资源空间资源(内存资源)。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法的运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小,所以对空间复杂度要求很高,但是现在随着计算机行业的快速发展,计算机的存储容量已经达到了很高的程度,内存成本逐渐降低。所以我们如今已经不再需要特别关心一个算法的空间复杂度。

📖时间复杂度

定义:
 在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所消耗的时间,从理论上来说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要算法都上机测试嘛?是可以都上机测试,但是这很麻烦,并且同一个程序在不同的机器上运行时间可能差异很大,所以我们有了时间复杂度的分析方法。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作执行的次数,为算法的时间复杂度。
 即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

🔖大O的渐进表示法

 实际中我们计算时间复杂度的时候其实并不需要计算精确的执行次数,而只需要知道大概执行次数,所以这里我们引入了大O的渐进表示法。其中大O符号是用于描述函数渐进行为的数学符号

推导大O阶的方法:

  1. 用常数1取代运行时间中的所有加法常数
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且系数是不唯1的常系数,则去除最高项的系数,得到的结果就是大O阶
  4. 如果最终结果是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;//如果没有条语句,最好情况也是O(N*N)
	}
}
  • 最好情况 O ( N ) O(N) O(N)
  • 最坏情况 O ( N 2 ) O(N^2) O(N2)

 最好情况就是数组本身有序,虽然它有序,但是计算机最初并不知道它是有序的,仍需要遍历一遍数组才能知道它是有序的,因此冒泡排序的最好情况是: O ( N ) O(N) O(N)
 冒泡排序一趟可以排好一个元素,最坏情况是数组完全逆序,则第一趟需要交换 N − 1 N-1 N1次,第二趟需要交换 N − 2 N-2 N2次…直到最后一趟只交换一次,把所有的交换次数加起来就得到了冒泡排序最坏情况下的时间复杂度,其实也就是一个等差数列求和,所以最会情况下的时间复杂度是 O ( N 2 ) O(N^2) O(N2)

二分查找

// 计算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 ( 1 ) O(1) O(1)
  • 最坏情况 O ( l o g 2 N ) O(log_2N) O(log2N)

 最好情况是第一次查找就找到目标值,此时时间复杂度就是 O ( 1 ) O(1) O(1)
 二分查找中每一次查找要么找到目标值,要么可以排除掉一半数据,因此最坏情况是执行到当区间只剩下一个值的时候,用数学语言来描述这个过程就是:每折半查找一次就会除一次二,一直到结果为 1 1 1的时候,二分查找结束。 N / 2 / 2 / 2..... / 2 = 1 N/2/2/2...../2=1 N/2/2/2...../2=1。计算执行次数就是看除了多少次2。其结果是除了 l o g 2 N log_2N log2N个2,所以最坏情况就是 O ( l o g 2 N ) O(log_2N) O(log2N),一般可以把简写成 O ( l o g N ) O(logN) O(logN)(仅限于时间复杂度,且只有当底数是2的时候才能简化)

O ( N ) O(N) O(N) O ( l o g 2 N ) O(log_2N) O(log2N)的对比

N N N 1000 1000 1000 100 W 100W 100W 10 亿 10亿 10亿
O ( N ) O(N) O(N) 1000 1000 1000 100 W 100W 100W 10 亿 10亿 10亿
O ( l o g 2 N ) O(log_2N) O(log2N) 10 10 10 20 20 20 30 30 30

 可见 O ( l o g 2 N ) O(log_2N) O(log2N)相较于 O ( N ) O(N) O(N)在效率上有很大的提升,虽然二分查找的效率很高,但是他有一个致命的限制条件就是数组有序,对数组排序也是需要消耗时间的,因此二分查找在实际中使用的并不是很多,用的更多的是红黑树,它的时间复杂度也是 O ( l o g 2 N ) O(log_2N) O(log2N)

递归阶乘

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

	return Fac(N - 1) * N;
}
  • 时间复杂度 O ( N ) O(N) O(N)

 这里涉及到了递归,Fac一共被递归调用了N次,且每一次Fac中的执行次数是1,所以总的执行次数就是 N + 1 N+1 N+1 1 1 1相加,因此时间复杂度就是 O ( N ) O(N) O(N)

斐波那契数列

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

	return Fib(N - 1) + Fib(N - 2);
}
  • 时间复杂度 O ( 2 N ) O(2^N) O(2N)

在这里插入图片描述
 用递归去求解斐波那契额数列,它的时间复杂度是等比数列求和,最终的时间复杂度就是 O ( 2 N ) O(2^N) O(2N)

常见时间复杂度关系
在这里插入图片描述

🔖面试题:消失的数字

在这里插入图片描述
题目出处:消失的数字
分析:
 这道题大家很容易想到的方法就是先对数组进行排序然后再利用二分查找找到缺失的数字。但是注意题目限制了时间复杂度只能在 O ( N ) O(N) O(N),而我们常用的冒泡排序时间复杂度是 O ( N 2 ) O(N^2) O(N2),快排的时间复杂度是 O ( N ∗ l o g N ) O(N*logN) O(NlogN),都不符合题目要求,因此这条路要被我们Pass掉。我们可以考虑异或来解决这个问题,异或有下面两条性质:a^a=0a^0=a,并且异或满足交换律和结合律,所以我们可以先让0~N的所有数字进行异或,然后再和数组里面的所有元素进行异或,最终的结果就是缺失的那个数字,这种方法只需要遍历两边数组,时间复杂度是 O ( N ) O(N) O(N)。下面来看一下这种思路的代码实现:

int missingNumber(int* nums, int numsSize){
    int num = 0;
    for(int i = 0; i <= numsSize; i++)
    {
        num = num ^ i;
    }

    for(int i = 0; i < numsSize; i++)
    {
        num = num ^ nums[i];
    }
    return num;
}

在这里插入图片描述
 处理上面的这种思路还可以直接利用求和公式算出0~N的总和,再减去数组中的所有元素,最终的结果就是缺失的数字,此种方法只需要遍历一遍数组,因此时间复杂度也是 O ( N ) O(N) O(N)

📖空间复杂度

定义:
 空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度的计算规则和时间复杂度类似,也采用大O渐进表示法
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
 一般常见的空间复杂度都是 O ( 1 ) O(1) O(1)或者 O ( N ) O(N) O(N)(额外开辟数组)。

🔖递归的空间复杂度

  • 情形一
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
	if (0 == N )
		return 1;

	return Fac(N - 1) * N;
}

 首先每调用一次函数都会创建一个函数栈帧,而每个函数的空间复杂度可以看作是 O ( 1 ) O(1) O(1),一共递归调用了N+1次Fac函数,因此空间复杂度是 O ( N ) O(N) O(N)。 这里调用的N+1次Fac函数栈帧的地址是不同的,因为它们是递归调用的。
在这里插入图片描述

void Fun1()
{
	int a = 0;
	printf("%p\n", &a);
}

void Fun2()
{
	int b = 0;
	printf("%p\n", &b);
}

int main()
{
	Fun1();
	Fun2();
	return 0;
}

在这里插入图片描述
 程序先调用Fun1函数,创建其相应的函数栈帧,调用结束的时候函数栈帧会被销毁,也就是把空间还给操作系统,接下来调用Fun2函数,创建其相应的函数栈帧,这也就是为什么两次打印的地址是一样的,因为Fun1调用结束的时候把空间还给了操作系统。

void Fun1()
{
	int a = 0;
	printf("%p\n", &a);
}

void Fun2()
{
	int b = 0;
	printf("%p\n", &b);
	Fun1();
}

int main()
{
	Fun2();
	return 0;
}

在这里插入图片描述
 此时两次打印的地址不同,因为是在Fun2函数中调用的Fun1函数,它的执行过程是:先在主函数中调用Fun2函数,创建相应的函数栈帧,接着在Fun2函数中调用了Fun1函数,所以接下来会去创建Fun1的函数栈帧,这就是为什么两次打印的地址不同,函数的递归调用就是类似这样。

  • 情形二
// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
	if (N < 3)
		return 1;

	return Fib(N - 1) + Fib(N - 2);
}

在这里插入图片描述  与普通递归不同的是,这里一次函数调用中会再递归调用两次,具体的调用过程以及调用次序如上图所示,第四次调用结束后函数栈指会销毁,接着进行第五次函数调用,因此第四次调用与第五次调用所创建的函数栈帧其实是同一块,同理第三次和第六次函数调用所创建的函数栈帧也是同一块。综上所述,当求N的斐波那契数列时,一共只会占用 N − 1 N-1 N1个函数栈帧的空间,因为有很多函数栈帧的创建用的是同一块空间,因此递归求解斐波那契数列的空间复杂度是 O ( N ) O(N) O(N),这也说明时间一去不复返,而空间可以重复利用。

🔖面试题:轮转数组

在这里插入图片描述
 这道题可以采用暴力求解的方法,即一次旋转一个数字然后轮转k次,但这样做的时间复杂度是 O ( N 2 ) O(N^2) O(N2),对于这种轮转问题我们可以采用下面时间复杂度为 O ( N ) O(N) O(N),空间复杂度是 O ( 1 ) O(1) O(1)的三步来解决:

  1. 前n-k个逆置
  2. 后k个逆置
  3. 整体逆置
void rotate(int* nums, int numsSize, int k){
    k = k % numsSize;//一定要记得模,这样可以节约时间还可以避免越界
    int i = 0;
    int j = 0;
    int tmp = 0;
    for (i = 0, j = numsSize - k - 1; i < j; i++,j--)//对前n-k个进行逆置
    {
        tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
    for (i = numsSize - k, j = numsSize - 1; i < j; i++, j--)//对后k个进行逆置
    {
        tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
    for (i = 0, j = numsSize - 1; i < j; i++, j--)//对整体进行逆置
    {
        tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

在这里插入图片描述
 针对上面这个题我们还可以采用以空间换时间的办法,即新创建一个数组把原数组的后k个拷贝到新数组的前面,再把原数组的前n-k个拷贝到新数组的后面:

void rotate(int* nums, int numsSize, int k)
{
    k %= numsSize;
    int* arr = (int*)malloc(sizeof(int)*numsSize);
    memcpy(arr, nums+(numsSize-k), sizeof(int)*k);
    memcpy(arr+k, nums, sizeof(int)*(numsSize-k));
    memcpy(nums, arr, sizeof(int)*numsSize);
    free(arr);
}

 此时的时间复杂度是 O ( N ) O(N) O(N),表面上我们看着时间复杂度像是 O ( 1 ) O(1) O(1),但其实并不是,因为memcpy函数内部是一个字节一个字节进行拷贝的,它的时间复杂度是 O ( N ) O(N) O(N),由于新开了一个数组,所以空间复杂度也是 O ( N ) O(N) O(N)


 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!
在这里插入图片描述

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

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

相关文章

I2C协议应用(嵌入式学习)

I2C协议&应用 0. 前言1. 概念2. 特点&工作原理3. 应用示例代码模板HAL模板 0. 前言 I2C是Inter-Integrated Circuit的缩写&#xff0c;它是一种广泛使用的串行通信协议。它由飞利浦&#xff08;现在是NXP Semiconductors&#xff09;开发&#xff0c;并已成为各种电子…

无迹卡尔曼滤波在目标跟踪中的作用(一)

在前一节中&#xff0c;我们介绍了扩展卡尔曼滤波算法EKF在目标跟踪中的应用&#xff0c;其原理是 将非线性函数局部线性化&#xff0c;舍弃高阶泰勒项&#xff0c;只保留一次项 &#xff0c;这就不可避免地会影响结果的准确性&#xff0c;除此以外&#xff0c;实际中要计算雅各…

软件测试面试试卷,答对90%直接入职大厂

一&#xff0e;填空 1、 系统测试使用&#xff08; C &#xff09;技术, 主要测试被测应用的高级互操作性需求, 而无需考虑被测试应用的内部结构。 A、 单元测试 B、 集成测试 C、 黑盒测试 D、白盒测试 2、单元测试主要的测试技术不包括&#xff08;B &…

Linux 如何刷新 DNS 缓存

Linux 如何刷新 DNS 缓存 全文&#xff1a;如何刷新 DNS 缓存 (macOS, Linux, Windows) Unix Linux Windows 如何刷新 DNS 缓存 (macOS, FreeBSD, RHEL, CentOS, Debian, Ubuntu, Windows) 请访问原文链接&#xff1a;https://sysin.org/blog/how-to-flush-dns-cache/&#…

Elasticsearch:install

ElasticSearch Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。 Elasticsearch结合Kibana、Logstash、Beats&#xff0c;也就是elastic stack(ELK)。被广泛应用在日志分析、实时监控&#xff08;CPU、Memory、Program&#xff09;等领域。 elasticsearch是…

【Linux 驱动篇(一)】字符设备驱动开发

文章目录 一、字符设备驱动简介二、字符设备驱动开发步骤1. 驱动模块的加载和卸载2. 字符设备注册与注销3. 实现设备的具体操作函数3.1 能够对 chrtest 进行打开和关闭操作3.2 对 chrtest 进行读写操作 4. 添加 LICENSE 和作者信息 三、Linux 设备号1. 设备号的组成 一、字符设…

网工内推 | 2023应届生专场,上市公司招网工,CCNP以上认证优先

01 浙江宇视科技有限公司 招聘岗位&#xff1a;IT网络工程师 职责描述&#xff1a; 1、负责公司内部核心网络建设&#xff0c;进行网络架构的规划、设计、调整、性能优化&#xff1b; 2、负责公司网络环境的管理&#xff0c;配置&#xff0c;监控、排错&#xff0c;维护&#…

津津乐道设计模式 - 适配器模式详解(家里电器电源标准不统一的问题都解决了)

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

Servlet 相关内容

1. Servlet 1.1 Servlet概述 Servlet 是 SUN 公司提供的一套规范&#xff0c;名称就叫 Servlet 规范&#xff0c;它也是 JavaEE 规范之一&#xff0c;可以通过API来学习。目前在Oracle官网中的最新版本是JavaEE8&#xff0c;该网址中介绍了JavaEE8的一些新特性。当然&#xff…

【C语言初阶】带你轻松玩转所有常用操作符(2) ——赋值操作符,单目操作符

君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 Hello,这里是君兮_&#xff0c;今天给大家带来的是有关操作符的第二部分内容&#xff0c;废话不多说&#xff0c;咱们直接开始吧&#xff01; 在正式开始之前&#xff0c;我们还是借助一张思维导图帮助大致简单回忆一下有…

Docker-compose的使用

目录 Docker-compose 简介 docker-compose的安装 docker-compose.yaml文件说明 compose的常用命令 总结 Docker-compose 简介 Docker-compose 是用于定义和运行多容器的 Docker 应用程序的工具。可以使用YAML文件来配置应用程序的服务。&#xff08;通俗讲是可以通过yml文…

LeetCode108-将有序数组转换为二叉搜索树

题目来源 108. 将有序数组转换为二叉搜索树 - 力扣&#xff08;LeetCode&#xff09; 题目 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵高度平衡 二叉搜索树。 高度平衡二叉树是一棵满足「每个节点的左右两个子树的高度差的…

智慧地下采矿,“像素游戏”智能呈现

在这个像素世界里&#xff0c;我们需要一个智能地下采矿可视化综合管理平台&#xff0c;来帮助我们管理和监控地下采矿全流程。 图扑软件依托自主研发的 HT for Web 产品&#xff0c;结合三维定制化渲染、动态模拟、物理碰撞、5G、物联网、云计算及大数据等先进技术&#xff0c…

从零开始理解Linux中断架构(8)---执行上下文之CPU上下文

1 CPU上下文的来由 CPU上下文是切换任务到CPU时需要保存和恢复的CPU寄存器。ARM64需要保存的寄存器如下图所示 X19-X29作为CPU上下文的依据是什么? 实际上这里使用了一个隐含的事实:Linux所有的任务切换都是在内核中__switch_to函数中进行的,当前任务通过__…

KubeSphere 社区双周报 | OpenFunction 发布 v1.1.1 | 2023.6.9-6.22

KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书、新增的讲师证书以及两周内提交过 commit 的贡献者&#xff0c;并对近期重要的 PR 进行解析&#xff0c;同时还包含了线上/线下活动和布道推广等一系列社区动态。 本次双周报涵盖时间为&#xff1a;2023.6.9-6.22。 …

Elisp之定时器run-with-timer、run-with-idle-timer、run-at-time 区别(二十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

libevent(11)libevent中的循环和退出函数

一、libevent基本原理介绍 一个 event_base 对象相当于一个 Reactor 实例&#xff08;不了解Reactor的读者可自行查询相关文章&#xff09;。libevent默认情况下是单线程的&#xff0c;每个线程有且只有一个event_base&#xff0c;对应一个struct event_base结构体以及附于其上…

由于找不到msvcp120.dll无法继续执行代码怎么办?

msvcp120.dll是微软软件包的一部分。它是一个库文件&#xff0c;可用于支持软件运行时&#xff0c;msvcp120.dll的作用是提供计算机程序所需的标准库&#xff0c;msvcp120.dll还负责管理堆内存、线程和异常处理函数等。在使用windows编写的应用程序中&#xff0c;通常需要使用此…

android 如何分析应用的内存(八)——malloc debug

android 如何分析应用的内存&#xff08;八&#xff09; 接上文&#xff0c;介绍六大板块中的第三个————malloc调试和libc回调 上一篇文章中&#xff0c;仅仅是在分配和释放的时候&#xff0c;拦截对应的操作。而不能进一步的去检查内存问题。比如&#xff1a;释放之后再…

深入理解Android Jetpack Compose的Box

Box是一个提供了一种快速、简便的方式来对其子元素进行层叠布局的布局组件。 一、什么是Box? 二、如何使用Box? 三、Box中的contentAlignment属性 四、使用Modifier在Box内进行更复杂的布局 一、什么是Box? 在Compose中&#xff0c;Box是一个简单的布局组件&#xff0c…