C++:STL——栈队列和优先级队列的模拟实现

Posted It‘s so simple

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++:STL——栈队列和优先级队列的模拟实现相关的知识,希望对你有一定的参考价值。


前言

在讲这些之前,我们需要了解一下双端队列:deque

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

deque的底层并不是真正连续的空间,而是由不连续的连续空间段组成的。其效果类型下图:

在这里插入图片描述

但是,deque有一个致命缺陷:不适合遍历因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

1.stack

1.1 stack的介绍和使用

  • stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
  • stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
  • stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类
  • 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque

更为详细的可以查看stack文档介绍

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

1.2 stack的模拟实现

STL标准库中通常都是使用双端队列来作为stack默认的底层数据结构的,因此,本次模拟实现也是使用deque作为底层数据结构。

代码如下:

#include <iostream>
#include <deque>

using namespace std;

namespace myTest{
    template<typename T,class Cond = deque<T>>
        class stack{
            public:
                stack(){}
                ~stack(){}
                void push(T& x)
                {
                    c_.push_back(x);
                }
                void pop()
                {
                    c_.pop_back();
                }
                T& top() const
                {
                    return c_.back();
                }
                size_t size()const 
                {
                    return c_.size();
                }
                bool empty()const
                {
                    return c_.empty();
                }

            private:
                Cond c_;
        };
}

2. queue

2.1 queue的介绍与使用

  • 队列是一种容器适配器,专门用于在FIFO上下文(先进先出) 中操作,其中从容器一端插入元素,另一端提取元素。
  • 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列
  • 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。

更为详细的可以查看queue文档介绍

2.2 queue的模拟实现

要实现一个queue,标准容器类deque和list均满足了queue的接口的要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque

代码如下:

#include <iostream>
#include <deque>

namespace myTest{
    template<typename T, class Cond = deque<T>>
        class queue
        {
            public:
                queue(){}
                ~queue(){}

                void push(const T& x)
                {
                    c_.push_back(x);
                }
                void pop()
                {
                    c_.pop_front();
                }
                T& back()const
                {
                    return c_.back();
                }
                T& front()const
                {
                    return c_.front();
                }
                size_t size() const
                {
                    return c_.size();
                }
                bool empty()const
                {
                    return c_.empty();
                }
            private:
                Cond c_;
        };
}

3. priority_queue

3.1 priority_queue的介绍和使用

  • 优先队列是一种容器适配器根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。

  • 此上下文类似于堆在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

  • 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。

  • 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问.

  • 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。

  • 需要支持随机访问迭代器以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。

更为详细的可以查看priority_queue文档介绍

3.2 priority_queue的模拟实现

优先级队列默认使用vector作为其底层存储数据的容器在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。

3.2.1 使用算法函数来实现priority_queue

① make_heap方法

template <class RandomAccessIterator, class Compare>
  void make_heap (RandomAccessIterator first, RandomAccessIterator last);

它的作用是将范围[first,last]中的元素重新排列,使它们形成一个堆

② push_heap方法

template <class RandomAccessIterator>
  void push_heap (RandomAccessIterator first, RandomAccessIterator last);

它的作用是给定一个范围[first,last-1]的堆,这个函数通过将(last-1)中的值放入其中的相应位置,将该范围视为堆扩展到[first,last]。

③ pop_heap方法

template <class RandomAccessIterator>
  void pop_heap (RandomAccessIterator first, RandomAccessIterator last);

它的作用是重新排列堆范围[first,last]中的元素,使被认为是堆的部分缩短了一个。具有最高值的元素被移到(last-1)。虽说是对堆进行删除,但是其本身的数组的值并没有被删除,只是仅仅作为堆的发生了变化,每次删除,堆的长度均会减一。

通过调用make_heap,一个范围可以被组织成一个堆。之后,如果分别使用push_heap和pop_heap从其中添加和删除元素,其堆的属性将被保留下来。

这些算法函数均包含在#include <algorithm>

使用算法函数来实现priority_queue的代码如下:

#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>

using namespace std;

namespace myTest{
    //less<T> 是一个仿函数,表示当前比较的准则是小于,则建的堆就是大堆
    //greater<T> 也是仿函数,表示比较的准则是大于,则建的堆就是小堆
    template<typename T,class Cond = vector<T> , class Pred = less<T>>
        class priority_queue{
            public:
                explicit priority_queue() {}
                priority_queue(const T* first,const T* last)
                {
                    //1.首先建立一个数组
                    c_.insert(c_.begin(),first,last);
                    //2.其次再将建立的数组使用算法函数建成一个堆
                    make_heap(c_.begin(),c_.end(),pr_);
                }

                void push(const T x)
                {
                    //首先将数据插入到数组中
                    c_.push_back(x);
                    //调整数组,使其再次成为堆
                    push_heap(c_.begin(),c_.end(),pr_);
                }

                void pop()
                {
                    //首先需要将堆顶元素进行调整,在删除
                    pop_heap(c_.begin(),c_.end(),pr_);

                    c_.pop_back();
                }

                bool empty()const
                {
                    return c_.empty();
                }

                size_t size() const
                {
                    return c_.size();
                }
                T& top()const
                {
                    return c_.front();
                }
                //为了测试方便,我们编写一个打印函数
                void Print() const
                {
                    auto it = c_.begin();
                    while(it != c_.end())
                    {
                        cout << *it << " ";
                        it++;
                    }
                    cout << endl;

                }

            private:
                Cond c_;
                Pred pr_;//比较谓词
        };
}

void test()
{
    int iv[] = {1,7,9,0,3,5,2,4};
    int n = sizeof(iv) / sizeof(iv[0]);
    cout << "normal vector is ";
    for(int i = 0 ;i < n; ++i)
    {
        cout << iv[i] << " ";
    }
    cout << endl;
    myTest::priority_queue<int,vector<int>,greater<int>> pq(iv,iv+n);

    cout << "make_heap" << endl;
    pq.Print(); 
    cout << "push_heap(12)" << endl;
    pq.push(12);
    pq.Print();
    cout << "pop_heap" << endl;
    pq.pop();
    pq.Print();
}


int main()
{
    test();
    return 0;
}

运行验证如下:

在这里插入图片描述

3.2.2 使用向上、向下调整算法来实现priority_queue

关于向上调整算法和向下调整算法,大家可以查看面试考点–堆(向下调整算法,向上调整算法,建堆,堆排序)以及堆排序、建堆的时间复杂度分析(图文并茂)

这里就不再做过多描述,直接上代码。

代码如下:

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

namespace myTest{
    template<typename T,class Cond = vector<T>,class Pred = less<T>>
        class priority_queue{
            public:
                explicit priority_queue() 
                {}
                priority_queue(const T* first,const T* last)
                {
                    c_.insert(c_.begin(),first,last);

                    //建堆----使用向下调整算法
                    //从最后一个非叶子节点开始调
                    for(int i = ((c_.size()-1)-1) >> 1;i >= 0; --i)
                    {
                        AdjustDown(i);
                    }

                }

                void push(const T x)
                {
                    c_.push_back(x);

                    //插入到数组的末尾,使用向上调整算法进行调堆
                    AdjustUp(c_.size()-1);

                }


                void pop()
                {
                    //首先交换堆顶元素和堆尾元素,然后再进行调堆
                    std::swap(c_.front(),c_.back());
                    
                    c_.pop_back();

                    AdjustDown(0);

                }

                T& top()const
                {
                    return c_.front();
                }

                size_t size() const
                {
                    return c_.size();
                }

                bool empty()const
                {
                    return c_.empty();
                }
                //为了测试方便,我们编写一个打印函数
                void Print() const
                {
                    auto it = c_.begin();
                    while(it != c_.end())
                    {
                        cout << *it << " ";
                        it++;
                    }
                    cout << endl;

                }

            protected:
                void AdjustDown(int parent)
                {
                    int child = parent * 2 + 1;
                    while(child < c_.size())
                    {
                        if(child + 1 < c_.size() && pr_(c_[child],c_[child + 1]))
                            ++child;
                        if(pr_(c_[parent],c_[child]))
                        {
                            std::swap(c_[child],c_[parent]);
                            parent = child;
                            child = parent * 2 + 1;
                        }
                        else
                            break;
                    }
                }
                void AdjustUp(int child)
                {
                    int parent = (child-1) >> 1;
                    while(parent > 0)
                    {
                        if(pr_(c_[parent],c_[child]))
                        {
                            std::swap(c_[child],c_[parent]);
                            child = parent;
                            parent = (child-1) >> 1;
                        }
                        else
                            break;
                    }
                }

            private:
                Cond c_;
                Pred pr_;

        };
}


void test()
{
    int iv[] = {1,7,9,0,3,5,2,4};
    int n = sizeof(iv) / sizeof(iv[0]);
    cout << "normal vector is ";
    for(int i = 0 ;i < n; ++i)
    {
        cout << iv[i] << " ";
    }
    cout << endl;
    myTest::priority_queue<int,vector<int>,greater<int>> pq(iv,iv+n);

    cout << "create heap" << endl;
    pq.Print();
    cout << "push_heap(12)" << endl;
    pq.push(12);
    pq.Print();
    cout << "pop_heap" << endl;
    pq.pop();
    pq.Print();

}

int main()
{
    test();
    return 0;
}

运行结果如下:

在这里插入图片描述

以上是关于C++:STL——栈队列和优先级队列的模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解

[ C++ ] STL_stack(栈)queue(队列)使用及其重要接口模拟实现

STL优先级队列剖析及模拟实现

优先队列实现dijkstra算法C++代码

C++STL之优先级队列详解

C++STL之优先级队列详解