C++复习2 STL

5 篇文章 0 订阅

STL基础知识

在这里插入图片描述

写在前面:

什么是STL? Standard Template Library 。 C++ STL从广义来讲包括了三类:算法,容器和迭代器。

算法包括排序,复制等常用算法,以及不同容器特定的算法。 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

这部分内容主要参考 岁与禾 的简书博客 C++进阶-STL容器,你看我就够了。其次,还有参考C++ string类(C++字符串)完全攻略。一些补充内容的参考来源也会在文中注明。

  • 容器中的一般节点类,需要提供拷贝构造函数( 避免出现浅拷贝问题,导致崩溃 ),并重载等号操作符(用来赋值)

  • 每个容器都提供了一个默认构造函数和一个默认拷贝构造函数

string

  1. 创建string对象,首先要#include <string>
string s1();  // si = ""

//通过const char * 初始化,这两个效果一样
string s1 = "aaaa";
string s2("bbbbb");

//还可以选取部分字符串内容
string s3("12345", 1, 3);  //s3 = "234",即 "12345" 的从下标 1 开始,长度为 3 的子串

//用10个'a'字符来初始化字符串
string s4(10, 'a');

//通过拷贝构造函数来初始化对象s5
string s5 = s2;

此外, swap 成员函数可以交换两个 string 对象的内容。例如:

string s1("West”), s2("East");
s1.swap(s2);  // s1 = "East",s2 = "West"
  1. 遍历:
string str("abcdefg");
//数组形式遍历(不会抛出异常),程序直接终端。
for (int i = 0; i < str.length(); i++) {
	cout<< str[i] << endl;
}

//at方法遍历,根据index取值 (会抛出异常,可以捕获处理)
for (int i = 0; i < str.length(); i++) {
	cout << str.at(i) << endl;
}

//迭代器遍历
for (string::iterator it = str.begin(); it != str.end(); it++) {
	cout << *it << endl;
}

测试[]和at是否抛出异常:

//创建字符串对象
string str("abcdefg");
// 这个异常可以捕获
try{
    cout<<str.at(10)<<endl;
}catch(...){
    cout<<"使用at"<<endl;
}

// 这个异常不可以捕获
try{
    cout<<str[10]<<endl;
}catch(...){
    cout<<"使用[]"<<endl;
}

输出结果:

使用at

[此处无输出]

  1. 与char *的转换
//1 string转const char *
string str("aaaaaa");
const char *c_str = str.c_str();

//2 string 转char*
const int len = str.length();
c = new char[len+1];
strcpy(c,str.c_str());

//3 string拷贝到buf[]中
char buf[128] = {0};
str.copy(buf, 3, 0);	//从0开始,拷贝3个字符到buf指向的内存空间

string提供了copy(buf,size,begin)成员函数来讲string从begin位置开始的size个字符拷贝到buf中。如果buf空间容纳不下,会越界;拷贝过去时,不会给buf末尾添加\0

  1. 拼接,使用+或成员函数append.
string s1 = "123456";
string s2 = "abcdef";

//直接使用加号运算符拼接
string s3 = s1 + s2;

//使用成员函数拼接
string s4 = s1.append(s2);		// 此时s4和s1都是123456abcdef

与拼接相反的,就是子串了。参考C++ string类(C++字符串)完全攻略。substr 成员函数可以用于求子串 (n, m),原型如下:

string substr(int n = 0, int m = string::npos) const;

其中,看两个都是默认参数。注意,第二个参数m指的是子串长度。

string s1 = "this is ok";
string s2 = s1.substr(2, 4);  // s2 = "is i"
s2 = s1.substr(2);  // s2 = "is is ok"
  1. 查找和替换

find函数,用来查找字符串中指定的字符。第2个参数用于指定查找开始的位置。

string s1 = "hello hello 012345";
    
//从0位置开始查找第一个hello出现的首位位置
size_t index1 = s1.find("hello",0);
cout << index1 << endl;			// 0

//查找第一个hello出现时的首位位置
size_t index2 = s1.find_first_of("hello");
cout << index2 << endl;			// 0

//查找最后一个hello出现时的末尾位置
size_t index3 = s1.find_last_of("hello");
cout << index3 << endl;			// 10

另外参考C++ string类(C++字符串)完全攻略,查找时:

与find相对的是rfind,从后往前查找子串或字符出现的位置。

此外,还有find_first_not_of:从前往后查找何处出现另一个字符串中没有包含的字符。
find_last_not_of:从后往前查找何处出现另一个字符串中没有包含的字符。

replace函数,用来替换字符串中指定位置的字符串。replace函数先删除指定位置开始的指定长度歌字符,然后在当前指定位置插入新的字符。

// 子串(2,4)的含义是:从位置2开始的4个字符.
string s1("Real Steel");
s1.replace(1, 3, "123456", 2, 4);  //用 "123456" 的子串(2,4) 替换 s1 的子串(1,3)
cout << s1 << endl;  //输出 R3456 Steel

string s2("Harry Potter");
s2.replace(2, 3, 5, '0');  //用 5 个 '0' 替换子串(2,3)
cout << s2 << endl;  //输出 HaOOOOO Potter

int n = s2.find("OOOOO");  //查找子串 "00000" 的位置,n=2
s2.replace(n, 5, "XXX");  //将子串(n,5)替换为"XXX"
cout << s2 < < endl;  //输出 HaXXX Potter
  1. 区间删除和插入

string提供了inserterase分别实现插入和删除操作。下面的操作中, 使用int指示位置的,会返回新的string; 使用指针指示位置的,会返回操作后的当前指针位置。

string s1 = "Hello2World!";

// 删除字符串中的'2',
// 通过find函数,查找'2'所在的迭代器位置(find函数需要引入#include<algorithm>)
// 然后删除位置,会返回当前迭代器位置
string::iterator it = find(s1.begin(), s1.end(), '2');
if (it != s1.end()) {
    s1.erase(it);
}
cout << s1 << endl;		// HelloWorld!


// 删除两个迭代器之间的字符,左闭右开区间
s1.erase(s1.begin(), s1.begin() + 5);
cout << s1 << endl;		// World!


// 在0位置插入"AAA"
s1.insert(0, "AAA");
cout << s1 << endl;		// AAAWorld!

// 也可以用string对象来插入
string s2("BBB");
s1.insert(3,s2);
cout<< s1 << endl;		// AAABBBWorld!

// 位置6插入3个字符'C'
s1.insert(6,3,'C');
cout << s1 << endl;		// AAABBBCCCWorld!

// 删除从位置6开始的3个字符
s1.erase(6,3);
cout << s1 << endl;		// AAABBBWorld!

  1. 大小写转换

参考1. 使用C++的transform函数实现string的大小写转换,2. string类中字符的大小写转换.

string str = "abcdefgHIJKLMN";
//转小写
transform(str.begin(),str.end(),str.begin(),::tolower);
cout<<str<<endl;
//转大写
transform(str.begin(),str.end(),str.begin(),::toupper);
cout<<str<<endl;

string str = "ab123cdefg456,. HIJKLMN";时,上面的代码会将str中的字符进行相应的转化,其余的字符不会发生变化。

模板函数transform在#include<algorithm>中引入。原型如下:

template <class InputIterator, class OutputIterator, class UnaryOperator>
  OutputIterator transform (InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op)
{
  while (first1 != last1) {
    *result = op(*first1);  // or: *result=binary_op(*first1,*first2++);
    ++result; ++first1;
  }
  return result;
}

vector

vector是将元素放到动态数组中加以管理的容器。

  1. 首先介绍基本的初始化方法,以及 front() , back() , push_back() , pop_back() 四个方法的用法。

push_back()添加元素时会进行拷贝一份。对于类对象,由于默认生成的拷贝构造函数执行浅拷贝,这样的话对于指针类型的成员变量,可能会存在原对象和副本对象指向同一块内存区域,在删除对象时两次释放这块区域的错误。stl中push_back和浅拷贝和深拷贝的问题.

vector的多种初始化方式,参考C++:vector 六种初始化方法.

//默认构造函数定义一个vector容器
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);	// 1 2 3

//修改头部元素的值(front()返回是引用,可以当左值)
v1.front() = 44;	// 44 2 3

//修改尾部的值(back()返回是引用,可以当左值)
v1.back() = 99;		// 44 2 99

//删除元素(从尾部删除)
v1.pop_back();		// 44 2

//迭代器遍历打印
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
    cout << *it << " ";
}
cout << endl;		// 44 2

// 其他初始化方法
//通过拷贝构造函数初始化
vector<int> v2 = v1;
vector<int> v2(v1);

//使用部分元素来构造
vector<int> v3(v1.begin(), v1.begin() + 2);  // 1 2
vector<int> v4(v1.begin(), v1.end());

//指定元素个数
vector<int> v5(10);		// v5中有10个元素,都是0
vector<int> v6(3,9);	//v6中有3个元素,都是9

// 其他的初始化方式,已经运行过代码.这里可以查漏补缺
// 使用数组
vector<int> v7 = {3,4,6,3,7};
vector<int> v8{3,4,6,3,7};
  1. vector容器可以随机存取元素,也就是说支持[]运算符和at方式存取。这与string类的用法一致。 此外,迭代器提供了逆向遍历,可以通过迭代器来实现逆向遍历。
vector<int> v1;
for (int i = 0; i < 10; i++) {
	v1.push_back(i);
}

//遍历-迭代器逆向遍历
for (vector<int>::reverse_iterator it = v1.rbegin(); it != v1.rend(); it++) {
	cout << *it << " ";
}
cout << endl;		// 9 8 7 6 5 4 3 2 1 0
  1. 删除/插入元素, 需要根据元素的迭代器位置进行。
// 接2测试,v1开始是 0 1 2 3 4 5 6 7 8 9
// 删除前3个元素
v1.erase(v1.begin(), v1.begin() + 3);	// 3 4 5 6 7 8 9

// 删除指定位置的元素
v1.erase(v1.begin() +3);				// 3 4 5 7 8 9

//在第3个位置插入数字600
v1.insert(v1.begin() + 3, 600);			// 3 4 5 600 7 8 9

//在最后的位置插入3个元素10的拷贝
v1.insert(v1.end(), 3, 10);				// 3 4 5 600 7 8 9 10 10 10
  1. 排序
    greater()的用法[用sort+greater()实现降序快排].可以使用<algorithm>中的sort函数对vector进行排序。
#include <vector>
#include <algorithm>

// 默认从小到大排序
vector<int> vi;
vi.push_back(3);
vi.push_back(35);
vi.push_back(22);
sort(vi.begin(), vi.end());

// 这样,可以直接从大到小排序
sort(vi.begin(),vi.end(),greater<int>());
  1. 参考指定C++ vector、二维vector的大小及resize与reserve的区别.

resize的原型:void resize (size_type n, value_type val = value_type());

resize()的作用是改变vector中元素的数目。如果n比当前的vector元素数目要小,vector的容量要缩减到n大小,并移除那些超出n的元素。如果n比当前vector元素数目要大,会在vector的末尾扩展需要的元素数目,如果第二个参数val指定了,扩展的新元素初始化为val的副本,否则按类型默认初始化。
注意:

如果n大于当前的vector的容量(是容量capacity,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。

指定一维vector的大小

如果我们声明了一个vector:vector v1;此时我们不能使用v1[i]来访问它。但是如果调用v1.resize(5);我们就把v1的大小设置成了5,就可以使用v[i]来访问它。

指定二维vector的大小

类似的,在对二维vector如vector< <vector> vec;如果使用vec[i].push_back()会出现下标越界。这是因为此时的vec为空。解决办法是在push_back之前先定义vec的长度,如vec.resize(5)。
但是此时使用vec[i][j]仍然会出现下标越界。如果想使用vec[i][j],需要同时指定vecvec[i]的大小,此时用到一个临时一维vector:

vector <vector<int> > vec;
vector<int> temp(4)
vec.resize(5,temp)
  • resize和reserve的区别:

首先必须弄清楚两个概念:

capacity:指容器在分配新的存储空间之前能存储的元素总数。
size:指当前容器所存储的元素个数

在弄清这两个概念以后,很容易懂resize和reserve的区别:

reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。resize既分配了空间,也创建了对象。
reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。

两者的形参个数不一样。

resize带两个参数,一个表示容器大小,一个表示初始值(默认为0)
reserve只带一个参数,表示容器预留的大小。

deque

根据stl容器的统一迭代器访问方式,我们可以定义一个可以打印任何stl容器的模板函数(参考:使用模板打印任何容器的所有数据)。下面就只需要调用该函数就可以打印容器中的内容了~

template <typename T>
void print_stl(const T &obj){
    for(typename T::const_iterator it=obj.begin();it!= obj.end();++it){
        std::cout<<*it<<" ";
    }
    std::cout<<std::endl;
}

deque是一个双端数组容器:可以在头部和尾部操作元素。 使用deque,首先要引入头文件: #include<deque>".

//定义deque对象
deque<int> d;

//尾部插入元素
d.push_back(10);
d.push_back(20);
d.push_back(30);
print_stl(d);		// 10 20 30		

//头部插入元素
d.push_front(1);
d.push_front(2);
d.push_front(3);
print_stl(d);		// 3 2 1 10 20 30

//尾部删除元素
d.pop_back();
print_stl(d);		// 3 2 1 10 20


//头部删除元素
d.pop_front();
print_stl(d);		// 2 1 10 20


//修改头部和尾部的值
d.front() = 111;
d.back()  = 222;
print_stl(d);		// 111 1 10 222


//查找元素为10的下标
//首先查找指向10的迭代器位置,然后通过distance求该迭代器距开始的距离。
for(deque<int>::iterator it = d.begin();it != d.end(); it++){
    if (*it == 10) {
        cout << "下标:" << distance(d.begin(), it) << endl;
    }
}					// 下标:2

queue

队列是一种先入先出的容器,首先引入 #include<queue>

queue<int> q;
q.push(10);
q.push(20);
q.push(30);
q.push(40);

while (!q.empty()){

    //取出栈顶元素
    cout << "当前队首元素" << q.front() << ",  ";

    //获取栈的大小
    cout << "当前队列的大小" << q.size() << endl;

    q.pop();
}

输出:

当前队首元素10, 当前队列的大小4
当前队首元素20, 当前队列的大小3
当前队首元素30, 当前队列的大小2
当前队首元素40, 当前队列的大小1

stack

栈是一种先入后出的容器,首先引入 #include<stack>

//定义stack对象
stack<int> s1;

//入栈
s1.push(10);
s1.push(20);
s1.push(30);
s1.push(40);

//打印栈顶元素,并出栈
while (!s1.empty()) {
    //取出栈顶元素
    cout << "当前栈顶元素" << s1.top() << ",  ";
    
    //获取栈的大小
    cout << "当前栈的大小" << s1.size() << endl;
    
    //出栈
    s1.pop();
}

输出:

当前栈顶元素40, 当前栈的大小4
当前栈顶元素30, 当前栈的大小3
当前栈顶元素20, 当前栈的大小2
当前栈顶元素10, 当前栈的大小1

注意queue和stack中:

print_stl(s1); 无法使用

cout << “弹出元素:” << s1.pop() << endl; 无法使用,因为pop()不会返回值。

list

list 是一个双向链表。 list节点的结构见如下源码:

template <class T>
struct __list_node{
 typedef void* void_pointer;
 void_pointer prev;
 void_pointer next;
 T data;
}

首先引入#include<list>

//创建list对象
list<int> l;

//尾部添加元素
for (int i = 0; i < 5; i++) {
    l.push_back(i+3);
}
print_stl(l);		// 3 4 5 6 7

//头部添加元素
l.push_front(111);	// 111 3 4 5 6 7

//list不能随机访问,要改变指针位置只能++或--
list<int>::iterator it = l.begin();
it++;
it++;
it++;
cout << *it <<endl;		// 5
it--;
cout << *it <<endl;		// 4
//    it = it + 1;  //编译报错,不能随机访问
//    it = it + 5;  //编译报错,不能随机访问

// erase删除元素通过指针来实现
// 删除区间内的,it此时指向4
l.erase(l.begin(), it);		
print_stl(l);			// 4 5 6 7
// 删除单个
l.erase(l.begin());
print_stl(l);			// 5 6 7

//romove按移除值为5的所有元素
l.push_back(5);
print_stl(l);			// 5 6 7 5
l.remove(5);
print_stl(l);			// 6 7

此外,其他方法(参考C++ list,STL list(双向链表)详解。 除了顺序容器都有的成员函数外, 还有:

void pop_front()删除链表最前面的元素
void sort()将链表从小到大排序
remove_if删除符合某种条件的元素
void unique()删除所有和前一个元素相等的元素
void merge(list & x)将链表 x 合并进来并清空 x。要求链表自身和 x 都是有序的
void splice(iterator i, list & x, iterator first, iterator last)在位置 i 前面插入链表 x 中的区间 [first, last),并在链表 x 中删除该区间。链表自身和链表 x 可以是同一个链表,只要 i 不在 [first, last) 中即可

由于list的双向特性,其支持在头部(front)和尾部(back)两个方向进行push和pop操作,当然还支持 erase,splice,sort,merge,reverse,sort等操作,这里不再详细阐述。

priority_queue

首先需要包含头文件#include<queue>

priority_queue<int>默认定义int类型的最大值队列,等价于priority_queue<int, vector<int>, less<int>>定义int型的最大值优先队列。而priority_queue<int, vector<int>, greater<int>>定义int型的最小值队列。 上面的定义中,lessgreater相当于谓词,是预定义好的排序函数,我们称之为“仿函数”。

/定义优先级队列(默认是最大值优先级队列)
priority_queue<int> p1;

//队列入队
p1.push(5);
p1.push(3);
p1.push(7);
p1.push(1);
p1.push(9);

// 打印最大优先级的队头元素
cout<<"最大值优先级,队列的大小:"<< p1.size() <<endl;
while (!p1.empty()){
    //取出队首元素
    cout <<  p1.top() << " ";
    p1.pop();
}
cout<<endl;

// 与上相反,值越小优先级越大
priority_queue<int, vector<int>, greater<int>> p2;

//给最小值优先级队列入栈
p2.push(5);
p2.push(3);
p2.push(7);
p2.push(1);
p2.push(9);

//打印最小优先级队列的对头元素
cout<<"最小值优先级,队列的大小:"<< p2.size() <<endl;
while (!p2.empty()){
    //取出队首元素
    cout <<  p2.top() << " ";
    p2.pop();
}
cout<<endl;

除了仿函数,对于自定义的数据类型,还可以使用一种轻便的比较函数定义方法。以pair为例,定义如下:

typedef pair<int, int> P;
auto cmp = [](const P& a, const P& b){ return a.second < b.second; };
priority_queue<P, vector<P>, decltype(cmp)> que(cmp);

其中,decltype(cmp)可以推测得到cmp的类型。关于仿函数的自定义,在后面set部分会讲到。

set

set中元素唯一且有序。其结构是红黑二叉树,插入数据的效率比vector快 。

  1. 插入元素,删除元素

int rand(void);产生0~RAND_MAX(32767)的伪随机整数。C++ 随机数

set<int> s;

//插入元素
for (int i = 0; i<5; i++) {
    int tmp = rand();
    s.insert(tmp);
}
print_stl(s);	// 41 6334 18467 19169 26500

//重复插入元素(只会保留一个)
s.insert(100);
s.insert(100);
print_stl(s);	// 41 100 6334 18467 19169 26500

// 删除元素
// 此时,s.erase(100)也是可以的.
cout <<"删除前两个元素"<< endl;
set<int>::iterator it = s.begin();
it ++;
it ++;
s.erase(s.begin(),it);
print_stl(s);	// 6334 18467 19169 26500


//依次删除集合中的元素
while(!s.empty())
{
    set<int>::iterator it = s.begin();
    cout <<"删除"<<  *it << endl;

    //删除头部元素
    s.erase(s.begin());

    print_stl(s);	// 集合一次次在缩短		
}

那么,对于上面重复插入100,最后只保留了一个结果的情况,如何知道,是否插入成功了呢?

可以根据set的insert操作的返回值得到。insert的声明为:

pair<iterator,bool> insert(const value_type& __v)

其中,pair是一个能盛放两个值的结构体,而且这两个值可以是不同的类型。它的定义是个模板:

template <class _T1, class _T2>
struct pair
{
    typedef _T1 first_type;
    typedef _T2 second_type;

    _T1 first;
    _T2 second;
}

那么,返回的pair<iterator, bool>类型中,pair的第一个属性表示当前插入的迭代器的位置,第二个属性表示插入是否成功的bool值。所以,我们可以通过第二个属性来判断元素是否插入成功。

set<int> s;

//重复插入元素
for(int i = 0;i<3;i++){
    cout<< "第"<<i+1<<"次";   
    
    pair<set<int>::iterator, bool> ps = s.insert(100);
    if(ps.second == true){
        cout << "插入100成功" <<endl;
    }else{
        cout << "插入失败" <<endl;
    }
    
    print_stl(s);
}

输出结果:

第1次插入100成功
100
第2次插入失败
100
第3次插入失败
100

  1. 改变set中的元素排列顺序: 设置从大道小排序
//从大到小
set<int, greater<int>> s2;

//添加元素
for (int i = 0; i < 5; i++) {
    s2.insert(rand());
}

//遍历
cout<<"从大到小s2: "<<endl;
print_stl(s2);			// 输出: 26500 19169 18467 6334 41

为什么上面的可以呢?因为greater<int>是一个仿函数,可以用于设置集合元素的排序规则。

参考C++:greater和less: greater表示内置类型从大到小排序,less表示内置类型从小到大排序。

参考关联容器也可使用greater<int>来改变大小排序顺序: greater配合sort使用, 配合容器使用。

// 去除模板定义的简化版本,int类型变量的比较函数定义
struct greater
{	
    //重载了括号操作符,用来比较大小
    bool operator()(const int &left, const int &right) const
    {
        //如果左值>右值,为真。从大到小的排列
        return left > right;
    }
};

// 完整版本(模板), 调用函数时:greater<int>,int换为其他内置类型也都可以
template<class _Ty = void>
struct greater
{    // functor for operator>
    typedef _Ty first_argument_type;
    typedef _Ty second_argument_type;
    typedef bool result_type;

    constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
    {    // apply operator> to operands
        return (_Left > _Right);
    }
};

那么,对于自定义的类,在定义了类似的比较函数后,也可以加入到set中,使之唯一且有序排列。下面定义一个Student类,并将它放进set中。

  1. 自定义类,放进set中。

首先,定义类,定义类的用于比较的仿函数:

class Student {
public:
    char name[64]; 	// 变量定义为private将不能在外部访问
    int  age;
public:
    Student(const char *name, int age)
    {
        strcpy(this->name, name);
        this->age = age;
    }
};

// 仿函数
struct Scomp
{    
    bool operator() (const Student &left, const Student &right)
    {
        // 从小到大按照年龄排序
        return left.age < right.age;
    }
};

然后,放入set中,遍历:

set<Student, Scomp> ss;

//插入数据
ss.insert(Student("s1",55));
ss.insert(Student("s2",33));
ss.insert(Student("s3",99));
ss.insert(Student("s4",11));
ss.insert(Student("s5",77));
ss.insert(Student("s5",77));
//遍历
for (set<Student>::iterator it = ss.begin(); it != ss.end(); it++) {
	cout << it->age << "\t" << it->name <<endl;
}

输出结果:

11 s4
33 s2
55 s1
77 s5
99 s3

另外,上面第一部分,定义类+定义仿函数,等价于直接下面这样定义。下面这样定义后,可以直接set<Student> ss;来定义集合。这样定义的最大优势在于,即使数据设置为private,比较也能进行,因为类的成员函数可以访问到它们。

class Student {
public:
    char name[64];
    int  age;
public:
    Student(const char *name, int age)
    {
        strcpy(this->name, name);
        this->age = age;
    }
    bool operator<(const Student &other)const
    {
        return this->age < other.age;
    }
}
  1. 查找元素

iterator find(const key_type& __k) 查找元素为k的迭代器位置;

iterator lower_bound(const key_type& __k) 函数查找大于等于元素k的迭代器位置;

iterator upper_bound(const key_type& __k) 函数查找大于元素k的迭代器位置;

pair<iterator,iterator> equal_range(const key_type& __k) 返回一个pair类型,第一个属性表示大于等于k的迭代器位置,第二个是大于k的迭代器位置.

set<int> s;

for (int i = 0; i < 5; i++) {
	s.insert(i+3);
}
print_stl(s);					//3 4 5 6 7

// find: 用于查找元素是5的迭代器位置
set<int>::iterator it0 = s.find(5);
cout << "s.find(5):" << *it0 <<endl;	// s.find(5):5

//lower_bound: 查找不小于5的元素迭代器位置
set<int>::iterator it1 = s.lower_bound(5);
cout << "不小于5:" << *it1 <<endl;		// 5

//upper_bound: 查找大于5的元素迭代器位置
set<int>::iterator it2 = s.upper_bound(5);
cout << "大于5:" << *it2 <<endl;		// 6

//equal_range: 返回的pair第一个迭代器是>=5,另一个是>5
pair<set<int>::iterator, set<int>::iterator> mypair = s.equal_range(5);
set<int>::iterator it3 = mypair.first;
set<int>::iterator it4 = mypair.second;
cout << "it3:" << *it3 <<endl;		// 5
cout << "it4:" << *it4 <<endl;		// 6


int num1 = s.count(5);
cout << "num1:" << num1 << endl;	// 1


// 当待查找的元素不存在时
s.erase(5);
print_stl(s);			// 3 4 6 7

it0 = s.find(5);
cout << "s.find(5):" << *it0 <<endl;	// 4 find的处的迭代器位置不可靠

//查找不小于5的迭代器
it1 = s.lower_bound(5);
cout << "不小于5:" << *it1 <<endl;		// 6

//查找大于5的迭代器
it2 = s.upper_bound(5);
cout << "大于5:" << *it2 <<endl;		// 6


//返回的pair第一个迭代器是>=5,另一个是>5
mypair = s.equal_range(5);
it3 = mypair.first;
it4 = mypair.second;
cout << "it3:" << *it3 <<endl;		// 6
cout << "it4:" << *it4 <<endl;		// 6

num1 = s.count(5);
cout << "num1:" << num1 << endl;	// 0
  1. multiset容器

multiset容器,与set容器相似,但是multiset容器中的元素可以重复。另外,他也是自动排序的,容器内部的值不能随便修改,因为有顺序的。

map

引入#include<map>。 map中的键值对都是唯一的,即不能出现同名键,而且一个键对应一个值(multimap中一个键可以对应多个值 )。

  1. 首先介绍基本的插入、删除操作。map中的元素都是键值对,以pair的结构存在。为了方便打印map中的内容,定义一个模板函数:
// 打印map类型的数据
template <typename T>
void print_map(const T& m){
    for (typename T::const_iterator it = m.begin(); it != m.end(); it++ ){
        if (it != m.begin()){std::cout<<",";}
        std::cout << it->first << ":" << it->second;
    }
    std::cout<<std::endl;
}

map元素的插入,有两种方式:

调用insert函数插入pair类型的键值对

直接使用[]来插入键并插入值;如果要插入的键已经存在,则修改值就完事了。

map<int, string> m;

//insert方法插入
//--1 通过pair<int, string>(1,"One") 构造pair元素
m.insert(pair<int, string>(1,"One"));
//--2 通过make_pair构造pair元素
m.insert(make_pair(2,"Two"));
//--3 通过value_type构造pair元素
m.insert(map<int, string>::value_type(3,"Three"));


//[]直接插入
m[4] = "Four";
print_map(m);       //遍历

//重复插入(插入会不成功)
pair<map<int, string>::iterator, bool> pair1 = m.insert(make_pair(2, "haha"));
if (pair1.second) {
    cout << "重复插入成功!" << endl;
}else{
    cout << "重复插入失败!" << endl;
}

//元素的修改(成功)
// m[2] = "haha";的方式,如果不存在键则插入,存在键则修改
m[2] = "haha";
print_map(m);       //遍历



//元素的删除
//--删除值为"haha"的元素
cout<<"删除值为haha的键值对:"<<endl;
for (map<int, string>::iterator it = m.begin(); it != m.end(); it++) {
    if (it->second.compare("haha") == 0) {
        m.erase(it);
    }
}
print_map(m);       //遍历

输出:

1:One,2:Two,3:Three,4:Four
重复插入失败!
1:One,2:haha,3:Three,4:Four
删除值为haha的键值对:
1:One,3:Three,4:Four

  1. 查找

    find,lower_bound,upper_bound,equal_range。

首先, 用find函数来定位数据出现位置,它返回的一个迭代器,当数据出现时,它返回数据所在位置的迭代器,如果map中没有要查找的数据,它返回的迭代器等于end函数返回的迭代器。 c++ 中map 的find 用法.

// 接上面的测试,m中的值为 1:One,3:Three,4:Four
map<int, string>::iterator it = m.find(3);
cout << it->first << "\t" << it->second << endl;

//查找key=100
it = m.find(100);
if (it != m.end()) {
    cout << "存在key=100的键值对";
}else{
    cout << "不存在" << endl;
}

输出:

3 Three
不存在

其次,看lower_bound,upper_bound。参考stl map中的lower_bound和 upper_bound:map中的lower_bound和upper_bound的意思其实很简单,就两句话:

map::lower_bound(key):返回map中第一个大于或等于key的迭代器指针

map::upper_bound(key):返回map中第一个大于key的迭代器指针

所以,理解这两个函数请不要按照字面意义思考太复杂,因为仅仅是不小于(lower_bound)和大于(upper_bound)这么简单。

// 接上面的测试,m中的值为 1:One,3:Three,4:Four
//lower_bound
map<int, string>::iterator it1 = m.lower_bound(2);
cout << it1->first << "\t" << it1->second << endl;		// 3       Three
map<int, string>::iterator it2 = m.lower_bound(3);
cout << it2->first << "\t" << it2->second << endl;		// 3       Three

//upper_bound
map<int, string>::iterator it3 = m.upper_bound(2);
cout << it3->first << "\t" << it3->second << endl;		// 3       Three
map<int, string>::iterator it4 = m.upper_bound(3);
cout << it4->first << "\t" << it4->second << endl;		// 4       Four

// 不存在时
map<int, string>::iterator it5 = m.lower_bound(5);
if(it5 == m.end()){
    cout<< "大于等于5的值不存在"<<endl;	
}else{
    cout << it5->first << "\t" << it5->second << endl;
}														// 大于等于5的值不存在

最后,equal_range。返回两个指针组成的pair,其中第一个指向的键不小于,第二个指向的键大于。

// 接上面的测试,m中的值为 1:One,3:Three,4:Four
// equal_range
pair<map<int, string>::iterator, map<int, string>::iterator> mypair2 = m.equal_range(2);
cout << "equal_range(2):  " << mypair2.first->first << "\t" << mypair2.second->first << endl;
auto mypair3 = m.equal_range(3);
cout << "equal_range(3):  " << mypair3.first->first << "\t" << mypair3.second->first << endl;

// 边缘元素的返回值
auto mypair = m.equal_range(4);
if (mypair.first == m.end()) {
    cout << "大于等于4的位置不存在" << endl;
}else{
    cout << mypair.first->first << "\t" << mypair.first->second << endl;
}
if (mypair.second == m.end()) {
    cout << "大于4的位置不存在" << endl;
}else{
    cout << mypair.second->first << "\t" << mypair.second->second << endl;
}

// 元素不存在时
auto mypair5 = m.equal_range(5);
if (mypair5.first == m.end()) {
    cout << "大于等于5的位置不存在" << endl;
}else{
    cout << mypair5.first->first << "\t" << mypair5.first->second << endl;
}
if (mypair5.second == m.end()) {
    cout << "大于5的位置不存在" << endl;
}else{
    cout << mypair5.second->first << "\t" << mypair5.second->second << endl;
}

输出:

equal_range(2): 3 3
equal_range(3): 3 4
4 Four
大于4的位置不存在
大于等于5的位置不存在
大于5的位置不存在

  1. map是有默认值

参考C++——自定义map的value默认值:当取一个不存在的key值的value时:如果value为内置类型,其值将被初始化为0;如果value为自定义数据结构且用户定义了默认值则初始化为默认值,否则初始化为0。

// 内置类型
map<int,int> emptyMap{};
int i = emptyMap[100]; 	// i = 0

// 自定义了默认值
struct IntDefaultedToOne {
		int num = 1;
	};
map<int,IntDefaultToOne> emptyMap{};
int i = map[0].num; 	// i = 1

multimap

multimap容器,与map容器的区别是:

multimap支持多个键值。 由于支持多个键值,multimap提供了count函数来计算同一个key的元素个数。

不能用[]插入键值对。

multimap<int, string> m2;

//sale部门
m2.insert(make_pair(1, "one"));
m2.insert(make_pair(1, "Jan"));
m2.insert(make_pair(1, "first"));

//    m2[2] = "two";    无法使用[]运算符
//    m2[2] = "Feb";
//    m2[2] = "second";



m2.insert(pair<int, string>(3,"three"));
m2.insert(pair<int, string>(3,"March"));
m2.insert(pair<int, string>(3,"third"));


//遍历
for (multimap<int, string>::iterator it = m2.begin(); it != m2.end(); it++) {

    cout << it->first << "\t" << it->second << endl;
}

cout << "键为3的键值对个数:" << (int)m2.count(3) << endl;
// 使用equal_range查看这些
auto range3 = m2.equal_range(3);
for (multimap<int,string>::iterator  it = range3.first; it != range3.second; it++){
    cout << it->first << "\t" << it->second<<endl;
}

输出结果:

1 one
1 Jan
1 first
3 three
3 March
3 third
键为3的键值对个数:3
3 three
3 March
3 third

unordered_map

参考C++ unordered_map的使用: 它允许根据键快速检索单个元素。在内部,unordered_map 使用散列表来组织它们的元素,以比map容器能更快地通过它们的键访问单个元素,但是它们通常对于元素子集的范围迭代效率较低。

template < class Key,                                    // 键,um::key_type可得
           class T,                                      // 值,um::mapped_type
           class Hash = hash<Key>,                       // 返回一个散列值的一元函数,um::hasher
           class Pred = equal_to<Key>,                   // 判断两个键是否相同的函数,um::key_equal
           class Alloc = allocator< pair<const Key,T> >  // 容器,um::allocator_type
           > class unordered_map;

无序映射实现了直接访问操作符(operator[]),使用键值作为参数直接访问映射值。 容器中的任何两个元素都不能具有相同的键。
容器中的迭代器至少是前向迭代器forward iterators

查找一个元素是否在map中C++学习之map与unordered_map的使用与注意事项:

unordered_map<key, value> mp;  //或map<key,value> mp;
mp.find(x)!=mp.end()
mp.count(x)!=0

C++中unordered_map的使用: unordered_map是类似于map的关联容器,其中存储的是键值对pair。哈希表的搜索、插入和元素移除拥有平均常数时间复杂度,元素在内部的存储是没有顺序的,而是通过键的哈希来确定元素具体放在具体的某个位置。

函数名函数作用
empty判定容器是否为空
size返回容器的元素
max_size返回可容纳的最大元素数
clear清除内容
insert插入元素或者结点
emplace原位构造元素
erase擦除元素
swap交换内容
at访问指定位置的元素,并且进行越界检查
count返回匹配特定键的元素数量
find返回特定键的元素

疑难点

++it、it++

++前置会返回一个对象本身。后置++则必须产生临时对象以保留原来的值并返回。 临时对象会导致效率降低 。

// ++i实现代码为:
int& operator++()
{
     *this += 1;
     return *this;
} 

//i++实现代码为:
int operator++(int)
{
    int temp = *this;
     ++*this;
     return temp;
} 

list删除元素

使用erase删除一个元素,会返回下一个元素的有效迭代器。

//创建list对象
list<int> l;
//尾部添加元素
for (int i = 0; i < 9; i++) {
    l.push_back(i+3);
}
print_stl(l);			// 3 4 5 6 7 8 9 10 11

// erase删除元素通过指针来实现
// 删除区间内的
list<int>::iterator it = l.begin();
it++;
it++;
auto it2 = l.erase(it);
print_stl(l);			// 3 4 6 7 8 9 10 11
cout<< "删除第3个元素*it"<<endl;
cout<< "*it " << *it << endl;		// 5
cout<< "*it2 " << *it2 << endl;		// 6

cout<< "*(++it) " << *(++it) << endl;   // 2003788910 it已失效
cout<< "*(++it2) " << *(++it2) << endl;	// 7


// 删除一个区间的
auto it3 = l.erase(l.begin(),it2);
print_stl(l);							// 7 8 9 10 11
cout<< "删除前3个元素begin(),it2"<<endl;
cout<< "*it2 " << *it2 << endl;			// 7
cout<< "*it3 " << *it3 << endl;			// 7

cout<< "*(++it2) " << *(++it2) << endl;   	// 8
cout<< "*(++it3) " << *(++it3) << endl;		// 8

// romove删除元素通过值来实现
cout<< "remove 9"<<endl;
l.remove(9);      				// 7 8 10 11
print_stl(l);

3 4 5 6 7 8 9 10 11
3 4 6 7 8 9 10 11
删除第3个元素*it
*it 5
*it2 6
*(++it) 2003788910
*(++it2) 7
7 8 9 10 11
删除前3个元素begin(),it2
*it2 7
*it3 7
*(++it2) 8
*(++it3) 8
remove 9
7 8 10 11

slist(forward_list)

(single linked list)是单向链表, 迭代器属于单向的Forward iterator。list是双向链表。 虽然slist的很多功能不如list灵活,但是其所耗用的空间更小,操作更快。

根据STL的习惯,插入操作会将新元素插入到指定位置之前,而非之后,然而slist是不能回头的,只能往后走,因此slist特别提供了insert_after()和erase_after供灵活应用。在slist开头是可取的,slist只提供push_front()操作,元素插入到slist后,存储的次序和输入的次序是相反的。

forward_list<int> fl;
fl.push_front(1);
fl.push_front(3);
fl.push_front(2);
fl.push_front(6);
fl.push_front(5);

// 链表的实际存放顺序与插入相反。
for(forward_list<int>::iterator it1 = fl.begin();it1 != fl.end(); ++it1)
{
    cout << *it1 <<" "; 	// 5 6 2 3 1
}
cout << endl;


// 在2的位置后插入99
forward_list<int>::iterator it1 = find(fl.begin(), fl.end(), 2); //寻找2的位置
if (it1 != fl.end())
    fl.insert_after(it1, 99);
for (auto it : fl)
{
    cout << it << " "; 		//5 6 2 99 3 1
}
cout << endl;

// 删除6后面的元素
it1 = find(fl.begin(), fl.end(), 6); //寻找6的位置
if (it1 != fl.end())
    fl.erase_after(it1);
print_stl(fl);				// 5 6 99 3 1

// 删除头部元素
fl.pop_front();
print_stl(fl);				// 6 99 3 1

// 取头部元素,并改变头部元素
cout<< fl.front() << endl;	// 6
fl.front() = 20;
print_stl(fl);   			// 20 99 3 1

stack的实现

stack(栈)是一种先进后出(First In Last Out)的数据结构,只有一个入口和出口,那就是栈顶,除了获取栈顶元素外,没有其他方法可以获取到内部的其他元素。

template <class T, class Sequence = deque<T> >
class stack
{
 ...
protected:
 Sequence c;
public:
 bool empty(){return c.empty();}
 size_type size() const{return c.size();}
 reference top() const {return c.back();}
 const_reference top() const{return c.back();}
 void push(const value_type& x){c.push_back(x);}
 void pop(){c.pop_back();}
};

从stack的数据结构可以看出,其修改了deque数据结构,关闭一些访问接口成为adapter(配接器)( 常将其归类为container adapter而非container) 。 stack除了默认使用deque作为其底层容器之外,也可以使用双向开口的list,只需要在初始化stack时,将 list作为第二个参数即可。

stack<int,list<int>> sls;
for(int i=0;i<5;++i){
    sls.push(i*2+1);
}

while (!sls.empty()) {
    cout << "当前栈顶元素" << sls.top() << ",  ";
    cout << "当前栈的大小" << sls.size() << endl;
    sls.pop();
}

输出:

当前栈顶元素9, 当前栈的大小5
当前栈顶元素7, 当前栈的大小4
当前栈顶元素5, 当前栈的大小3
当前栈顶元素3, 当前栈的大小2
当前栈顶元素1, 当前栈的大小1

queue的实现

queue(队列)是一种先进先出(First In First Out)的数据结构,只有一个入口和一个出口。默认也是基于deque实现,同样也可以由list改写:

template <class T, class Sequence = deque<T> >
class queue
{
     ...
    protected:
     Sequence c;
    public:
     bool empty(){return c.empty();}
     size_type size() const{return c.size();}
     reference front() const {return c.front();}
     const_reference front() const{return c.front();}
     void push(const value_type& x){c.push_back(x);}
     void pop(){c.pop_front();}
};

priority_queue实现

默认情况下,priority_queue使用一个max-heap完成,底层容器使用的是一般为vector,堆heap 为处理规则来管理底层容器实现 。priority_queue的这种实现机制导致其不被归为容器,而是一种容器配接器。关键的源码如下:

template <class T, class Squence = vector<T>,class Compare = less<typename Sequence::value_type> >
    class priority_queue{
        ...
        protected:
            Sequence c; // 底层容器
            Compare comp; // 元素大小比较标准
        public:
            bool empty() const {return c.empty();}
            size_type size() const {return c.size();}
            const_reference top() const {return c.front()}
            void push(const value_type& x)
            {
                c.push_heap(x);
                push_heap(c.begin(), c.end(),comp);
            }
            void pop()
            {
                pop_heap(c.begin(), c.end(),comp);
                c.pop_back();
            }
    };

heap

heap(堆)并不是STL的容器组件,是priority queue(优先队列)的底层实现机制。binary heap本质是一种complete binary tree(完全二叉树),binary max heap(大根堆)总是最大值位于堆的根部,优先级最高。

整棵binary tree除了最底层的叶节点之外,都是填满的,但是叶节点从左到右不会出现空隙, 因此可以使用array或vector来存储所有的节点。当其中某个节点位于 i i i处,其左节点必定位于 2 i 2i 2i处,右节点位于 2 i + 1 2i+1 2i+1处,父节点位于 i / 2 i/2 i/2(向 下取整)处。这种以array表示tree的方式称为隐式表述法。

那heap算法有哪些?常见有的插入、弹出、排序和构造算法。

为了方便打印array或vector中的堆元素,先定义一个打印函数,如下:

template <typename T>
void print_ele(const T & h){
    for (auto v : h)
    {
        std::cout << v << " "; // 6 4 5 3 1 0 2
    }
    std::cout << std::endl;
}

然后,进行插入、弹出、排序和构造算法。

    // 注意下面的操作,v.XX是vector容器的成员函数操作,函数调用是stl算法操作。
    // 将vector建立为堆
    vector<int> v = { 0,1,2,3,4,5,6 };
    make_heap(v.begin(), v.end()); //以vector为底层容器
    print_ele(v);       // 6 4 5 3 1 0 2

    // 添加一个元素,并使之仍然是堆,2步完成.
    v.push_back(7);		// 先后面插入
    push_heap(v.begin(), v.end());  // 该函数将最后一个元素进行排序
    print_ele(v);       // 7 6 5 4 1 0 2 3


    // pop_heap弹出根节点,实际上是移到了vector的最后一个位置
    pop_heap(v.begin(), v.end());
    cout << v.back() << endl; // 7

    // vector的成员方法pop_back可以删除元素,早就知道了~~
    v.pop_back();
    print_ele(v);       // 6 4 5 3 1 0 2


    // sort得到一个递增序列,调用pop_heap每次确定一个元素位置.
    sort_heap(v.begin(), v.end());
    print_ele(v);       // 0 1 2 3 4 5 6
  • 默认是大根堆,排序后将是升序排列
    参考make_heap(), pop_heap(), push_heap()用法.观察上面的make_heap,push_heap,pop_heap等方法,格式都是一样的。自定义排序,只需要添加一个比较函数即可。需要注意的是,添加时,上面三个函数就都需要添加一样的比较函数
vector<int> nums({8, 3, 4, 8, 9, 2, 3, 4, 10};);
make_heap(nums.begin(), nums.end(), less<int> ());
make_heap(nums.begin(), nums.end(), greater<int> ());

或者

make_heap(nums.begin(), nums.end(),[](const int& a,const int& b){return a>b;});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值