第一章 数据结构、算法与STL学习笔记
本章主要基本介绍数据结构、算法与STL的相关知识。学习后可能会较为模糊,需要在后续的长期学习中不断加深理解。
数据结构
什么是数据结构
定义:在计算机科学中,数据结构是⼀种数据组织、管理和存储的格式。它是相互之间存在⼀种或多种特定关系的数据元素的集合。
翻译:数据结构就是数据的组织形式,研究的是把数据按照何种形式存储在计算机中。
为什么会有数据结构?
- 随着计算机的发展和应用范围的拓展,计算机需要处理的数据量越来越大,类型越来越多,关系也越来越复杂。
- 数据结构的出现是为了解决数据存储和处理的问题。
- 数据结构是计算机科学的基础,是计算机科学的重要组成部分。
对数据结构更深层次的理解:数据结构研究的是数据本⾝的特性、数据与数据之间的关系,以及如何在计算机中存储数据,从⽽⾼效的处理这些数据。
数据结构的三要素
-
逻辑结构:各元素之间的逻辑关系。
- 集合结构:数据元素同属于⼀个集合,数据元素之间没有任何关系。
- 线性结构:数据元素之间存在⼀对⼀的关系,如按字母顺序排列的通讯录。
- 树形结构:数据元素之间存在⼀对多的关系,如公司架构表。
- 图结构:数据元素之间存在多对多的关系,如人物关系表。
-
存储结构:又称作物理结构,即在计算机中的存储方式,包括顺序存储和链式存储。
- 顺序存储:将数据元素存储在⼀块连续的存储空间中,如数组。
- 链式存储:将数据元素存储在⼀个⼀个的节点中,节点之间通过指针来存储元素地址从而进行连接,如链表。
-
数据的运算:数据的运算(针对数据的各种操作),包括数据结构的实现,以及基于数据结构上的各种操作。
翻译:在了解元素之间的关系与存储关系后,我们需要对数据进行各种操作。
操作包括:
创建数据结构;
增加数据;
删除数据;
查找数据;
修改数据;
排序数据;
输出数据;
…
总结包括:创+增删查改+其他例如:在设计学生信息管理系统时,我们按照学号排序,此时学生之间存在先后关系,我们可以确定:
逻辑结构:线性结构;
存储结构:顺序存储;那么就有以下操作:
- 创建信息管理系统
- 增加学生信息
- 删除学生信息
- 查找学生信息
- 修改学生信息
- 按照成绩排序
…
这些就是数据运算。
算法
什么是算法?
算法(Algorithm)是指对于解题⽅案的准确⽽完整的描述,是⼀系列解决问题的清晰指令,算法代表着⽤系统的⽅法描述解决问题的策略机制。也就是说,能够对⼀定规范的输⼊,在有限时间内获得所要求的输出。
- 算法是可以没有输⼊的,⼀定要有输出的。没有输出的算法是没有意义的。
简单来说算法就是⼀系列的步骤,⽤来将输⼊数据转化成输出结果。
因此,在以往C语言和C++学习阶段的每一个练习题、写的程序都可以称为一个算法。
算法好坏的度量
算法设计好后,根据其原理和问题规模,算法基本语句的执行次数和需求资源个数基本也就确定了。
例如以下代码:
const int N = 10000000;
int a[N];
int sum(int n) {
for (int i = 0; i < N; i++) {
a[i] = i;
}
int ret = 0;
for (int i = 0; i < N; i++) {
ret += a[i];
}
return ret;
}
int sum(int n) {
int ret = 0;
for (int i = 0; i < N; i++) {
ret += i;
}
return ret;
}
以上两段代码都是计算1到N的和,但是第一段代码需要开辟一个大小为N的数组空间,而第二段代码不需要,直接进行求和。
以上算法执行所需资源与问题规模N有关,因此可以根据算法执行过程中对空间的消耗来衡量算法的好坏,即空间复杂度。
基于第二种算法,我们再来看第三种算法:
int sum(int n) {
return (1 + n) * n / 2;
}
这种算法直接利用求和公式进行计算,与第二种算法相同,需求资源与N无关,但同时执行次数与问题规模N有关,因此可以根据算法执行过程中对时间的消耗来衡量算法的好坏,即时间复杂度。
综上,衡量算法好坏的标准包括对于时间和空间的消耗,即时间复杂度和空间复杂度。
时间复杂度
在计算机科学中,算法的时间复杂度是⼀个函数式T(N),它定量描述了该算法的运⾏时间。T(N)函数式计算了程序中语句的执⾏次数。
例:计算以下函数中++count的执行次数:
void func(int n) {
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
count++; /*n*n次*/
}
}
for (int k = 0; k < 2 * n; k++) {
count++; /*2*n次*/
}
int m = 10;
while (m--) {
count++; /*10次*/
}
}
因此,以上函数中++count的执行次数:T(N) = n*n + 2*n + 10。
时间复杂度的大O表示法
显然,在以上计算中,随着N的增大,对结果影响最大的一项是n*n,2*n和10对结果影响较小,T(N)的增长趋势与n*n的增长趋势相同。在计算时间复杂度时,一般会忽略对于结果影响较小的项,这种表示时间复杂度的方式即为大O渐进时间复杂度,即、粗略的进行估算。对于以上的计算其时间复杂度可以用T(N) = O(n*n)表示。
具体规则:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
根据下⾯各个算法时间复杂度的函数式,写出它⼤O表⽰下的时间复杂度。
T(N) = N + 100,T(N) = O(N)。T(N) = 2N + 10,T(N) = O(N)。T(N) = 3N^2 + 2N + 10,T(N) = O(N^2)。T(N) = 4950,T(N) = O(1)。
最优、平均和最差时间复杂度
例:在n个整形元素数组中,检测x是否存在,若存在返回其在数组中的下标,否则返回-1。
int findX(int *a, int n, int x) {
for (int i = 0; i < n; i++) {
if (a[i] == x) return i;
}
return -1;
}
- 最优时间复杂度:在最理想的情况下,算法的时间复杂度,例如在以上算法中x为数组第一项。
- 平均时间复杂度:算法在所有可能情况下的时间复杂度的平均值,例如在以上算法中所有情况之和除以总情况。
- 最差时间复杂度:在最坏的情况下,算法的时间复杂度,例如在以上算法中x为数组最后一项。
则以上算法的时间复杂度为:
- 最优时间复杂度:
T(N) = O(1)。 - 平均时间复杂度:
T(N) = O(N)。 - 最差时间复杂度:
T(N) = O(N)。
但是,⽆论是在竞赛还是⼯程中,算法的时间复杂度⼀般为最差情况。因为最差情况是⼈对⼀件事情所能承受的底线,因此findx算法的时间复杂度为O(n)。
时间复杂度计算案例
案例一:
void func(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的执行次数:T(N) = 2*n + 10,时间复杂度:T(N) = O(n)。
案例二:
void func(int n) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
}
printf("%d\n", count);
}
基本语句++count的执行次数:T(N) = 100,时间复杂度:T(N) = O(1)。
案例三:
void func(int m,int n) {
for (int i = 0; i < n; i++) {
printf("hello\n");
}
for (int j = 0; j < m; j++) {
printf("world\n");
}
}
基本语句printf("hello\n")和printf("world\n")的执行次数:T(N) = n + m,时间复杂度:T(N) = O(n + m),也可以表示为O(max(m,n))
案例四:
void func(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)的执行次数:T(N) = m*n,时间复杂度:T(N) = O(m*n)。
案例五:
void func(int n) {
int cnt = 1;
while (cnt < n) {
cnt = cnt * 2;
}
}
假设执行次数为x,则2^x = n,即x = log2(n),时间复杂度:T(N) = O(log2(n))。
案例六:
long long fac(int n) {
if (n == 1) return 1;
return func(n - 1) + n;
}
递归算法时间复杂度的粗略求解⽅式为单次递归时间*总的递归次数。
fac(5)需要递归6次,则fac(n)需要递归n+1次,时间复杂度:T(1*(N+1)) = O(n)。
空间复杂度
相较于时间复杂度,空间复杂度的计算更为简单,它是对一个算法在运行过程中临时占用存储空间大小的量度,但是在算法竞赛中,大多数题目所给的内存限制都是十分宽裕,因此空间复杂度并不太受关注,这并不表明可以不考虑空间复杂度,一旦超出给定限制,就会被系统判定为内存超限 -MLE。
例:冒泡排序
#include <iostream>
using namespace std;
const int N = 20;
int arr[N];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
return 0;
}
空间复杂度:需要⼀个和问题规模⼀致的数组来存数据。因此空间复杂度为O(N)。
例:递归求阶乘
#include <iostream>
using namespace std;
long long fac(int n) {
if (n == 1) return 1;
return fac(n - 1) * n;
}
递归算法空间复杂度求解⽅式,单次递归空间复杂度*递归次数。
fac(5)需要递归6次,则fac(n)需要递归n+1次,空间复杂度:S(1*(n+1)) = O(n)。
常⻅复杂度的增⻓对⽐
O(1)<O(logN)<O(N)<O(NlogN)<O(N)<O(N^2)<O(N^3)<O(N^3)<O(n!)
算法竞赛中的时间限制和空间限制
- 算法竞赛中,C++通常设定1到2秒的时间限制,即107到108次的运算。
- 通常设定128MB或256MB的空间限制,可以开一个310^7的int数组,或者50005000的二位int数组。
STL
STL是C++标准库中的⼀部分,包括容器、算法和迭代器。
STL的出现是为了解决数据结构和算法的快速开发问题,是C++的⼀部分,因此被称为C++的准标准库。
每⼀个版本的C++标准不仅规定了C++的语法、语⾔特性,还规定了⼀套C++内置库的实现规范,这个库便是C++标准库。C++标准库中包含⼤量常⽤代码的实现,如输⼊输出、基本数据结构、内存管理、多线程⽀持等。
我们可以这样简单的理解,C++给我们提供了⼀⼤堆的代码,这些代码⾥⾯包含特别多已经实现好的数据结构和算法。因此,在做题的时候,如果涉及的数据结构和算法C++标准库已经帮我们实现好了,那么就可以直接使⽤,避免重复造轮⼦。
什么是STL?
STL是Standard Template Library的简称,即标准模板库。
STL是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
4851

被折叠的 条评论
为什么被折叠?



