摘要
- C++11一些基本的数据结构
- C++常用函数调用
- 基本的使用方法(代码以及表格)
- 主要是为了方便索引查找,助力代码自动化(代码那么多,靠索引字典吧,我尽量的实现了位置索引)
一、基本的数据结构
1.1 栈stack
1.1.1 stack的基本特性
C++ stack(堆栈)是一种基于后进先出(LIFO)原则操作的容器。它具有以下基本特性:
- 后进先出(LIFO):只有位于堆栈顶部的元素可以被访问和删除,新添加的元素也会被放在堆栈的顶部。
- 动态性:stack的大小是动态的,可以根据需要自动增长或缩小。
- 随机访问:stack支持随机访问,可以通过索引直接访问任意位置的元素。但是注意,由于stack是后进先出(LIFO)的,所以最后一个添加到stack中的元素是第一个被访问的。
- 容器的通用性:stack可以容纳任何类型的元素,包括基本类型和自定义类型。
1.1.2 stack的代码使用例程
#include <stack> //头文件
std::stack<int> myStack; // 栈的声明创建
using namespace std; //忽略std::
stack<int> myStack;
myStack.push(10); //通过push添加元素
myStack.push(20);
int top = myStack.top(); // 获取栈顶元素
myStack.pop(); // 删除栈顶元素
bool isEmpty = myStack.empty(); // 判断栈是否为空
int size = myStack.size(); // 获取栈的大小
//一些高级使用方法,自定义栈(固定类型和固定长度)
std::stack<int, std::vector<int>> myStack;
1.1.3 stack的用法总结(表格)
函数名 | 功能描述 |
---|---|
push() | 向栈顶添加一个元素 |
pop() | 删除栈顶元素 |
top() | 返回栈顶元素的值 |
empty() | 判断栈是否为空,如果为空则返回true,否则返回false |
size() | 返回栈中元素的个数 |
clear() | 清空栈中的所有元素。 |
1.2 单端队列queue
1.2.1 queue的基本特性
C++ queue是一种基于先进先出(FIFO)原则操作的容器。它具有以下基本特性:
- 先进先出(FIFO):队列的头部是第一个被添加的元素,也是第一个可以被删除的元素。新添加的元素总是被放在队列的尾部。
- 动态性:queue的大小是动态的,可以根据需要自动增长或缩小。
- 随机访问:queue不支持随机访问,不能通过索引直接访问任意位置的元素。只能从队头开始依次访问。
- 容器的通用性:queue可以容纳任何类型的元素,包括基本类型和自定义类型。
1.2.2 queue的代码使用例程
#include <queue> //头文件声明
using namespace std;
queue<int> q; // 创建一个整数类型的队列
q.push(10); // 在队列尾部添加元素
bool em=q.empty(); //判断队列是否为空
int ans=q.front(); //获取队列的头部第一个元素
int ans=q.back(); //获取队列的尾部第一个元素
q.pop(); //删除队列头部第一个元素
int qsize=q.size(); //获取队列大小
1.2.3 queue的用法总结(表格)
函数名 | 功能描述 |
---|---|
push() | 在队列尾部添加一个元素 |
pop() | 删除队列的头部元素 |
front() | 返回队列的头部元素 |
back() | 返回队列的尾部元素 |
empty() | 判断队列是否为空(元素个数为0) |
size() | 返回队列中元素的个数 |
clear() | 清空栈中的所有元素。 |
1.3 双端队列deque
1.3.1 deque的基本特性
C++ deque(双端队列)是一种具有队列和栈的性质的数据结构。它支持在队列的前端和后端进行插入和删除操作。其基本特性包括:
- 双向队列:deque在队列的两端都可以进行插入和删除操作。
- 动态数组:deque底层通常使用动态数组实现,可以动态地增长和缩小。
- 顺序存储:deque中的元素是顺序存储的,相邻元素在内存中相邻存储。
- 支持随机访问:deque支持使用索引进行访问,可以随机访问队列中的元素。
- 高效的插入和删除操作:deque在队列的两端插入和删除元素的复杂度为O(1)。
1.3.2 deque的代码使用例程
#include <iostream>
#include <deque> //头文件声明
using namespace std;
deque<int> d; // 创建一个空的deque
d.push_front(1); // 在队列前端插入元素
d.push_back(3); // 在队列后端插入元素
int ans=d.size();//队列大小
d.pop_front(); //删除队列第一个元素
d.pop_back(); //删除队列最后一个元素
bool em=d.empty();//队列是否为空
1.3.3 deque的用法总结(表格)
函数名 | 功能描述 | 时间复杂度 |
---|---|---|
push_front() | 在队列前端插入一个元素 | O(1) |
push_back() | 在队列后端插入一个元素 | O(1) |
pop_front() | 删除队列前端的一个元素 | O(1) |
pop_back() | 删除队列后端的一个元素 | O(1) |
front() | 返回队列前端的元素 | O(1) |
back() | 返回队列后端的元素 | O(1) |
empty() | 判断空 |
1.4 数组容器vector
1.4.1 vector的基本特性
C++ vector是一种动态数组,它具有以下基本特性:
- 动态性:vector的大小是动态的,可以在运行时添加或删除元素。
- 随机访问:vector支持随机访问,可以通过索引直接访问任意位置的元素。
- 容器的通用性:vector可以容纳任何类型的元素,包括基本类型和自定义类型。
- 高效性:vector在内存中连续存储元素,因此访问元素的速度非常快。
1.4.2 vector的代码使用例程
#include <iostream>
#include <vector>
std::vector<int> v; // 创建一个空的vector
v.push_back(1); // 在vector末尾添加元素
int siz=v.size(); //返回vector数组大小
v.insert(v.begin(), 0); // 在vector前端插入元素
v.erase(v.begin() + 1); // 删除vector中的元素
v.clear(); // 清空vector中的元素
1.4.3 vector的用法总结(表格)
函数名 | 描述 |
---|---|
push_back() | 在vector末尾添加一个元素 |
emplace_back() | 在vector末尾添加一个元素(但不通过临时变量存储) |
insert() | 在vector前端或后端插入一个或多个元素 |
erase() | 删除vector中的一个或多个元素 |
clear() | 清空vector中的所有元素 |
resize() | 调整vector的大小(会进行变量的初始化) |
reserve() | 调整vector的内存大小(不进行变量的初始化) |
empty() | 判断vector是否为空 |
size() | 获取vector的大小 |
capacity() | 获取vector的容量 |
front() | 获取vector第一个元素的值 |
back() | 获取vector最后一个元素的值 |
operator[] | 通过索引访问vector中的元素 |
begin() | 获取vector的起始迭代器 |
end() | 获取vector的结束迭代器 |
rbegin() | 获取vector的逆向起始迭代器(从尾部开始) |
rend() | 获取vector的逆向结束迭代器(从尾部开始) |
1.4.4 vector的高级使用(优化)
1.4.4.1 元素插入(优化)
对于vector的元素插入存在两个函数push_back()以及emplace_back(),其都是向容器尾部插入元素,但存在一些小的差异点:
- push_back()的机理:1.构造一个临时对象——2.调用移动构造函数把临时对象的副本拷贝到容器末尾增加的元素中——3.调用析构释放临时对象。
- emplace_back()调用构造函数在容器末尾增加一个元素。
可以通过如下代码进行展示:
#include<iostream>
#include <vector>
using namespace std;
class MyTest
{
public:
//普通构造
MyTest(int id,int age):m_id(id),m_age(age)
{
cout << "创建" << this << endl;
}
//拷贝构造
MyTest(const MyTest &t):m_id(t.m_id),m_age(t.m_age)
{
cout << "拷贝" << this << endl;
}
//移动构造
MyTest(const MyTest &&t)
{
m_id = std::move(t.m_id);
m_age = std::move(t.m_age);
cout << "移动" << this << endl;
}
//析构
~MyTest()
{
cout << "析构" << this << endl;
}
private:
int m_id; //id成员
int m_age;//age成员
};
int main(int argc, char *argv[])
{
vector<MyTest> vec(10);
cout << "\n ------ push_back --------" << endl;
vec.push_back(MyTest(1,20));
cout << "\n ------ emplace_back --------" << endl;
vec.emplace_back(1,20);
cout << "\n -------- finish -------- " << endl;
}
通过编译可以得到如下结果:
------ push_back --------
创建
移动
析构
------ emplace_back --------
创建
-------- finish --------
析构
析构
因此emplace_back()的效率要比push_back()高得多,在处理大数据量的问题时效果更加明显。
1.4.4.2 容器大小声明(内存初始化)
在C++的std::vector
中,resize
和reserve
是两个用于处理动态数组的方法,它们的区别在于它们的目标和行为不同。
resize
:此方法用于改变vector
的大小。如果你将vector
的大小设为新的大小,那么vector
中超出新大小的部分会被删除,而小于新大小的部分会被添加空值(对于基本类型)或默认构造的对象(对于类类型)。reserve
:此方法用于改变vector
的容量。reserve
方法会预先分配足够的内存以容纳新的元素,但并不会构造这些元素。如果分配的容量大于当前的需求,那么多余的内存将被闲置,不会进行任何初始化。
std::vector<int> v;
v.resize(10); // v现在包含10个int类型的默认值(通常是0)
std::vector<int> v;
v.reserve(10); // v现在具有预先分配的内存,可以容纳10个int类型的元素,但v中仍然没有元素
1.5 双向链表list *
1.5.1 list的基本特性
C++的list是一种双链表,这意味着每个元素都有一个指向前一个和后一个元素的指针。相比于单链表,双链表在插入和删除操作中更为高效,因为它不需要遍历链表来找到插入或删除的位置。
list支持常见的序列操作,如添加元素(push_front, push_back),删除元素(erase),查找元素(find),访问元素(front, back)等。同时,list还支持一些更高级的操作,如合并(splice),排序(sort),反转(reverse)等。
1.5.2 list的代码使用例程
#include <iostream>
#include <list>
using namespace std;
list<int> my_list; // 创建一个整数类型的链表
my_list.push_back(1); // 添加元素
// 删除元素
my_list.pop_back(); // 删除末尾元素
my_list.pop_front(); // 删除头部元素
my_list.erase(2); // 删除指定元素
// 查找元素
auto it = my_list.find(1); // 在链表中查找元素1,返回指向它的迭代器
if (it != my_list.end()) { // 如果找到了元素1
std::cout << "Found: " << *it << std::endl; // 输出:Found: 1 }
1.5.3 list的用法总结(表格)
函数名 | 描述 |
---|---|
push_back() | 在链表末尾添加元素 |
push_front() | 在链表头部添加元素 |
pop_back() | 删除链表末尾的元素 |
pop_front() | 删除链表头部的元素 |
erase() | 删除链表中的指定元素 |
insert() | 在链表中指定位置插入元素 |
splice() | 将两个链表连接起来 |
sort() | 对链表进行排序 |
reverse() | 反转链表的顺序 |
find() | 在链表中查找元素 |
front() | 获取链表的第一个元素 |
back() | 获取链表的最后一个元素 |
empty() | 检查链表是否为空 |
size() | 获取链表的长度 |
1.6 基本数据结构容器的用法对比(表格)
stack | queue | vector | |||
---|---|---|---|---|---|
函数名 | 功能描述 | 函数名 | 功能描述 | 函数名 | 描述 |
push() | 向栈顶添加一个元素 | push() | 在队列尾部添加一个元素 | push_back() | 在vector末尾添加一个元素 |
pop() | 删除栈顶元素 | pop() | 删除队列的头部元素 | insert() | 在vector前端或后端插入一个或多个元素 |
top() | 返回栈顶元素的值 | front() | 返回队列的头部元素 | erase() | 删除vector中的一个或多个元素 |
empty() | 判断栈是否为空,如果为空则返回true,否则返回false | back() | 返回队列的尾部元素 | clear() | 清空vector中的所有元素 |
size() | 返回栈中元素的个数 | empty() | 判断队列是否为空(元素个数为0) | resize() | 调整vector的大小 |
clear() | 清空栈中的所有元素。 | size() | 返回队列中元素的个数 | empty() | 判断vector是否为空 |
clear() | 清空栈中的所有元素。 | size() | 获取vector的大小 | ||
capacity() | 获取vector的容量 | ||||
front() | 获取vector第一个元素的值 | ||||
back() | 获取vector最后一个元素的值 |
二、高级容器(unordered_map等)
2.1 unordered_map
2.1.1 unordered_map的基本特性
C++中的 unordered_map
是一种关联容器,它包含了键值对,并且是无序的。每一个键都是唯一的,与对应的值相关联。unordered_map
在内部使用了哈希表来实现,因此它的插入、删除和查找操作的时间复杂度通常是 O(1)。然而,由于哈希冲突的存在,最坏的情况可能会达到 O(n)。
在C++的unordered_map中,实际插入时是插入的键值对,而不是单独的值。unordered_map是一种关联容器,它存储键值对(key-value pairs)的映射关系。当你插入一个键值对时,unordered_map会根据键的哈希值将该键值对存储在相应的桶(bucket)中。因此,当你插入一个键值对时,实际上是同时插入了键和值两个元素。
注意:unordered_map
不保证元素的排序,无论是按照键还是值。
2.1.2 unordered_map的代码使用例程
#include <iostream>
#include <unordered_map>
#include <string>
int main() {
std::unordered_map<std::string, int> my_map;
// 插入键值对
my_map["apple"] = 1;
my_map["banana"] = 2;
my_map["cherry"] = 3;
// 查找值
int value = my_map["apple"]; // value 现在是1
std::cout << "The value of apple is: " << value << std::endl;
// 检查键是否存在
if (my_map.find("orange") == my_map.end()) {
std::cout << "The map does not contain orange" << std::endl;
}
// 删除键值对
my_map.erase("banana");
// 遍历键值对 (其中first表示键,而second表示值)
for (const auto& pair : my_map) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
2.1.3 unordered_map的用法总结(表格)
函数名 | 描述 |
---|---|
insert() | 插入一个或多个键值对 |
erase() | 删除一个或多个键值对 |
find() | 查找指定键的值,如果找不到则返回尾迭代器 |
count() | 返回指定键值的数量 |
empty() | 检查容器是否为空 |
size() | 返回容器中键值对的数量 |
clear() | 删除容器中的所有键值对 |
swap() | 与另一个容器交换内容 |
begin() | 返回指向第一个键值对的迭代器 |
end() | 返回指向尾部的迭代器 |
cbegin() | 返回指向第一个键值对的常量迭代器 |
cend() | 返回指向尾部的常量迭代器 |
hash_function() | 返回用于哈希的函数对象 |
key_eq() | 返回用于比较键的函数对象 |
2.1.4 map与unordered_map的对比
- 实现方式:map基于红黑树实现,保持元素有序,插入和查找的时间复杂度是O(log n)。unordered_map基于哈希表实现,不保持元素有序,插入和查找的时间复杂度平均为O(1),但最坏情况下可能会达到O(n)。
- 元素顺序:map中的元素按照键的自然顺序(或者通过自定义比较函数)进行排序,所以遍历得到的元素是有序的。unordered_map中的元素没有特定的顺序,遍历得到的元素顺序与插入顺序无关。
- 查找效率:map适用于需要有序数据以及频繁查找的场景。unordered_map适用于对插入和查找性能要求较高,对元素顺序无特别要求的场景。
- 内存开销:由于map使用红黑树来维护有序性,它的内存开销通常比unordered_map要高。unordered_map使用哈希表,其内存开销通常较低,但在存储大量数据时,由于哈希表冲突可能需要更多内存。
- 需要的关键字类型:map要求关键字类型有比较函数或支持<运算符来进行元素排序。unordered_map要求关键字类型支持哈希函数和==运算符。
- C++标准要求:map和unordered_map都是C++标准库的一部分,但在C++11之前,只有map是标准库的部分,而unordered_map是在C++11引入的新容器。
- 迭代器稳定性:map的迭代器在插入和删除元素时不会失效,因为它使用红黑树来保持顺序。unordered_map的迭代器在插入元素时可能会失效,因为哈希表的重新哈希过程可能导致元素位置改变。
2.1.5 unordered_multimap与unordered_map的区别
- 键的唯一性:unordered_map要求每个键必须是唯一的,而unordered_multimap允许键的重复,即它可以有多个相同的键,每个键可以关联不同的值。
- 值的存储方式:对于unordered_map,每个键只对应一个值,因此,当插入一个新的键值对时,如果该键已经存在,则新的值会覆盖旧的值。相反,unordered_multimap可以存储多个具有相同键的值,因此插入操作不会覆盖现有的值,而是添加一个新的键值对。
- 查找操作:由于unordered_map的键是唯一的,查找操作返回的是与键关联的单个值。然而,对于unordered_multimap,查找操作返回的是一个值的范围,因为可能有多个值与给定的键关联。
例如:在unordered_map中插入(key=2,value=1)、(key=2,value=2)两个键值对时:
在C++的unordered_map中,那么第二个插入操作会覆盖第一个插入操作的值。最终,unordered_map中只会存在一个键值对(key=2,value=2),而不是两个。
#include <unordered_map>
int main() {
std::unordered_map<int, int> my_map;
my_map.insert(std::make_pair(2, 1)); // 插入键值对(key=2,value=1)
my_map.insert(std::make_pair(2, 2)); // 插入键值对(key=2,value=2)
return 0;
}
注:unordered_map插入相同键值对时仅保留第一个,因为再次插入相同的元素会失败false;
例如:unordered_multimap中插入(key=2,value=1)、(key=2,value=1)、(key=2,value=1)三个键值对时:unordered_multimap允许键的重复,因此可以存储多个具有相同键的键值对。在这种情况下,插入操作不会覆盖现有的值,而是将新的键值对添加到unordered_multimap中。
#include <unordered_map>
int main() {
std::unordered_multimap<int, int> my_map;
my_map.insert(std::make_pair(2, 1));
my_map.insert(std::make_pair(2, 1));
my_map.insert(std::make_pair(2, 1));
return 0;
}
2.2 set
2.2.1 set的基本特性
C++中的set是一种基于红黑树实现的关联容器,它包含了唯一键值的集合。每个键在set中只能出现一次,不能重复。set的元素按照键的升序排列。
2.2.2 set的代码使用例程
#include <iostream>
#include <set>
#include <string>
int main() {
std::set<std::string> my_set;
// 插入元素
my_set.insert("apple");
my_set.insert("banana");
my_set.insert("cherry");
// 查找元素
auto it = my_set.find("banana");
if (it != my_set.end()) {
std::cout << "Found banana" << std::endl;
} else {
std::cout << "Banana not found" << std::endl;
}
// 删除元素
my_set.erase("banana");
// 遍历元素
for (const auto& item : my_set) {
std::cout << item << std::endl;
}
return 0;
}
2.2.3 set的用法总结(表格)
函数名 | 描述 |
---|---|
insert() | 在集合中插入一个或多个元素 |
erase() | 删除集合中的一个或多个元素 |
find() | 在集合中查找指定的元素,如果找到返回一个指向它的迭代器,否则返回尾迭代器 |
count() | 返回集合中等于给定值的元素数量 |
empty() | 检查集合是否为空 |
size() | 返回集合中元素的数量 |
clear() | 删除集合中的所有元素 |
begin() | 返回指向第一个元素的迭代器 |
end() | 返回指向尾部的迭代器 |
rbegin() | 返回指向最后一个元素的反向迭代器 |
rend() | 返回指向尾部的反向迭代器 |
2.3 unordered_set
2.3.1 unordered_set的基本特性
C++的unordered_set是一种基于哈希表实现的关联容器,它包含了唯一键值的集合。每个键在unordered_set中只能出现一次,不能重复。unordered_set的元素按照键的哈希值进行存储,因此它没有特定的顺序。与有序集合(set)不同,unordered_set不保证元素的排序。
2.3.2 unordered_set的代码使用例程
#include <iostream>
#include <unordered_set>
#include <string>
int main() {
// 创建一个unordered_set对象
std::unordered_set<std::string> my_set;
// 插入元素
my_set.insert("apple");
my_set.insert("banana");
my_set.insert("cherry");
// 查找元素
auto it = my_set.find("banana");
if (it != my_set.end()) {
std::cout << "Found banana" << std::endl;
} else {
std::cout << "Banana not found" << std::endl;
}
// 删除元素
my_set.erase("banana");
// 遍历元素
for (const auto& item : my_set) {
std::cout << item << std::endl;
}
return 0;
}
2.3.3 unordered_set的用法总结(表格)
函数名 | 描述 |
---|---|
insert() | 在集合中插入一个或多个元素 |
erase() | 删除集合中的一个或多个元素 |
find() | 在集合中查找指定的元素,如果找到返回一个指向它的迭代器,否则返回尾迭代器 |
count() | 返回集合中等于给定值的元素数量 |
empty() | 检查集合是否为空 |
size() | 返回集合中元素的数量 |
clear() | 删除集合中的所有元素 |
swap() | 与另一个集合交换内容 |
begin() | 返回指向第一个元素的迭代器 |
end() | 返回指向尾部的迭代器 |
cbegin() | 返回指向第一个元素的常量迭代器 |
cend() | 返回指向尾部的常量迭代器 |
hash_function() | 返回用于哈希的函数对象 |
key_eq() | 返回用于比较键的函数对象 |
2.4 pair
2.4.1 pair的基本特性
C++的std::pair
是一个用于存储两个不同类型对象的容器,这两个对象可以是不同的类型,而且这两个对象在pair
中并不是关联的,也就是说它们的类型并没有直接关系。这两个元素分别称为first和second,pair的所有构造函数都保持first和second的值不变。
2.4.2 pair的代码使用例程
#include <iostream>
#include <utility> // pair在这个头文件中定义
int main() {
// 创建一个pair对象
std::pair<int, std::string> p1(42, "Hello");
// 使用first和second成员函数
std::cout << "First element: " << p1.first << std::endl;
std::cout << "Second element: " << p1.second << std::endl;
// 使用赋值操作符
std::pair<int, std::string> p2;
p2 = p1; // 将p1的值赋给p2
std::cout << "p2 first element: " << p2.first << std::endl;
std::cout << "p2 second element: " << p2.second << std::endl;
// 使用swap函数交换两个pair的值
std::swap(p1, p2);
std::cout << "After swap: " << "p1 first element: " << p1.first << ", p1 second element: " << p1.second << std::endl;
std::cout << "After swap: " << "p2 first element: " << p2.first << ", p2 second element: " << p2.second << std::endl;
return 0;
}
2.4.3 pair的用法总结(表格)
函数名 | 描述 |
---|---|
first | 返回存储在pair中的第一个元素的引用 |
second | 返回存储在pair中的第二个元素的引用 |
operator= | 对pair的赋值操作符 |
swap() | 交换两个pair对象的值 |
2.5 使用场景及对比
pair | unordered_map | set | unordered_set |
key:value模式,区别是一个元素或者多个元素 | value单一元素,区别为是否有序 | ||
三、STL容器使用方法
3.1 STL简介
STL是C++标准库中的一个模板库,全称为Standard Template Library,它提供了一系列的通用数据结构和算法。stl包括六大组件,其中3个主要组件:算法(algorithm)、容器(container)、迭代器(iterator)和3个辅助组件:配置器(allocators)、适配器(adapters)、函数对象(function object)。stl中几乎所有的代码都采用了(T)模板类和(T)模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机制。
3.2 STL高级函数
3.2.1 遍历实现
1.迭代器遍历方式:遍历的实现主要取决于STL的迭代器函数,通过迭代器获取位置信息进行数据访问。
函数名 | 描述 | 返回值类型 |
---|---|---|
begin() | 返回一个指向容器中第一个元素的迭代器。 | 迭代器 |
end() | 返回一个指向容器中最后一个元素之后位置的迭代器。 | 迭代器 |
rbegin() | 返回一个指向容器最后一个元素之前的反向迭代器。 | 反向迭代器 |
rend() | 返回一个指向容器第一个元素之前的反向迭代器。 | 反向迭代器 |
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
2.使用下标进行遍历。
for(int idx =0;idx != numbers.szie(); ++idx) {
cout << numbers[idx] <<"";
}
cout << endl;
3.使用C++11的基于范围的for循环遍历。
std::vector<int> v = {1, 2, 3, 4, 5};
for(const auto &i : v) { //元素的遍历过程i
std::cout << i << " ";
}
std::cout << std::endl;
注意:C++11的基于范围是一种新的循环语法,它允许程序员更简洁地遍历容器中的元素。基于范围意味着我们只需要指定容器的起始和结束迭代器,然后循环会自动迭代并处理每个元素。
for (auto element : container) {
// 处理element
}
在上面的代码中,container
是一个表示容器的名称,element
是每个迭代的变量名,它将自动解引用以访问容器中的元素。这种语法不需要显式地声明迭代器,也不需要编写循环条件语句,因此代码更加简洁和易读。
3.2.2 常见算法表格
类别 | 算法名 | 功能 |
---|---|---|
排序算法 | sort | 对元素进行排序 |
stable_sort | 对元素进行稳定排序(相同元素的相对顺序保持不变) | |
merge | 将两个已排序的序列合并为一个排序的序列 | |
stable_merge | 将两个已排序的序列合并为一个稳定排序的序列(相同元素的相对顺序保持不变) | |
partial_sort | 将序列部分排序,指定前N个元素为排序后的元素 | |
Partial_sort_copy | 将序列的部分元素复制并排序,返回排序后的序列 | |
查找算法 | find | 在序列中查找某个元素,返回第一个出现的迭代器位置 |
find_if | 在序列中查找满足某个条件的元素,返回第一个出现的迭代器位置 | |
binary_search | 在已排序的序列中查找某个元素,返回其迭代器位置或未找到的迭代器位置 | |
lower_bound | 在已排序的序列中查找第一个大于或等于给定值的元素,返回其迭代器位置或未找到的迭代器位置 | |
upper_bound | 在已排序的序列中查找第一个大于给定值的元素,返回其迭代器位置或未找到的迭代器位置 | |
equal_range | 在已排序的序列中查找范围[l, r]内第一个大于或等于给定值的元素,返回其迭代器位置或未找到的迭代器位置 | |
遍历/变换算法 | transform | 对序列中的每个元素应用一个函数,并将结果存储在另一个序列中(可以与algorithm库结合使用) |
swap | 对于STL容器中的swap成员函数,如vector、list等,其功能是交换两个容器的内容。这个函数会交换两个容器中的元素,而不仅仅是交换值。在交换过程中,元素会被重新排序,以保持正确的顺序。 | |
fill | 将序列中的每个元素替换为某个给定的值(可以与algorithm库结合使用) | |
fill_n | 将序列中的前N个元素替换为某个给定的值(可以与algorithm库结合使用) | |
generate | 使用一个函数生成序列中的每个元素(可以与algorithm库结合使用) | |
generate_n | 使用一个函数生成序列中的前N个元素(可以与algorithm库结合使用) | |
最大最小算法 | max_element | 返回序列中最大元素的迭代器位置(可以与algorithm库结合使用) |
min_element | 返回序列中最小元素的迭代器位置(可以与algorithm库结合使用) | |
max_value | 返回序列中的最大值(可以与algorithm库结合使用) | |
min_value | 返回序列中的最小值(可以与algorithm库结合使用) |
3.3 泛型编程template
3.3.1 泛型编程基础
基本模式:
- C++泛型编程是一种编程范式,它允许程序员编写可适用于多种数据类型的代码。泛型编程的核心思想是将数据类型参数化,即通过使用类型参数来代替具体的数据类型,以实现代码的通用性和复用性。
- 在C++中,泛型编程主要通过模板来实现。模板是一种编译时多态性,它允许程序员定义与数据类型无关的函数或类,而将具体的数据类型作为模板参数在编译时进行替换。
- C++模板可以分为函数模板和类模板两种类型。函数模板定义了一组与数据类型无关的函数,而类模板定义了一组与数据类型无关的类。在函数模板中,类型参数可以用于函数的返回类型、形参类型和函数体中的变量类型。在类模板中,类型参数可以用于类的成员变量类型、成员函数返回类型和形参类型。
意义:
- 泛型编程可以提高代码的复用性和可维护性,同时也可以提高代码的可读性和可理解性。通过使用泛型编程,程序员可以编写出更加通用的代码,而无需针对不同的数据类型编写不同的代码。此外,泛型编程还可以提高代码的可测试性和可扩展性,从而降低代码的维护成本和提高代码的生命周期。
3.3.2 泛型编程函数与类用例
分别定义泛型编程里的函数模板、类模板以及常用的STL库迭代器。
//1.函数模版
template <typename T>
T add(T x, T y) {
return x + y;
}
//2.类模板
template <typename T>
class MyClass {
public:
T value;
void setValue(T val) {
value = val;
}
};
//3.泛型迭代器
template <typename T>
class Iterator {
public:
T* ptr;
Iterator(T* ptr) : ptr(ptr) {}
bool hasNext() {
return ptr != nullptr;
}
T next() {
T val = *ptr;
ptr++;
return val;
}
};
注意:template <typename T> 的含义是C++中的一个模板声明,其中typename
是一个关键字,用于指示编译器将后面的类型名称视为类型参数。
在模板声明中,T
是一个类型参数,它表示一个占位符,可以代表任何类型。当使用模板函数或类时,可以将具体的类型替换为T
,以实现代码的通用性和复用性。
3.3.3 泛型编程实战(全局变量函数注入)
四、C++基础(基本类型---库---函数)
4.1 基本类型(string、char、枚举等)
4.1.1 string与char
- 当需要存储和处理单个字符时,通常使用
char
。 - 当需要存储和处理一个或多个字符串时,通常使用
string
类。
string与char的声明与定义:
string a="xxxx";
char a='b';
注意:
-
双引号(""):
- 双引号主要用于表示字符串。在C++中,字符串是由一系列字符组成的,以空字符'\0'结束。双引号用于定义一个字符串,例如"Hello, World!"。
- 双引号内可以包含转义字符,例如'\n'表示换行,'\t'表示制表符等。
-
单引号('):
- 单引号主要用于表示字符。例如,'a'表示字符a,'B'表示字符B,以此类推。
- 在单引号内,不能包含转义字符。如果尝试在单引号内使用转义字符,例如'\n'或'\t',编译器会报错。
string的一些常用函数
- 构造函数:用于创建字符串。例如,
std::string a("Hello");
- 连接:使用
+
运算符或者append
函数可以连接字符串。 - 获取长度:使用
size()
或length()
函数可以得到字符串的长度a.size()
。 - 查找:可以使用
find()
函数查找子串或字符的位置。 - 替换:
replace()
函数可以替换字符串中的子串或字符。 - 比较:使用
==
、!=
、<
、<=
、>
、>=
运算符或者compare()
函数可以比较两个字符串。 - 子串:可以使用
substr()
函数获取字符串的子串。 - 插入:
insert()
函数可以在字符串的特定位置插入子串。 - 删除:
erase()
函数可以删除字符串中的子串。 - 遍历:可以使用下标
[]
或者at()
函数访问字符串中的单个字符。 - 字符串转义:可以使用
replace()
函数对特定字符进行转义处理。 - 查找第一个非空字符:可以使用
find_first_not_of()
函数查找第一个非空字符的位置。 - 查找最后一个非空字符:可以使用
find_last_not_of()
函数查找最后一个非空字符的位置。 - 反转字符串:可以使用
reverse()
函数反转字符串。 - 替换所有出现的子串:可以使用
replace_all()
函数替换所有出现的特定子串。 - 将数字转换为字符串:可以使用
to_string()
函数将数字转换为字符串。 - 判断是否以某个字符串开头或结尾:可以使用
starts_with()
或ends_with()
函数判断字符串是否以特定字符串开头或结尾。
#include <iostream>
#include <string>
int main() {
// 构造函数
std::string str1("Hello");
std::cout << str1 << std::endl;
// 连接
std::string str2 = "World";
std::string str3 = str1 + str2; // "HelloWorld"
std::cout << str3 << std::endl;
// 获取长度
size_t len = str3.size();
std::cout << "Length of string: " << len << std::endl;
// 查找子串位置
size_t pos = str3.find("World");
std::cout << "Position of \"World\": " << pos << std::endl;
// 替换子串
str3.replace(pos, 5, "Universe"); // "HelloUniverse"
std::cout << str3 << std::endl;
// 比较字符串
std::string str4 = "Hello";
bool isEqual = (str3 == str4);
std::cout << "Is str3 equal to str4? " << isEqual << std::endl;
// 子串
std::string subStr = str3.substr(0, 5); // "Hello"
std::cout << subStr << std::endl;
// 插入字符串
std::string str5 = " in ";
str3.insert(6, str5); // "Hello in World"
std::cout << str3 << std::endl;
// 删除子串
str3.erase(0, 5); // "World"
std::cout << str3 << std::endl;
// 遍历字符串
for(char c : str3) {
std::cout << c << " ";
}
std::cout << std::endl;
// 字符串转义
std::string str6 = "Hello\n"; // 新行字符转义为换行符
std::cout << str6 << std::endl;
// 查找第一个非空字符位置和最后一个非空字符位置
size_t firstNonBlankPos = str6.find_first_not_of(" \t\n"); // 忽略空格、制表符和换行符
size_t lastNonBlankPos = str6.find_last_not_of(" \t\n"); // 忽略空格、制表符和换行符
std::cout << "First non-blank position: " << firstNonBlankPos << std::endl;
std::cout << "Last non-blank position: " << lastNonBlankPos << std::endl;
// 反转字符串
std::string str = "Hello World";
std::reverse(str.begin(), str.end());
std::cout << str << std::endl; // 输出:dlroW olleH
// 分割字符串
std::string str2 = "apple,banana,orange";
std::vector<std::string> fruits = std::sregex_token_iterator(str2.begin(), str2.end(), ",");
for (const auto& fruit : fruits) {
std::cout << fruit << std::endl;
}
// 输出:apple
// 输出:banana
// 输出:orange
// 替换所有出现的子串
std::string str3 = "Hello World";
std::replace_all(str3, "World", "Universe");
std::cout << str3 << std::endl; // 输出:Hello Universe
// 将数字转换为字符串
int num = 123;
std::string str4 = std::to_string(num);
std::cout << str4 << std::endl; // 输出:123
// 判断是否以某个字符串开头或结尾
std::string str5 = "Hello World";
bool startsWith = str5.starts_with("Hello");
bool endsWith = str5.endsWith("World");
std::cout << std::boolalpha; // 输出:true
std::cout << startsWith << std::endl; // 输出:true
std::cout << endsWith << std::endl; // 输出:true
return 0;
}
4.1.2 位运算
4.1.2.1 位运算操作表格
位运算操作 | 描述 | 示例(假设操作数为二进制数) |
---|---|---|
按位与(&) | 将两个操作数的每个位进行比较,如果两个操作数的相应位都为1,则结果为1,否则为0。 | 1010 & 0110 = 0010(二进制) |
按位或(|) | 将两个操作数的每个位进行比较,如果两个操作数的相应位中有至少一个为1,则结果为1,否则为0。 | 1010 | 0110 = 1110(二进制) |
按位异或(^) | 将两个操作数的每个位进行比较,如果两个操作数的相应位不同,则结果为1,否则为0。 | 1010 ^ 0110 = 1100(二进制) |
按位取反(~) | 将操作数的每个位取反,即0变为1,1变为0。 | ~1010 = 0101(二进制) |
左移(<<) | 将操作数的所有位向左移动指定的位数,右侧用0填充。 | 1010 << 2 = 101000(二进制) |
右移(>>) | 将操作数的所有位向右移动指定的位数,左侧用0填充。 | 1010 >> 2 = 0010(二进制) |
4.1.2.2 位运算操作代码
#include <iostream>
using namespace std;
int main() {
int a = 60; // 60 = 0011 1100
int b = 13; // 13 = 0000 1101
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "a & b = " << (a & b) << endl; // 12 = 0000 1100
cout << "a | b = " << (a | b) << endl; // 61 = 0011 1101
cout << "a ^ b = " << (a ^ b) << endl; // 49 = 0011 0001
cout << "~a = " << (~a) << endl; // -61 = 1100 0011
cout << "a << 2 = " << (a << 2) << endl; // 240 = 1111 0000
cout << "a >> 2 = " << (a >> 2) << endl; // 15 = 0000 1111
return 0;
}
4.1.2.3 位运算的一些技巧算法
1.判断一个int类型存在多少个bit=1的位数,可以采用常规的暴力方式,也可以采用Brian Kernighan算法;
- 暴力求解:通过不断地左移bit与1进行相与比较;n&1;n>>1;
-
Brian Kernighan 算法:对于任意整数 x,令 x=x & (x−1),该运算将 x 的二进制表示的最后一个 1变成 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x 的「一比特数」。每一次的x-1都会使32进制表示的bit位中最后一个1翻转为0;
//1.常规右移
cont+=n & 1;
n=n>>1;
//2.依次最低位
x &= (x - 1);//x=111000 x-1=110111 x & (x - 1)=110000
2.最高有效位,当上面的Brian Kernighan 算法存在如下的特殊形式时表明该比特为最高有效位;
(x & (x - 1)) == 0//100000 011111
4.1.3 枚举enum
C++的枚举类型可以用来表示一组有限且固定的值。比如在如下代码中:
enum Color { RED, GREEN, BLUE };
定义了Color的枚举类型,分别对应三种不同的颜色。C++编译器会为枚举类型的常量分配整数值,从0开始递增。因此,在这个例子中,RED将被分配值为0,GREEN将被分配值为1,BLUE将被分配值为2。当然也可以通过指定数值进行赋值:
enum Color { RED = 1, GREEN = 2, BLUE = 3 };
4.1.3 宏定义#define
在C++中,宏定义可以用来在编译时进行文本替换和处理。
- 宏定义:宏定义是C++预处理器的一个功能,它可以在编译时进行文本替换。宏定义的基本语法可以分宏定义变量和宏定义函数。
//宏定义变量
#define 宏名称 替换文本
//宏定义函数
#define ADD(a, b) (a + b)
注意:对于定义的函数可能会存在混合类型运算的情况,比如a是int类型而b是float类型会引发错误。
当你使用这个宏时,比如 ADD(5, 3)
,预处理器会将其替换为 (5 + 3)
,即8。
但是,如果你给这个宏传递不同类型的参数,比如 ADD(5, 3.0)
,这会引起一个编译错误。这是因为预处理器在宏展开时不会进行任何类型检查或转换。在这种情况下,编译器会报出一个混合类型运算的错误,因为 5
是整数,而 3.0
是浮点数。
4.2 常用的C++库介绍(有表格形式,方便查找)
4.2.1 printf的格式化显示
格式说明符 | 输出内容 |
---|---|
%d | 十进制整数 |
%i | 十进制整数 |
%u | 无符号十进制整数 |
%f | 浮点数 |
%s | 字符串 |
%c | 字符 |
%p | 指针的值 |
%x | 十六进制整数 |
%o | 八进制整数 |
%% | 百分号(%) |
- | 无符号整数左对齐输出,空格填充负数右对齐输出。 |
+ | 表示在输出正数时显示正负号。 |
0 | 用0来填充输出的宽度。 |
%10.2f | 例如,我们可以使用格式说明符%10.2f,这意味着如果数的长度小于10,那么将在其左侧填充空格,使其总长度为10。如果数的长度大于10,那么将按原样输出。 |
#include <stdio.h>
int main() {
//1.不同类型的输出显示
int a = 10;
unsigned int b = 20;
float c = 3.14;
char d = 'A';
char* e = "Hello, world!";
int* p = &a;
printf("整数:%d\n", a); // 输出:整数:10
printf("无符号整数:%u\n", b); // 输出:无符号整数:20
printf("浮点数:%f\n", c); // 输出:浮点数:3.140000
printf("字符:%c\n", d); // 输出:字符:A
printf("字符串:%s\n", e); // 输出:字符串:Hello, world!
printf("指针:%p\n", p); // 输出:指针:0x7fffc564fb1c (这会是你进程的地址空间中的某个位置)
printf("十六进制整数:%x\n", a); // 输出:十六进制整数:0xa
printf("八进制整数:%o\n", a); // 输出:八进制整数:012
printf("百分比:%%\n"); // 输出:百分比:%
//2.对于特定类型的整齐显示
int a = 123;
int b = 4567;
float c = 3.14159;
float d = 0.234567;
printf("整数:%10d\n", a); // 输出:整数: 123
printf("整数:%10d\n", b); // 输出:整数: 4567 4567
printf("浮点数:%.2f\n", c); // 输出:浮点数:3.14
printf("浮点数:%.2f\n", d); // 输出:浮点数:0.23
//3.对于显示+-号以及左对齐
int a = 10;
int b = -20;
printf("整数:%+d\n", a); // 输出:整数: +10
printf("整数:%-10d\n", a); // 输出:整数: 10 (左侧填充空格)
printf("整数:%+d\n", b); // 输出:整数: -20
printf("整数:%-10d\n", b); // 输出:整数: -20 (左侧填充空格)
return 0;
return 0;
}
4.2.2 sprintf的格式化输入
sprintf
是一个 C 语言库函数,用于将格式化的数据写入字符串中。其与printf的根本区别主要是printf是将信息显示到界面cmd中,而sprintf
是将printf要输出的信息写入到字符串里。
#include <cstdio>
int main() {
char buffer[50];
int a = 10;
float b = 3.14;
std::sprintf(buffer, "整数是 %d, 浮点数是 %f", a, b);
std::printf("%s\n", buffer);
return 0;
}
需要注意的是,std::sprintf
可以导致缓冲区溢出,因此在使用时需要确保目标缓冲区足够大,能够容纳写入的字符串。同时,由于 std::sprintf
是 C 语言库函数,因此在使用时需要小心处理数据类型和格式化字符串的匹配问题,以避免出现类型不匹配和格式化错误等问题。
其与printf公用格式参数表。
4.3 常用的功能函数以及函数传参
4.4 常用的修饰词表(auto等)
4.4.1 auto
在C++中,auto
是一个关键字,它用于声明自动类型推导的变量或函数参数。在C++11及之后的版本中,auto
关键字的使用变得更加普遍和重要。
使用auto
关键字可以简化代码并提高可读性。通过自动类型推导,编译器可以根据变量的初始值或函数返回值来推断变量的类型。这使得程序员不必显式地指定变量的类型,从而减少了代码的冗余和错误的可能性。
// 自动类型推导示例
auto a = 10; // a 的类型被推导为 int
auto b = 3.14; // b 的类型被推导为 double
auto c = "Hello, world!"; // c 的类型被推导为 const char[14]
4.4.2 const
在C++中,const
关键词有几种不同的用法,都用于增强代码的安全性和可读性。
常见的用法可以分为如下几种:
- 声明常量
- 修饰指针
- 修饰引用
- 修饰函数参数
- 修饰成员函数
- 修饰类成员函数
//1.声明常量:const关键词可以用来声明常量。常量是在程序运行期间不能被改变的值。
//其中MAX_VALUE被定义为一个常量,并且被初始化为100。你不能在之后的代码中改变MAX_VALUE的值。
const int MAX_VALUE = 100;
——————————————————————————————————————————————————————————————————
//2. 修饰指针:const关键词可以用来修饰指针,产生常量指针(指针所指向的内容不能被改变)或只读指针(指针本身不能被改变)。例如:
int value = 50;
int* const ptr = &value; // 常量指针,指针不能被改变,但是可以改变它所指向的值
const int* ptr2 = &value; // 只读指针,它所指向的值不能被改变,但是指针本身可以改变
——————————————————————————————————————————————————————————————————
//3.修饰引用:const关键词可以用来修饰引用,产生常量引用。常量引用在声明时必须被初始化,而且不能通过常量引用来修改其引用的对象。例如:
const int& ref = value; // 正确,ref 是一个常量引用,它引用的是 value
int& const ref2 = value; // 错误,const 应该修饰引用前面的部分
——————————————————————————————————————————————————————————————————
//4.修饰函数参数:如果在函数参数前加上const,这意味着该参数在函数体内不能被修改。
void func(const int param) { // 在函数体内部,param 不能被改变
// ...
}
——————————————————————————————————————————————————————————————————
//5.修饰成员函数:在成员函数后面使用const关键词,表示这个成员函数不会修改类的任何数据成员。
class MyClass {
public:
int getSomeValue() const { // 这个成员函数不会修改类的任何数据成员
return someValue;
}
private:
int someValue;
};
——————————————————————————————————————————————————————————————————
//6.修饰类成员函数:在类定义中,如果希望某个成员函数不修改类的任何数据成员,可以在该函数后面添加const关键词。
class MyClass {
public:
int getSomeValue() const; // 这个成员函数不会修改类的任何数据成员
private:
int someValue;
};
注意:对于修饰问题可以参考先左再右的规则,即首先判断关键词的左边,比如对于这两句代码;
1---- int* const ptr = &value; // 常量指针,指针不能被改变,但是可以改变它所指向的值
2---- const int* ptr2 = &value; // 只读指针,它所指向的值不能被改变,但是指针本身可以改变
第一句中int* const ptr修饰左边的指针,因此也就意味着ptr的指针指向地址不能变动;
第二句中const int* ptr由于const左边已经没了,因此向右看,因此他修饰的int值类型,意味着不可以修改指针指向的值;
4.4.3 stastic
- 静态成员变量;
- 静态成员函数;
- 静态局部变量;
- 静态全局变量和函数;
//1.静态成员变量:静态成员变量不属于类的实例,而是属于类本身。这意味着无论有多少类的实例,都只有一个静态成员变量的副本。静态成员变量在所有实例之间共享。
class MyClass {
public:
static int staticVar;
};
MyClass C1;
MyClass C2;
C1.staticVar++;
C2.staticVar++; // 意味着staticVar+2
___________________________________________________
//2.静态成员函数:在类中,可以使用static关键字来定义静态成员函数。静态成员函数只能访问静态成员变量或其他静态成员函数。静态成员函数可以在没有类的实例的情况下调用。
class MyClass {
public:
static void staticFunc() {
// 使用静态变量或静态函数
}
};
//例如如下:
class MyClass {
public:
static int staticVar;
static void staticFunc() { //静态成员函数
cout << "This is a static function." << endl;
}
};
int MyClass::staticVar = 10;
int main() {
// 访问静态成员变量
cout << "StaticVar = " << MyClass::staticVar << endl;
// 调用静态成员函数
MyClass::staticFunc();
return 0;
}
___________________________________________________
//3.静态局部变量:在函数中,可以使用static关键字来定义静态局部变量。静态局部变量在函数调用之间保持其值。每次函数被调用时,静态局部变量的值都会被保留。和类静态成员变量类似
void myFunction() {
static int callCount = 0; // 静态局部变量
//对于上一句的赋值操作仅仅调用一次,不会每次都进行
callCount++;
// do something with callCount
}
___________________________________________________
//4.静态全局变量和函数:在文件中,可以使用static关键字来定义静态全局变量和函数。静态全局变量和函数只在定义它们的文件内可见,而不是在其他文件中可见。这对于限制变量和函数的可见性非常有用。
static int staticGlobalVar; // 静态全局变量
static void staticGlobalFunc() { // 静态全局函数
// do something
}
五、笔者一些代码看法
- 单单谈论代码是没有任何意义的,代码基础使用就仿佛汉字,而写出的代码就像作文,其结果因人而异,只不过这里的作文偏重点更偏向硬逻辑;
- 这部分仅仅阐述的是关于C++代码的基础部分,遇到了特定问题,用哪个数据结构?怎么设计算法?怎么设计重用架构?使用那些已有的上层架构?才是在编程代码过程中需要不断总结和提高的,重复的造轮子只会浪费时间;
- 我认为代码的种类可以分为两部分,其中一类是逻辑代码,而另一类是优化算法;
- 逻辑代码:可以认为是实现一种特定功能需求的概率为100%的NP问题;
- 优化算法:可以从统计的角度考虑为依概率收敛的问题(重复无穷次必然会有局部最优,但是不是全局最优不能保证),其具体的时间复杂度或可能是不可估量的;
- 这部分的内容我也会不断地更新完善,同时也会考虑关于算法的讲解以及python、java的模板,一方面可以将学到的知识进行高概括性总结,另一方面也可以有利于索引办公;