数据结构
什么是数据结构
在计算机科学中,数据结构是⼀种数据组织、管理和存储的格式。它是相互之间存在⼀种或多种特定关系的数据元素的集合。
说点通俗易懂的话,数据结构就是数据的组织形式,研究的就是把数据按照何种形式存储在计算机中
为什么会有数据结构
- 随着计算机的发展和应⽤范围的拓展,计算机需要处理的数据量越来越⼤,数据类型越来越多,数据之间的关系也越来越复杂。
- 这就要求⼈们对计算机加⼯处理的数据进⾏系统的研究,即研究数据的特性、数据之间存在的关系,以及如何有效的组织、管理存储数据,从⽽提⾼计算机处理数据的效率。
- 数据结构这⻔学科就是在此背景上逐渐形成和发展起来的。
对数据结构更深层次的理解:数据结构研究的是数据本⾝的特性、数据与数据之间的关系,以及如何在计算机中存储数据,从⽽⾼效的处理这些数据。
数据结构的三要素
逻辑结构
逻辑结构:数据中各元素之间的逻辑关系。
它只关⼼数据中各个元素之间的关系,并不关⼼数据是怎么在内存中存储的
常⻅的逻辑结构
- 集合
所有的数据只是在⼀个集合中,彼此之间再没有其他的联系 - 线性结构
数据之间只存在⼀对⼀的关系 - 树形结构
数据之间是⼀对多的关系 - 图结构
数据之间存在多对多的关系
存储结构
存储结构⼜称物理结构,但是存储⼆字更能体现本意,后续我们统称存储结构。
存储结构顾名思义,就是数据在计算机中是如何存储的。
⽤⼤⽩话就是,怎么把这个数据结构存到计算机中。
数据的存储结构主要有顺序存储以及链式存储。
- 顺序存储
把逻辑上相邻的元素存储在物理上也相邻的存储单元中。-数组
在程序中典型处理⽅式即数组。即数据不仅在逻辑上是连续的,物理上也是连续的 - 链式结构
通过指针来存储前⼀个或下⼀个数据元素的地址,从⽽实现元素和元素之间的关系
数据的运算
数据的运算(针对数据的各种操作),包括数据结构的实现,以及基于数据结构上的各种操作
- 创建数据结构;
- 增加数据;
- 删除数据;
- 查找数据;
- 修改数据;
- 排序数据;
- 输出数据;
算法
什么是算法
算法(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渐进时间复杂度的规则
- 时间复杂度函数式T (N)中,只保留最⾼阶项,去掉那些低阶项;
- 如果最⾼阶项存在且不是1 ,则去除这个项⽬的常数系数;
- 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;
}
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!)
算法竞赛中的时间限制和空间限制
- 信息学竞赛中,C++通常设定1到2秒的时间限制,要控制运⾏次数在107⾄108 之间。
- 空间限制在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(n∗logn) | 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 n≤10 | √ | √ | √ | √ | √ | √ | √ | √ |
n ≤ 25 n \le 25 n≤25 | √ | √ | √ | √ | √ | √ | √ | × |
n ≤ 100 n\le 100 n≤100 | √ | √ | √ | √ | √ | √ | × | × |
n ≤ 1 0 3 n \le 10^3 n≤103 | √ | √ | √ | √ | √ | × | × | × |
n ≤ 2 ∗ 1 0 5 n \le 2*10^5 n≤2∗105 | √ | √ | √ | √ | × | × | × | × |
n ≤ 1 0 7 n \le 10^7 n≤107 | √ | √ | √ | × | × | × | × | × |
n ≤ 1 0 9 n \le 10^9 n≤109 | √ | √ | × | × | × | × | × | × |
n ≤ 1 0 18 n \le 10^{18} n≤1018 | √ | × | × | × | × | × | × | × |
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的使⽤,能够极⼤的提⾼编写代码的效率