priority_queue 的模拟实现

priority_queue 的底层结构

我们已经学习过栈和队列了,他们都是用一种容器适配出来的。今天我们要学习的 prority_queue 也是一个容器适配器。在 priority_queue 的使用部分我们已经知道想要适配出 priority_queue,这个底层的容器必须有以下接口:

  • empty():检测容器是否为空。
  • size():返回容器中有效元素个数。
  • front():返回容器中第一个元素的引用。
  • push_back():在容器尾部插入元素。

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

在学习 C 语言的时候,我们已经学习过堆这种数据结构了,当时我们使用的是用数组作为堆的底层数据结构,因此,我们模拟实现 priority_queue 会使用 vector 作为 priority_queue 的底层数据结构。当然 deque 也是没有问题的,但是不及 vector 高效。因为我们会大量使用 operator[] ,对比下来我们会倾向于选择 vector 来模拟实现 priority_queue

priority_queue 的基本结构实现

实现 priority_queue 的大体思路和模拟实现 stackqueue 差不多。priority_queue 涉及更多的算法和细节处理。

#pragma once
namespace Tchey
{
    template<class T, class Container = vector<T>>
    class priority_queue
    {
    public:

    private:
        Container _con;
    };
}

但是我们这样定义出来的 priority_queue 的模板类好像不对啊!因为库里面的 priority_queue 想要构建小堆的时候,需要传递三个模板参数哒:存储 int 类型的小堆:priority_queue, greater> heap。这个greater 是个什么东西呢?

这个 greater 就是一个模板类 priority_queue 能够实例化出来大堆和小堆的关键所在!

仿函数

看他的结构,greater 应该也是一个模板类。在这个类的里面,重载了圆括号运算符,即:operator(),使得一个类的对象,能够像函数那样调用。这个东东就叫做仿函数。我们来看一个简单的例子吧:

struct GetLessNum
{
    int operator()(const int& a, const int& b)
    {
        return a < b ? a : b; 
    }
};

int main()
{
    GetLessNum getLessNum;
    int a = 10, b = 5;
    cout << getLessNum(a, b) << endl; //输出:5

    return 0;
}

在上面的例子中,我们定义了一个 GetLessNum 的类,类中重载了圆括号运算符。在 main 函数中,实例化出了一个对象,通过对象调用 operator() 从而通过类对象实现了函数调用的效果,这个就是仿函数啦!

揭秘 greater

同样地,在 greater 这个模板类中,也重载了圆括号运算符。当我们构建大堆的时候,没有传入后两个模板参数,可见是给了缺省值。在 C 语言阶段,我们已经学习了堆,知道了构建大堆与小堆的区别:仅仅是在向上调整算法,和向下调整算法中的比较逻辑上不同,其余均是相同的。

C语言数据结构初阶(11)----堆_姬如祎的博客-CSDN博客

于是我们就可用通过模板参数来控制 priority_queue 中两个算法实现的比较逻辑!这样就能够实现根据传入模板参数的不同,构建出来不同的堆了!

namespace Tchey
{
    template<class T>
    struct less
    {
        bool operator()(const T& e1, const T& e2)
        {
            return e1 < e2;
        }
    };

    template<class T>
    struct greater
    {
        bool operator()(const T& e1, const T& e2)
        {
            return e1 > e2;
        }
    };
}

在使用 priority_queuepush 函数的时候,就会使用向上调整算法:对于大堆,如果子节点的值大于父节点的值,那么就需要交换两个节点的值,对于小堆;如果子节点的值小于父节点的值,那么就需要交换两个节点的值。

于是我们就可以通过传入的第三个模板参数来控制:如果传入 less,刚好 less 中的 opertor() 是小于的比较逻辑,就是小堆的向上调整算法;如果传入 greater,刚好 greater 中的 opertor() 是大于的比较逻辑,就是大堆的向上调整算法。

通过模板参数的控制,priority_queue 同时能够实现大堆和小堆,而不用单独写大堆和小堆。

priority_queue 的基本结构就可以这么写:

namespace Tchey
{
    template<class T>
    struct less
    {
        bool operator()(const T& e1, const T& e2)
        {
            return e1 < e2;
        }
    };

    template<class T>
    struct greater
    {
        bool operator()(const T& e1, const T& e2)
        {
            return e1 > e2;
        }
    };

    template<class T, class Container = vector<T>, class Cmp = Less<T>>
    class priority_queue
    {
    public:
    private:
        Container _con;
    };
}

priority_queue 的函数实现

向上调整算法

我们在向一个堆插入数据的时候就会用到向上调整算法:就拿大堆来说,你向大堆里面插入了一个数据,自然是要通过调整,使得插入的数据和原来的大堆重新形成一个新的大堆!

priority_queue 的模拟实现_第1张图片

在这个插入的例子中,插入节点 9,不妨命名为 child。他的父节点 6,不妨命名为 parent。child 大于parent,交换两个节点的值。然后更新 child 和 parent,此时 child 依然大于 parent 交换两个节点的值。再次更新 child 和 parent。发现 parent < 0,结束循环。或者在比较的过程中 child < parent 也要结束循环。

这就是大堆的向上调整算法,至于小堆,比较逻辑反过来就行。

想要 priority_queue 根据传入的参数来决定是大堆的比较逻辑还是小堆的比较逻辑,这里就不能将比较逻辑写死,而是根据仿函数来比较!

void AdjustUp(int child)
{
    Cmp cmp; //根据第三个模板参数的类,实例化出来一个对象
    int parent = (child - 1) / 2; //根据child 找到 parent
    while(child)
    {
        if(cmp(_con[parent], _con[child])) //调用 operator() 来比较
        {
            swap(_con[parent], _con[child]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else //不满足条件直接退出循环
            break;
    }
}

向下调整算法

向下调整算法在 pop 函数中使用哈!pop 的逻辑就是将堆顶的数据与堆中最后一个数据交换,然后对下标为 0 的元素来一次向下调整算法,就满足堆的要求啦!

priority_queue 的模拟实现_第2张图片

我们来看上面的例子:这是一个大堆,删除堆顶的元素:我们先将堆顶的 7 和堆的最后一个数据 0 交换。然后对下标为 0 的元素,不妨命名为 parent。向下调整的逻辑是:选择 parent 的左右孩子中较大的那个孩子,不妨命名为 child,然后与 parent 进行比较,如果 child 大于 parent,那么交换 parent 和 child 两个节点。反之结束向下调整算法的逻辑。如果当 child 大于等于 vector 的 size 也要结束循环。

同向上调整算法,如果是小堆的话,只是比较逻辑不相同,其他的步骤均是一样的!因此向下调整算法 child 与 parent 的比较不能使用大于小于符号将比较逻辑写死。而是要使用仿函数来实现比较逻辑。

void AdjustDown(int parent)
{
    Cmp cmp; //实例化仿函数的对象
    int n = _con.size(); //vector 的大小
    int child = parent * 2; //通过 parent 找到 child

    while(child < n)
    {
        //选择左右孩子中较大的那个或者较小的那个,取决于第三个模板参数的传值
        if(child + 1 < n && cmp(_con[child], _con[child + 1]))
            child++;
        //如果满足条件,交换
        if(cmp(_con[parent], _con[child]))
        {
            swap(_con[parent], _con[child]);
            parent = child;
            child = parent * 2;
        }
        else //不满足直接退出
            break;
    }
}

void push(const T& val)

当你完成了向上调整算法与向下调整算法的书写,堆的接口实现就是信手拈来哈!push 函数是向堆中插入如一个元素,我们只需要在 vectorpush_back 一个元素,然后使用向上调整算法就可以啦!

void push(const T& val)
{
    _con.push_back(val);
    AdjustUp(_con.size() - 1);
}

void pop()

删除堆顶的元素只需要我们将 vector 中下标为 0 的元素与 vector 中的最后一个元素交换位置,然后调用 pop_back() 接口,最后对下标为 0 的元素使用一次向下调整算法就可以啦!


void pop()
{
    swap(_con[0], _con[_con.size() - 1]);
    _con.pop_back();
    AdjustDown(0);
}

bool empty()

我们只需要返回 vector 是否为空就行啦!

void empty()
{
    return _con.empty();
}

size_t size()

同样地,我们只需要返回 vector 的 size 就可以啦!


size_t size()
{
    return _con.size();
}

T& top()

返回堆顶的元素就是返回 vector 中下标为 0 的元素。


void top()
{
    assert(_con.size());
    return _con[0];
}

priority_queue 的模拟实现_第3张图片

你可能感兴趣的:(C++专题,c++,开发语言)