NO.42十六届蓝桥杯备战|数据结构|算法|时间复杂度|空间复杂度|STL(C++)

发布于:2025-03-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

数据结构

什么是数据结构

在计算机科学中,数据结构是⼀种数据组织、管理和存储的格式。它是相互之间存在⼀种或多种特定关系的数据元素的集合。
说点通俗易懂的话,数据结构就是数据的组织形式,研究的就是把数据按照何种形式存储在计算机中

为什么会有数据结构
  • 随着计算机的发展和应⽤范围的拓展,计算机需要处理的数据量越来越⼤,数据类型越来越多,数据之间的关系也越来越复杂。
  • 这就要求⼈们对计算机加⼯处理的数据进⾏系统的研究,即研究数据的特性、数据之间存在的关系,以及如何有效的组织、管理存储数据,从⽽提⾼计算机处理数据的效率。
  • 数据结构这⻔学科就是在此背景上逐渐形成和发展起来的。
    对数据结构更深层次的理解:数据结构研究的是数据本⾝的特性、数据与数据之间的关系,以及如何在计算机中存储数据,从⽽⾼效的处理这些数据。

数据结构的三要素

逻辑结构

逻辑结构:数据中各元素之间的逻辑关系。
它只关⼼数据中各个元素之间的关系,并不关⼼数据是怎么在内存中存储的

常⻅的逻辑结构

  1. 集合
    所有的数据只是在⼀个集合中,彼此之间再没有其他的联系
  2. 线性结构
    数据之间只存在⼀对⼀的关系
  3. 树形结构
    数据之间是⼀对多的关系
  4. 图结构
    数据之间存在多对多的关系
存储结构

存储结构⼜称物理结构,但是存储⼆字更能体现本意,后续我们统称存储结构。
存储结构顾名思义,就是数据在计算机中是如何存储的。
⽤⼤⽩话就是,怎么把这个数据结构存到计算机中。
数据的存储结构主要有顺序存储以及链式存储。

  1. 顺序存储
    把逻辑上相邻的元素存储在物理上也相邻的存储单元中。-数组
    在程序中典型处理⽅式即数组。即数据不仅在逻辑上是连续的,物理上也是连续的
  2. 链式结构
    通过指针来存储前⼀个或下⼀个数据元素的地址,从⽽实现元素和元素之间的关系
数据的运算

数据的运算(针对数据的各种操作),包括数据结构的实现,以及基于数据结构上的各种操作

  • 创建数据结构;
  • 增加数据;
  • 删除数据;
  • 查找数据;
  • 修改数据;
  • 排序数据;
  • 输出数据;

算法

什么是算法

算法(Algorithm)是指解题⽅案的准确⽽完整的描述,是⼀系列解决问题的清晰指令,算法代表着⽤系统的⽅法描述解决问题的策略机制。也就是说,能够对⼀定规范的输⼊,在有限时间内获得所要求的输出。

  • 算法是可以没有输⼊的,⼀定要有输出的。没有输出的算法是没有意义的。
    简单来说算法就是⼀系列的步骤,⽤来将输⼊数据转化成输出结果
算法好坏的度量

【事前分析法】
算法设计好后,根据算法的设计原理,只要问题规模确定,算法中基本语句执⾏次数和需求资源个数基本也就确定了

算法执⾏所需资源的个数与问题规模n有关。因此可以根据算法执⾏过程中对空间的消耗来衡量算法的好坏,这就是空间复杂度
算法中基本语句总的执⾏次数与问题规模n有关。因此可以根据算法执⾏过程中,所有语句被执⾏的次数之和来衡量算法的好坏,这就是时间复杂度。
综上所述,时间和空间的消耗情况就是我们度量⼀个算法好坏的标准,也就是时间复杂度和空间复杂度

时间复杂度

在计算机科学中,算法的时间复杂度是⼀个函数式 T ( N ) T(N) T(N) ,它定量描述了该算法的运⾏时间。这个函数式 T ( N ) T(N) T(N)计算了程序中语句的执⾏次数

计算⼀下fun中++count语句总共执⾏了多少次

void fun(int N)  
{  
	int count = 0;  
	for(int i = 0; i < N; i++)  
	{  
		for(int j = 0; j < N; j++)  
		{  
			++count; // 执⾏次数是 n*n,也就是 n^2  
		}  
	}  
	for(int k = 0; k < 2 * N; k++)  
	{  
		++count; // 执⾏次数是 2*n  
	}  
	int M = 10;  
	while(M--)  
	{  
		++count; // 执⾏次数 10  
	}  
}

fun 函数 ++count 语句的总执⾏次数:
T (N) = N^2 + 2 × N + 10

  • 当N = 10 时,T (N) = 100 + 20 + 10 = 130
  • 当N = 100 时,T (N) = 10000 + 200 + 10 = 10210
  • 当N = 1000 时,T (N) = 1000000 + 2000 + 10 = 1002010
  • 当N = 10000 时,T (N) = 100000000 + 20000 + 10 = 100020010
⼤O表⽰法

在上述案例中,对N取值进⾏分析,随着N的增⼤,对结果影响最⼤的⼀项是N^2 ,2xN和10可以忽略不计,算法执⾏时间的增⻓率与 T ( N ) T(N) T(N)中的N^2的增⻓率基本⼀致。
因此,在计算时间复杂度的时候,⼀般会把 T ( N ) T(N) T(N)中对结果影响不⼤的项忽略掉,该种表⽰时间复杂度的⽅式称为⼤O渐进时间复杂度-粗略的估计⽅式,只看影响时间开销最⼤的⼀项。
所以对于上述fun函数,其时间复杂度为 O ( T ( N ) ) = O ( N 2 + 2 N + 10 ) O(T(N))=O(N^2+2N+10) O(T(N))=O(N2+2N+10),取最⾼阶项最终为: O ( N 2 ) O(N^2) O(N2)

推导⼤O渐进时间复杂度的规则

  1. 时间复杂度函数式T (N)中,只保留最⾼阶项,去掉那些低阶项;
  2. 如果最⾼阶项存在且不是1 ,则去除这个项⽬的常数系数;
  3. T (N) 中如果没有N 相关的项⽬,只有常数项,⽤常数1 取代所有加法常数。
最优、平均和最差时间复杂度

在n 个整形元素数组中,检测x 是否存在,若存在返回其在数组中的下标,否则返回-1

int find (int a[], int n, int x)  
{  
	for (int i = 0; i < n; i++)
	{  
		if (a[i] == x)  
		return i;  
	}  
	return -1;  
}

![[Pasted image 20250315162034.png]]

21 15 66 34 82 95 53 78 46 17
0 1 2 3 4 5 6 7 8 9

在查找过程中,需⽤x 与数组中元素依次⽐较,因此总的⽐较次数就是该算法的时间复杂度。则:

  • 若待查找元素x 为21 ,只需要⽐较1 次,为算法的最优(好)情况。
  • 若带查找元素x 为17 ,或者是不存在的元素,需要⽐较n 次,为算法的最坏(差)情况。
  • 所有情况的⽐较次数之和,除以总情况,为算法的平均情况。
    查找第⼀个元素需⽐较1次,查找第2个元素需⽐较2次,…,查找第n个元素需⽐较n次,假设每个元素查找的概率相同,则平均⽐较次数为
    f ( n ) = 1 + 2 + 3 + ⋯ + n n = ( 1 + n ) × n 2 n = 1 + n n f(n)=\frac{1+2+3+\dots+n}{n}=\frac{(1+n)\times n}{2n}=\frac{1+n}{n} f(n)=n1+2+3++n=2n(1+n)×n=n1+n
  • 最好时间复杂度为O(1) ;
  • 最坏时间复杂度为O(n) ;
  • 平均时间复杂度为 O ( 1 + n 2 ) O(\frac{1+n}{2}) O(21+n) ,也就是 O ( n ) O(n) O(n)
    但是,⽆论是在竞赛还是⼯程中,算法的时间复杂度⼀般为最差情况。因为最差情况是⼈对⼀件事情所能承受的底线,因此find 算法的时间复杂度为O(n) 。

时间复杂度计算案例

案例⼀
void func1(int N)  
{  
	int count = 0;  
	for(int k = 0; k < 2 * N; k++)  
	{  
		++count;  
	}  
	int M = 10;  
	while (M--)  
	{  
		++count;  
	}  
	printf("%d\n", count);  
}

基本语句 ++count 关于问题规模n总执⾏次数的数学表达式为:f(n) = n × 2 + 10 ;
保留最⾼阶项,省略最⾼阶项的系数后的⼤O渐进表⽰法为:O(n)

案例⼆
void func2(int N)  
{  
	int count = 0;  
	for (int k = 0; k < 1000; k++)  
	{  
		++count;  
	}  
	printf("%d\n", count);  
}

基本语句 ++count 关于问题规模n总执⾏次数的数学表达式为:f(n) = 1000 ;
不论n如何变化, ++count 总的执⾏次数都是1000 ,故时间复杂度为:O(1)

案例三
void func3(int m, int n)  
{
	for (int i = 0; i < m; i++)  
	{  
		printf("hello\n");  
	}  
	for (int i = 0; i < n; i++)  
	{  
		printf("hello\n");  
	}  
}

基本语句 printf(“”) 总的执⾏次数为f(m, n) = m + n ;
m和n的变化都是影响基本语句执⾏次数,即m和n都是问题规模,故时间复杂度为 O ( m , n ) O(m,n) O(m,n) ,或者也可以表⽰为 O ( m a x ( m , n ) ) O(max(m, n)) O(max(m,n))

案例四
void func4(int m, int n)  
{  
	for (int i = 0; i < m; i++)  
	{  
		for(int j = 0; j < n; j++)  
		{  
			printf("%d, ", i);  
		}  
		printf("\n");  
	}  
}

基本语句 printf("%d, ", i) 总的执⾏次数为f(m, n) = m × n ;
m和n的变化都是影响基本语句执⾏次数,即m和n都是问题规模,故时间复杂度为 O ( m × n ) O(m \times n) O(m×n)

案例五
void func5(int n)  
{  
	int cnt = 1;  
	while (cnt < n)  
	{
		cnt *= 2;  
	}  
}

假设执⾏次数为x ,则 2 x = n 2^x=n 2x=n ;因此执⾏次数: x = log ⁡ 2 n x=\log_{2}n x=log2n
⼀般情况下不管底数是多少都可以省略不写,那么本案例的时间复杂度为 log ⁡ n \log n logn

案例六
// ⽤递归计算 N 的阶乘  
long long fac(int N)  
{  
	if(N == 0) return 1;  
	return fac(N - 1) * N;  
}

递归算法时间复杂度求解⽅式为,单次递归时间x总的递归次数。
注意,这⾥只是简易的估算⽅式。递归算法的时间复杂度严谨的计算⽅法是利⽤主定理(Master
Theorem)来求得递归算法的时间复杂度。
对于递归算法,仅需掌握这样简易的计算⽅式

单次递归没有循环之类,所以时间复杂度为常数。总的递归次数就是递归过程中,fac递归调⽤了多少次。
Fac(5) 需要递归6次,则Fac(n) 就需要递归n + 1次,故递归求阶乘的时间复杂度为O(n) 。

空间复杂度

有了时间复杂度的铺垫,空间复杂度的计算就⽐较容易了。
在算法竞赛中,空间复杂度就是整个程序在解决这个问题时,⼀共使⽤了多少空间。相⽐较于时间复杂度,空间复杂度就没那么受关注,因为多数情况下题⽬所给的内存限制是⾮常宽裕的。
但是,这并不表明可以肆⽆忌惮的使⽤空间,⼀旦超出给定的限制,程序也是不会通过的

冒泡排序
#include <iostream>  
using namespace std;  
const int N = 20;  
int arr[N];  
int main()  
{  
	int n = 0;  
	cin >> n;  
	int i = 0;  
	//输⼊  
	for(i=0; i < n; i++)  
	cin >> arr[i];  
	//排序  
	for(i = 0; i < n-1; i++)  
	{  
		int j = 0;  
		for(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;  
			}  
		}  
	}  
	//输出  
	for(i=0; i < n; i++)  
	{  
		cout << arr[i] << endl;  
	}  
	return 0;  
}

空间复杂度:需要⼀个和问题规模⼀致的数组来存数据。因此空间复杂度为O(N)

递归求阶乘

递归算法空间复杂度求解⽅式,单次递归空间复杂度*递归次数。

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

递归的调⽤会有空间消耗

常⻅复杂度的增⻓对⽐

各种常⻅的时间复杂度的⼤⼩对⽐:
O ( 1 ) < O ( l o g N ) < O ( N ) < O ( N l o g N ) < O ( N 2 ) < O ( N 3 ) < O ( 2 N ) < O ( n ! ) O(1) < O(logN) < O(N) < O(NlogN) < O(N^2) < O(N^3) < O(2^N ) < O(n!) O(1)<O(logN)<O(N)<O(NlogN)<O(N2)<O(N3)<O(2N)<O(n!)

算法竞赛中的时间限制和空间限制

  1. 信息学竞赛中,C++通常设定1到2秒的时间限制,要控制运⾏次数在107⾄108 之间。
  2. 空间限制在128MB或256MB ,可以开⼀个 3 × 1 0 7 3 \times 10^7 3×107⼤⼩的int类型数组,或者5000x5000⼤⼩的⼆维数组,⼀般情况下都是够⽤的

各种数据量下,可选⽤的算法的时间复杂度

O ( log ⁡ n ) O(\log n) O(logn) O ( n ) O(\sqrt{n}) O(n ) O ( n ) O(n) O(n) O ( n ∗ log ⁡ n ) O(n*\log n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n 3 ) O(n^3) O(n3) O ( 2 n ) O(2^n) O(2n) O ( n ! ) O(n!) O(n!)
n ≤ 10 n\le 10 n10
n ≤ 25 n \le 25 n25 ×
n ≤ 100 n\le 100 n100 × ×
n ≤ 1 0 3 n \le 10^3 n103 × × ×
n ≤ 2 ∗ 1 0 5 n \le 2*10^5 n2105 × × × ×
n ≤ 1 0 7 n \le 10^7 n107 × × × × ×
n ≤ 1 0 9 n \le 10^9 n109 × × × × × ×
n ≤ 1 0 18 n \le 10^{18} n1018 × × × × × × ×

STL

C++标准库

C++标准是C++编程语⾔的规范,由国际标准化组织(ISO)制定。C++标准的发展历程可以追溯到1998年,当时ISO/IEC14882:1998标准被发布,这是第⼀个C++标准,常被称为C++98。随后,C++标准经历了多次更新和修订,包括C++03(2003年)、C++11(2011年)、C++14(2014年)、C++17(2017年)以及C++20。
最新的C++标准是C++23,于2023年发布,引⼊了许多新特性,但⽬前⽀持完整的编译器较少。
每⼀个版本的C++标准不仅规定了C++的语法、语⾔特性,还规定了⼀套C++内置库的实现规范,这个库便是C++标准库。C++标准库中包含⼤量常⽤代码的实现,如输⼊输出、基本数据结构、内存管理、多线程⽀持等。
我们可以这样简单的理解,C++给我们提供了⼀⼤堆的代码,这些代码⾥⾯包含特别多已经实现好的数据结构和算法。因此,在做题的时候,如果涉及的数据结构和算法C++标准库已经帮我们实现好了,那么就可以直接使⽤,避免重复造轮⼦。
造轮⼦指的是重复发明已有的算法,或者重复编写现成优化过的代码。
造轮⼦通常耗时耗⼒,同时效果还没有别⼈好。但若是为了学习或者练习,造轮⼦则是必要的。 因此,学好C++标准库能极⼤的提⾼编程效率

什么是STL

STL即标准模板库(Standard Template Library),是C++标准库的⼀部分,⾥⾯包含了⼀些模板化的通⽤的数据结构和算法。由于其模板化的特点,它能够兼容⾃定义的数据类型,避免⼤量的造轮⼦⼯作。
NOI和ICPC赛事都⽀持STL库的使⽤,当然,蓝桥杯也是⽀持的。因此,⼀定要学习STL的使⽤,能够极⼤的提⾼编写代码的效率