C++(11)STL之线性容器

170 阅读7分钟

泛型编程

STL标准模板库

包含三个部分

1.容器
    存储和管理数据对象的集合,是数据结构的泛型化封装
2.迭代器
    不暴露容器内部细节的前提上,位用户提供一组统一的访问容器的方法
3.泛型算法
    借助迭代器,以泛型的方式操作容器中的数据元素   

特性

所有的STL组件全部通过模板来实现,支持数据类型的泛化
STL强调的是将数据结构和算法与具体的数据类型分离开来
STL的设计思想是在最小的框架内实现最大的弹性

STL容器

1.线性容器
    vector(向量/动态数组)  list(列表)  deque(双端队列)
    C++11 ------- array(静态数组)  foward_list(单链表)

2.适配器容器
    stack(栈)  queue(队列)  priority_queue(优先队列)

3.关联容器
    map(映射)  multimap(多重映射)  set(集合)  multiset(多重集合)
   C++11 ------- 四大无序关联容器(unordered_xxx) 

容器的共性:

1.所有的STL容器都支持深拷贝的拷贝构造和赋值
    但是元素对象本身要支持深拷贝
2.同类型的容器都重载"==""!="运算,容器相等的条件是
    元素类型相同,元素个数相同,对应元素满足"=="运算
3.存入容器中的是对象的副本,而不是对象本身    

向量(动态数组) -------- vector

基本特性

1.使用连续的空间存储数据元素,支持下标运算
2.使用动态内存管理数据,支持自动扩容,但是不自动收缩
3.通过预分配内存空间降低动态内存分配的消耗
4.支持随机的插入和删除,但是在尾部操作的效率最高

使用

需要包含头文件:#include

构造对象

1.空向量
    vector<元素类型> 向量对象;
    比如:vector<int> vi;
2.指定初始大小
    vector<元素类型> 向量对象(初始大小);
    比如:vector<int> vi(10);
3.指定初始大小和初始值
    vector<元素类型> 向量对象(初始大小,初始值);
    比如:vector<int> vi(10,5);
4.使用其他容器来初始化
   int a[5] = {1,2,3,4,5};
   vector<int> vi(a,a+5);//5个 vi(a,a+4);//4个

向量的迭代器

vector有4个迭代器

image-20220721230553973

正向迭代器:iterator
反向迭代器:reverse_iterator
常正向迭代器:const_iterator
常反向迭代器:const_reverse_iterator

//迭代器都支持+/-运算(整数)
//同类型的迭代器支持比较和-运算
//迭代器重载类指针的运算符(* ->)

成员函数

iterator begin();//返回起始正向迭代器
iterator end();//返回末尾正向迭代器(最后一个的下一个)
reverse_iterator rbegin();//返回起始反向迭代器
reverse_iterator rend();//返回末尾反向迭代器(第一个的前一个)

size_t size();//大小
size_t capacity();//容量
//调整大小,可增可减,第二个参数时增加时的初始值,增构造,减析构
void resize(size_t size,初始值);
void clear();//清空vector,相当于resize(0)
//调整容量,只增大,不缩小
void reserve(size_t size);

value_type& at(int n);//通过下标访问元素
value_type& front();//首元素
value_type& back();//尾元素
iterator insert(iterator pos,const value_type &value);//从指定位置插入
void push_back(const value_type &value);//从尾部插入
iterator erase(iterator pos);//删除指定位置的元素
void pop_back();//尾部删除
bool empty();//判空

//对容器的修改可能导致之前的迭代器失效
//修改容器后要重置之前的迭代器
//需要顺序存储并且可能频繁在尾部操作就可以选择使用vector

练习:

求最长公共子串

使用反向迭代器反向遍历vector

1.直观暴力的方法
    分别遍历两个字符串,每到一个位置就计算当前公共子串长度
    在比较中记录最长的公共子串的起始位置和长度,循环结束找到最长公共子串
    效率较低 ------ 时间复杂度n^3
    
2.矩阵法求解
str1:abcdefg
str2:abkdefx

构造一个矩阵,将str1的长度作为列数,将str2的长度作为行数
//比较每一行和每一列,如果字符相同矩阵对应位置置1,不同置0
   a  b  k  d  e  f  x
a  1  0  0  0  0  0  0
b  0  1  0  0  0  0  0
c  0  0  0  0  0  0  0 
d  0  0  0  1  0  0  0 
e  0  0  0  0  1  0  0 
f  0  0  0  0  0  1  0 
g  0  0  0  0  0  0  0 
//公共子串的长度就是对应矩阵中连续为1的对角线长度
//2 3

优化一下处理,将之前相同字符的个数(左上角)加入到数字中
   a  b  k  d  e  f  x
a  1  0  0  0  0  0  0
b  0  2  0  0  0  0  0
c  0  0  0  0  0  0  0 
d  0  0  0  1  0  0  0 
e  0  0  0  0  2  0  0 
f  0  0  0  0  0  3  0 
g  0  0  0  0  0  0  0
//在比较的同时把对角线相同字符的个数也记录下来
//1 2   1 2 3 
/*06-双端队列的使用*/
#include <iostream>
#include <deque>

using namespace std;

int main()
{
    deque<int> di(10,5);//长度为10,初始值为5

    cout<<"正向迭代:";
    for(deque<int>::iterator itt=di.begin();itt!=di.end();itt++){
        cout<<*itt<<" ";
    }
    cout<<endl;

    //头部插入
    di.push_front(100);
    di.push_back(200);

    cout<<"插入后:";
    for(deque<int>::iterator itt=di.begin();itt!=di.end();itt++){
        cout<<*itt<<" ";
    }
    cout<<endl;

    //头部删除
    di.pop_front();
    cout<<"删除后:";
    for(deque<int>::iterator itt=di.begin();itt!=di.end();itt++){
        cout<<*itt<<" ";
    }
    cout<<endl;

    //头部删除
    cout<<"反向迭代:";
    for(deque<int>::reverse_iterator itt=di.rbegin();itt!=di.rend();itt++){
        cout<<*itt<<" ";
    }
    cout<<endl;


    return 0;
}

静态数组 ------- array

基本特性

1.使用连续空间存储元素,支持下标运算
2.容量空间固定,不支持扩容和收缩
3.支持随机位置访问,末尾操作效率较高

使用

需要包含头文件,#include

构造array对象

array<元素类型,数组长度> array对象;
比如:array<int,5> ai;

//静态数组支持C语言数组初始化的语法
array<int,5> ai = {1,2,3,4,5};

迭代器

array有4个迭代器

正向迭代器:iterator
反向迭代器:reverse_iterator
常正向迭代器:const_iterator
常反向迭代器:const_reverse_iterator

//迭代器都支持+/-运算(整数)
//同类型的迭代器支持比较和-运算
//迭代器重载类指针的运算符(* ->)

成员函数

size_t size();//大小

value_type& at(int n);//通过下标访问元素
value_type& front();//首元素
value_type& back();//尾元素
bool empty();//判空
void fill(const value_type &value);//填充
void swap(array &other);

//array的效率高于vector

练习:

使用vector存储矩阵实现求最大公共子串

/*04-使用vector实现子最长公共子串*/
#include <iostream>
#include <vector>

using namespace std;

string getLCS(const string &str1,const string &str2)
{
    //记录最长子串在str1中的结束位置和长度
    int max_end = 0,max_len = 0;

    //创建比较矩阵(二维向量)
    vector<vector<int>> arr(str1.length(),vector<int>(str2.length()));
    for(size_t i=0;i<str1.length();i++){
        for(size_t j=0;j<str2.length();j++){
            //比较字符,将比较结果写入矩阵
            if(str1.at(i)==str2.at(j)){//赋值  左上角+1
                //如果左上角不存在
                if(i==0||j==0)
                    arr.at(i).at(j) = 1;
                else
                    arr.at(i).at(j) = arr.at(i-1).at(j-1)+1;

                //记录最长长度和此时str1中最后一个字符的位置
                if(arr.at(i).at(j)>max_len){
                    max_len = arr.at(i).at(j);
                    max_end = i;
                }
            }
            else{
                arr.at(i).at(j) = 0;
            }
        }
    }

    //返回最长公共子串
    return str1.substr(max_end-max_len+1,max_len);
}

int main()
{
    string str1 = "kkkhellowebyebye";
    string str2 = "ghsasabyehellod";

    cout<<getLCS(str1,str2)<<endl;

    return 0;
}
//vector可以递归实例化
vector<vector<int>> -------- 二维

双端队列 ------- deque

包含头文件:#include

特性

1.deque的物理结构几乎和vector一样,唯一的区别是deque的两端都是开放的,都可以插入和删除
2.接口比vector多了push_front/pop_front,去掉了capacity和reserve函数
3.性能上deque略低于vector,内存消耗高于vector,元素访问的时间长于vector
4.deque空间堆成,在头部和尾部操作的时间复杂度相同

image-20220721230459030

列表 ----------- list

需要包含头文件:#include

特性

1.列表的底层使用线性双向链表来实现
2.列表在任意位置插入和删除的操作都是常数时间
3.列表不支持随机访问([] 和 at)

成员函数

begin/end/rbegin/rend ------ 迭代器操作
front/push_front/pop_front ----- 头部操作
back/push_back/pop_back ----- 尾部操作
insert/erase ------ 指定位置插入和删除
size/resize/clear/empty ------ 大小操作

//删除所有匹配的元素
void remove(const value_type &val);
//排序
void sort();//元素支持"<"运算
void sort(比较器);//传入两个元素获得比较结果(布尔值) ----- 函数/函数对象
//将某个列表中的 指定/全部 元素到调用列表中的指定位置
void splice(const_iterator pos,list &other);
void splice(const_iterator pos,list &other,const_iterator first,const_iterator last);;
//合并两个已排序的链表,将参数列表合并到调用列表中,合并后任然有序
//合并的比较器和排序的比较器要保持一致
void merge(list &other);//使用<比较
void merge(list &other,比较器);//提供比较器
//删除连续的重复元素
void unique();

类类型的对象作为容器元素

能够放入容器中的对象必须满足以下条件:

1.必须支持无参构造
2.支持深拷贝的拷贝构造和赋值运算符函数
3.如果需要查找,重载"=="运算符
4.如果需要排序,重载"<"运算符或者提供比较器

单向列表 -------- forward_list

使用需要包含头文件:#include

特性

和list基本一致
唯一的区别底层实现使用单链表,单向访问       

和list不同的接口

1.forward_list只提供正向迭代器,成员函数之后begin/end,没有rbegin/rend
2.forward_list不提供尾部访问的接口,没有back/push_back/pop_back
3.forward_list不提供size成员函数

//forward_list只维护一个方向的链表,效率略高于list

作业:

班长和学位分别统计了学生的信息(student类),使用list将班长和学委的信息合并,去除重名后按照学号从小到大输出,年龄从大到小输出。

class student{
     int id;
     string name;
     int age;   
};

list<student> Three_Kingdom;
Three_Kingdom.push_back(student(4,"张飞",23));
....

班长:
    4 张飞 23
    2 关羽 26
    5 赵云 19
    1 刘备 31
    6 黄忠 50
    
学委:
    9 曹操 35
    7 张辽 27
    11 关羽 26
    8 典韦 24
    10 刘备 31    
#include <iostream>
#include <list>
​
using namespace std;
​
class student {
public:
    student(int id = 0, string name = "", int age = 0) : id(id), name(name), age(age) {}
​
    //按名字去重,==判断name,想要去重哪个值就要去重那个
    bool operator==(const student &s) const {  //为什么是返回布尔值的
        return this->name == s.name;
    }
​
    //默认按id从小到大排序
    bool operator<(const student &s) const {
        return this->id < s.id;
    }
​
    bool operator>(const student &s) const {
        return this->age > s.age;
    }
​
    //自己作为函数对象用作比较器 name排序
    bool operator()(const student &s1, const student &s2) {
        return s1.name < s2.name;
    }
​
    //重载输出
    friend ostream &operator<<(ostream &os, const student &s) {
        return os << s.id << ":" << s.name << ":" << s.age;
    }
​
private:
    int id;
    string name;
    int age;
};
​
//函数比较器 T-student ===> 年龄从大到小
template<typename T>
bool comp(const T &a, const T &b) {
    return a > b;
}
​
int main() {
    list<student> ban;
    list<student> xue;
​
    ban.push_back(student(4, "张飞", 23));
    ban.push_back(student(2, "关羽", 26));
    ban.push_back(student(5, "赵云", 19));
    ban.push_back(student(1, "刘备", 31));
    ban.push_back(student(6, "黄忠", 50));
​
    xue.push_back(student(9, "曹操", 35));
    xue.push_back(student(7, "张辽", 27));
    xue.push_back(student(11, "关羽", 26));
    xue.push_back(student(8, "典韦", 24));
    xue.push_back(student(10, "刘备", 31));
​
    //合并
    ban.splice(ban.end(), xue);
    //合并结果
    cout << "合并后:" << endl;
    for (list<student>::iterator it = ban.begin(); it != ban.end(); it++) {
        cout << *it << endl;
    }
    cout << "------------------------------------------------" << endl;
​
    //去除重名 ----- 按名字排序(函数对象比较器),去重
    ban.sort(student());
    ban.unique();//需要去重载---operator==
    cout << "姓名排序去重后:" << endl;
    for (list<student>::iterator it = ban.begin(); it != ban.end(); it++) {
        cout << *it << endl;
    }
    cout << "------------------------------------------------" << endl;
​
    //按学号升序排序
    ban.sort(); //需要去重载  operator<
    cout << "学号升序排序后:" << endl;
    for (list<student>::iterator it = ban.begin(); it != ban.end(); it++) {
        cout << *it << endl;
    }
    cout << "------------------------------------------------" << endl;
​
    //按年龄降序排序 
    ban.sort(comp<student>);  //需要去重载  operator>
    cout << "年龄降序排序后:" << endl;
    for (list<student>::iterator it = ban.begin(); it != ban.end(); it++) {
        cout << *it << endl;
    }
    cout << "------------------------------------------------" << endl;
​
    return 0;
}
​