30天速通C++(九):深入理解deque

目录

前言

一.容器适配器

1.1什么是容器适配器

[1.2 STL标准库中stack和queue的底层结构](#1.2 STL标准库中stack和queue的底层结构)

二.stack

2.1stack类模版

2.2头文件问题

三.queue

3.1queue类模版

3.2按需实例化

[四. priority_queue](#四. priority_queue)

4.1priority_queue的定义

4.2仿函数

4.3仿函数使用场景

[五. deque](#五. deque)

[5.1 deque的定义](#5.1 deque的定义)

​编辑

[5.2 deque的结构](#5.2 deque的结构)


前言

哈喽,各位小伙伴大家好!上期我们讲了list结构剖析及其模拟实现。今天我们来讲一下dequeue和模版进阶。话不多说,我们进入正题!向大厂冲锋

一.容器适配器

1.1什么是容器适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

容器适配器就像我们的电流适配器一样。

容器适配器就是一种转化接口,把一种接口转化为我们需要的接口。

1.2 STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:

如果我们需要写一个栈的类模版。我们会自己手搓一个出来。

cpp 复制代码
template<class T>
class stack
{
private:
    T* ptr;
    size_t size;
    size_t capacity;
};

但是栈只需要栈顶插入和删除,也就是说只需要支持一端插入和删除即可。那我们的vector和list容器都支持一端插入和删除。所以我们可以用vector和list容器封装实现。

所以我们的栈就可以用一个容器封装实现

//container适配转化出Stack

cpp 复制代码
    template<class T, class container = vector<T>>
    class Stack
    {
    public:
    private:
        container con;
    };

这里我们container传vector就是用vector适配转化的栈,传list就是list适配转化的栈。这就是我们通过container模版参数用容器适配器模式写的一个栈。

库里的栈也是这样做的。

二.stack

2.1stack类模版

这里我们用容器适配器写了一个栈的类模版。

成员是container。成员函数再调用容器的接口即可实现一个先进先出的栈。

默认使用vector容器适配。

//container适配转化出Stack

cpp 复制代码
template<class T, class container = vector<T>>
class Stack
{
public:
    void push(T x)
    {
        con.push_back(x);
    }
    void pop()
    {
        con.pop_back();
    }
    const T& Top() const
    {
        return con.back();
    }
    size_t size() const
    {
        return con.size();
    }
    bool empty() const
    {
        return con.empty();
    }
private:
    container con;
};

2.2头文件问题

我们自己写的文件包含在.cpp文件时尽量放在最后面。防止出现以下错误。

三.queue

3.1queue类模版

这里queue我们也是使用适配器模式。因为queue要求一端进一端出。

所以使用list容器适配更合适。也是调用对应容器的接口即可。

//container适配转化出Queue

cpp 复制代码
  template<class T, class container =list<T>>
    class Queue
    {
    public:
        void push(T x)
        {
            con.push_back(x);
        }
        void pop()
        {
            con.pop_front();
        }
        const T& front() const
        {
            return con.front();
        }
        const T& back() const
        {
            return con.back();
        }
        size_t size() const
        {
            return con.size();
        }
        bool empty() const
        {
            return con.empty();
        }
    private:
        container con;
    };

3.2按需实例化

类模版实例化是按需实例化。什么是按需实例化呢?

所以这里我们没报错,因为我们没有调用pop_front.

但当我们调用pop时就会报错。这就是因为按需实例化。

四. priority_queue

4.1priority_queue的定义

priority_queue也是一个容器适配器.priority_queue也叫优先级队列

通过对priority_queue的底层结构就是堆,因此此处只需对对进行通用的封装即可。

堆我们用下标计算父子关系。所以priority_queue的默认容器是vector.

这里我们直接用容器封装即可。

cpp 复制代码
template<class T, class container = vector<T>,class Compare =Less<T>>
class priority_queue
{
public:
    void AdjustDown( int parent)
    {
        Compare com;
        int child = parent * 2 + 1;//孩子节点
        while (child < con.size())
        {
            int tmp = child;//左右孩子中最小的孩子
            if (tmp + 1 < con.size() && com(con[tmp], con[tmp + 1]))//防止没有右孩子
            {
                tmp = child + 1;
            }//假设法
            if (com(con[parent], con[tmp]))//判断
            {
                swap(con[parent], con[tmp]);//交换
                parent = tmp;
                child = parent * 2 + 1;
            }
            else
            {
                break;//调整完毕
            }
        }
    }
    void AdjustUp(int size)
    {
        Compare com;
        int child = size - 1;//最后的节点
        while (child > 0)
        {
            int parent = (child - 1) / 2;//父亲节点
            if (com(con[parent],con[child]))//判断
            {
                swap(con[child], con[parent]);//交换
                child = parent;
            }
            else
            {
                break;//调整完成
            }
        }
    }
    void push(const T& x)
    {
        con.push_back(x);
        AdjustUp(con.size());
    }
    void pop()
    {
        swap(con[0], con[con.size() - 1]);
        con.pop_back();
        AdjustDown(0);
    }
    const T& top()
    {
        return con[0];
    }
    size_t size() const
    {
        return con.size();
    }
    bool empty() const
    {
        return con.empty();
    }
private:
    container con;
};

需要注意的是,算法库里提供了堆的一系列接口。如堆排序,判断是否是堆,以及建堆等接口。

同时库的priority_queue默认为大堆。

4.2仿函数

那我们怎么调整大堆小堆呢?

那就需要用到仿函数。

仿函数是个类。

cpp 复制代码
template<class T>
struct Less
{
    int operator()(const T& x, const T& y)
    {
        return x < y;
    }
};

仿函数这个类主要重载operator()。

这个()里面传两个对象的比较。

cpp 复制代码
Compare com;
if (com(con[parent],con[child]))//判断

这里看起来很像函数调用,但是实际上是operator()的调用。

所以被叫做仿函数。仿函数我们想支持各种类型的比较就可以写成类模版。

cpp 复制代码
template<class T, class container = vector<T>,class Compare =Less<T>>

所以我们在priority_queue的模版参数里传一个仿函数类型就可以控制大堆小堆。

通过传不同仿函数类型,我们就能控制不同的比较逻辑

从而控制大小堆。

4.3仿函数使用场景

库里已经实现了<和>两个仿函数,所以一般来说仿函数我们都不需要自己实现。直接用库里的即可。

但是一下两种情况需要自己实现仿函数

类类型比较且没有实现比较函数

例如现在我们现在堆存的是日期类,那我们的仿函数还能支持比较吗?

cpp 复制代码
int main()
{
    qcj::priority_queue<Date, vector<Date>> queue1;
    cout << endl;
    queue1.push(Date(2018, 6, 9));
    queue1.push(Date(2018, 6, 10));
    queue1.push(Date(2018, 6, 11));
    queue1.push(Date(2018, 6, 12));
    while (!queue1.empty())
    {
        cout << queue1.top() << endl;
        queue1.pop();
    }
    return 0;
}

支持因为日期类里重载了比较函数。他会调用日期类的比较。

但是如果日期类没有实现比较那我们就需要自己实现一个仿函数比较。

如果这个类是别人写的还需要保证日期类的成员是私有的或提供成员接口。

比较的逻辑不是我们期望的

现在我们堆存储日期类的地址。

cpp 复制代码
int main()
{
    qcj::priority_queue<Date*, vector<Date*>> queue1;
    cout << endl;
    queue1.push(new Date(2018, 6, 9));
    queue1.push(new Date(2018, 6, 10));
    queue1.push(new Date(2018, 6, 11));
    queue1.push(new Date(2018, 6, 12));
    while (!queue1.empty())
    {
        cout << *queue1.top() << endl;
        queue1.pop();
    }
    return 0;
}

我们发现比较的顺序是乱序的,因为比较的时候是按照new的地址去比较。

而我们希望的是按照日期比较。

所以我们就需要自己实现一个仿函数去按照日期比较即可。

cpp 复制代码
struct Dateless
{
    int operator()(Date* x, Date* y)
    {
        return *x > *y;//按照日期比较
    }
};

这时我们的比较结果就是我们所希望的。

五. deque

5.1 deque的定义

我们可以发现库的的stack和queue没有用list和vector。

而是用了一个dequeue的容器。

那deque是什么呢?

deque是vector和list的缝合怪。

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

5.2 deque的结构

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

deque是通过一个指针数组来控制数据存储的数组。

下标

迭代器

deque 迭代器结构有四个指针

deque借助两个迭代器维护,start就是第一个数据的迭代器,finish就是最后一个数据的迭代器。

迭代器遍历

可以看到库里的实现和我们的差不多

头插尾插

中间插入删除

deque中间插入删除也需要挪动数据。

operator[]

因为头插时第一个数组可能不满所以先用cur-first+n计算位置。

如果小于当前数组直接+n即可。

否则就用N去计算位置。

总结: