程序基础:数据类型、变量、输入与输出、运算符与表达式、程序结构、数组、函数、字符串、结构体、指针、类和对象。 以上内容参见PPT图。
数据结构
1.容器 vector 、 pair 、 set 、 map
4.链表、list
5.栈、队列
算法
2.高精度计算
3.排序
6.分治
7.贪心
【1.数据结构——容器】【数据对: pair】【动态数组: vector】【关联容器 set】【关联容器 map】C++:迭代器【2.算法:高精度计算】高精度加法高精度减法高精度乘法【3.算法:排序】1、冒泡排序(Bubble Sort) 2、选择排序(Selection Sort) 3、插入排序(Insertion Sort) 4、希尔排序(Shell Sort) 5、归并排序(Merge Sort) 6、快速排序(Quick Sort) 7、堆排序(Heap Sort) 8、计数排序(Counting Sort) 9、桶排序(Bucket Sort) 10、基数排序(Radix Sort) 11.应用实例【4.链表】什么是链表单向链表1.定义节点2.创建节点3.插入节点4.遍历链表5.删除节点双向链表1.定义节点结构体2.初始化节点(第一个节点)3.添加节点4.遍历链表标准模板库(STL): forward_list、list基本用法:list 应用举例【5.队列、栈】1.队列(queue)队列(queue)应用举例2.栈(stack)栈(stack)应用举例【6.分治算法】1.分治算法的基本思想2.分治法的适用范围3.分治法应用举例1.汉诺塔(递归)2. 2991:20113. 7617:输出前k大的数(排序)【7.贪心算法】1.贪心算法的简介2.贪心算法的实现步骤3.贪心算法应用举例1.1797:金银岛2.2469:电池的寿命
在C++标准库中,有多种容器(containers)可供使用,每种容器都有其特定的用途和特性。以下是一些主要的C++容器及其简要描述:
std::vector :动态数组,支持快速随机访问元素,但在元素插入或删除时(特别是在中间位置)可能需要移动大量元素。
std::array :固定大小的数组,与C风格的数组类似,但提供了更安全和方便的方法来访问和修改元素。
std::deque (双端队列):支持在序列的两端进行快速插入和删除操作的容器。
std::list :双向链表,支持在任何位置进行快速插入和删除操作,但不支持快速随机访问元素。
std::forward_list :单向链表,同样支持在任何位置进行快速插入和删除操作,但只能从头部进行迭代。
std::set :包含唯一元素的集合,元素自动按升序排序。
std::multiset :与std::set类似,但允许包含重复的元素。
std::map :关联容器,它包含可以重复的键值对(key-value pairs),其中键(key)是唯一的,并自动按升序排序。
std::multimap :与std::map类似,但允许包含具有相同键的多个元素。
std::unordered_set :与std::set类似,但它不保证元素的排序,而是通过哈希表来存储元素,从而提供了更快速的查找性能。
std::unordered_multiset :与std::unordered_set类似,但允许包含重复的元素。
std::unordered_map :与std::map类似,但它不保证键的排序,并通过哈希表来存储键值对,从而提供了更快速的查找性能。
std::unordered_multimap :与std::unordered_map类似,但允许包含具有相同键的多个元素。
std::stack :后进先出(LIFO)的容器适配器,它基于另一个底层容器(如std::deque或std::vector)实现。
std::queue :先进先出(FIFO)的容器适配器,它基于另一个底层容器(如std::deque或std::list)实现。
std::priority_queue :优先队列,它包含可以重复的元素,并且每个元素都有一个优先级。元素按优先级顺序出队,默认情况下,最高优先级的元素最先出队。它通常基于std::vector或std::deque实现,并使用std::less
每种容器都有其特定的用途和性能特性,因此在选择容器时,你应该考虑你的具体需求,如元素的数量、是否需要排序、是否需要快速随机访问等。
pair 是一个模板类,它表示一个包含两个元素的固定大小的异种容器。这两个元素可以是不同的类型,通常被称为 first 和 second。pair 可用于将两个可能不同类型的值组合成一个独立的单元。
1.包含头文件
12.创建pair对象:
xxxxxxxxxx41pair<int , string>aPair //创建了一个数据对 aPair2pair<int , string>bPair = {2 , ”b”}; //创建并使用列表初始化一个数据对bPair3pair<int , string>cPair {3 , ”c”}; //不要“=”,在声明时就初始化4dPair=make_pair(4,"d"); //使用make_pair()函数创建
3.给数据对赋值
xxxxxxxxxx21aPair.first=1;2aPair.second=”a”;
想要更多的数据对,可以用数据或array来存储pair 以数组为例: 4.创建一个pair数组:
xxxxxxxxxx11pair<int , string>ar[10] //10个pair元素的数组
5.初始化赋值
xxxxxxxxxx31ar[0].first = 3;2ar[0].second = "three";3ar[1] = pair<int , string>(2 , "two");
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个*/
参考代码:
xxxxxxxxxx15123using namespace std;4pair<float,int>stu[100];//第一个是成绩,第二个是学号 5int main() {6 int n,m;7 cin>>n>>m;8 for(int i=0;i<n;i++){9 //输入时,先输second学号 后输入first成绩10 cin>>stu[i].second>>stu[i].first; 11 }12 sort(stu,stu+n);//此时的sort()是默认升序排序13 //所以输出时是倒数第m个:stu[n-m]14 cout<<stu[n-m].second<<" "<<stu[n-m].first;15}
概念:类模板中的一种顺序容器,是一种动态数组,它可以根据需要增长或缩小。 性质: 动态大小:vector的大小不是固定的,它可以在运行时增长或缩小。 连续存储:vector的元素在内存中连续存储,这意味着可以通过指针算术快速访问任意元素。 自动管理内存:vector自动管理其内部元素的内存分配和释放,从而减少了内存泄漏和重复释放的风险。 随机访问迭代器:vector提供随机访问迭代器,允许在常数时间内访问任何元素(通过索引)。 插入和删除:虽然vector在元素末尾插入和删除的效率非常高(通常为常数时间),但在中间插入或删除元素可能需要移动其他元素,因此效率较低(通常为线性时间)。 用途: 动态数组:vector最常用的用途是作为动态数组,用于存储可变数量的同类型元素。 算法和数据结构:vector可以与C++标准库中的算法结合使用,以实现各种数据结构和算法,如排序、搜索和图形算法。 与其他容器的交互:vector可以与其他容器(如set、map等)进行交互,例如将vector中的元素插入到另一个容器中,或从另一个容器中复制元素到vector中。
声明一个动态数组:
xxxxxxxxxx21vector<int> vi; //不指定个数,可以使用vi.push_back(1); 向vi中添加元素(数字1)2vector<int> vi(5); //指定个数
使用说明:
1.要使用vector对象,必须包含头文件vector<int> vi; //声明vi是一个vectorvector<int> vi(5); //vi可以存5个int型元素。
vector的常用用法:
1.包含头文件
xxxxxxxxxx11#include <vector>
2.创建vector
xxxxxxxxxx41vector<int> vec; // 创建一个空的int类型的2vector<int> vec(10); // 创建一个包含10个元素的vector,所有元素初始化为03vector<int> vec(10, 5); // 创建一个包含10个元素的vector,所有元素初始化为54vector<int> vec1 = {1, 2, 3, 4, 5}; // 使用初始化列表创建ector
3.访问元素
使用下标操作符[]或at()成员函数访问元素。注意,at()成员函数会检查索引是否越界,如果越界会出错(抛出out_of_range异常)。
xxxxxxxxxx41cppint first = vec[0]; // 访问第一个元素2int last = vec.back(); // 访问最后一个元素3int second_to_last = vec[vec.size() - 2]; // 访问倒数第二个元素4int value_at_index = vec.at(index); // 使用at()成员函数访问元素
4.修改元素
使用下标操作符[]直接修改元素。
xxxxxxxxxx11vec[0] = 100; // 修改第一个元素为100
5.添加元素
使用push_back()成员函数在vector的末尾添加元素。
xxxxxxxxxx11vec.push_back(10); // 在vector末尾添加元素10
6.删除元素
使用pop_back()成员函数删除vector的最后一个元素。
xxxxxxxxxx11vec.pop_back(); // 删除vector的最后一个元素
使用erase()成员函数删除指定位置的元素或一段连续的元素。
xxxxxxxxxx21vec.erase(vec.begin()); // 删除第一个元素2vec.erase(vec.begin() + 2, vec.begin() + 5); // 删除从第三个元素开始的三个元素
7.遍历vector
可以使用范围for循环、迭代器或传统for循环遍历vector。
xxxxxxxxxx121// 使用范围for循环2for (int value : vec) {3// 循环体4}5// 使用迭代器,什么是迭代器:请参见后面迭代器部分内容6for (vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {7// 循环体8}9// 使用传统for循环10for (size_t i = 0; i < vec.size(); ++i) {11// 循环体12}
8.获取vector的大小
使用size()成员函数获取vector中元素的数量。
xxxxxxxxxx11cppsize_t size = vec.size(); // 获取vector的大小
9.判断vector是否为空
使用empty()成员函数检查vector是否为空。
xxxxxxxxxx11cppif (vec.empty()) { // vector为空}
10.清空vector
使用clear()成员函数删除vector中的所有元素。
xxxxxxxxxx11cppvec.clear(); // 清空vector
11.vector的容量和预留空间
使用capacity()成员函数获取vector的当前容量(即在不重新分配内存的情况下可以容纳的元素数量)。
使用reserve()成员函数预留额外的内存空间。这可以提高向vector中添加元素的性能,因为它减少了重新分配内存的次数。
xxxxxxxxxx21cppsize_t capacity = vec.capacity(); // 获取vector的容量2vec.reserve(100); // 预留100个元素的内存空间
12.vector的插入和赋值
使用insert()成员函数在指定位置插入一个或多个元素。
使用assign()成员函数替换vector中的元素。
xxxxxxxxxx31cppvec.insert(vec.begin() + 2, 100); // 在第三个位置插入元素1002vec.insert(vec.begin() + 2, 3, 200); // 在第三个位置插入三个元素2003vec.assign(10, 300); // 将vector替换为包含10个元素300的vector
例程序:vector
x1#include <iostream>2#include <vector>3using namespace std;4int main() {5// 声明一个空的int型向量6vector<int> vi;7// 向向量中添加元素8vi.push_back(1);9vi.push_back(2);10vi.push_back(3);11// 遍历向量并打印元素12for (int i = 0; i < vi.size(); i++) { //vi.size()返回vi的元素个数13cout << vi[i] << ' ';14} //输出结果:1 2 315cout << endl;16// 使用范围(rang-based for loop)循环遍历向量(基于范围的for循环,vi必须是一个可迭代对象或容器,什么是可迭代对象请自行百度)17for (int num : vi) {18cout << num << ' ';19} //输出结果:1 2 320cout << endl;21// 访问特定元素22cout << "第2个元素是: " << vi[1] << endl;23// 删除元素,删除后将后面的元素移前来24vi.erase(vi.begin() + 1); // 删除第二个元素,vi.erase(迭代器)删除25// 再次遍历并打印元素26for (int num : vi) {27cout << num << ' '; //输出结果:1 328}29cout << endl;3031return 0;3233}
输出结果: 1 2 3 1 2 3 第2个元素是: 2 1 3
在vector中存储pair: pair经常与vector一起使用,特别是vector中存储两种不同类型的数据对时。可以创建一个vector,其中每个元素都是一个pair。这样,就可以同时存储和访问两种类型的数据了。 例程序:vector与pair的结合(一)
xxxxxxxxxx18123int main() {4 std::vector<int> v = {1, 2, 3, 4, 5};5 for (int i : v) {6 std::cout << i << " ";7 }8 std::cout << std::endl;9
10// 在vector中存储std::pair11std::vector<std::pair<int, std::string>> vp;12vp.push_back(std::make_pair(1, "one"));13vp.push_back(std::make_pair(2, "two"));14for (const auto& p : vp) {15 std::cout << "First: " << p.first << ", Second: " << p.second << std::endl;16}17return 0;18}例程序:vector与pair的结合(二)
xxxxxxxxxx281#include <iostream>2#include <vector>3using namespace std;4int main() {5int n,w; //数量、架子宽度6int h=0,max_h=0,sum_w=0;//h-最终高度,max_h-每层的最大高度,sum_w-每层摆件宽度之和7cin>>n>>w;8vector<pair<int,int>>wh(n); //数据对动态数组{{2,1},{1,2},{1,3},{2,3},{2,2}}9//上句中指定了wh(n)数据对个数 ,可以用cin多次输入。没指定个数时用push_back()添加元素。10for(int i=0;i<n;i++){ //录入每个摆件的宽度、高度11cin>>wh[i].first>>wh[i].second; //first-宽度,second-高度12}13//处理数据:14// 宽度之和要小于架子宽度,每层高度取各摆件最大值15for(const auto &p:wh){ //范围循环,每次取一个数据对临时存入p16if(sum_w<=w){17sum_w+=p.first;18max_h=max(max_h,p.second);19}20else{21h+=max_h;22sum_w=0;23}24}25h+=max_h; //加上最上层的最大高度26cout<<h;27return 0;28}
在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中,然后将set中的数据复制回原始数据结构中,从而达到去重的效果。
快速查找:由于set中的元素是自动排序的,因此可以使用二分查找等算法在set中快速查找元素。这使得set在处理需要频繁查找的数据集时非常有效。
有序存储:如果你需要存储一组需要保持有序的数据,那么set是一个很好的选择。与vector或list等顺序容器相比,set可以在插入和删除元素时自动维护数据的顺序。
需要注意的是,虽然set提供了许多便利的功能,但由于其内部实现需要额外的空间来维护元素的顺序和唯一性,因此在使用时可能会消耗更多的内存和计算资源。因此,在选择是否使用set时需要根据具体的应用场景进行权衡。
去重例程序:
xxxxxxxxxx141234
5int main() {6 std::vector<int> nums = {1, 2, 3, 2, 4, 4, 5, 1};7 std::set<int> unique_nums(nums.begin(), nums.end()); // 构造set时传入vector的迭代器,实现去重8
9for (const auto& num : unique_nums) {10 std::cout << num << " ";11}12std::cout << std::endl;13return 0;14}查找例程序:
xxxxxxxxxx15123
4int main() {5 std::set<int> mySet = {1, 2, 3, 4, 5};6
7 int toFind = 3;8 if (mySet.find(toFind) != mySet.end()) {9 std::cout << "Found " << toFind << " in the set." << std::endl;10 } 11 else {12 std::cout << toFind << " not found in the set." << std::endl;13 } 14 return 0;15}插入与删除例程序:
xxxxxxxxxx21123
4int main() {5 std::set<int> mySet = {1, 2, 3, 4, 5};6
7 // 插入新元素8 mySet.insert(6);9 10 // 删除元素11 mySet.erase(3);12 13 // 遍历 set 中的元素14 for (const auto& elem : mySet) {15 std::cout << elem << " ";16 }17 std::cout << std::endl;18 19 return 0;20
21}
应用举例:
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
参考代码:
xxxxxxxxxx1312using namespace std;3int main(){4 int n,r;5 set<int>mm;6 cin>>n;7 for (int i = 0; i < n; ++i){8 cin>>r;9 mm.insert(r); //插入元素 10 }11 cout<<mm.size()<<endl; //输出大小 12 for(int val:mm) cout<<val<<" "; //遍历输出 13}
一、map简介 map是STL(标准模板库)的一个关联容器。 可以将任何基本类型映射到任何基本类型。如int array[100]事实上就是定义了一个int型到int型的映射。 map提供一对一的数据处理,key-value键值对,其类型可以自己定义,第一个称为关键字,第二个为关键字的值。
每个键在map中都是唯一的,而与之关联的值可以是任何类型。
map内部是自动排序的 二、map的用法 必须引入包
xxxxxxxxxx112.map的定义
xxxxxxxxxx21map<type1name,type2name> maps;//第一个是键的类型,第二个是值的类型2map<string,int> maps;3.map容器内元素的访问 通过下标进行访问 如:maps['c']=5; 通过迭代器进行访问 map可以使用it->first来访问键,使用it->second访问值
xxxxxxxxxx171#include<map>2#include<iostream>3using namespace std;int main(){4map<char,int>maps;5maps['d']=10;6maps['e']=20;7maps['a']=30;8maps['b']=40;9maps['c']=50;10maps['r']=60;11for(map<char,int>::iterator it=mp.begin();it!=mp.end();it++)12{13cout<<it->first<<" "<<it->second<<endl;14}15return 0;1617}
4.map的常用用法
xxxxxxxxxx411//插入2maps.insert()3// 定义一个map对象4map<int, string> m;5//用insert函数插入pair6m.insert(pair<int, string>(111, "kk"));7// 用insert函数插入value_type数据8m.insert(map<int, string>::value_type(222, "pp"));9// 用数组方式插入10m[123] = "dd";11m[456] = "ff";12maps.find() 查找一个元素13find(key): 返回键是key的映射的迭代器14map<string,int>::iterator it;15it=maps.find("123");16maps.clear()清空17maps.erase()删除一个元素18//迭代器刪除19it = maps.find("123");20maps.erase(it);21//关键字删除22int n = maps.erase("123"); //如果刪除了返回1,否则返回023//用迭代器范围刪除 : 把整个map清空24maps.erase(maps.begin(), maps.end());25//等同于mapStudent.clear()26maps.szie()长度27int len=maps.size();获取到map中映射的次数28maps.begin()返回指向map头部的迭代器29maps.end()返回指向map末尾的迭代器30//迭代31map< string,int>::iterator it;32for(it = maps.begin(); it != maps.end(); it++)33cout<<it->first<<" "<<itr->second<<endl;//输出key 和value值34maps.rbegin()返回指向map尾部的逆向迭代器35maps.rend()返回指向map头部的逆向迭代器36//反向迭代37map<string,int>::reverse_iterator it;38for(it = maps.rbegin(); it != maps.rend(); it++)39cout<<it->first<<' '<<it->second<<endl;40maps.empty()判断其是否为空41maps.swap()交换两个map
什么是迭代器
迭代器分类
获取迭代器
什么是迭代器
C++的迭代器是一种用于遍历容器元素的对象。迭代器提供了一种通用的访问容器元素的方式,无论容器的类型和数据结构如何。通过迭代器,我们可以依次访问容器中的每个元素,对其进行读取、修改或删除操作。
使用迭代器可以极大地简化对容器元素的访问和操作,同时提高程序的灵活性和可扩展性。迭代器的使用方式类似于指针,我们可以通过迭代器来遍历容器中的每个元素,并通过迭代器进行读写操作,而不需要关心容器内部的具体实现细节。
迭代器分类 在C++中,迭代器是一种用于访问容器中的元素的对象。C++标准库中提供了多种类型的迭代器,每种迭代器都有不同的功能和特性。以下是C++中常用的迭代器类型:
输入迭代器(Input Iterator):只能读取容器中的元素,而不能修改或重复读取。输入迭代器支持++、*、==、!=等操作符。适用于遍历容器中的元素,如std::istream_iterator。 *
xxxxxxxxxx18123
4int main() {5 int arr[] = {1, 2, 3, 4, 5};6 std::istream_iterator<int> input_iter(std::cin);7
8 for (auto it = std::begin(arr); it != std::end(arr); ++it) {9 *it = *input_iter;10 ++input_iter;11 }12
13 for (int num : arr) {14 std::cout << num << " ";15 }16
17 return 0;18}输出迭代器(Output Iterator):只能写入容器中的元素,而不能读取或重复写入。输出迭代器支持++、*等操作符。适用于向容器中写入数据,如std::ostream_iterator。
xxxxxxxxxx181234
5int main() {6 std::vector<int> vec;7 std::ostream_iterator<int> output_iter(std::cout, " ");8 9
10 for (int i = 1; i <= 5; ++i) {11 *output_iter = i;12 ++output_iter;13 vec.push_back(i);14 }15 16 return 0;17
18}前向迭代器(Forward Iterator):支持读写操作,可以向前遍历容器中的元素。前向迭代器支持++、*、==、!=等操作符。适用于需要多次遍历容器中的元素,如std::forward_list。 *
xxxxxxxxxx16123
4int main() {5 std::forward_list<int> flist = {1, 2, 3, 4, 5};6 7
8 auto it = flist.begin();9 while (it != flist.end()) {10 std::cout << *it << " ";11 ++it;12 }13 14 return 0;15
16}双向迭代器(Bidirectional Iterator):支持读写操作,可以向前和向后遍历容器中的元素。双向迭代器支持++、–、*、==、!=等操作符。适用于需要反向遍历容器中的元素,如std::list。
xxxxxxxxxx16123
4int main() {5 std::list<int> list = {1, 2, 3, 4, 5};6 7
8 auto it = list.begin();9 while (it != list.end()) {10 std::cout << *it << " ";11 ++it;12 }13 14 return 0;15
16}随机访问迭代器(Random Access Iterator):支持读写操作,可以在常数时间内进行随机访问。随机访问迭代器支持++、–、*、[]、+、-、<、<=、>、>=等操作符。适用于需要通过下标访问容器中的元素,如std::vector和std::array。
xxxxxxxxxx19123
4int main() {5 std::vector<int> vec = {1, 2, 3, 4, 5};6 7
8 for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {9 std::cout << *it << " ";10 }11 12 std::cout << std::endl;13 14 std::cout << "Element at index 2: " << vec[2] << std::endl;15 std::cout << "Element at index 4: " << *(vec.begin() + 4) << std::endl;16 17 return 0;18
19}以上是C++中常见的迭代器类型及其示例。通过使用适当的迭代器类型,可以方便地对容器中的元素进行访问和操作。
以上就是迭代器常见的功能,接下来我用一张表格为大家展示迭代器的功能关系:
| 随机访问迭代器 | 双向 迭代器 | 正向 迭代器 | |
|---|---|---|---|
| 支持 ++递增 | √ | √ | √ |
| 支持 == !=判等 | √ | √ | √ |
| 支持 -- 递减 | √ | √ | × |
| 支持算数操作符 + - | √ | × | × |
| 支持 > < >= <= 的比较操作 | √ | × | × |
| 支持+= -=的操作 | √ | × | × |
| 支持下标随机访问 | √ | × | × |
在C++中,可以使用不同的方法来获取迭代器,具体取决于数据结构类型。以下是获取迭代器的几种常见方式:
使用begin()和end()方法: 这是最常用的获取迭代器的方式。通过调用容器的begin()方法,可以返回指向第一个元素的迭代器;而调用end()方法则返回指向容器末尾的迭代器。例如:
xxxxxxxxxx19123
4int main() {5 std::vector<int> vec{1, 2, 3, 4, 5};6 7
8 // 使用begin()和end()方法获取迭代器9 auto it = vec.begin();10 auto end = vec.end();11 12 // 遍历容器13 for (; it != end; ++it) {14 std::cout << *it << " ";15 }16 17 return 0;18
19}输出结果为:1 2 3 4 5
使用rbegin()和rend()方法: rbegin()方法返回指向容器最后一个元素的逆向迭代器,而rend()方法则返回指向容器开始的逆向迭代器。逆向迭代器可以从容器的末尾开始向前遍历容器。例如:
xxxxxxxxxx19123
4int main() {5 std::vector<int> vec{1, 2, 3, 4, 5};6 7
8 // 使用rbegin()和rend()方法获取逆向迭代器9 auto it = vec.rbegin();10 auto end = vec.rend();11 12 // 遍历容器13 for (; it != end; ++it) {14 std::cout << *it << " ";15 }16 17 return 0;18
19}输出结果为:5 4 3 2 1
使用advance()方法: advance()方法允许我们在迭代器上进行偏移,以便访问容器中的其他元素。它需要两个参数,第一个是迭代器,第二个是偏移量。使用advance()方法可以在容器中跳过指定数量的元素,然后获取新位置上的迭代器。例如:
xxxxxxxxxx201234
5int main() {6 std::list<int> lst{1, 2, 3, 4, 5};7 8
9 // 获取迭代器,并在容器中移动到第三个元素之后10 auto it = lst.begin();11 std::advance(it, 2);12 13 // 输出第三个元素之后的元素14 for (; it != lst.end(); ++it) {15 std::cout << *it << " ";16 }17 18 return 0;19
20}输出结果为:3 4 5
使用next()和prev()方法: next()方法返回当前迭代器的下一个迭代器,prev()方法返回当前迭代器的前一个迭代器。这些方法也需要两个参数,第一个是迭代器,第二个是偏移量。它们在使用时可以更加方便,无需像advance()方法一样显式指定迭代器的类型。例如:
xxxxxxxxxx181234
5int main() {6 std::array<int, 5> arr{1, 2, 3, 4, 5};7 8
9 // 获取第三个元素的下一个迭代器和前一个迭代器10 auto nextIt = std::next(arr.begin(), 2);11 auto prevIt = std::prev(arr.begin(), 2);12 13 // 输出第三个元素和其周围的元素14 std::cout << *prevIt << " " << *nextIt << std::endl;15 16 return 0;17
18}输出结果为:2 4
这些是获取C++中迭代器的几种常见方式,可以根据具体的需求选择合适的方法来获取迭代器
高精度计算通常用于处理大数的加减乘除等运算。在C++中,可以通过字符串或数组来模拟大整数的处理。
对于位数较大的整数的加法,使用数组来存储整数,数组的长度等于整数的位数,按照字符串的形式输入,先统计长度,然后倒置过来利用数组进位来模拟加法,最终将数组最大位置的前导0判断去除,倒序输出即为最终的结果。
大整数 输入
我们用一个数组来保存整个高精度整数,并记录高精度整数的长度len。 因为我们在模拟一个计算竖式的过程,需要从右往左读入以解决进位的问题。
xxxxxxxxxx71string num;2cin >> num;3int a[105], len = num.size(); //num.size() 取 num 字符串的长度 num.length() 也可以4for (int i = 0; i < len; i++) {5 a[i] = num[len - 1 - i] - '0';// 减去'0'是为了把字符数字变成整数数字6}7//这里的 num[len - 1 - i] 是把字符串从右向左的读取,i=0时为最右边的数。大整数 输出
输出一个高精度的数就非常容易了,直接把数组中的元素倒序输出就可以了。
xxxxxxxxxx31for (int i = len - 1; i >= 0; i--) {2 cout << a[i];3}高精度加高精度参考代码(多位加多位)
xxxxxxxxxx4412using namespace std;3string num1, num2;4int a1[105], a2[105], len1, len2;5int main() {6 7 //1.输入数字字符串8 cin >> num1 >> num2;9 len1 = num1.size(); //取 num1 的长度10 11 //2.把字符串转存到数组中,存入类型为整数12 for (int i = 0; i < len1; i++) {13 a1[i] = num1[len1 - 1 - i] - '0';14 }15 len2 = num2.size();//取 num2 的长度16 for (int i = 0; i < len2; i++) {17 a2[i] = num2[len2 - 1 - i] - '0';18 }19 len1 = max(len1, len2);//取位数多的那个值做为后序处理长度20 21 //3.按位依次相加,此时不管进位问题22 for (int i = 0; i < len1; i++) {23 a1[i] += a2[i];24 }25 26 //4.处理进位,不包括最后一位的进位,最后一位的值可能大于10,下一步单独处理27 for (int i = 0; i < len1; i++) {28 a1[i + 1] += a1[i] / 10;29 a1[i] %= 10;30 }31 32 //5.处理最后一位的进位33 while (a1[len1]) {34 a1[len1 + 1] += a1[len1] / 10;35 a1[len1] %= 10;36 len1++;37 }38 39 //6.输出40 for (int i = len1 - 1; i >= 0; i--) {41 cout << a1[i];42 }43 return 0;44}
高精度减高精度参考代码(多位减多位)
xxxxxxxxxx6312using namespace std;3string num1, num2;4int a1[105], a2[105], len1, len2;5bool sgn; //设一个标志,判断正负6
7//比较两个字符串的长度,a 大返回假,b 大返回真8bool cmp(string a, string b) {9 if (a.size() != b.size()) {10 return a.size() < b.size();11 }12 return a < b;13}14
15int main() {16 cin >> num1 >> num2;17 18 //1.比较两串数字的长度,把长的放num1,短的放num219 if (cmp(num1, num2)) {20 sgn = true;21 swap(num1, num2);22 }23 24 len1 = num1.size(); //取较长字符串的位数做为计算位数25 26 //2.转存字符串到整数数组27 for (int i = 0; i < len1; i++) {28 a1[i] = num1[len1 - 1 - i] - '0';29 }30 len2 = num2.size();31 for (int i = 0; i < len2; i++) {32 a2[i] = num2[len2 - 1 - i] - '0';33 }34 35 //3.按位相减,此时不管借位36 for (int i = 0; i < len1; i++) {37 a1[i] -= a2[i];38 }39 40 //4.处理借位41 for (int i = 0; i < len1; i++) {42 while (a1[i] < 0) {43 a1[i + 1]--;44 a1[i] += 10;45 }46 }47 48 //5.当最高位借完为0时,位数要减 149 while (len1 > 1 && a1[len1 - 1] == 0) {50 len1--;51 }52 53 //按标志判断正负(本程序没处理位数相同时,相减为负的情况)54 if (sgn) {55 cout << "-";56 }57 58 //6.输出59 for (int i = len1 - 1; i >= 0; i--) {60 cout << a1[i];61 }62 return 0;63}
高精度乘高精度参考代码(多位乘多位)
xxxxxxxxxx4112using namespace std;3string num1, num2;4int a1[105], a2[105], len1, len2, a[205], len;5int main() {6 //1.输入并转存到数组7 cin >> num1 >> num2;8 len1 = num1.size();9 for (int i = 0; i < len1; i++) {10 a1[i] = num1[len1 - 1 - i] - '0';11 }12 len2 = num2.size();13 for (int i = 0; i < len2; i++) {14 a2[i] = num2[len2 - 1 - i] - '0';15 }16 17 //2.按乘法竖式依次相乘,把结果放到相应数组单元18 for (int i = 0; i < len1; i++) {19 for (int j = 0; j < len2; j++) {20 a[i + j] += a1[i] * a2[j];21 }22 }23 len = len1 + len2 - 1;//得数的基础长度为两个相乘数长度之和-124 25 //3.处理进位26 for (int i = 0; i < len; i++) {27 a[i + 1] += a[i] / 10;28 a[i] %= 10;29 }30 while (a[len]) {31 a[len + 1] += a[len] / 10;32 a[len] %= 10;33 len++;34 }35 36 //4.输出37 for (int i = len - 1; i >= 0; i--) {38 cout << a[i];39 }40 return 0;41}
冒泡排序、选择排序、插入排序、桶排序、快速排序、归并排序……、
冒泡排序(Bubble Sort):基础直观的排序方法,通过反复交换相邻元素将最大值逐渐“冒”到数组尾部。
插入排序(Insertion Sort):逐个将元素插入已排序部分的适当位置,适合小规模数据或部分有序的数据。
选择排序(Selection Sort):每次从未排序的部分找到最小(或最大)元素并放到已排序部分的末尾。
快速排序(Qualk Sort):分治策略的经典应用,通常采用递归的方式,选取一个基准元素划分数组,然后对两部分分别排序。
归并排序(Merge Sort):也属于分治法,将数组分成两半,独立排序后再合并。
堆排序(Heap Sort):利用堆这种数据结构的特性,构建最大堆或最小堆来进行排序。
希尔排序(Sort):改进版的插入排序,通过一系列间隔序列实现更高效的排序。
计数排序(Counting Sort):适用于整数排序,通过统计每个元素出现的次数直接得到结果。
桶排序(Bucket Sort):将元素分配到有限数量的桶里,再分别对每个桶内的元素排序。
基数排序(Radix Sort):按位数从低位到高位排序,先处理最低位,再处理次低位,直到所有位都处理完毕。
各种排序方法的优缺点与应用范围:
冒泡排序(Bubble Sort)
优点:实现简单,代码量少。
缺点:性能较差,对于大规模数据排序效率低下。
应用范围:适用于小规模或几乎已排序的数据。
时间复杂度:最好情况下为O(n),最坏情况下为O(n^2)。
插入排序(Insertion Sort)
优点:对于小规模数据或基本有序的数据,性能较好。
缺点:对于大规模数据排序效率低下。
应用范围:适用于小规模数据。
时间复杂度:最好情况下为O(n),最坏情况下为O(n^2)。
选择排序(Selection Sort)
优点:实现简单,不占用额外的内存空间。
缺点:性能较差,对于大规模数据排序效率低下。
应用范围:适用于小规模数据。
时间复杂度:无论数据的初始状态如何,都需要进行n-1次比较,时间复杂度为O(n^2)。
快速排序(Quick Sort)
优点:在平均情况下,时间复杂度为O(n log n),效率较高。
缺点:在最坏情况下,时间复杂度为O(n^2),且空间复杂度可能较高。
应用范围:适用于大规模数据排序。C++的sort函数就使用了快速排序算法。
归并排序(Merge Sort)
优点:稳定,且时间复杂度始终为O(n log n),无论数据的初始状态如何。
缺点:空间复杂度较高,需要额外空间O(n)。
应用范围:适用于需要稳定排序的场合。
堆排序(Heap Sort)
优点:时间复杂度为O(n log n),且空间复杂度较低。
缺点:不稳定,且实现相对复杂。
应用范围:适用于需要较高性能且对数据稳定性要求不高的场合。
其他排序算法:
希尔排序(Shell Sort):是插入排序的一种改进版本,通过比较相距一定间隔的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。
计数排序(Counting Sort)、桶排序(Bucket Sort)、基数排序(Radix Sort):这些排序算法通常用于特定类型的数据,如非负整数或具有特定分布的数据,它们的时间复杂度可以低于O(n^2),但通常需要额外的空间。
在选择排序算法时,需要考虑数据的规模、稳定性要求、内存使用情况等因素。对于小规模或基本有序的数据,简单排序算法(如冒泡排序、插入排序、选择排序)可能足够高效。对于大规模数据,通常选择时间复杂度较低的排序算法(如快速排序、归并排序、堆排序)。如果数据具有特定分布或类型,可以考虑使用计数排序、桶排序或基数排序等算法。
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1 算法描述
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
1.2 动图演示

1.3 代码实现
xxxxxxxxxx231template<class T>2void swap(T& a, T& b) {3 T temp = a;4 a = b;5 b = temp;6}7
8void BubbleSort(std::vector<int>& nums, int n) {9 if (n <= 1) return;10 bool is_swap;11 for (int i = 1; i < n; ++i) {12 is_swap = false;13 //设定⼀个标记,若为false,则表示此次循环没有进⾏交换,也就是待排序列已经有序,排序已经完成。14 for (int j = 1; j < n - i + 1; ++j) {15 if (nums[j] < nums[j - 1]) {16 std::swap(nums[j], nums[j - 1]);17 is_swap = true;//表示有数据交换18 }19 }20 if (!is_swap) break;//没有数据交集,提前退出21 }22}23
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
初始状态:无序区为R[1..n],有序区为空;
第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
n-1趟结束,数组有序化了。
2.2 动图演示

2.3 代码实现
xxxxxxxxxx211template<class T>2void swap(T& a, T& b) {3 T temp = a;4 a = b;5 b = temp;6}7
8void SelectSort(std::vector<int>& nums, int n) {9 if (n <= 1) return;10 int mid;11 for (int i = 0; i < n - 1; ++i) {12 mid = i;13 for (int j = i + 1; j < n; ++j) {14 if (nums[j] < nums[mid]) {15 mid = j;16 }17 }18 std::swap(nums[mid], nums[i]);19 }20}21
2.4 算法分析
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素,在已经排序的元素序列中从后向前扫描;
如果该元素(已排序)大于新元素,将该元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
重复步骤2~5。
3.2 动图演示

3.2 代码实现
xxxxxxxxxx161template<class T>2void swap(T& a, T& b) {3 T temp = a;4 a = b;5 b = temp;6}7
8void InsertSort(std::vector<int>& nums, int n) {9 if (n <= 1) return;10 for (int i = 0; i < n; ++i) {11 for (int j = i; j > 0 && nums[j] < nums[j - 1]; --j) {12 std::swap(nums[j], nums[j - 1]);13 }14 }15}16
3.4 算法分析
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图演示


4.3 代码实现
xxxxxxxxxx111// 希尔排序2void shellSort(vector<int>& nums) {3 for (int gap = nums.size() / 2; gap > 0; gap /= 2) {4 for (int i = gap; i < nums.size(); ++i) {5 for (int j = i; j - gap >= 0 && nums[j - gap] > nums[j]; j -= gap) {6 std::swap(nums[j - gap], nums[j]);7 }8 }9 }10}11
4.4 算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1 算法描述
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
5.2 动图演示

5.3 代码实现
xxxxxxxxxx311void mergeCount(int a[], int L, int mid, int R) {2 int* tmp = new int[L + mid + R];3 int i = L;4 int j = mid + 1;5 int k = 0;6 while (i <= mid && j <= R) {7 if (a[i] < a[j])8 tmp[k++] = a[i++];9 else10 tmp[k++] = a[j++];11 }12 ///判断哪个⼦数组中有剩余的数据13 while (i <= mid)14 tmp[k++] = a[i++];15 while (j <= R)16 tmp[k++] = a[j++];17 /// 将 tmp 中的数组拷⻉回 A[p...r]18 for (int p = 0; p < k; ++p)19 a[L + p] = tmp[p];20 delete tmp;21}22void mergeSort(int a[], int L, int R) {23 ///递归终⽌条件 分治递归24 /// 将 A[L...m] 和 A[m+1...R] 合并为 A[L...R]25 if (L >= R) { return; }26 int mid = L + (R - L) / 2;27 mergeSort(a, L, mid);28 mergeSort(a, mid + 1, R);29 mergeCount(a, L, mid, R);30}31
5.4 算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.2 动图演示

6.3 代码实现
xxxxxxxxxx131void QuickSort(std::vector<int>& nums, int l, int r) {2 if (l + 1 >= r) return;//如果只有一个数据,就返回。3 int first = l, last = r - 1, key = nums[first];//key 就是 piv4 while (first < last) {5 while (first < last && nums[last] >= key) last--;//右指针 从右向左扫描 将⼩于piv的放到左边6 nums[first] = nums[last];7 while (first < last && nums[first] <= key) first++;//左指针 从左向右扫描 将⼤于piv的放到右边8 nums[last] = nums[first];9 }10 nums[first] = key;//更新piv11 QuickSort(nums, l, first);//递归排序 //以L为中间值,分左右两部分递归调⽤12 QuickSort(nums, first + 1, r);13}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
7.1 算法描述
将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=r[n];< span=""></=r[n];<>
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2 动图演示

7.3 代码实现
xxxxxxxxxx461// 下沉操作2void downAdjust(vector<int> &num, int parent, int n) { 3 // 临时保存要下沉的元素 4 int temp = num[parent]; 5 // 定位左孩子节点的位置6 int child = 2 * parent + 1; 7 8
9 // 开始下沉10 while (child <= n) {11 // 如果右孩子节点比左孩子大,则定位到右孩子12 if (child + 1 <= n && num[child] < num[child + 1])13 ++child;14 15 // 如果孩子节点小于或等于父节点,则下沉结束16 if (num[child] <= temp) 17 break;18 19 // 父节点进行下沉20 num[parent] = num[child];21 parent = child;22 child = 2 * parent + 1;23 }24 num[parent] = temp;25
26}27
28// 堆排序29void HeapSort(vector<int> &num) {30 int len = num.size();31 32
33 // 构建大顶堆34 for (int i = (len - 2) / 2; i >= 0; --i) {35 downAdjust(num, i, len - 1);36 }37 38 // 进行堆排序39 for (int i = len - 1; i >= 1; --i) {40 // 把堆顶元素与最后一个元素交换41 std::swap(num[0], num[i]);42 // 把打乱的堆进行调整,恢复堆的特性43 downAdjust(num, 0, i - 1);44 }45
46}
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.1 算法描述
找出待排序的数组中最大和最小的元素;
统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
8.2 动图演示

8.3 代码实现
xxxxxxxxxx311// 计数排序2void CountingSort(vector<int> &num) { 3 int len = num.size();4 5
6 // 得到数列的最大和最小值7 int max = num[0], min = num[0]; 8 for (int i = 1; i < len; ++i) { 9 if(num[i] > max) 10 max = num[i]; 11 12 if (num[i] < min)13 min = num[i];14 }15 // 根据数列最大值确定统计数组的长度16 vector<int> countArray(max - min + 1, 0);17 18 // 遍历数列,填充统计数组19 for (int i = 0; i < len; ++i) {20 countArray[num[i] - min]++;21 }22 23 // 遍历统计数组,输出结果 24 int index = 0; 25 for (int i = 0; i < countArray.size(); ++i) {26 for (int j = 0; j < countArray[i]; ++j) {27 num[index++] = i + min;28 }29 } 30
31}8.4 算法分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
桶排序是计数排序的升级版。它利用了函数的映射关系,是否高效的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
设置一个定量的数组当作空桶;
遍历输入数据,并且把数据一个一个放到对应的桶里去;
对每个不是空的桶进行排序;
从不是空的桶里把排好序的数据拼接起来。
9.2 图片演示

9.3 代码实现
// 桶排序 // 有负数的话需要进行预处理,本函数包含预处理部分
xxxxxxxxxx381void BucketSort(vector<int>& num) {2 int len = num.size();3
4 // 得到数列的最大最小值5 int max = num[0], min = num[0];6 for (int i = 1; i < len; ++i) {7 if (num[i] > max)8 max = num[i];9 10 if (num[i] < min)11 min = num[i];12 }13 14 // 计算桶的数量并初始化15 int bucketNum = (max - min) / len + 1;16 vector<int> vec;17 vector<vector<int>> bucket;18 for (int i = 0; i < bucketNum; ++i)19 bucket.push_back(vec);20 21 // 将每个元素放入桶22 for (int i = 0; i < len; ++i) {23 // 减去最小值,处理后均为非负数24 int pos = (num[i] - min) / len;25 bucket[pos].push_back(num[i]);26 }27 28 // 对每个桶进行排序,此处可选择不同排序方法29 for (int i = 0; i < bucket.size(); ++i)30 sort(bucket[i].begin(), bucket[i].end());31 32 // 将桶中的元素赋值到原序列33 int index = 0;34 for (int i = 0; i < bucketNum; ++i)35 for (int j = 0; j < bucket[i].size(); ++j)36 num[index++] = bucket[i][j];37
38}9.4 算法分析
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
10.1 算法描述
取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示

10.3 代码实现
xxxxxxxxxx271int getDigit(int x, int d) {2 int t[] = { 1,1,10,100 };3 return (x / t[d]) % 10;4}5void RadixSort(int* a, int begin, int end, int d) {6 const int radix = 10;7 int c[1000];8 int bucket[1000];9 for (int k = 1; k <= d; ++k) {10 memset(c, 0, sizeof(c));11 for (int i = begin; i <= end; ++i) {12 c[getDigit(a[i], k)]++;//计算i号桶⾥要放多少数13 }14 for (int i = 1; i < radix; ++i) c[i] += c[i - 1];15 // 把数据依次装⼊桶(注意:装⼊时的分配技巧)16 for (int i = end; i >= begin; --i) {17 //求出关键码的第k位的数值, 例如:576的第3位是518 int j = getDigit(a[i], k);19 //放⼊对应的桶中(count[j]-1)表示第k位数值为j的桶底索引20 bucket[c[j] - 1] = a[i];21 --c[j]; //当前第k位数值为j的桶底边界索引减⼀22 }23 for (int i = begin, j = 0; i <= end; ++i, ++j) {// 从各个桶中收集数据24 a[i] = bucket[j];25 }26 }27}10.4 算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
参考:NOI / 1.10编程基础之简单排序
链表(Linked List)是一种常见的数据结构,由一系列节点(Node)组成,每个节点包含两个部分:一个是存储数据元素的数据域(Data Field),另一个是存储下一个节点地址的指针域(Pointer Field)或链接(Link)。这些节点链接起来形成一个线性结构。
链表与数组不同,数组在内存中是一块连续的空间,而链表中的节点在内存中不是连续存储的,它们是通过指针链接在一起的。链表可以分为单向链表、双向链表和循环链表等类型。
单向链表是最简单的链表结构,每个节点只包含一个指向下一个节点的指针。在单向链表中,只能从头节点开始遍历整个链表。
双向链表中的每个节点除了包含一个指向下一个节点的指针外,还包含一个指向前一个节点的指针。这样可以在任何节点处向前或向后遍历链表。
循环链表是一种特殊的链表,其尾节点的指针指向头节点,从而形成一个环。在循环链表中,可以从任何节点开始遍历整个链表。
链表的主要优点是可以动态地分配内存空间,不需要预先知道数据的大小。此外,链表在插入和删除操作上具有优势,因为只需要修改相关节点的指针即可,而不需要移动大量数据。然而,链表在访问特定位置的元素时效率较低,因为需要从头节点开始遍历链表。
链表在编程中有很多应用,如实现栈、队列、哈希表等数据结构,以及用于存储动态数据等场景。
单向链表(Single Linked List)是一种线性表的数据结构,它的每个节点只包含一个指向下一个节点的指针。具体来说,单向链表由一系列节点(Node)组成,每个节点包含两个域,一个信息域(用于存储数据元素)和一个指针域(用于存储指向下一个节点的指针)。

单向链表的特点如下:
单向性:每个节点只能指向它的下一个节点,不能指向前一个节点。因此,在单向链表中,从头部开始可以遍历整个链表,但无法从尾部直接找到链表的开始位置。
非循环:在普通的单向链表中,最后一个节点的指针域通常设置为空(NULL),表示链表的结束。但在循环链表中,最后一个节点的指针域指向链表的第一个节点,形成一个环。
动态分配:链表中的节点是在程序执行过程中动态分配和释放的,因此链表的大小可以动态地改变。
单向链表的主要操作包括:
插入:在链表的某个位置插入一个新的节点。
删除:删除链表中的某个节点。
遍历:从链表的头部开始,依次访问链表中的每个节点。
单向链表在许多应用中都有广泛的用途,如实现栈、队列、哈希表等数据结构,以及用于表示具有线性关系的数据集合。由于单向链表的结构相对简单,因此在编程中经常被用作数据结构的基础。
首先,需要定义一个链表节点(创建链表的结构)。每个节点包含一个数据成员(可以是任何类型,如整数、字符等)和一个指向下一个节点的指针。如下代码:
xxxxxxxxxx71struct Node {2 int data; // 节点中的数据部分3 Node* next; // 指向下一个节点的指针4
5 // 构造函数6 Node(int x) : data(x), next(nullptr) {}7};此段代码定义了一个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++中空指针类型的关键字,用来表示空指针类型。
xxxxxxxxxx1312using namespace std;3struct Node {4 int data; // 节点中的数据部分5 Node* next; // 指向下一个节点的指针6 Node(int x) : data(x), next(nullptr) {}7};8int main(){9 Node *node1 = new Node(1); //创建一个名为 node1 的节点10 Node *node2 = new Node(2);11 std::cout<<node1->data<<endl;12 std::cout<<node2->data<<endl;13}
上段代码创建了两个节点,但这两个节点都是独立了,并没有链在一起。
通过添加下段代码即可把它们链在一起。
node1->next=node2;
xxxxxxxxxx1512using namespace std;3struct Node {4 int data; // 节点中的数据部分5 Node* next; // 指向下一个节点的指针6 Node(int x) : data(x), next(nullptr) {}7};8int main(){9 Node *node1 = new Node(1); //创建一个名为 node1 的节点10 Node *node2 = new Node(2);11 node1->next = node2;//将node2接到node1后面12 //还可以用下面的方式来创建并插入节点。13 node2->next = new Node(3);//node2的next指向了一个新节点14 node2->next->next = new Node(4); 15}如果要创建很多个节点,可以使用循环。
使用循环建立链表时,不方便给每个节点取不同的名字,所以除了第一个节点外,其他节点貌似名字相同,要访问就不方便,这里可以用结构指针来指向链表的头,用指针找节点就方便了。只需用 p=p->next 就可找到下一个节点。
xxxxxxxxxx2012using namespace std;3struct Node {4 int data; // 节点中的数据部分5 Node* next; // 指向下一个节点的指针6 Node(int x) : data(x), next(nullptr) {}7};8int main(){9 //创建第一个节点,取名为头 head 10 Node *head = new Node(1);11 //建个指针来指向这个头12 Node *p = head;13 14 //使用循环建立链表15 for(int i=2;i<10;i++){16 Node *jd = new Node(i);17 p->next = jd;18 p = p->next;19 } 20}有了指针指向链表的头,我们可以使用指针来遍历链表的每一个节点。 只需在上一代码后面添加如下代码段即可。
xxxxxxxxxx51 p = head; //先将指针指向链表的头2 while(p){ //最后一个节点的指针指向的是nullptr,为空。3 cout<<p->data<<" ";4 p = p->next;5 }如果要查找某个值,可以让遍历到这个值时停下。 比如:
xxxxxxxxxx51p = head; //先将指针指向链表的头2int k=7;3 while(p->data!=7){4 p = p->next;5 }//循环完后,指针指向 7 。
使用 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;
xxxxxxxxxx3312using namespace std;3struct Node {4 int data; // 节点中的数据部分5 Node* next; // 指向下一个节点的指针6 Node(int x) : data(x), next(nullptr) {}7};8int main(){9 //创建第一个节点,取名为头 head 10 Node *head = new Node(1);11 //建个指针来指向这个头12 Node *p = head;13 14
15 //使用循环建立链表16 for(int i=2;i<10;i++){17 Node *jd = new Node(i);18 p->next = jd;19 p = p->next;20 } 21 int k=7;22 p = head;23 //找到7的前一个节点 24 while(p->next->data!=7){25 p = p->next; 26 }27 28 //删除7节点 29 Node *tem=p->next; 30 p->next=p->next->next; 31 delete tem;32
33}双向链表(Double-Linked List)是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。这样的结构赋予了双向链表更高的操作灵活性和更多的应用场景。 单向链表相比,双向链表的主要优点是可以很方便地从任意一个结点访问它的前驱结点和后继结点,从而克服了单向链表在查找链表中某结点不方便的缺点。 然而,双向链表也有其缺点。在插入或删除某个节点时,需要处理四个节点的引用(两个前驱指针和两个后继指针),而不是两个,这增加了实现的复杂性。同时,由于每个节点都需要额外的空间来存储前驱指针,所以双向链表相对于单向链表会占用更多的内存空间。

xxxxxxxxxx51typedef struct Node{2 struct line * prior; //指向直接前趋3 int data;4 struct line * next; //指向直接后继5}Node;xxxxxxxxxx71Node* createNode(int data) {2 Node* newNode = new Node;3 newNode->data = data;4 newNode->prev = nullptr;5 newNode->next = nullptr;6 return newNode;7}xxxxxxxxxx511void insertAtPosition(Node** head, Node** tail, int position, int data) {2 if (position < 0) {3 std::cerr << "Invalid position!" << std::endl;4 return;5 }6
7 Node* newNode = createNode(data);8
9 // 如果链表为空或插入在头部10 if (*head == nullptr || position == 0) {11 newNode->next = *head;12 if (*head != nullptr) {13 (*head)->prev = newNode;14 }15 *head = newNode;16 if (*tail == nullptr) {17 *tail = newNode;18 }19 } else {20 Node* current = *head;21 int index = 0;22
23 // 找到插入位置的前一个节点24 while (current != nullptr && index < position - 1) {25 current = current->next;26 index++;27 }28
29 // 如果位置超出链表长度,则在尾部插入30 if (current == nullptr) {31 if (*tail != nullptr) {32 (*tail)->next = newNode;33 newNode->prev = *tail;34 }35 *tail = newNode;36 } else {37 // 插入节点38 newNode->next = current->next;39 if (current->next != nullptr) {40 current->next->prev = newNode;41 }42 current->next = newNode;43 newNode->prev = current;44
45 // 如果插入位置是尾部,则更新尾指针46 if (current->next == nullptr) {47 *tail = newNode;48 }49 }50 }51}xxxxxxxxxx81void printList(Node* head) {2 Node* current = head;3 while (current != nullptr) {4 std::cout << current->data << " ";5 current = current->next;6 }7 std::cout << std::endl;8}forward_list表示一个单向链表,list表示一个双向链表。
list的介绍
list是序列容器,允许在序列中的任何位置执行固定O(1)时间复杂度的插入和删除操作,并在两个方向进行迭代。
list容器使用双链表实现;双链表将每个元素存储在不同的位置,每个节点通过next,prev指针链接成顺序表。
list与其他标准序列容器(array,vector和deque)相比,list通常可以在容器内的任何位置插入、提取和移动元素。
list与其他标准序列容器(array,vector和deque)相比,list和forward_list(单链表实现)的主要缺点是他们不能通过位置直接访问元素;例如,要访问列表中的第五个元素,必须从已知位置(开始或结束)迭代到该位置,需要哦线性时间开销。
存储密度低,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.包含头文件:
xxxxxxxxxx11#include <list>
2.声明:
xxxxxxxxxx11list<int> myList; // 声明一个int类型的list
3.插入元素:
使用 push_back() 在链表末尾插入元素。
使用 push_front() 在链表开头插入元素。
使用 insert() 在指定位置插入元素。
xxxxxxxxxx11cppmyList.push_back(10);myList.push_front(5);myList.insert(myList.begin(), 20); // 在开头插入20
4.删除元素:
使用 pop_back() 删除链表末尾的元素。
使用 pop_front() 删除链表开头的元素。
使用 erase() 删除指定位置的元素或范围内的元素。
xxxxxxxxxx11cppmyList.pop_back();myList.erase(myList.begin()); // 删除开头元素
5.遍历元素: 使用迭代器(iterator)或基于范围的for循环遍历元素。
xxxxxxxxxx11cppfor (std::list<int>::iterator it = myList.begin(); it != myList.end(); ++it) { std::cout << *it << " ";}// 或者使用基于范围的for循环for (const auto& elem : myList) { std::cout << elem << " ";}
6.其他操作:
size():获取链表中的元素数量。
empty():检查链表是否为空。
front() 和 back():访问链表的第一个和最后一个元素。
sort():对链表进行排序。
reverse():反转链表中的元素顺序。
std::forward_list基本用法:
1.包含头文件:
xxxxxxxxxx11cpp#include <forward_list>
2.声明:
xxxxxxxxxx11cppstd::forward_list<int> myForwardList; // 声明一个int类型的forward_list
3.插入元素:
使用 push_front() 在链表开头插入元素。
使用 insert_after() 在指定位置之后插入元素。
xxxxxxxxxx11cppmyForwardList.push_front(10);auto it = myForwardList.begin();myForwardList.insert_after(it, 20); // 在开头元素之后插入20
4.删除元素:
使用 pop_front() 删除链表开头的元素。
使用 erase_after() 删除指定位置之后的元素或范围内的元素。
xxxxxxxxxx11cppmyForwardList.pop_front();auto it = myForwardList.begin();myForwardList.erase_after(it); // 删除开头元素之后的元素
5.遍历元素:
使用迭代器(iterator)或基于范围的for循环遍历元素(但请注意,基于范围的for循环在C++11中不适用于std::forward_list,因为它没有end()之前的元素来初始化迭代器)。
xxxxxxxxxx11cppfor (std::forward_list<int>::iterator it = myForwardList.begin(); it != myForwardList.end(); ++it) { std::cout << *it << " ";}
6.其他操作
size():获取链表中的元素数量。
empty():检查链表是否为空。
front():访问链表的第一个元素。
由于std::forward_list是单向的,因此它通常比std::list更节省空间,并且在某些情况下可能具有更好的性能,尤其是当不需要向后遍历或访问时。然而,由于其单向性,某些操作(如insert_after和erase_after)可能需要额外的注意来正确管理迭代器。
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号节点:
xxxxxxxxxx41for(int i=1; i<m; i++) { 2 it++;3 if(it == node.end()) it = node.begin(); //循环:到末尾了再回头4 }2). 删除这个第m号节点,删除之前要把下一个节点的位置记下来
xxxxxxxxxx51list<int>::iterator next = ++it; //next迭代器用来记录m号节点之后的那个节点位置2 //循环链表,如果后面没有了就指向第一个3 if(next==node.end()) next=node.begin(); 4 node.erase(--it); //删除这个结点,node.size()自动减15 it = next; //将迭代器指向删除的节点后面那一个完整代码参考 :
xxxxxxxxxx25123using namespace std;4int main() {5 int n, m;6 cin>>n>>m;7 while(n!=0&&m!=0){8 list<int>node;9 for(int i=1; i<=n; i++) node.push_back(i); //建立链表10 list<int>::iterator it = node.begin();11 while(node.size()>1) { //list的大小由STL自己管理12 for(int i=1; i<m; i++) { //数到m13 it++;14 if(it == node.end()) it = node.begin(); //循环:到末尾了再回头15 }16 list<int>::iterator next = ++it;17 if(next==node.end()) next=node.begin(); //循环链表18 node.erase(--it); //删除这个结点,node.size()自动减119 it = next;20 }21 cout << *it<<endl;22 cin>>n>>m;23 }24 return 0;25}
C++队列(Queue)是一种常见的数据结构,它遵循先进先出(FIFO,First In First Out)的原则,即先进入队列的元素将先被移出队列。C++标准库提供了queue容器适配器来实现队列的功能。

1.包含头文件
xxxxxxxxxx11#include <queue>
2.定义队列
xxxxxxxxxx11queue<int> myQueue;
3.基本操作
push():在队列尾部插入一个元素。
xxxxxxxxxx11myQueue.push(10); // 在队列尾部插入整数10
pop():移除队列头部的元素。注意,这个操作不会返回被移除的元素。
xxxxxxxxxx11myQueue.pop(); // 移除队列头部的元素
front():返回队列头部的元素引用,但不移除它。
xxxxxxxxxx11int frontElement = myQueue.front(); // 获取队列头部的元素
back():返回队列尾部的元素引用,但不移除它。
xxxxxxxxxx11int backElement = myQueue.back(); // 获取队列尾部的元素
empty():检查队列是否为空。如果队列为空,则返回true;否则返回false。
xxxxxxxxxx11if (myQueue.empty()) { std::cout << "Queue is empty." << std::endl;}
size():返回队列中元素的数量。
xxxxxxxxxx11csize_t queueSize = myQueue.size(); // 获取队列的大小
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行,包含一个整数,为软件需要查词典的次数。(入队次数)
样例输入
xxxxxxxxxx71样例 #1:23 731 2 1 5 4 4 145样例 #2:62 1078 824 11 78 11 78 11 78 8 264
样例输出
xxxxxxxxxx51样例 #1:2534样例 #2:56
提示
输入输出样例 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就表示没在,这样会快很多,代码也少。(哈希算法)
参考代码:
xxxxxxxxxx25123using namespace std;4//建个数组用来存队列中有哪些数字,数组下标代表数字 5//该下标元素为1表示有这个数字,为0表示没有。 6int ha[1100]= {0};7queue<int>mem; //开个队列 8int main() {9 int en,m,n,c=0;10 cin>>m>>n;11 while(n--) {12 cin>>en;13 if(!ha[en]) {//如果这个数字没在数组中 14 c++;//入队次数 +115 mem.push(en);//入队 ,把这个数字加到队尾 16 ha[en]=1; //在数组中标记该数据存在 17 if(mem.size()>m) { //如果队列满了18 ha[mem.front()]=0; //把队首的数据在数组中标记为0 19 mem.pop(); //出队并删除队首20 }21 }22 }23 cout<<c;24 return 0;25}
栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。栈的基本操作包括压栈(push,也叫入栈)和弹栈(pop,也叫出栈)。压栈是将一个元素添加到栈的顶部,而弹栈则是移除栈顶部的元素。栈通常用于需要按照特定顺序处理元素的情况,例如函数调用和表达式求值。
包含头文件
xxxxxxxxxx11#include <stack>
创建栈
xxxxxxxxxx11cppstd::stack<int> myStack; // 创建一个存储int类型元素的栈
基本操作
push():将一个元素压入栈中。
xxxxxxxxxx11cppmyStack.push(10); // 将10压入栈中
pop():移除栈顶的元素。注意,这个操作不会返回被移除的元素。
xxxxxxxxxx11cppmyStack.pop(); // 移除栈顶的元素
top():返回栈顶元素的引用,但不移除它。
xxxxxxxxxx11cppint topElement = myStack.top(); // 获取栈顶元素的值
empty():检查栈是否为空。如果栈为空,则返回true;否则返回false。
xxxxxxxxxx11cppif (myStack.empty()) { std::cout << "Stack is empty." << std::endl;}
size():返回栈中元素的数量。
xxxxxxxxxx11cppstd::size_t stackSize = myStack.size(); // 获取栈的大小
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(); },遍历完字符串后,栈中还存有最后一个单词,再次弹出栈中所有字符,结束。
参考代码:
xxxxxxxxxx25123using namespace std;4int main(){5 stack<char>w;6 string s;7 getline(cin,s);8 for(const char &c:s){9 if(c!=' '){10 w.push(c); //入栈 11 }12 else{13 //判断栈不为空,如果是空栈w.empty()返回真。 14 while(!w.empty()){ 15 cout<<w.top(); //出栈, w.top()只是读取栈顶 16 w.pop(); //删除栈顶 17 }18 cout<<' ';19 } 20 }21 while(!w.empty()){22 cout<<w.top();23 w.pop();24 }25}
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.出栈。
参考代码:
xxxxxxxxxx2412 3 //istringsteam 所需头文件4 using namespace std;5 int main(){6 stack<string>w;7 string str;8 getline(cin,str);9 //将字符串str转换为字符流iss,iss是字符流的名字。 10 istringstream iss(str);11 string word;12 //iss>>word 从字符流读取数据到word,以空格为分隔标志,以回车符结束。13 while(iss>>word){14 w.push(word);15 }16 while(!w.empty()){17 cout<<w.top()<<" ";18 w.pop();19 }20 }21
22
23
24
分治算法(Divide and Conquer)的基本思想是将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。这些被分割出的小问题与原问题的性质相同,并且它们的解可以合并起来得到整个问题的解。在解决这些小问题的过程中,如果小问题规模仍然较大,则继续采用分治策略,直到问题规模足够小,可以直接求解为止。
分治算法一般包含以下三个步骤:
分解(Divide):将原问题分解为若干个规模较小,但结构与原问题相同的子问题。这些子问题是相互独立的,即子问题之间不包含公共的子问题。
解决(Conquer):递归地求解各个子问题。如果子问题的规模仍然较大,则继续分解为更小的子问题,直到子问题可以直接求解为止。这一步通常会采用递归的方式来实现。
合并(Combine):将各个子问题的解合并为原问题的解。这一步需要根据原问题的性质,确定如何合并子问题的解。
分治算法具有以下几个优点:
高效性:通过将问题分解为多个子问题并并行解决,可以显著提高算法的效率。
简化问题:将一个大问题分解为若干个小问题,使得每个小问题都相对容易解决。
并行性:子问题之间通常是相互独立的,因此可以并行处理,进一步提高算法的效率。
常见的分治算法有归并排序(Merge Sort)、快速排序(Quick Sort)、二分搜索(Binary Search)等。
分治算法在现实生活中的应用:
找出伪币(二分查找):假设你有一个装有16个硬币的袋子,其中有一个是伪造的,并且伪造的硬币比真硬币轻。为了找出这个伪造的硬币,你可以采用分治算法。首先,你可以将16个硬币分成两组各8个,然后用天平比较这两组的重量。如果两组重量相等,则伪币不存在于这两组中;如果重量不等,则伪币存在于较轻的那一组中。然后,你可以继续将较轻的那一组硬币分成更小的组,并重复上述过程,直到最终找出伪币。
汉诺塔问题(递归):汉诺塔是一个经典的递归问题,也可以使用分治算法来解决。假设你有三个柱子,初始时,有n个盘子从小到大依次放在第一个柱子上。目标是将这些盘子移动到第三个柱子上,并保持它们的相对顺序不变。每次只能移动一个盘子,并且大盘子不能放在小盘子上面。你可以将这个问题分解成两个子问题:将n-1个盘子从第一个柱子移动到第二个柱子,然后将最大的盘子从第一个柱子移动到第三个柱子,最后将n-1个盘子从第二个柱子移动到第三个柱子。
大规模数据排序: 对于非常大的数据集,使用传统的排序算法可能会非常耗时。然而,你可以使用分治算法(如归并排序)来加速排序过程。你可以将数据集分割成多个较小的部分,并对每个部分进行排序。然后,你可以将这些已排序的部分合并成一个完全排序的数据集。
社交网络分析: 在社交网络中,你可能需要分析用户之间的关系、查找群组或社区等。这些问题通常可以通过图算法来解决,而许多图算法都采用了分治策略。例如,你可以将社交网络图分割成多个较小的子图,并在每个子图上并行地执行图算法。然后,你可以将子图的结果合并起来以获得整个社交网络的分析结果。
这些只是分治算法在现实生活中的一些应用示例。实际上,分治算法在许多领域都有广泛的应用,包括计算机科学、工程、物理、生物学等。
能用分治法处理的问题通常具有以下特征:
问题可分解性:问题可以被分解为若干个规模较小、结构与原问题相似的子问题。子问题之间通常是独立的,即一个子问题的求解不会影响另一个子问题的求解。
子问题最优性:如果子问题的解是最优的,那么可以通过合并这些最优解来得到原问题的最优解。这意味着子问题的解对于整个问题的解是有意义的,并且可以直接利用。
递归性:分治法通常通过递归实现,即子问题的求解过程与原问题的求解过程相同或相似。这意味着可以使用相同的算法框架来解决不同规模的问题。
合并解的简单性:子问题的解可以很容易地合并成原问题的解。如果合并过程过于复杂,可能会抵消分治带来的优势。
子问题独立性:子问题之间通常是相互独立的,即一个子问题的求解不会影响到其他子问题的求解。这使得可以并行处理子问题,从而提高算法的效率。
问题规模足够大:对于规模很小的问题,直接求解可能比分治法更高效。因此,分治法通常适用于规模较大的问题。
适用分治法解决问题的例子:
归并排序:将待排序的序列划分为若干个子序列,对每个子序列进行排序,然后将有序子序列合并为整体有序序列。
快速排序:通过选择一个基准元素,将待排序的序列划分为两个子序列,其中一个子序列的元素都比基准元素小,另一个子序列的元素都比基准元素大,然后对这两个子序列递归进行快速排序。
二分搜索:在有序数组中查找某一特定元素的算法。算法将数组分成两半,然后确定目标元素是否存在于其中一半,然后在这一半中重复搜索过程。
大整数乘法:将两个大整数分解为较小的部分,并递归地计算这些部分的乘积,然后将结果合并起来得到最终的乘积。
矩阵乘法:将两个大矩阵分解为较小的子矩阵,并递归地计算这些子矩阵的乘积,然后将结果合并起来得到最终的乘积。
最近点对问题:在平面上给定n个点,找出距离最近的两个点。可以将问题分解为递归地在四个象限中查找最近点对,并合并结果。
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个看成一个,像这样:
只有两个盘子时移动起来很容易,程序实现也很简单,但上面的是n-1个,还不能直接移动,所以上面的n-1个还要继续分,直到剩下最后两个盘子时(递),就可以移动了。(这就是递归的过程)
2.解决:处理只有一个盘子和有两个盘子时的移动顺序
3.合并:递归数据返回,输出移动的顺序。(归)。
“递”是分解问题为子问题的过程,而“归”是解决子问题并将解合并成原问题解的过程。这两个步骤相互结合,构成了分治算法的核心思想。
解题步骤: 可以写一个函数来移动move(): 1.先写只有一个盘子时的移动顺序。 2.再加一个盘子该怎么移动?把两个盘子的移动顺序写到函数内。 本题的关键在于移动move()的 a b c 三个参数不好调,想要调好三个参数: 我们可以模拟只有两个盘子时的移动顺序,函数参数传值时的变化。
参考代码:
xxxxxxxxxx2312using namespace std;3int i=1;4void move(int n,char a,char b,char c){5 if(n==1) //当只有1个盘子时,直接从A移到B 6 cout<<a<<"->"<<n<<"->"<<b<<endl;7 else{8 //当盘子数量=2时,要先将上面的1个(除最下面那个之外)移到C柱。9 //所以下面递归调用move()时,要调整:A不动,B和C调换,递归调用时n-1为1,输出A->C 10 move(n-1,a,c,b); 11 //递归调用返回后,把A移到B 12 cout<<a<<"->"<<n<<"->"<<b<<endl;13 //最后还要把C移到B,由于move()的参数是 a b c, 14 //所以传值时 b不变, C调到 a 的位置 15 move(n-1,c,b,a);16 }17}18int main(){19 int h;20 char a,b,c;21 cin>>h>>a>>b>>c;22 move(h,a,b,c);23}
NOI / 2.4基本算法之分治
描述 已知长度最大为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位是循环出现。 像以下实例的运行结果可以看出规律。
xxxxxxxxxx1612using namespace std;3int M=343;4int main(){5 int i,k=M,b=0;6 for(i=1;;i++){7 k=(k*M)%1000;8 cout<<k<<" ";9 if(k==M){10 b++;11 cout<<" "<<i<<endl;12 if(b==3)13 return 0;14 }15 }16}以下输出结果是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.输出数组中对应的数。
参考代码:
xxxxxxxxxx2712using namespace std;3int a[510],Num=2011;4int main(){5 int mi=Num;6 //存2011幂次方的后4位所有可能的结果7 for(int i=2;;i++){8 mi=mi*Num%10000;9 a[i]=mi;10 if(mi==Num)break; 11 }12 //下面处理输入的K个数 13 string s; //s用来存大数14 int k,len; //len 表示s的长度,方便取后三位 15 cin>>k;16 for(int i=0;i<k;i++){17 int b=0;//b用来存s后三位的数值数据,注意每次循环都要初始化。 18 cin>>s;19 len=s.length(); 20 //取s的后三位,这里要注意len小于3的情况 21 for(int j=len-3;j<len;j++){22 if(j>=0) b=b*10+s[j]-'0';23 } 24 b%=500;25 cout<<a[b]<<endl;26 }27}NOI / 2.4基本算法之分治
描述 给定一个数组,统计前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的特点:
一个键对应多个值:这是 Multimap 最核心的特点。它允许我们向 Multimap 中添加一个键和多个值,并且可以通过键来检索到对应的值集合。(输入的序号为键,输入的值为值,可以证键唯一)
值集合不必唯一:普通的 Multimap 允许值集合中的值重复(输入的数可能有相同的)。
允许重复键:Multimap 允许存储具有相同键的多个值。这与传统的 Map 容器不同,后者通常要求每个键只能关联一个唯一的值。
自动排序:会根据键的排序准则自动对存储的键值对进行排序。(正是本题所需要的功能)
动态大小:Multimap 的大小可以根据需要动态增长或缩小,这使得它具有很好的灵活性。
参考代码:
xxxxxxxxxx19123using namespace std;4int main(){5 multimap<int ,int>mul;6 int n,num;7 cin>>n;8 for(int i=1;i<=n;i++){9 cin>>num;10 mul.emplace(num,i);11 //向 mul容器中插入一个新的键值对(如果键 num 已存在,则添加一个新的值 i 到与 num 关联的值集合中;如果 num 不存在,则创建一个新的键值对)。12 }13 int k;14 cin>>k;15 //反向迭代,rbegin()是末尾 rend()是开头16 for(auto it=mul.rbegin();it!=mul.rend()&&k>0;++it,k--){17 cout<<it->first<<endl;18 }19} 2.使用优先队列priority_queue存储数据(127ms) priority_queue的特点: 它提供了一种管理元素集合的方式,其中每个元素都有一个优先级,优先级最高的元素总是被首先访问(自动排序-降序)。 参考代码:
xxxxxxxxxx23123using namespace std;4 int main(){5 int n;6 priority_queue<int> Q;7 cin>>n;8 //入队9 for(int i=0;i<n;i++){10 int a;11 cin>>a;12 Q.push(a);//入队操作13 }14 int k=0;15 cin>>k;16 //出队17 for(int i=0;i<k;i++){18 cout << Q.top() <<endl;//取队头数据19 Q.pop(); //删除队头20 }21 return 0;22
23}3.使用快速排序算法(递归)(125ms) 快速排序算法的具体实现步骤如下:
选择枢轴:从待排序的序列中任意选择一个元素作为枢轴(pivot-比大小的参考值)。为了优化性能,可以选择序列的第一个元素、最后一个元素、中间元素,或者这三个元素的中位数作为枢轴。
分区操作:以枢轴为界,将所有小于等于枢轴的元素移到枢轴的左边,将所有大于枢轴的元素移到枢轴的右边。这一步骤完成后,枢轴就处于了序列的中间位置(注意,这里的中间位置并不是指序列的中点,而是指枢轴左边都是比它小的元素,右边都是比它大的元素)。
递归排序:递归地对枢轴左边和右边的两个子序列进行快速排序。
合并:由于快速排序是原地排序算法,不需要额外的存储空间来合并子序列,因为分区操作已经将子序列排序好了,只需要递归地对子序列进行相同的操作即可。
快速排序算法的关键在于分区操作,通过这一步骤,我们可以将一个大问题分解为两个小问题,然后递归地解决这两个小问题。在分区操作中,我们通常使用两个指针(或者索引)来遍历序列,一个指针从序列的左端开始向右移动,另一个指针从序列的右端开始向左移动,通过比较指针所指向的元素与枢轴的大小,将小于等于枢轴的元素移到左边,大于枢轴的元素移到右边。
参考代码(以下代码还可以进一步优化):
xxxxxxxxxx42123using namespace std;4vector<int> arr;5// 快速排序的分区函数6int partition(vector<int>& arr, int low, int high) {7 int pivot = arr[high]; // 选择最后一个元素作为基准值8 int i = (low - 1); // 小于基准值的元素的索引9 for (int j = low; j <= high - 1; j++) {10 // 如果当前元素小于或等于基准值11 if (arr[j] <= pivot) {12 i++; // 增加索引13 swap(arr[i], arr[j]); // 交换元素14 }15 }16 swap(arr[i + 1], arr[high]); // 将基准值交换到它的最终位置17 return (i + 1);18}19// 快速排序的主函数20void quickSort(vector<int>& arr, int low, int high) {21 if (low < high) {22 // pi 是基准值的索引23 int pi = partition(arr, low, high);24 // 分别对基准值左右两侧的元素进行递归排序25 quickSort(arr, low, pi - 1);26 quickSort(arr, pi + 1, high);27 }28}29int main() {30 int a,k;31 cin>>a;32 for(int i=0,tem;i<a;i++){33 cin>>tem;34 arr.push_back(tem);35 }36 int n = arr.size();37 quickSort(arr, 0, n - 1);38 cin>>k;39 for (int i = 1; n-i>=0&&k>0; i++,k--)40 cout << arr[n-i] <<endl;41 return 0;42}4.使用归并排序算法(递归)(128ms)实现步骤
拆分:将待排序的数组不断拆分为更小的子数组,直到每个子数组的长度为1或0。此时,每个子数组都是有序的(因为只有一个元素或没有元素)。
递归排序:对拆分得到的每个子数组递归地进行归并排序。由于每个子数组在递归开始时都是有序的(或为空),所以这一步实际上是在准备合并操作所需的有序子序列。
合并:将相邻的有序子数组合并为一个较大的有序数组。合并过程中,通过比较两个子数组的首元素,按照从小到大的顺序逐个将元素放入一个辅助数组,直到所有元素都被合并完毕。然后,将辅助数组中的元素复制回原数组,以完成合并操作。
重复合并:重复上述的拆分、递归排序和合并过程,直到最终得到完全排序的数组。
参考代码:
xxxxxxxxxx82123
4using namespace std;5
6// 归并排序的合并函数7void merge(vector<int>& arr, int left, int mid, int right) {8 int n1 = mid - left + 1;9 int n2 = right - mid;10
11 // 创建临时数组12 vector<int> L(n1), R(n2);13 14 // 拷贝数据到临时数组15 for (int i = 0; i < n1; i++)16 L[i] = arr[left + i];17 for (int j = 0; j < n2; j++)18 R[j] = arr[mid + 1 + j];19 20 // 合并临时数组回到原数组arr[left..right]21 int i = 0, j = 0, k = left;22 while (i < n1 && j < n2) {23 if (L[i] <= R[j]) {24 arr[k] = L[i];25 i++;26 } else {27 arr[k] = R[j];28 j++;29 }30 k++;31 }32 33 // 拷贝L[]的剩余元素34 while (i < n1) {35 arr[k] = L[i];36 i++;37 k++;38 }39 40 // 拷贝R[]的剩余元素41 while (j < n2) {42 arr[k] = R[j];43 j++;44 k++;45 }46}47
48// 归并排序的主要函数49void mergeSort(vector<int>& arr, int left, int right) {50 if (left < right) {51 // 找到中间点52 int mid = left + (right - left) / 2;53
54 // 分别对左右子数组进行排序55 mergeSort(arr, left, mid);56 mergeSort(arr, mid + 1, right);57 58 // 合并两个已排序的子数组59 merge(arr, left, mid, right);60 }61}62
63int main() {64 int n, k;65 cin >> n;66 vector<int> arr(n);67 for (int i = 0; i < n; i++) {68 cin >> arr[i];69 }70 cin >> k;71
72 // 对数组进行归并排序73 mergeSort(arr, 0, n - 1);74 75 // 输出前k个最大的数76 for (int i = n - 1; i >= n - k; i--) {77 cout << arr[i] << endl;78 }79 80 return 0;81
82}
1.贪心算法的基本思想:
它总是做出在当前看来最好的选择,即它希望通过局部最优解来构造全局最优解。这种策略并不保证总是能得到全局最优解,但在很多情况下,它确实能给出令人满意的答案,并且实现起来相对简单。
2.贪心算法的基本思路:
从问题的某一个初始解出发,逐步逼近给定的目标,以尽可能快地求得更好的解。在每一步选择中,它都只考虑一个数据,而不考虑子问题的解是否达到最优。
3.贪心算法的适用范围:
贪心算法因其简洁性和高效性,在多种情况下都有广泛应用。它尤其适合那些能够分解成多个简单子问题,并且子问题的最优解能够合并成全局最优解的问题。具体来说,贪心算法常用于以下几种类型的问题:
优化问题:当问题可以表示为一系列的选择,且每次选择都依赖于当前状态,而不需要考虑未来的选择时,贪心算法往往能给出不错的解。比如,在任务调度中,贪心算法可以用来选择当前可执行且完成时间最短的任务,从而优化整体任务的完成时间。
决策问题:在某些决策过程中,如果每次决策都基于当前的最优选择,且这种局部最优选择能够导致全局较优解(尽管不一定是全局最优),那么贪心算法就非常有用了。例如,在数据压缩中,贪心算法可以根据数据的统计特性选择最优的编码方式,从而减小数据的存储空间。
需要快速得到近似解的情况:当问题的精确解求解难度过大或耗时过长时,贪心算法提供了一种快速获得近似解的方法。这种近似解在很多情况下已经足够好,能够满足实际需求。
4.贪心算法的确定与设计策略
贪心算法的应用规则并不是一成不变的,但它确实有一些基本的原则和考虑因素。以下是一些关键的规则和建议,用于确定何时以及如何应用贪心算法:
确定问题是否适合贪心算法:
可分解性:问题应该能够被分解成一系列相对独立的子问题,且每个子问题的最优解可以合并成全局问题的解。
贪心选择性质:必须存在一种选择方法,使得每一步都选择当前状态下的最优解,且这种选择能够导向全局的最优或较优解。
无后效性:即之前的选择不会影响未来的最优选择。换句话说,一旦做出某个选择,后续的决策就不应该再考虑这个选择之前的任何信息。
设计贪心策略:
明确贪心准则:定义一种衡量“最优”的准则,这种准则应该能够引导我们做出每一步的最优选择。
考虑边界条件和特殊情况:确保贪心策略能够处理所有可能的输入情况,包括边界条件和特殊情况。
验证贪心策略的正确性:通过数学证明、反例分析或实验验证等方式,确保贪心策略的正确性。
初始化解决方案列表:首先,需要初始化一个用于存储解决方案的列表或数据结构。这个列表在算法执行过程中会逐渐填充,最终包含问题的解。
选择局部最优解:在每一步中,根据贪心策略选择当前状态下的最优解。这个选择是基于当前的信息和贪心原则做出的,并不考虑未来的影响或整体最优性。
更新状态:根据选择的局部最优解,更新问题的状态。这可能意味着从问题中移除已处理的部分,或者更新剩余部分以反映已做出的选择。
重复选择直到达到目标:继续重复步骤2和步骤3,直到达到问题的目标或无法再做出进一步的选择。在某些情况下,可能需要检查是否已经达到了问题的边界条件,如无法再添加更多元素到解决方案中。
返回解决方案:当算法结束时,返回填充好的解决方案列表或数据结构。这个列表包含了根据贪心策略逐步构建的问题的解
NOI / 4.6算法之贪心
描述 某天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.输出结果。
参考代码:
xxxxxxxxxx48123using namespace std;4//1.声明结构体数组,用于存储各种金属信息。5struct gold6{7 int num;//重量8 int value;//价值9 double price;//单价10}a[110]; //金属种类不超过10011
12//3.1.设置排序规则 cmp ,按结构体的price成员降充排13bool cmp(gold x, gold y) { //传入两个结构体x和y14 return x.price > y.price; //返前面大于后面,即降序15}16
17//2.输入数据18int main() {19 int k, w, s;//k几组数,w口袋大小,s金属种类,t口袋中已有多少价值20 double t;21 cin >> k;22 for (; k > 0; k--) {//一共有k组数,循环处理k次。23 cin >> w >> s;24 t = 0;//每组数据t都要初始化 25 for (int i = 0; i < s; i++) {//有s种金属,循环输入s次26 cin >> a[i].num; cin >> a[i].value;//输入每种金属的重量和价值到结构数组27 a[i].price=double(a[i].value)/a[i].num;//计算并存入单价。注意要转浮点数28 }29
30 //3.按单价排序结构数组31 sort(a, a + s, cmp);32 33 //4.遍历结构体数组,使得口袋刚好装满34 for (int i = 0; i < s; i++) {35 if ((w - a[i].num) > 0) {//如果口袋空间大于该金属重量,就全部拿36 t += a[i].value;37 w -= a[i].num; //拿了后要减掉口袋的空间38 }39 else {//执行到else时,表示口袋即将装满,只需再拿后面金属的一部分即可40 t += w * a[i].price;//装不下就拿一部分。剩余空间乘单价41 break;//已装满,不再拿取42 }43 }44 45 //5.输出结果46 printf("%.2f\n", t);//输出结果,进入下一组数据47 }48}NOI / 4.6算法之贪心
描述 小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。
参考代码:
xxxxxxxxxx1912using namespace std;3int a[1010];4int main(){5 int n;6 while(cin>>n){7 int m=0,sum=0;8 for(int i=0;i<n;i++){9 cin>>a[i];10 sum+=a[i];11 if(m<a[i])m=a[i];12 }13 if(m<sum-m)14 printf("%.1f\n",sum/2.0);15 else 16 printf("%.1f\n",(sum-m)*1.0);17 }18 return 0;19}