程序基础:数据类型、变量、输入与输出、运算符与表达式、程序结构、数组、函数、字符串、结构体、指针、类和对象。 以上内容参见PPT图。

数据结构

算法


 


【1.数据结构——容器】

在C++标准库中,有多种容器(containers)可供使用,每种容器都有其特定的用途和特性。以下是一些主要的C++容器及其简要描述:

每种容器都有其特定的用途和性能特性,因此在选择容器时,你应该考虑你的具体需求,如元素的数量、是否需要排序、是否需要快速随机访问等。

【数据对: pair】

pair 是一个模板类,它表示一个包含两个元素的固定大小的异种容器。这两个元素可以是不同的类型,通常被称为 first 和 second。pair 可用于将两个可能不同类型的值组合成一个独立的单元。

1.包含头文件

2.创建pair对象:

3.给数据对赋值

想要更多的数据对,可以用数据或array来存储pair 以数组为例: 4.创建一个pair数组:

5.初始化赋值

6.pair应用举例

NOI / 1.10编程基础之简单排序 01:谁考了第k名 描述 在一次考试中,每个学生的成绩都不相同,现知道了每个学生的学号和成绩,求考第k名学生的学号和成绩。 输入 第一行有两个整数,分别是学生的人数n(1≤n≤100),和求第k名学生的k(1≤k≤n)。 其后有n行数据,每行包括一个学号(整数)和一个成绩(浮点数),中间用一个空格分隔。 输出 输出第k名学生的学号和成绩,中间用空格分隔。(注:请用%g输出成绩) 样例输入 5 3 90788001 67.8 90788002 90.3 90788003 61 90788004 68.4 90788005 73.9 样例输出 90788004 68.4

解题思路: 1.每个学生有两个数据,可以选用 pair数据对 来存数据。 2.用 sort() 函数对数据对排序,sort()默认按第一个元素排,这里可以把数据对的first存为成绩,second存为学号。 3.输出第k个*/

参考代码:

 

 

 

【动态数组: vector】

概念:类模板中的一种顺序容器,是一种动态数组,它可以根据需要增长或缩小。 性质: 动态大小:vector的大小不是固定的,它可以在运行时增长或缩小。 连续存储:vector的元素在内存中连续存储,这意味着可以通过指针算术快速访问任意元素。 自动管理内存:vector自动管理其内部元素的内存分配和释放,从而减少了内存泄漏和重复释放的风险。 随机访问迭代器:vector提供随机访问迭代器,允许在常数时间内访问任何元素(通过索引)。 插入和删除:虽然vector在元素末尾插入和删除的效率非常高(通常为常数时间),但在中间插入或删除元素可能需要移动其他元素,因此效率较低(通常为线性时间)。 用途: 动态数组:vector最常用的用途是作为动态数组,用于存储可变数量的同类型元素。 算法和数据结构:vector可以与C++标准库中的算法结合使用,以实现各种数据结构和算法,如排序、搜索和图形算法。 与其他容器的交互:vector可以与其他容器(如set、map等)进行交互,例如将vector中的元素插入到另一个容器中,或从另一个容器中复制元素到vector中。

声明一个动态数组:

使用说明: 1.要使用vector对象,必须包含头文件 2.vector包含在名称空间std中,所以需要加 using namespace std; 或 vector。 3.模板使用不同的语法来指出它存储的数据类型。 如:vector<int> vi; //声明vi是一个vector对象,是一个存储整数的动态数组。 4.vector类使用不同的语法来指定元素个数。 如:vector<int> vi(5); //vi可以存5个int型元素。

vector的常用用法:

1.包含头文件

2.创建vector

3.访问元素

使用下标操作符[]at()成员函数访问元素。注意,at()成员函数会检查索引是否越界,如果越界会出错(抛出out_of_range异常)。

4.修改元素

使用下标操作符[]直接修改元素。

5.添加元素

使用push_back()成员函数在vector的末尾添加元素。

6.删除元素

7.遍历vector

可以使用范围for循环、迭代器或传统for循环遍历vector。

8.获取vector的大小

使用size()成员函数获取vector中元素的数量。

9.判断vector是否为空

使用empty()成员函数检查vector是否为空。

10.清空vector

使用clear()成员函数删除vector中的所有元素。

11.vector的容量和预留空间

12.vector的插入和赋值

 

例程序:vector

输出结果: 1 2 3 1 2 3 第2个元素是: 2 1 3

在vector中存储pair: pair经常与vector一起使用,特别是vector中存储两种不同类型的数据对时。可以创建一个vector,其中每个元素都是一个pair。这样,就可以同时存储和访问两种类型的数据了。 例程序:vector与pair的结合(一)

例程序:vector与pair的结合(二)

 

【关联容器 set】

在C++中,set是一种关联容器,它包含唯一元素的集合。这些元素在插入时按照某种排序规则(默认为升序)自动排序。C++标准库提供了两种主要的集合类型:std::set和std::unordered_set1。

std::set:它的元素按照排序规则自动排序,并且不允许重复元素。插入、删除、查找操作的时间复杂度都是O(log n),其中n是集合中元素的数量。std::set的元素是不可变的,一旦插入到集合中就不能修改1。

std::unordered_set:基于哈希表实现的无序集合。它的元素没有特定的顺序,并且不允许重复元素。插入、删除、查找操作的平均时间复杂度是O(1),但在最坏情况下可能会是O(n),其中n是集合中元素的数量。同样,std::unordered_set的元素也是不可变的1。 C++ set的应用

set容器在C++编程中有广泛的应用场景,其特性和有序性使其成为管理有序唯一数据集合的首选。使用set不仅可以提高数据处理的效率,还能在底层自动维护数据的完整性和顺序2。

以下是一些set的应用场景处理唯一元素的有序集合

需要注意的是,虽然set提供了许多便利的功能,但由于其内部实现需要额外的空间来维护元素的顺序和唯一性,因此在使用时可能会消耗更多的内存和计算资源。因此,在选择是否使用set时需要根据具体的应用场景进行权衡。

去重例程序:

查找例程序:

插入与删除例程序:

 

应用举例:

NOI / 1.10编程基础之简单排序 09:明明的随机数描述 明明想在学校中请一些同学一起做一项问卷调查,为了实验的客观性,他先用计算机生成了N个1到1000之间的随机整数(N≤100),对于其中重复的数字,只保留一个,把其余相同的数去掉,不同的数对应着不同的学生的学号。然后再把这些数从小到大排序,按照排好的顺序去找同学做调查。请你协助明明完成“去重”与“排序”的工作。 输入 有2行,第1行为1个正整数,表示所生成的随机数的个数:N; 第2行有N个用空格隔开的正整数,为所产生的随机数。 输出 也是2行,第1行为1个正整数M,表示不相同的随机数的个数。第2行为M个用空格隔开的正整数,为从小到大排好序的不相同的随机数。 样例输入 10 20 40 32 67 40 20 89 300 400 15 样例输出 8 15 20 32 40 67 89 300 400

参考代码:

 

【关联容器 map】

一、map简介 map是STL(标准模板库)的一个关联容器。 可以将任何基本类型映射到任何基本类型。如int array[100]事实上就是定义了一个int型到int型的映射。 map提供一对一的数据处理,key-value键值对,其类型可以自己定义,第一个称为关键字,第二个为关键字的值。

每个键在map中都是唯一的,而与之关联的值可以是任何类型

map内部是自动排序的 二、map的用法 必须引入包

2.map的定义

3.map容器内元素的访问 通过下标进行访问 如:maps['c']=5; 通过迭代器进行访问 map可以使用it->first来访问键,使用it->second访问值

4.map的常用用法

 

C++:迭代器

C++的迭代器是一种用于遍历容器元素的对象。迭代器提供了一种通用的访问容器元素的方式,无论容器的类型和数据结构如何。通过迭代器,我们可以依次访问容器中的每个元素,对其进行读取、修改或删除操作。

使用迭代器可以极大地简化对容器元素的访问和操作,同时提高程序的灵活性和可扩展性。迭代器的使用方式类似于指针,我们可以通过迭代器来遍历容器中的每个元素,并通过迭代器进行读写操作,而不需要关心容器内部的具体实现细节。

迭代器分类 在C++中,迭代器是一种用于访问容器中的元素的对象。C++标准库中提供了多种类型的迭代器,每种迭代器都有不同的功能和特性。以下是C++中常用的迭代器类型:

输入迭代器(Input Iterator):只能读取容器中的元素,而不能修改或重复读取。输入迭代器支持++、*、==、!=等操作符。适用于遍历容器中的元素,如std::istream_iterator。 *

输出迭代器(Output Iterator):只能写入容器中的元素,而不能读取或重复写入。输出迭代器支持++、*等操作符。适用于向容器中写入数据,如std::ostream_iterator。

前向迭代器(Forward Iterator):支持读写操作,可以向前遍历容器中的元素。前向迭代器支持++、*、==、!=等操作符。适用于需要多次遍历容器中的元素,如std::forward_list。 *

双向迭代器(Bidirectional Iterator):支持读写操作,可以向前和向后遍历容器中的元素。双向迭代器支持++、–、*、==、!=等操作符。适用于需要反向遍历容器中的元素,如std::list。

随机访问迭代器(Random Access Iterator):支持读写操作,可以在常数时间内进行随机访问。随机访问迭代器支持++、–、*、[]、+、-、<、<=、>、>=等操作符。适用于需要通过下标访问容器中的元素,如std::vector和std::array。

以上是C++中常见的迭代器类型及其示例。通过使用适当的迭代器类型,可以方便地对容器中的元素进行访问和操作。

以上就是迭代器常见的功能,接下来我用一张表格为大家展示迭代器的功能关系:

 随机访问迭代器双向 迭代器正向 迭代器
支持 ++递增
支持 == !=判等
支持 -- 递减×
支持算数操作符 + -××
支持 > < >= <= 的比较操作××
支持+= -=的操作××
支持下标随机访问××

在C++中,可以使用不同的方法来获取迭代器,具体取决于数据结构类型。以下是获取迭代器的几种常见方式:

使用begin()和end()方法: 这是最常用的获取迭代器的方式。通过调用容器的begin()方法,可以返回指向第一个元素的迭代器;而调用end()方法则返回指向容器末尾的迭代器。例如:

输出结果为:1 2 3 4 5

使用rbegin()和rend()方法: rbegin()方法返回指向容器最后一个元素的逆向迭代器,而rend()方法则返回指向容器开始的逆向迭代器。逆向迭代器可以从容器的末尾开始向前遍历容器。例如:

输出结果为:5 4 3 2 1

使用advance()方法: advance()方法允许我们在迭代器上进行偏移,以便访问容器中的其他元素。它需要两个参数,第一个是迭代器,第二个是偏移量。使用advance()方法可以在容器中跳过指定数量的元素,然后获取新位置上的迭代器。例如:

输出结果为:3 4 5

使用next()和prev()方法: next()方法返回当前迭代器的下一个迭代器,prev()方法返回当前迭代器的前一个迭代器。这些方法也需要两个参数,第一个是迭代器,第二个是偏移量。它们在使用时可以更加方便,无需像advance()方法一样显式指定迭代器的类型。例如:

输出结果为:2 4

这些是获取C++中迭代器的几种常见方式,可以根据具体的需求选择合适的方法来获取迭代器

 

【2.算法:高精度计算】

高精度计算通常用于处理大数的加减乘除等运算。在C++中,可以通过字符串或数组来模拟大整数的处理。

高精度加法

对于位数较大的整数的加法,使用数组来存储整数,数组的长度等于整数的位数,按照字符串的形式输入,先统计长度,然后倒置过来利用数组进位来模拟加法,最终将数组最大位置的前导0判断去除,倒序输出即为最终的结果。

大整数 输入

我们用一个数组来保存整个高精度整数,并记录高精度整数的长度len。 因为我们在模拟一个计算竖式的过程,需要从右往左读入以解决进位的问题。

大整数 输出

输出一个高精度的数就非常容易了,直接把数组中的元素倒序输出就可以了。

高精度加高精度参考代码(多位加多位)

 

高精度减法

高精度减高精度参考代码(多位减多位)

 

高精度乘法

高精度乘高精度参考代码(多位乘多位)

 

【3.算法:排序】

冒泡排序、选择排序、插入排序、桶排序、快速排序、归并排序……、

各种排序方法的优缺点与应用范围:

在选择排序算法时,需要考虑数据的规模、稳定性要求、内存使用情况等因素。对于小规模或基本有序的数据,简单排序算法(如冒泡排序、插入排序、选择排序)可能足够高效。对于大规模数据,通常选择时间复杂度较低的排序算法(如快速排序、归并排序、堆排序)。如果数据具有特定分布或类型,可以考虑使用计数排序、桶排序或基数排序等算法。

1、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1 算法描述

1.2 动图演示

1.3 代码实现

 

2、选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

2.2 动图演示

img

2.3 代码实现

2.4 算法分析

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

 

3、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

3.2 动图演示

插入排序(Insertion Sort)

3.2 代码实现

3.4 算法分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

 

4、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

4.1 算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

4.2 动图演示

希尔排序(Shell Sort)

4.3 代码实现

4.4 算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。

 

5、归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1 算法描述

5.2 动图演示

归并排序(Merge Sort)

5.3 代码实现

5.4 算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

 

6、快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1 算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

6.2 动图演示

 

6.3 代码实现

 

7、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1 算法描述

7.2 动图演示

7.3 代码实现

 

8、计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1 算法描述

8.2 动图演示

8.3 代码实现

8.4 算法分析

计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

9、桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,是否高效的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

9.1 算法描述

9.2 图片演示

9.3 代码实现

// 桶排序 // 有负数的话需要进行预处理,本函数包含预处理部分

9.4 算法分析

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

10、基数排序(Radix Sort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

10.1 算法描述

10.2 动图演示

10.3 代码实现

10.4 算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

11.应用实例

参考:NOI / 1.10编程基础之简单排序

 

【4.链表】

什么是链表

链表(Linked List)是一种常见的数据结构,由一系列节点(Node)组成,每个节点包含两个部分:一个是存储数据元素的数据域(Data Field),另一个是存储下一个节点地址的指针域(Pointer Field)或链接(Link)。这些节点链接起来形成一个线性结构。

链表与数组不同,数组在内存中是一块连续的空间,而链表中的节点在内存中不是连续存储的,它们是通过指针链接在一起的。链表可以分为单向链表、双向链表和循环链表等类型。

单向链表是最简单的链表结构,每个节点只包含一个指向下一个节点的指针。在单向链表中,只能从头节点开始遍历整个链表。

双向链表中的每个节点除了包含一个指向下一个节点的指针外,还包含一个指向前一个节点的指针。这样可以在任何节点处向前或向后遍历链表。

循环链表是一种特殊的链表,其尾节点的指针指向头节点,从而形成一个环。在循环链表中,可以从任何节点开始遍历整个链表。

链表的主要优点是可以动态地分配内存空间,不需要预先知道数据的大小。此外,链表在插入和删除操作上具有优势,因为只需要修改相关节点的指针即可,而不需要移动大量数据。然而,链表在访问特定位置的元素时效率较低,因为需要从头节点开始遍历链表。

链表在编程中有很多应用,如实现栈、队列、哈希表等数据结构,以及用于存储动态数据等场景。

单向链表

单向链表(Single Linked List)是一种线性表的数据结构,它的每个节点只包含一个指向下一个节点的指针。具体来说,单向链表由一系列节点(Node)组成,每个节点包含两个域,一个信息域(用于存储数据元素)和一个指针域(用于存储指向下一个节点的指针)。

单向链表的特点如下:

  1. 单向性:每个节点只能指向它的下一个节点,不能指向前一个节点。因此,在单向链表中,从头部开始可以遍历整个链表,但无法从尾部直接找到链表的开始位置。

  2. 非循环:在普通的单向链表中,最后一个节点的指针域通常设置为空(NULL),表示链表的结束。但在循环链表中,最后一个节点的指针域指向链表的第一个节点,形成一个环。

  3. 动态分配:链表中的节点是在程序执行过程中动态分配和释放的,因此链表的大小可以动态地改变。

单向链表的主要操作包括:

单向链表在许多应用中都有广泛的用途,如实现栈、队列、哈希表等数据结构,以及用于表示具有线性关系的数据集合。由于单向链表的结构相对简单,因此在编程中经常被用作数据结构的基础。

1.定义节点

首先,需要定义一个链表节点(创建链表的结构)。每个节点包含一个数据成员(可以是任何类型,如整数、字符等)和一个指向下一个节点的指针。如下代码:

此段代码定义了一个Node结构体,并提供了一个构造函数 Node(int x) : data(x), next(nullptr) {},该构造函数接受一个整数x作为参数,并将data成员设置为该值,同时将指针初始化为nullptr空指针。

在以上代码中 Node(int x) : data(x), next(nullptr) {} int x 是传递给构造函数的参数,它用于初始化data成员变量。 data(x) 使用初始化列表语法来将data成员变量初始化为参数x的值。 next(nullptr) 同样使用初始化列表语法来将next指针成员变量初始化为nullptr,表示新创建的节点不指向任何其他节点,它是链表的末尾(或者至少是一个新链表的开始)。 冒号 : 用来分隔成员变量和它们的初始化值,表示初始化列表的开始。(把 x 的值赋给data。 nullptr,是C++中空指针类型的关键字,用来表示空指针类型。

2.创建节点
3.插入节点

image-20240708164436134

上段代码创建了两个节点,但这两个节点都是独立了,并没有链在一起。 通过添加下段代码即可把它们链在一起。 node1->next=node2;

如果要创建很多个节点,可以使用循环。 使用循环建立链表时,不方便给每个节点取不同的名字,所以除了第一个节点外,其他节点貌似名字相同,要访问就不方便,这里可以用结构指针来指向链表的头,用指针找节点就方便了。只需用 p=p->next 就可找到下一个节点。

4.遍历链表

有了指针指向链表的头,我们可以使用指针来遍历链表的每一个节点。 只需在上一代码后面添加如下代码段即可。

如果要查找某个值,可以让遍历到这个值时停下。 比如:

5.删除节点

image-20240708164459401

使用 p->next=p->next->next; 语句即可从链表中删除p后面的那个节点。 如果要从内存中释放,还要使用 delete 语句将节点删除。

假如现在要删除值为7的节点: 1.找到7的前一个节点,循环遍历p->next->data != 7 2.创建一个结构指针 *tem,让指针指向p->next,Node *tem=p->next 3.使用 p->next=p->next->next; 删除节点。 4.释放节点 delete tem;

双向链表

双向链表(Double-Linked List)是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。这样的结构赋予了双向链表更高的操作灵活性和更多的应用场景。 单向链表相比,双向链表的主要优点是可以很方便地从任意一个结点访问它的前驱结点和后继结点,从而克服了单向链表在查找链表中某结点不方便的缺点。 然而,双向链表也有其缺点。在插入或删除某个节点时,需要处理四个节点的引用(两个前驱指针和两个后继指针),而不是两个,这增加了实现的复杂性。同时,由于每个节点都需要额外的空间来存储前驱指针,所以双向链表相对于单向链表会占用更多的内存空间。

双向链表

1.定义节点结构体
2.初始化节点(第一个节点)
3.添加节点
4.遍历链表

标准模板库(STL): forward_list、list

forward_list表示一个单向链表,list表示一个双向链表。

list的介绍

  1. list是序列容器,允许在序列中的任何位置执行固定O(1)时间复杂度的插入和删除操作,并在两个方向进行迭代。

  2. list容器使用双链表实现;双链表将每个元素存储在不同的位置,每个节点通过next,prev指针链接成顺序表。

  3. list与其他标准序列容器(array,vector和deque)相比,list通常可以在容器内的任何位置插入、提取和移动元素。

  4. list与其他标准序列容器(array,vector和deque)相比,list和forward_list(单链表实现)的主要缺点是他们不能通过位置直接访问元素;例如,要访问列表中的第五个元素,必须从已知位置(开始或结束)迭代到该位置,需要哦线性时间开销。

  5. 存储密度低,list要使用一些额外的内容空间(next,prev)来保持与每个元素相关联(前后续的线性)的链接信息,从而导致存储小元素类型(如char,short,int等)的列表的存储密度低。

std::list 的基本特点: std::list 提供了在任意位置插入和删除元素的常数时间复杂度。 与数组和 std::vector 不同,std::list 不需要在内存中连续存储元素,因此它可以在分配和释放内存时更加灵活。 由于 std::list 是双向链表,所以它可以高效地向前和向后遍历。 std::list 不支持直接通过索引访问元素(但可以通过迭代器和算法间接实现)。

std::forward_list 的基本特性如下: 单向性:forward_list 中的迭代器只支持向前移动,不支持向后移动,因此不能从尾部向前遍历链表。 插入和删除操作:由于forward_list 是链表结构,因此在链表头部插入和删除元素的时间复杂度是常数。但需要注意的是,由于它只支持前向迭代,所以插入和删除操作在链表的其他位置可能会相对复杂一些。 空间效率:相比于 std::list(双向链表),std::forward_list 节省了用于存储指向下一个和上一个节点的指针的空间,因此其空间开销较小。

`

 

基本用法:

list基本用法

1.包含头文件

2.声明

3.插入元素

4.删除元素

5.遍历元素 使用迭代器(iterator)或基于范围的for循环遍历元素。

6.其他操作

std::forward_list基本用法:

1.包含头文件

2.声明

3.插入元素

4.删除元素

5.遍历元素 使用迭代器(iterator)或基于范围的for循环遍历元素(但请注意,基于范围的for循环在C++11中不适用于std::forward_list,因为它没有end()之前的元素来初始化迭代器)。

6.其他操作

由于std::forward_list是单向的,因此它通常比std::list更节省空间,并且在某些情况下可能具有更好的性能,尤其是当不需要向后遍历或访问时。然而,由于其单向性,某些操作(如insert_aftererase_after)可能需要额外的注意来正确管理迭代器。

list 应用举例

NOI / 3.2数据结构之指针和链表 1748:约瑟夫问题 描述 约瑟夫问题:有n只猴子,按顺时针方向围成一圈选大王(编号从1到n),从第1号开始报数,一直数到m,数到m的猴子退出圈外,剩下的猴子再接着从1开始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王,编程求输入n,m后,输出最后猴王的编号。 输入 每行是用空格分开的两个整数,第一个是 n, 第二个是 m ( 0 < m,n <=300)。最后一行是: 0 0 输出 对于每行输入数据(最后一行除外),输出数据也是一行,即最后猴王的编号 样例输入 6 2 12 4 8 3 0 0 样例输出 5 1 7

解题思路:用list建个链表(用list比较快),从头开始数,数到m就删除这个节点,数到最后一个结点时,就前往第一个结点。直到剩下最后一个结点就是结果。 解题步骤: 1.按题目意思,要输出多对数据,所以要处理多次,输入 0 0 时结束,可用while(n!=0&&m!=0)控制。 2.用 list<int>node; 建链表,并添加节点。for(int i=1; i<=n; i++) node.push_back(i); 3.创建链表 迭代器 便于遍历链表。list<int>::iterator it = node.begin(); 4.循环定位并删除第m号节点,直到只剩1个结点为止。node.size()>1 1). 数到第m号节点:

2). 删除这个第m号节点,删除之前要把下一个节点的位置记下来

完整代码参考 :

 

 

【5.队列、栈】

1.队列(queue)

C++队列(Queue)是一种常见的数据结构,它遵循先进先出(FIFO,First In First Out)的原则,即先进入队列的元素将先被移出队列。C++标准库提供了queue容器适配器来实现队列的功能。

1.包含头文件

2.定义队列

3.基本操作

队列(queue)应用举例

NOI / 1.12编程基础之函数与过程抽象 07:机器翻译描述

小晨的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章。

这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换。对于每个英文单词,软件会先在内存中查找这个单词的中文含义,如果内存中有,软件就会用它进行翻译;如果内存中没有,软件就会在外存中的词典内查找,查出单词的中文含义然后翻译,并将这个单词和译义放入内存,以备后续的查找和翻译。

假设内存中有M个单元,每单元能存放一个单词和译义。每当软件将一个新单词存入内存前,如果当前内存中已存入的单词数不超过M−1,软件会将新单词存入一个未使用的内存单元;若内存中已存入M 个单词,软件会清空最早进入内存的那个单词,腾出单元来,存放新单词。

假设一篇英语文章的长度为N个单词。给定这篇待译文章,翻译软件需要去外存查找多少次词典?假设在翻译开始前,内存中没有任何单词。

输入

输入文件共2行。每行中两个数之间用一个空格隔开。 第一行为两个正整数M和N,代表内存容量和文章的长度。 第二行为N个非负整数,按照文章的顺序,每个数(大小不超过1000)代表一个英文单词。文章中两个单词是同一个单词,当且仅当它们对应的非负整数相同。

对于10%的数据有M = 1,N ≤ 5。 对于100%的数据有0 < M ≤ 100,0 < N ≤ 1000。

输出

共1行,包含一个整数,为软件需要查词典的次数。(入队次数)

样例输入

样例输出

提示

输入输出样例 1 说明:

整个查字典过程如下:每行表示一个单词的翻译,冒号前为本次翻译后的内存状况:

空:内存初始状态为空。 1. 1:查找单词1并调入内存。(入队) 2. 1 2:查找单词2并调入内存。(入队) 3. 1 2:在内存中找到单词1。 4. 1 2 5:查找单词5并调入内存。(入队) 5. 2 5 4:查找单词4并调入内存替代单词1。(1出队,4入队) 6. 2 5 4:在内存中找到单词4。 7. 5 4 1:查找单词1并调入内存替代单词2。(2出队,1入队)

共计查了5 次词典。

 

解题思路:按题目给出的信息不难看出这是一个队列问题,输入的第一个数字为队列的大小 ,第二行输入的每一个数都要在队列中查找,直到输入完成 如果不在队列中:就入队次数 +1,把该数字入队。 如果队列满了:就出队并删除队首。 这里要解决的问题是:怎样判断该数字是否在队列中,如果一个一个的找(暴力搜索)肯定太慢了,要是用一个数组的下标来代表数字,该下标的元素是1,就表示在队列中,元素是0就表示没在,这样会快很多,代码也少。(哈希算法)

参考代码:

 

 

 

2.栈(stack)

栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。栈的基本操作包括压栈(push,也叫入栈)和弹栈(pop,也叫出栈)。压栈是将一个元素添加到栈的顶部,而弹栈则是移除栈顶部的元素。栈通常用于需要按照特定顺序处理元素的情况,例如函数调用和表达式求值。

  1. 包含头文件

  1. 创建栈

  1. 基本操作

栈(stack)应用举例

NOI / 1.7编程基础之字符串 27:单词翻转 样例输入 hello world 样例输出 olleh dlrow

 

解题思路:把输入的字符串按字符一个个入栈。遍历输入的字符串for(const char &c:s),如果不是空格if(c!=' ')就压入栈中w.push(c),否则就出栈,直到栈为空while(!w.empty()) { cout<<w.top(); w.pop(); },遍历完字符串后,栈中还存有最后一个单词,再次弹出栈中所有字符,结束。

参考代码:

 

NOI / 1.7编程基础之字符串 28:单词倒排 样例输入 I am a student 样例输出 student a am I

 

 

解题思路:这次是按单词倒排,跟上一题按字母的不一样。栈中要存的是单词(字符串),怎样把输入字符串里的单词提取出来成为关键。 这里我们可以使用istringstream ,它是C++ 标准库中的一个类,属于<sstream> 头文件,并且是一个输入字符串流。istringstream 对象允许从字符串中读取数据,就像从文件或标准输入读取一样。它特别适合处理从某个源(如用户输入或文件)接收的字符串,并需要从中解析出多个值的情况。 1.输入一个字符串到 str。 2.使用语句 istringstream iss(str); 从字符串str读取数据到 iss 中。 3.从 iss 向另一个字符串变量 word 中循环输入字符串 while(iss>>word)(自动以空格分隔,以回车符结束),在循环中处理每一个 word 字符串(把word压入栈)。 4.出栈。

参考代码:

 

 

【6.分治算法】

1.分治算法的基本思想

分治算法(Divide and Conquer)的基本思想是将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。这些被分割出的小问题与原问题的性质相同,并且它们的解可以合并起来得到整个问题的解。在解决这些小问题的过程中,如果小问题规模仍然较大,则继续采用分治策略,直到问题规模足够小,可以直接求解为止。

分治算法一般包含以下三个步骤:

  1. 分解(Divide):将原问题分解为若干个规模较小,但结构与原问题相同的子问题。这些子问题是相互独立的,即子问题之间不包含公共的子问题。

  2. 解决(Conquer):递归地求解各个子问题。如果子问题的规模仍然较大,则继续分解为更小的子问题,直到子问题可以直接求解为止。这一步通常会采用递归的方式来实现。

  3. 合并(Combine):将各个子问题的解合并为原问题的解。这一步需要根据原问题的性质,确定如何合并子问题的解。

分治算法具有以下几个优点:

常见的分治算法有归并排序(Merge Sort)、快速排序(Quick Sort)、二分搜索(Binary Search)等。

分治算法在现实生活中的应用:

  1. 找出伪币(二分查找):假设你有一个装有16个硬币的袋子,其中有一个是伪造的,并且伪造的硬币比真硬币轻。为了找出这个伪造的硬币,你可以采用分治算法。首先,你可以将16个硬币分成两组各8个,然后用天平比较这两组的重量。如果两组重量相等,则伪币不存在于这两组中;如果重量不等,则伪币存在于较轻的那一组中。然后,你可以继续将较轻的那一组硬币分成更小的组,并重复上述过程,直到最终找出伪币。

  2. 汉诺塔问题(递归):汉诺塔是一个经典的递归问题,也可以使用分治算法来解决。假设你有三个柱子,初始时,有n个盘子从小到大依次放在第一个柱子上。目标是将这些盘子移动到第三个柱子上,并保持它们的相对顺序不变。每次只能移动一个盘子,并且大盘子不能放在小盘子上面。你可以将这个问题分解成两个子问题:将n-1个盘子从第一个柱子移动到第二个柱子,然后将最大的盘子从第一个柱子移动到第三个柱子,最后将n-1个盘子从第二个柱子移动到第三个柱子。

  3. 大规模数据排序 对于非常大的数据集,使用传统的排序算法可能会非常耗时。然而,你可以使用分治算法(如归并排序)来加速排序过程。你可以将数据集分割成多个较小的部分,并对每个部分进行排序。然后,你可以将这些已排序的部分合并成一个完全排序的数据集。

  4. 社交网络分析 在社交网络中,你可能需要分析用户之间的关系、查找群组或社区等。这些问题通常可以通过图算法来解决,而许多图算法都采用了分治策略。例如,你可以将社交网络图分割成多个较小的子图,并在每个子图上并行地执行图算法。然后,你可以将子图的结果合并起来以获得整个社交网络的分析结果。

这些只是分治算法在现实生活中的一些应用示例。实际上,分治算法在许多领域都有广泛的应用,包括计算机科学、工程、物理、生物学等。

2.分治法的适用范围

能用分治法处理的问题通常具有以下特征

  1. 问题可分解性:问题可以被分解为若干个规模较小、结构与原问题相似的子问题。子问题之间通常是独立的,即一个子问题的求解不会影响另一个子问题的求解。

  2. 子问题最优性:如果子问题的解是最优的,那么可以通过合并这些最优解来得到原问题的最优解。这意味着子问题的解对于整个问题的解是有意义的,并且可以直接利用。

  3. 递归性:分治法通常通过递归实现,即子问题的求解过程与原问题的求解过程相同或相似。这意味着可以使用相同的算法框架来解决不同规模的问题。

  4. 合并解的简单性:子问题的解可以很容易地合并成原问题的解。如果合并过程过于复杂,可能会抵消分治带来的优势。

  5. 子问题独立性:子问题之间通常是相互独立的,即一个子问题的求解不会影响到其他子问题的求解。这使得可以并行处理子问题,从而提高算法的效率。

  6. 问题规模足够大:对于规模很小的问题,直接求解可能比分治法更高效。因此,分治法通常适用于规模较大的问题。

适用分治法解决问题的例子:

3.分治法应用举例

1.汉诺塔(递归)

NOI / 2.2基本算法之递归和自调用函数 6261:汉诺塔问题 描述 约19世纪末,在欧州的商店中出售一种智力玩具,在一块铜板上有三根杆,最左边的杆上自上而下、由小到大顺序串着由64个圆盘构成的塔。目的是将最左边杆上的盘全部移到中间的杆上,条件是一次只能移动一个盘,且不允许大盘放在小盘的上面。 这是一个著名的问题,几乎所有的教材上都有这个问题。由于条件是一次只能移动一个盘,且不允许大盘放在小盘上面,所以64个盘的移动次数是:18,446,744,073,709,551,615 这是一个天文数字,若每一微秒可能计算(并不输出)一次移动,那么也需要几乎一百万年。我们仅能找出问题的解决方法并解决较小N值时的汉诺塔,但很难用计算机解决64层的汉诺塔。 假定圆盘从小到大编号为1, 2, ... 输入 输入为一个整数后面跟三个单字符字符串。 整数为盘子的数目,后三个字符表示三个杆子的编号。 输出 输出每一步移动盘子的记录。一次移动一行。 每次移动的记录为例如 a->3->b 的形式,即把编号为3的盘子从a杆移至b杆。 样例输入 2 a b c 样例输出 a->1->c a->2->b c->1->b

解题思路: 按分治法解决问题的一般步骤: 分解 -> 解决 -> 合并 来处理该问题。 1.分解: 1)把n个盘子看成一个,移一次就完成,A -> B。这个分解,子问题够细,但结构与原问题不相符,子问题大小不合适。 2)把n个盘子看成两个,最下面(最大)的是一个,上面n-1个看成一个,像这样: image-20240709121704881 只有两个盘子时移动起来很容易,程序实现也很简单,但上面的是n-1个,还不能直接移动,所以上面的n-1个还要继续分,直到剩下最后两个盘子时(递),就可以移动了。(这就是递归的过程) 2.解决:处理只有一个盘子和有两个盘子时的移动顺序 3.合并:递归数据返回,输出移动的顺序。(归)。

“递”是分解问题为子问题的过程,而“归”是解决子问题并将解合并成原问题解的过程。这两个步骤相互结合,构成了分治算法的核心思想。

解题步骤: 可以写一个函数来移动move(): 1.先写只有一个盘子时的移动顺序。 2.再加一个盘子该怎么移动?把两个盘子的移动顺序写到函数内。 本题的关键在于移动move()的 a b c 三个参数不好调,想要调好三个参数: 我们可以模拟只有两个盘子时的移动顺序,函数参数传值时的变化。

参考代码:

 

NOI / 2.4基本算法之分治

2. 2991:2011

描述 已知长度最大为200位的正整数n,请求出2011^n的后四位。 输入 第一行为一个正整数k,代表有k组数据,k<=200接下来的k行,每行都有一个正整数n,n的位数<=200 输出 每一个n的结果为一个整数占一行,若不足4位,去除高位多余的0 样例输入 3 5 28 792 样例输出 1051 81 5521

题意: 2011的大数次方,而且这个数可以大得离谱,不能用常规方法暴力求解,这里需要用到一个数学知识:一个n(4)位数(2011),它的第k次方的后n(4)位等于这个数。比如,2011的第500次方的后四位就是2011,343的第20次方的后3位是343。而且每k次方中的后n位是循环出现。 像以下实例的运行结果可以看出规律。

以下输出结果是343的60次方(后3位): 我们可以看到每20次方循环一次。 对2011做同样处理得到每500次方循环一次。 那么,再大的K次方,只需要把k的低3位取出来计算即可(类似于 k%500)。 结论:每500次方循环一次,可以先把这500个数的后四位存起来备查a[ ],然后处理输入的大数K,把K的后3位取出来对500取模,得到一个数 i ,a[i]就是我们要的结果。

解题步骤: 1.把500个可能的数先存起来。 2.有k组数要处理,循环k次。 3.输入大数字符串,取后3位转成数值,不足3位的取全部。 4.输出数组中对应的数。

参考代码:

NOI / 2.4基本算法之分治

3. 7617:输出前k大的数(排序)

描述 给定一个数组,统计前k大的数并且把这k个数从大到小输出。 输入 第一行包含一个整数n,表示数组的大小。n < 100000。 第二行包含n个整数,表示数组的元素,整数之间以一个空格分开。每个整数的绝对值不超过100000000。 第三行包含一个整数k。k < n。 输出 从大到小输出前k大的数,每个数一行。 样例输入 10 4 5 6 9 8 7 1 2 3 0 5 样例输出 9 8 7 6 5

解题方法: 1.使用multimap容器存储数据,输出后K个数(148ms)。 multimap的特点:

参考代码:

2.使用优先队列priority_queue存储数据(127ms) priority_queue的特点: 它提供了一种管理元素集合的方式,其中每个元素都有一个优先级,优先级最高的元素总是被首先访问(自动排序-降序)。 参考代码:

3.使用快速排序算法(递归)(125ms) 快速排序算法的具体实现步骤如下:

快速排序算法的关键在于分区操作,通过这一步骤,我们可以将一个大问题分解为两个小问题,然后递归地解决这两个小问题。在分区操作中,我们通常使用两个指针(或者索引)来遍历序列,一个指针从序列的左端开始向右移动,另一个指针从序列的右端开始向左移动,通过比较指针所指向的元素与枢轴的大小,将小于等于枢轴的元素移到左边,大于枢轴的元素移到右边。

参考代码(以下代码还可以进一步优化):

4.使用归并排序算法(递归)(128ms)实现步骤

  1. 拆分:将待排序的数组不断拆分为更小的子数组,直到每个子数组的长度为1或0。此时,每个子数组都是有序的(因为只有一个元素或没有元素)。

  2. 递归排序:对拆分得到的每个子数组递归地进行归并排序。由于每个子数组在递归开始时都是有序的(或为空),所以这一步实际上是在准备合并操作所需的有序子序列。

  3. 合并:将相邻的有序子数组合并为一个较大的有序数组。合并过程中,通过比较两个子数组的首元素,按照从小到大的顺序逐个将元素放入一个辅助数组,直到所有元素都被合并完毕。然后,将辅助数组中的元素复制回原数组,以完成合并操作。

  4. 重复合并:重复上述的拆分、递归排序和合并过程,直到最终得到完全排序的数组。

参考代码:

 

 

【7.贪心算法】

1.贪心算法的简介

  1. 1.贪心算法的基本思想:

它总是做出在当前看来最好的选择,即它希望通过局部最优解来构造全局最优解。这种策略并不保证总是能得到全局最优解,但在很多情况下,它确实能给出令人满意的答案,并且实现起来相对简单。

  1. 2.贪心算法的基本思路

从问题的某一个初始解出发,逐步逼近给定的目标,以尽可能快地求得更好的解。在每一步选择中,它都只考虑一个数据,而不考虑子问题的解是否达到最优。

  1. 3.贪心算法的适用范围:

贪心算法因其简洁性和高效性,在多种情况下都有广泛应用。它尤其适合那些能够分解成多个简单子问题,并且子问题的最优解能够合并成全局最优解的问题。具体来说,贪心算法常用于以下几种类型的问题:

  1. 4.贪心算法的确定与设计策略

贪心算法的应用规则并不是一成不变的,但它确实有一些基本的原则和考虑因素。以下是一些关键的规则和建议,用于确定何时以及如何应用贪心算法:

2.贪心算法的实现步骤

  1. 初始化解决方案列表:首先,需要初始化一个用于存储解决方案的列表或数据结构。这个列表在算法执行过程中会逐渐填充,最终包含问题的解。

  2. 选择局部最优解:在每一步中,根据贪心策略选择当前状态下的最优解。这个选择是基于当前的信息和贪心原则做出的,并不考虑未来的影响或整体最优性。

  3. 更新状态:根据选择的局部最优解,更新问题的状态。这可能意味着从问题中移除已处理的部分,或者更新剩余部分以反映已做出的选择。

  4. 重复选择直到达到目标:继续重复步骤2和步骤3,直到达到问题的目标或无法再做出进一步的选择。在某些情况下,可能需要检查是否已经达到了问题的边界条件,如无法再添加更多元素到解决方案中。

  5. 返回解决方案:当算法结束时,返回填充好的解决方案列表或数据结构。这个列表包含了根据贪心策略逐步构建的问题的解

3.贪心算法应用举例

NOI / 4.6算法之贪心

1.1797:金银岛

描述 某天KID利用飞行器飞到了一个金银岛上,上面有许多珍贵的金属,KID虽然更喜欢各种宝石的艺术品,可是也不拒绝这样珍贵的金属。但是他只带着一个口袋,口袋至多只能装重量为w的物品。岛上金属有s个种类, 每种金属重量不同,分别为n1, n2, ... , ns,同时每个种类的金属总的价值也不同,分别为v1,v2, ..., vs。KID想一次带走价值尽可能多的金属,问他最多能带走价值多少的金属。注意到金属是可以被任意分割的,并且金属的价值和其重量成正比。 输入 第1行是测试数据的组数k,后面跟着k组输入。 每组测试数据占3行,第1行是一个正整数w (1 <= w <= 10000),表示口袋承重上限。第2行是一个正整数s (1 <= s <=100),表示金属种类。第3行有2s个正整数,分别为n1, v1, n2, v2, ... , ns, vs分别为第一种,第二种,...,第s种金属的总重量和总价值(1 <= ni <= 10000, 1 <= vi <= 10000)。 输出 k行,每行输出对应一个输入。输出应精确到小数点后2位。 样例输入 2 50 4 10 100 50 30 7 34 87 100 10000 5 1 43 43 323 35 45 43 54 87 43 样例输出 171.93 508.00

解题思路: 1.提取关键信息: 口袋的最大载重W,金属各类S,每种金属有总重量n和价值v,由于金属可以分割,所以,通过简单计算可得各金属的单价p=v/n。 2.问题分析: 所需结果:最大价值。 如何获得最大价值:按单价从高到低装入口袋,直到装满。

解题过程: 1.创建结构体数组:用于存储各种金属的信息。 2.输入数据到结构体。 3.对结构体数组排序,这里需要按单价排序,得编写排序规则。 4.遍历结构体数组,使得刚好把口袋装满。 5.输出结果。

参考代码:

NOI / 4.6算法之贪心

2.2469:电池的寿命

描述 小S新买了一个掌上游戏机,这个游戏机由两节5号电池供电。为了保证能够长时间玩游戏,他买了很多5号电池,这些电池的生产商不同,质量也有差异,因而使用寿命也有所不同,有的能使用5个小时,有的可能就只能使用3个小时。显然如果他只有两个电池一个能用5小时一个能用3小时,那么他只能玩3个小时的游戏,有一个电池剩下的电量无法使用,但是如果他有更多的电池,就可以更加充分地利用它们,比如他有三个电池分别能用3、3、5小时,他可以先使用两节能用3个小时的电池,使用半个小时后再把其中一个换成能使用5个小时的电池,两个半小时后再把剩下的一节电池换成刚才换下的电池(那个电池还能用2.5个小时),这样总共就可以使用5.5个小时,没有一点浪费。 现在已知电池的数量和电池能够使用的时间,请你找一种方案使得使用时间尽可能的长。 输入 输入包含多组数据。每组数据包括两行,第一行是一个整数N (2 ≤ N ≤ 1000),表示电池的数目,接下来一行是N个正整数表示电池能使用的时间。 输出 对每组数据输出一行,表示电池能使用的时间,保留到小数点后1位。 样例输入 2 3 5 3 3 3 5 样例输出 3.0 5.5

 

题意:一堆电池,每节的使用时间各不相同,游戏机由两节供电,使用的电池电量耗完就换下一节,这堆电池最多能玩多久?

解题思路: 按题目给出以 3 3 5 三节为例,两节3小时同时使用0.5小时(3-0.5=2.5),然后用这两节分别去消耗5小时的那一节可以使用5小时。那么一共可以使用5.5小时。 得到结果:最大使用时间为 (3+3+5)/2=5.5 可以写成 sum/2 这是因为三节中的最大值 5 小于除这节之外其他的总和,即max<=sum-max。 如果是 3 3 7 ,情况就不一样了,用两节 3 分别去消耗7都不能把7耗完,那结果就是:sum-max。

参考代码: