数据结构与算法的精髓是什么?复杂度分析【数据结构与算法】

news2024/9/29 19:21:06

  • 代码跑一遍存在什么问题?
  • 什么是 O 复杂度表示法?
  • 如何分析时间复杂度?
  • 常见时间复杂度量级有哪些?
    • O(1)
    • O(logn)
    • O(n)
    • O(nlogn)
    • O(m+n)
    • O(m*n)
    • O(n^2)
    • O(2^n)
    • O(n!)
    • 不同时间复杂度差距有多大?
    • 时间复杂度分析总结
  • 如何分析空间复杂度?
  • 实际项目开发中都会进行不同数据规模的真实测试,有没有必要进行事前的复杂度分析?
  • 最好、最坏情况时间复杂度
  • 平均情况时间复杂度
  • 均摊时间复杂度

数据结构与算法解决的是执行更快和更省资源的问题,快和省通过复杂度来衡量。

代码跑一遍存在什么问题?

  1. 结果依赖环境。
  2. 结果受数据规模影响很大,无法覆盖所有数据场景。

需要使用复杂度分析进行粗略的估计。

什么是 O 复杂度表示法?

以C++为例子,按照语句生成机器语言指令,我们假设每个机器指令执行时间相同为 unit_time,为了方便对每个语句进行分析,将代码写成如下格式:

int func(int n) 
{
    int sum = 0;        //1 * unit_time  

    for (int i = 1;     //1 * unit_time
        i <= n;         //n *  unit_time
        ++i)            //n *  unit_time
    {
        sum = sum + i;  //n *  unit_time
    }
    return sum;//1 * unit_time
}

执行总时间为:(3n + 3 ) unit_time

下面代码进行分析:

int func(int n) 
{
    int sum = 0;    //1 * unit_time  
    for (int i = 1; //1 * unit_time  
        i <= n;     //n *  unit_time
        ++i)        //n *  unit_time
    {
        for (int j = 1; //n *  unit_time
            j <= n;     //n^2 *  unit_time
            ++j)        //n^2 *  unit_time
        {
            sum = sum + i * j;//n^2 *  unit_time
        }
    }
    return sum;//1 * unit_time
}

执行总时间为:(2 * n^2 + 3n + 3) * unit_time

结论:所有代码的执行时间 T(n)与语句执行次数f(n)成正比。

O表示
T(n):代码执行时间。
f(n):每个语句的执行次数总和。
n:数据规模。

大O表示:代码执行时间随数据规模增长的变化趋势,称为渐进时间复杂度。

公式中,高阶对于增长趋势影响最大,所以低阶、常数、系数可以忽略。
上面两个例子忽略之后:
(3n + 3 ) unit_time 时间复杂度为 O(n)
(2 * n^2 + 3n + 3) * unit_time 时间复杂度为 O(n^2)

如何分析时间复杂度?

只关心循环执行最多的一段代码,这段代码是最高阶量级,对增长趋势影响最大。
上面两个例子代码:
(3n + 3 ) unit_time 时间复杂度为 O(n)
(2 * n^2 + 3n + 3) * unit_time 时间复杂度为 O(n^2)

加法法则:不同代码段取最高阶量级。

int func(int n) 
{
    int sum_1 = 0; //1 * unit_time

    for (int p = 1; //1 * unit_time
        p < 100;    //100 * unit_time
        ++p)        //100 * unit_time
    {
        sum_1 = sum_1 + p;  //100 * unit_time
    }

    //上面一段求sum_1代码时间复杂度为:302 * unit_time

    int sum_2 = 0;//1 * unit_time

    for (int q = 1; //1 * unit_time
        q < n;  //n *  unit_time
        ++q)    //n *  unit_time
    {
        sum_2 = sum_2 + q; //n *  unit_time
    }


    //上面一段求sum_2代码时间复杂度为:(3n + 2) * unit_time


    int sum_3 = 0;//1 * unit_time


    for (int i = 1; //1 * unit_time
        i <= n;     //n *  unit_time
        ++i)        //n *  unit_time
    {
        for (int j = 1; //n *  unit_time
            j <= n;     //n^2 *  unit_time
            ++j)        //n^2 *  unit_time
        {
            sum_3 = sum_3 + i * j;//n^2 *  unit_time
        }
    }

    //上面一段求sum_3代码时间复杂度为:(3n^2 + 3n + 2) * unit_time

    return sum_1 + sum_2 + sum_3;//1 * unit_time
}

三段代码时间复杂度求和为最高阶:O(n^2)

总结: T1(n)=O(f(n)),T2(n)=O(g(n)); 那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n)))

乘法法则:嵌套代码,嵌套内外代码复杂度的乘积。

int func(int n) 
{
    int num = 0;    1 * unit_time

    for (int i = 1; //1 * unit_time
        i <= n;     //n *  unit_time
        ++i)        //n *  unit_time
    {
        for (int j = 1; //n *  unit_time
            j <= n;     //n^2 *  unit_time
            ++j)        //n^2 *  unit_time
        {
            fun(n, num);//n^3 *  unit_time
        }
    }
}

int fun(int n, int num)
{
    for (int k = 1; //1 * unit_time
        k <= n;     //n *  unit_time
        ++k)        //n *  unit_time
    {
        num++;      //n *  unit_time
    }
}

func 函数执行时间复杂度为:O(n^2) * O(n) = O(n^3)

总结:T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))

常见时间复杂度量级有哪些?

O(1)

代码执行时间不随数据规模 n 增大而增大,时间复杂度都为O(1)。

int func(int n) 
{
    int sum = 0;    //1 * unit_time
    for (int p = 1; //1 * unit_time
        p < 100;    //100 * unit_time
        ++p)        //100 * unit_time
    {
        sum = sum + p;  //100 * unit_time
    }
}

O(logn)

下面代码:

int func(int n) 
{
    int i = 1;
    while (i <= n) 
    {
        i = i * 2;
    }
}

i 取值为等比数量,每一次取值:
时间复杂度分析
代码修改:

int func(int n) 
{
    int i = 1;
    while (i <= n) 
    {
        i = i * 3;
    }
}

i 取值为等比数量,每一次取值:
时间复杂度分析
对数阶时间复杂度中,忽略对数的 “底”,同意表示为O(logn)。

O(n)

int func(int n)
{
	int sum = 0;//1 * unit_time

	for (int i = 1; //1 * unit_time
		i <= n;     //n *  unit_time
		++i)        //n *  unit_time
	{
		sum++;
	}

	return sum;//1 * unit_time
}

单循环,最高阶量级为O(n)

O(nlogn)

将对数嵌套在循环中:

int func(int n) 
{
    int i = 1;
    for (int j = 0; j < n; ++j)
    {
        while (i <= n)
        {
            i = i * 2;
        }
    }
}

O(m+n)

int func(int m, int n) 
{
	int sum_1 = 0;	//1 * unit_time

	for (int i = 1; //1 * unit_time
		i < m;		//m * unit_time
		++i)		//m * unit_time
	{
		sum_1 = sum_1 + i;//m * unit_time
	}

	int sum_2 = 0;	//1 * unit_time

	for (int j = 1; //1 * unit_time
		j < n;		//n * unit_time
		++j)		//n * unit_time
	{
		sum_2 = sum_2 + j;//n * unit_time
	}

	return sum_1 + sum_2; //1 * unit_time
}

无法实现评估 m 和 n 哪个数据规模更大,所以取最高阶量级之后为:T1(m) + T2(n) = O(f(m) + g(n))

O(m*n)

int func(int n, int m)
{
	int sum = 0;//1 * unit_time
	for (int i = 1; //1 * unit_time
		i <= n;     //n *  unit_time
		++i)        //n *  unit_time
	{
		for (int j = 1; //n *  unit_time
			j <= m;     //n * m *  unit_time
			++m)        //n * m *  unit_time
		{
			sum = sum + i * j;//n * m *  unit_time
		}
	}
}

乘法法则:嵌套代码,嵌套内外代码复杂度的乘积。
总结:T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))

O(n^2)

int func(int n)
{
	int sum = 0;//1 * unit_time

	for (int i = 1; //1 * unit_time
		i <= n;     //n *  unit_time
		++i)        //n *  unit_time
	{
		for (int j = 1; //n *  unit_time
			j <= n;     //n^2 *  unit_time
			++j)        //n^2 *  unit_time
		{
			sum = sum + i * j;//n^2 *  unit_time
		}
	}

	return sum;//1 * unit_time
}

乘法法则:嵌套代码,嵌套内外代码复杂度的乘积。

总结:k层嵌套时间复杂度为O(n^k)。

O(2^n)

int fibonacci(int n) 
{
	if (n <= 1) 
	{
		return n;
	}
	return fibonacci(n - 1) + fibonacci(n - 2);
}

上面代码获取斐波那契数列的第 n 个元素,每个 fibonacci 函数都递归的调用自己 2 次。时间复杂度为O(2^n)

O(n!)

#include <iostream>
#include <vector>

using namespace std;

void permute(vector<int>& nums, int start, int end) 
{
	if (start == end) 
	{
		for (int i = 0; i < nums.size(); i++) 
		{
			cout << nums[i] << " ";
		}
		cout << endl;
	}
	else 
	{
		for (int i = start; i <= end; i++) 
		{
			swap(nums[start], nums[i]);
			permute(nums, start + 1, end);
			swap(nums[start], nums[i]);
		}
	}
}

int main() {
	int n;
	cout << "请输入整数n: ";
	cin >> n;

	vector<int> nums(n);
	for (int i = 0; i < n; i++) {
		nums[i] = i + 1;
	}

	permute(nums, 0, n - 1);

	return 0;
}

上面代码输出全排列,permute 总共会有 n! 次函数调用。时间复杂度为 O(n!)

不同时间复杂度差距有多大?

时间复杂度分析总结

单段代码看高频:比如循环。
多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。
嵌套代码求乘积:比如递归、多重循环等
多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加。

如何分析空间复杂度?

算法的存储空间随数据规模的变化趋势,称为渐进空间复杂度。

void func(int n) 
{
	int* a = new int[n];

	for (int i = 0; i < n; ++i) 
	{
		a[i] = i ;
	}
}

上面代码,空间复杂度为O(n),
常见的空间复杂度就是 O(1)、O(n)、O(n2 )。

实际项目开发中都会进行不同数据规模的真实测试,有没有必要进行事前的复杂度分析?

有必要。
提供了理论分析方向和效率上的感性认识。
不会浪费很多时间。
有这种理论分析的思维,写代码时会尽可能去寻找最优解,也有助于产出性能更高的程序,降低系统开发和维护的成本。
不依赖于环境。

最好、最坏情况时间复杂度

// n表示数组array的长度
int find(int* array, int n, int x) 
{
	int i = 0;
	int pos = -1;
	for (; i < n; ++i) 
	{
		if (array[i] == x) 
		{
			pos = i;
			break;
		}
	}
	return pos;
}

最好情况,第一个元素时查找的变量 x,时间复杂度:O(1)
最坏情况,没有查找到变量x,遍历整个数字,时间复杂度:O(n)

平均情况时间复杂度

// n表示数组array的长度
int find(int* array, int n, int x) 
{
	int i = 0;
	int pos = -1;
	for (; i < n; ++i) 
	{
		if (array[i] == x) 
		{
			pos = i;
			break;
		}
	}
	return pos;
}

总共有 n + 1种情况:

  • 在数组 0 ~ n - 1。
  • 不在数组中。

在数组中概率为1/2,
不在数组中概率为1/2。

平均时间复杂度:
TODO

同一段代码在不同情况下有数量级的差距时才详细分析最好、最坏和平均时间复杂度。

均摊时间复杂度

int* m_array = new int[N];
int m_count = 0;

void insert(int val) 
{
	if (m_count == N)
	{
		int sum = 0;
		for (int i = 0; i < N; ++i) 
		{
			sum = sum + m_array[i];
		}
		m_array[0] = sum;
		m_count = 1;
	}

	m_array[m_count] = val;
	++m_count;
}

向 m_array 插入元素,元素满时将 m_array 当前所有元素值相加放在数组第一个位置。

最好情况时间复杂度,数组未满:O(1)。
最坏情况时间复杂度,数组满:O(n)。
平均情况时间复杂度:
数组中有空闲时,有n种情况,时间复杂度为O(1)。数组中没有空闲时时间复杂度为O(n),总共 n + 1 中情况出现的概率相同。
根据加权平均计算平均时间复杂度为:
TODO

可以使用摊还分析的情况:

  1. 算法大部分操作时间复杂度低,只有个别操作时间复杂度高。
  2. 时间复杂度低的操作和时间复杂度高的操作有时序关系且循环往复。

一般均摊时间复杂度等于最好情况时间复杂度。

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

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

相关文章

2021年06月 Scratch图形化(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共10题,每题2分,共20分) 第1题 执行下列程序,输出的结果为? A:12 B:24 C:8 D:30 答案:B 第2题 执行下列程序,角色说出的内容是? A:2 B:3 C:4 D:5 答案:A 第3题 执行下列程序,输出结果为?

Spring Security 6.x 系列(5)—— Servlet 认证体系结构介绍

一、前言 本章主要学习Spring Security中基于Servlet 的认证体系结构&#xff0c;为后续认证执行流程源码分析打好基础。 二、身份认证机制 Spring Security提供个多种认证方式登录系统&#xff0c;包括&#xff1a; Username and Password&#xff1a;使用用户名/密码 方式…

接口测试:Jmeter和Postman测试方法对比

前阶段做了一个小调查&#xff0c;发现软件测试行业做功能测试和接口测试的人相对比较多。在测试工作中&#xff0c;有高手&#xff0c;自然也会有小白&#xff0c;但有一点我们无法否认&#xff0c;就是每一个高手都是从小白开始的&#xff0c;所以今天我们就来谈谈一大部分人…

Leetcode98 验证二叉搜索树

题意理解&#xff1a; 首先明确二叉树的定义&#xff0c;对于所有节点&#xff0c;根节点的值大于左子树所有节点的值&#xff0c;小于右子树所有节点的值。 注意一个误区&#xff1a; 根节点简单和左孩子&#xff0c;右孩子比大小是不够的&#xff0c;要和子树比&#xff0c;…

30.0/集合/ArrayList/LinkedList

目录 30.1什么是集合? 30.1.2为什么使用集合 30.1.3自己创建一个集合类 30.1.3 集合框架有哪些? 30.1.2使用ArrayList集合 30.2增加元素 30.3查询的方法 30.4删除 30.5 修改 30.6泛型 30.1什么是集合? 我们之前讲过数组&#xff0c;数组中它也可以存放多个元素。集合…

C++基础 -6-二维数组,数组指针

二维数组在内存中的存放方式和一维数组完全相同 下表把二维数组抽象成了行列形式方便理解 a[0]指向第一行首元素地址 a指向第一行的首地址 所以a地址和a[0]地址相同,因为起点相同 但a[0]1往右偏移 但a1往下方向偏移 方便理解 an控制行 a[0]n控制列(相当于*an) 数组指针指向二…

【EI会议投稿】第四届物联网与智慧城市国际学术会议(IoTSC 2024)

第四届物联网与智慧城市国际学术会议 2024 4th International Conference on Internet of Things and Smart City 继IoTSC前三届的成功举办&#xff0c;第四届物联网与智慧城市国际学术会议&#xff08;IoTSC 2024&#xff09;将于2024年3月22-24日在河南洛阳举办。 智慧城市的…

Redis常用操作及应用(二)

一、Hash结构 1、常用操作 HSET key field value //存储一个哈希表key的键值 HSETNX key field value //存储一个不存在的哈希表key的键值 HMSET key field value [field value ...] //在一个哈希表key中存储多个键值对 HGET key fie…

二叉树OJ题讲解之一

今天我们一起来做一道初级的二叉树OJ题&#xff0c;都是用递归思想解答 力扣965.单值二叉树 链接https://leetcode.cn/problems/univalued-binary-tree/description/ 所谓单值二叉树就是这棵二叉树的所有节点的值是相同的&#xff0c;那我们要做这道题&#xff0c;肯定要…

sql注入靶场

第一关&#xff1a; 输入&#xff1a;http://127.0.0.1/sqli-labs-master/Less-1/?id1 http://127.0.0.1/sqli-labs-master/Less-1/?id1%27 http://127.0.0.1/sqli-labs-master/Less-1/?id1%27-- 使用--来闭合单引号&#xff0c;证明此处存在字符型的SQL注入。 使用order …

mybatis collection 错误去重

一、需求背景 一条银行产品的需求&#xff0c;不同阶段&#xff0c;可能对应不同的银行(也有可能是同一个银行处理不同的阶段)去处理&#xff0c;如&#xff1a; 发布阶段: —> A银行处理立项阶段: —> B银行处理审核阶段: —> A银行处理出账阶段: —> C银行处理 …

第二十章——多线程

Windows操作系统是多任务操作系统&#xff0c;它以进程为单位。一个进程是一个包含有自身地址的程序&#xff0c;每个独立执行的程序都称为进程。也就是说每个正在执行的程序都是一个进程。系统可以分配给每一个进程有一段有限的使用CPU的时间&#xff08;也可以称为CPU时间片&…

YOLOv8独家原创改进: AKConv(可改变核卷积),即插即用的卷积,效果秒杀DSConv | 2023年11月最新发表

💡💡💡本文全网首发独家改进:可改变核卷积(AKConv),赋予卷积核任意数量的参数和任意采样形状,为网络开销和性能之间的权衡提供更丰富的选择,解决具有固定样本形状和正方形的卷积核不能很好地适应不断变化的目标的问题点,效果秒殺DSConv 1)AKConv替代标准卷积进行…

第二十章总结

继承Thread 类 Thread 类时 java.lang 包中的一个类&#xff0c;从类中实例化的对象代表线程&#xff0c;程序员启动一个新线程需要建立 Thread 实例。 Thread 对象需要一个任务来执行&#xff0c;任务是指线程在启动时执行的工作&#xff0c;start() 方法启动线程&…

什么是交流负载的特点和特性?

交流负载往往存在不平衡的特性&#xff0c;即三相电流和电压的幅值和相位存在差异。这是由于不同负载的性质和使用情况不同导致的&#xff0c;交流负载的功率因数是描述负载对电网的有功功率需求和无功功率需求之间关系的重要参数。功率因数可以分为正功率因数和负功率因数&…

希尔伯特和包络变换

一、希尔伯特变换 Hilbert Transform&#xff0c;数学定义&#xff1a;在数学与信号处理的领域中&#xff0c;一个实值函数的希尔伯特变换是将信号x(t)与h(t)1/(πt)做卷积&#xff0c;以得到其希尔伯特变换。因此&#xff0c;希尔伯特变换结果可以理解为输入是x(t)的线性时不…

仓库代码迁移,从一个仓库迁移到另一个仓库

A-B 1、在B创建一个新的远程仓库 2、 git clone <原Git仓库地址>git remote add origin1 <新Git仓库地址>git push -u origin1 masterorigin1自定义&#xff0c;上下保持一致

机器视觉中精度和分辨率详解!

在机器视觉中&#xff0c;分辨率作为衡量镜头和工业相机的重要参数&#xff0c;被大家熟知。精度是机器视觉中最核心的参数之一。我们一起来了解下这两个参数以及在实际组合应用中&#xff0c;如何有效匹配镜头分辨率和相机分辨率。 精度需要从多个角度来说明&#xff0c;根据…

免费部署开源大模型 ChatGLM-6B

参考&#xff1a;【大模型-第一篇】在阿里云上部署ChatGLM3-CSDN博客 ChatGLM 是一个开源的、支持中英双语的对话语言模型&#xff0c;由智谱 AI 和清华大学 KEG 实验室联合发布&#xff0c;基于 General Language Model (GLM) 架构&#xff0c;具有 62 亿参数。ChatGLM3-6B 更…

家政预约服务管理系统,轻松搭建专属家政小程序

家政预约服务管理系统&#xff0c;轻松搭建专属家政小程序app&#xff1b; 家政服务app开发架构包括&#xff1a; 1. 后台管理端&#xff1a;全面管理家政服务、门店、员工、阿姨信息、订单及优惠促销等数据&#xff0c;并进行统计分析。 2. 门店端&#xff1a;助力各门店及员工…