数据结构初阶---复杂度

一、数据结构前言

1.数据结构与算法

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

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

2.数据结构与数据库

数据结构--->在内存中管理、存储数据--->具有速度快、带电存储(关闭失去数据)的特点。

数据库--->在磁盘中管理、存储数据--->具有速度慢、不带电存储(关闭不会失去数据)的特点。

关于带电存储与不带电存储,简单理解就是,内存中存储数据,断电、程序关闭会销毁数据回收空间,因此称为带电存储;而不带电存储,在磁盘中存储数据,会保存数据到磁盘中,哪怕关闭程序关闭机器,数据依旧保存在了磁盘中。当我们创建一个文件,将数据写入文件,此阶段相当于内存中数据存储,关闭前进行保存,保存操作就是将数据存储在磁盘的一个过程,如果没有保存,机器因特殊情况关闭,数据相当于带电存储,关闭导致数据流失,当我们下次开机时打开文件不会有上次未保存的数据。

我们在C语言中写的通讯录其实就是一个对于顺序表的使用,用到了数据结构相关的知识。

二、复杂度

复杂度的研究无关机器硬件,算法在编写成可执行程序后,运行需要消耗时间与空间(内存)资源,因此衡量一个算法的好坏一般是从时间维度和空间维度两个层面上考虑的--->即时间复杂度空间复杂度

时间复杂度主要衡量一个算法运行速度的快慢;空间复杂度主要衡量一个算法运行所需要消耗的额外空间。

1.时间复杂度

时间复杂度是一个数学意义上的函数(数学表达式),指算法中基本操作的执行次数F(N),N只是一个符号,可以用其他符号,但一般习惯于用N表示。时间复杂度定量描述了算法的运行时间。

这个数学表达式是某条基本语句与问题规模N之间的关系式,所以时间复杂度其实是问题规模N的一个数学意义上的函数。

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 C = 10;
	while (C--)
	{
		count++;
	}
	printf("%d\n", count);
}

上面的代码中,关于count++,执行了多少次呢?

第一个for循环执行了N^2次,第二个for循环执行了2N次,while循环执行了10次,所以一共执行了N^2+2N+10次。

那么对于函数Func1,它的基本操作的执行次数F(N) = N^2+2N+10。

当N逐渐变大的时候,我们发现N^2所占的权重其实逐渐变大,而2N与10所带来的影响越来越小,因此对于时间复杂度而言,采用了大O渐进表示法--->选择基本操作执行次数的数学函数中影响最大的一项作为时间复杂度的函数,对于上图而言,O(N^2)就是上图中Func1函数的时间复杂度。

大O渐进表示法

所谓的渐进,实际上就是估算的意思,即省略了影响较小的函数项,保留影响最大(阶数最大)的那一项。

对于大O渐进表示法来表示时间复杂度,有以下三个标准
①若程序操作执行次数F(N)是常数,无论是1还是100000000(CPU处理100000000次循环的速度与处理1次循环的速度差不多),时间复杂度均为O(1),1代表执行次数为常数。

②若程序操作执行次数F(N)是与问题规模N有关的函数表达式,那么我们保留影响最大的那一项作为该程序的时间复杂度,其余项省略。

③若程序操作执行次数F(N)是与问题规模N有关的函数表达式,我们在省略影响较小项同时,对于影响最大项的常数系数,也进行省略。如O(2*N^2)省略为O(N^2)。

下面是几个简单的能够直接求出操作执行次数的实例:

// 计算Func2的时间复杂度?
void Func2(int N)
{
 int count = 0;
 for (int i = 0; i < 2 * N ; i++)
 {
 count++;
 }
 int C = 10;
 while (C--)
 {
 count++;
 }
 printf("%d\n", count);
}

如上图F(N)=2N+10,根据大O渐进表示法--->时间复杂度为O(N)。

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

上图的F(N)=M+N,由于M与N都是未知的,因此一般而言时间复杂度为O(M+N)或O(max(M,N)),如果能够判断M与N的大小关系,那么①M远大于N--->O(M) ②N远大于M--->O(N) ③M与N差不多大小--->O(M)或者O(N)。

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

上图for循环执行100次,常数次的一个循环--->时间复杂度为O(1),1代表常数次执行次数。同理,k<100000000也是O(1)。

那么还有一些无法简单判断出时间复杂度的代码:

const char * my_strchr ( const char * str, int character )
{
    while(*str)
    {
        if(*str == character)
            return str;
        str++;
    }
    return NULL;
}

前面学过一个函数strchr的模拟实现,在字符串中查找是否存在字符character,存在返回字符串,不存在返回NULL。那么对于这样一个模拟实现,它的时间复杂度是多少呢?我们可以判断,这个while循环是不一定走完字符长度的次数的。可能在中途查找到了字符character而直接返回,也可能开头首字符就直接返回,也有可能最后一个字符才匹配,甚至是返回空指针。因此我们发现,针对这个函数模拟实现内部的循环,无法做到准确判断时间复杂度,这个时候对于大O渐进表示法,它给出了标准:如果无法判断时间复杂度,那么根据最坏的情况进行计算时间复杂度,那么对于strchr函数的模拟实现而言,最坏的情况就是没有字符匹配而返回了空指针,这个时候相当于while循环遍历了整个字符数组,所以时间复杂度为O(N)。

综上,上图strchr模拟实现的函数的时间复杂度为O(N)。

当有些算法的时间复杂度存在最好、平均和最坏情况时:

        最坏情况:任意输入规模的最大运行次数(上界);

        平均情况:任意输入规模的期望运行次数;

        最好情况:任意输入规模的最小运行次数(下界)。

在实际中一般情况关注的是算法的最坏运行情况。

思路判断时间复杂度是本质

我们在实际情况中,并不是先写出代码再来判断时间复杂度,通常是,我们想出解决问题的逻辑--->思考逻辑实现的时间复杂度--->选择出时间复杂度最低的逻辑方法--->实现代码。

因此,不能只会判断简单代码、能够直接计算复杂度的代码的时间复杂度,更重要的是我们需要学会通过逻辑与思路,来判断代码的时间复杂度。

那么下面会举一些相关思路的复杂的例子:

//冒泡排序---降序
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n-1; i++)
	{
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (arr[j] < arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

对于上图降序的冒泡排序函数,我们不能片面理解复杂度,认为复杂度看循环就可以了,那其实我们就把复杂度看窄了,对于简单代码,确实可以直接通过循环判断,但是一旦复杂,通过循环无法清晰得到结论,我们需要看思路:冒泡排序思路是分两大块,①n个元素排n-1趟,②每一趟交换的次数。那么我们很容易就能够发现,第一趟---交换n-1次,第二趟---交换n-2次,第三趟---交换n-3次,......第n-1趟---交换1次,那么其实对于冒泡排序而言,它整体的这个执行次数F(N)是一个等差数列n-1、n-2、n-3、......、3、2、1的和,那么就是(n-1)*n/2,那么时间复杂度就是O(n^2)。

Extra---题目---消失的数字

那么我们接下来看一个题:数组nums包含了从0--n的所有整数,但其中缺失了一个,请编写代码找出缺失的那个整数,实现Missing_Num函数即可,int Missing_Num(int* arr,int arrsize)。

如果是一个项目,给我们的任务是解决上述问题,那么我们应该想出解题逻辑思路,大致在脑海中计算思路所需要的时间复杂度,然后选择时间复杂度最低的思路,也就是最好的、速度最快的思路进行实现。

那么我们能够想到什么方法去解决这个问题呢? 

①冒泡排序升序数组nums,然后遍历数组逐个与下标比较,不相等处的下标即为缺失的元素。

方法①,使用了冒泡排序,遍历数组--->F(N)=(n-1)*n/2+n=(n+1)*n/2--->时间复杂度O(n^2)。

②找单身狗1中使用过异或操作符,相同为0相异为1,我们将0-n异或,再异或数组所有元素得到的就是缺失的整数。

方法②,使用一次for循环或者使用两次(不嵌套)就可以解决问题F(N)=2N+1--->时间复杂度O(N)。

③可以0-n所有整数相加,然后减去数组中所有元素,最后的值就是缺失整数。

方法③,减数组所有元素遍历一次即可,F(N)=N--->时间复杂度O(N)。

那么如此进行分析,我们就可以排除方法①,选择方法②或者方法③来解决这个问题。

使用方法②:

int Missing_Num(int* arr, int arrsize)
{
	int ret = 0;
	for (int i = 0; i < arrsize; i++)
	{
		ret ^= arr[i];
	}
	for (int i = 0; i <= arrsize; i++)
	{
		ret ^= i;
	}
	return ret;
}

其实也可以写成一个循环:

使用方法③:

int Missing_Num(int* arr, int arrsize)
{
	int n = arrsize;
	int sum = ((n + 0) * (n + 1)) / 2;
	for (int i = 0; i < arrsize; i++)
	{
		sum -= arr[i];
	}
	return sum;
}

时间复杂度的进阶案例


二分查找函数:前提条件是,这个数组是有序数组。

// 计算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-1;
 else
 return mid;
 }
 return -1;
}

那么对于一个二分查找的函数,它的时间复杂度是多少呢?二分查找每次,将需要查找的数据与数组中间的数进行比较,然后每次缩减数组一半的数据:

所以依据思想,有关于二分查找的时间复杂度为O(logN)。

阶乘递归函数:

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

虽然函数内没有循环,但是是一个递归函数,函数会被多次重复调用,因此执行次数会随着传入参数的变化而增加:

所以传入参数N,其实一共就递归调用了N次函数,因此时间复杂度为O(N)。

在阶乘递归函数中加入一个for循环:

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

在递归函数中加入一个有关于参数N的循环,那么我们依旧通过图解来判断这个递归函数的循环次数:

斐波那契函数的递归:

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

依旧是函数内没有循环,只有函数递归的调用次数:

F(N)执行次数的计算是一个等比数列的和,所以时间复杂度为O(2^N)。

2^N的时间复杂度,效率太低了,因此要对递归的斐波那契函数进行修改:可以使用for循环进行前两个数累加求得下一个数的方式,这样的话时间复杂度为O(N)。

// 返回斐波那契数列的前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;
}

2.空间复杂度

对于空间复杂度而言,同样采用大O渐进表示法,也同样遵循大O渐进表示法的三个准则。

空间复杂度也是一个数学表达式的函数,是对一个算法在运行过程中临时占用存储空间大小的量度。它的衡量标准不是操作的执行次数,而是在函数中为了完成任务解决问题额外创建的变量个数

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

那么我们拿出前面的冒泡排序函数的例子:

//冒泡排序---降序
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n-1; i++)
	{
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (arr[j] < arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

对于这个冒泡排序函数,我们在函数中额外创建的变量就是i、j、tmp,空间大小是能够确定的,因此空间复杂度为O(1)。

对于上面改装的斐波那契函数,它的空间复杂度是多少呢?

//空间复杂度?
// 返回斐波那契数列的前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;
}

为了返回斐波那契数列的前n项,我们使用malloc动态开辟了一块空间,空间内有n+1个长整型的斐波那契数,那么这个空间就是属于额外开辟的空间,就会计算在空间复杂度当中,因此空间复杂度为O(N)(依据标准简化)。

对于前面的阶乘的递归函数,它的空间复杂度是多少呢?

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

函数递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N),一共调用N次--->空间复杂度为O(N)。

有关递归函数空间复杂度的计算,也是空间累加,不同的是空间可以重复利用!

如下面斐波那契函数:

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

我们在调用Fib(N-1)的时候,分裂导致会向下依次调用Fib(N-2)与Fib(N-3),那么我们调用Fib(N-1)时创建的栈帧其实没有销毁,这个栈帧要等到Fib(N-1)计算完之后才能销毁,因此在分裂的空间利用中,存在重复的空间多次利用!

关于函数栈帧销毁与空间回收后的重复利用,我们可以用下图来做一个验证:

如图我们发现,在func1函数栈帧中创建的临时变量a的地址与在func1函数栈帧中创建的临时变量b的地址相同,说明了由于func1与func2函数的调用相差不大,系统在回收了func1的空间后将该空间给func2使用,因此a与b前后具有相同的地址!

3.常见复杂度对比

1314O(1)常数阶
2N+9O(N)线性阶
2N^2+7N+10O(N^2)平方阶
7log(2)N+1O(logN)对数阶
3Nlog(2)N+9N+9O(NlogN)NlogN阶
N^3+3N^2+10O(N^3)立方阶
2^N+8O(2^N)指数阶

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

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

相关文章

二叉树的层次遍历

二叉树的层次遍历 题目 https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ 描述 给你一个二叉树,请你返回其按 层次遍历 得到的节点值(即逐层地,从做到右访问所有节点) 代码实现 通过两个数组来交替打印 class Solution(object):def levelOrder

网络安全中的数据科学如何重新定义安全实践?

组织每天处理大量数据&#xff0c;这些数据由各个团队和部门管理。这使得全面了解潜在威胁变得非常困难&#xff0c;常常导致疏忽。以前&#xff0c;公司依靠 FUD 方法&#xff08;恐惧、不确定性和怀疑&#xff09;来识别潜在攻击。然而&#xff0c;将数据科学集成到网络安全中…

【Linux系统】—— 基本指令(四)

【Linux系统】—— 基本指令&#xff08;三&#xff09; 1「find」指令2 「grep」指令2.1 初识「grep」指令2.2 「grep」指令 选项 3 打包压缩基本知识4 「zip / unzip」指令5「tar」命令6 文件互传6.1 Linux 与 Windows 互传6.1.1 Linux向Windows传输6.1.2 Windows向Linux传输…

将django+vue项目发布部署到服务器

1.部署django后端服务 部署架构 1.1 下载依赖插件 pip3.8 freeze > requirements.txt1.2 安装依赖插件 pip3 install -r requirements.txt1.3 安装mysql数据库 apt install mysql-server初始化数据库 CREATE USER admin% IDENTIFIED WITH mysql_native_password BY 123…

网络层协议IP

对于网络层我们直接通过IP协议来了解其内容 一.IP协议 首先我们先来了解几个概念&#xff1a; 主机&#xff1a;配有IP地址&#xff0c;但是不进行路由控制的设备 路由器&#xff1a;配有IP地址&#xff0c;同时进行路由控制的设备 节点&#xff1a;主机和路由器的统称 所以现在…

AIGC-----AIGC在虚拟现实中的应用前景

AIGC在虚拟现实中的应用前景 引言 随着人工智能生成内容&#xff08;AIGC&#xff09;的快速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的应用也迎来了新的契机。AIGC与VR的结合为创造沉浸式体验带来了全新的可能性&#xff0c;这种组合不仅极大地降低了VR内容的…

如何利用 Puppeteer 的 Evaluate 函数操作网页数据

介绍 在现代的爬虫技术中&#xff0c;Puppeteer 因其强大的功能和灵活性而备受青睐。Puppeteer 是一个用于控制 Chromium 或 Chrome 浏览器的 Node.js 库&#xff0c;提供了丰富的 API 接口&#xff0c;能够帮助开发者高效地处理动态网页数据。本文将重点讲解 Puppeteer 的 ev…

【运维】 使用 shell 脚本实现类似 jumpserver 效果实现远程登录linux 服务器

实现效果 通过序号选择登录&#xff1a; 配置证书登录 配置证书登录可以免去每次都输入密码的麻烦。详见另一篇博文&#xff1a; 【ssh】使用秘钥对&#xff08;公钥/私钥&#xff09;登录linux主机以及原理介绍 自动登录脚本 直接复用以下脚本即可&#xff0c;在 server…

sqlmap学习,打靶sqli-labs.(1-19)

前言&#xff1a;用于学习sqlmap的简单使用&#xff0c;使用sqli-labs靶场进行测试。 当然,在实战中,考虑的更多&#xff0c;例如如何隐藏自己(特征码),编码加解密、sqlmap抓包调试分析等... 不过那些都是后话&#xff0c;太遥远...基础NO.1&#xff01;&#xff01; 先贴上我…

A045-基于spring boot的个人博客系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

[RabbitMQ] 保证消息可靠性的三大机制------消息确认,持久化,发送方确认

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

Unity中动态生成贴图并保存成png图片实现

实现原理&#xff1a; 要生成长x宽y的贴图&#xff0c;就是生成x*y个像素填充到贴图中&#xff0c;如下图&#xff1a; 如果要改变局部颜色&#xff0c;就是从x1到x2(x1<x2),y1到y2(y1<y2)这个范围做处理&#xff0c; 或者要想做圆形就是计算距某个点&#xff08;x1,y1&…

sklearn学习

介绍&#xff1a;scaler&#xff1a;换算的意思 1. 归一化MinMaxScaler() 归一化的意思是将一堆数&#xff0c;如果比较离散&#xff0c;为了让数据更适合模型训练&#xff0c;将离散的数据压缩到0到1之间&#xff0c;以方便模型更高效优质的学习&#xff0c;而对数据的预处理…

windows下安装wsl的ubuntu,同时配置深度学习环境

写在前面&#xff0c;本次文章只是个人学习记录&#xff0c;不具备教程的作用。个别信息是网上的&#xff0c;我会标注&#xff0c;个人是gpt生成的 安装wsl 直接看这个就行&#xff1b;可以不用备份软件源。 https://blog.csdn.net/weixin_44301630/article/details/1223900…

Flutter:启动屏逻辑处理02:启动页

启动屏启动之后&#xff0c;制作一个启动页面 新建splash&#xff1a;view 视图中只有一张图片sliding.png就是我们的启动图 import package:flutter/material.dart; import package:get/get.dart; import index.dart; class SplashPage extends GetView<SplashController…

【AIGC】如何准确引导ChatGPT,实现精细化GPTs指令生成

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | 提示词Prompt应用实例 文章目录 &#x1f4af;前言&#x1f4af;准确引导ChatGPT创建爆款小红书文案GPTs指令案例&#x1f4af; 高效开发GPTs应用的核心原则明确应用场景和目标受众构建多样化风格模板提问与引…

【通俗理解】隐变量的变分分布探索——从公式到应用

【通俗理解】隐变量的变分分布探索——从公式到应用 关键词提炼 #隐变量 #变分分布 #概率模型 #公式推导 #期望最大化 #机器学习 #变分贝叶斯 #隐马尔可夫模型 第一节&#xff1a;隐变量的变分分布的类比与核心概念【尽可能通俗】 隐变量的变分分布就像是一场“捉迷藏”游戏…

云计算-华为HCIA-学习笔记

笔者今年7月底考取了华为云计算方向的HCIE认证&#xff0c;回顾从IA到IE的学习和项目实战&#xff0c;想整合和分享自己的学习历程&#xff0c;欢迎志同道合的朋友们一起讨论&#xff01; 第三章&#xff1a;常见设备 交换机 二层交换机和三层交换机&#xff0c;所谓二层交换机…

「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用

1. 谷歌浏览器安装及使用流程 1.1 准备篡改猴扩展程序包。 因为谷歌浏览器的扩展商城打不开&#xff0c;所以需要准备一个篡改猴压缩包。 其他浏览器只需打开扩展商城搜索篡改猴即可。 没有压缩包的可以进我主页下载。 也可直接点击下载&#xff1a;Chrome浏览器篡改猴(油猴…

I.MX6U 裸机开发20. DDR3 内存知识

I.MX6U 裸机开发20. DDR3 内存知识 一、DDR3内存简介1. DDR发展历程SRAMSDRAMDDR1DDR2DDR3DDR4DDR5 2. 开发板资源3. DDR3的时间参数1. 传输速率2. tRCD3. CL 参数作用取值范围工作原理4. tRC参数原理单位与取值5. tRAS重要性及作用 二、I.MX6U MMDC 控制器1. MMDC简介&#xf…