模拟实现stack && queue/dequeue/适配器/优先级队列/仿函数

简介: ⭐学会适配器的使用,学会仿函数、了解deque,掌握stack、queue和优先级队列的底层实现方法

 ⭐前言:学习C++的STL,我们不仅仅要要求自己能够熟练地使用上层语法,我们还必须要求自己了解其底层原理,不需要了解得太深入,但一定得知道我们写出的各种代码后面,究竟采用了哪种设计思想,为什么要这样设计,有什么好处?这将会帮助我们打下坚实地C++基础,不再是只会表面语法的伪高手!

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

适配器模式:用现有的东西封装转换出我们想要的东西

迭代器模式:通过封装从而不暴露底层细节,在上层的中按照统一的方式进行使用。

7_9ED[8LK$E2E`3TA`WUPZ2.png

使用适配器模式模拟实现stack

数据结构中的栈,可以通过顺序表,也能通过链表实现,不过大多数情况,都会使用顺序表来实现,但是在STL中,不管哪种方法,我们都可以兼顾,那就是通过类模板即可。

实现stack,很简单,我们只需将vector或list作为适配器,利用适配器里面的接口进行封装转换就能完成stack的模拟实现。

首先先写出基本的框架。对于T,是stack使用的类型,而Container是适配器,就是我们想要stack是适配器是vector还是list,就传进去。而Container的缺省值是deque<T>。

#include<vector>#include<list>namespacemy_Stack {
template<classT,classContainer=deque<T>>classstack    {
public:
private:
Container_con;
    };
}

image.gif

接下来就好办了,复用适配器中的接口,实现在栈顶入,栈顶出,取栈顶元素,获取元素个数,判空等待。

①在栈顶插入数据:

voidpush(constT&x)
        {
_con.push_back(x);
        }

image.gif

②在栈顶出数据:

voidpop()
        {
_con.pop_back();
        }

image.gif

③获取栈顶元素:

T&top()
        {
return_con.back();
        }

image.gif

④判空

boolempty()
        {
return_con.empty();

image.gif

⑤获取元素个数

size_tsize()
        {
return_con.size();
        }

image.gif

我们不需要写析构函数,构造函数,拷贝构造函数等待,因为成员函数的类型是适配器推演出来的类型,它会去调用自己本身的构造函数和析构函数!

使用适配器模式模拟实现queue

要实现queue,跟实现stack一样!简单得很。数据结构中的队列,可以通过顺序表,也能通过链表实现,不过大多数情况,都会使用链表来实现,一样地,我们可以通过类模板来兼顾各种情况。

#include<vector>#include<list>namespacemy_Queue {
template<classT,classContainer=deque<T>>classqueue    {
public:
private:
Container_con;
    };
}

image.gif

跟stack不一样的,无非就是对头出数据,队尾入数据,然后是可以取队头和队尾的数据。

①队尾入:

voidpush(constT&x)
        {
_con.push_back(x);
        }

image.gif

②队头出:

voidpop()
        {
_con.pop_front();
        }

image.gif

③取队头或队尾数据:

constT&front()//取队头数据        {
return_con.front();
        }
constT&back()//取队尾数据        {
return_con.back();
        }

image.gif

了解deque

我们可以发现不管是stack还是queue,它们的适配器的缺省值是deque。使用deque的原因是有以下:

①stack理想的适配器是vector,而vector的缺点是扩容消耗,不支持头插头删和中间插入,因为这样效率很低。

②queue理想的适配器是vector,而list的缺点是不能随机访问。

对于deque,可以兼具vector和list的优点,解决掉缺点。

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

8$JD1V~{LHG{PNAYM61AT81.png

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

G~(Q_G)Y0E8C@)9FTVHKNTC.png

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,其设计结构如下:

N@TP@`}_A]}FLZF}N%PNW@Y.png

从图中我们可以知道,头插尾插的时候,不需要挪动数据。而随机访问,比如每个数组的容量为10,我们要找20,那么就先计算出20是在第几个数组中(用20/10 = 2),然后再算出在这个数组的哪里(找到是在第二个数组后,再20%10 = 0,在第0个下标上)。

deque的缺陷

①与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的,但是在中间位置插入,也依然效率低,需要挪动数据。

②与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

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

选择deque作为stack和queue的底层默认适配容器的原因:

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:

①stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。

②在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高

仿函数

仿函数是一个类,它的对象叫做函数对象。函数对象可以像函数一样去使用。仿函数的类要求重载一个函数:operator()()

下面就来实现一下,priority_queue中的仿函数:less和greater。

namespacemy_functor{
template<classT>classless    {
public:
booloperator()(constT&x, constT&y)
        {
returnx<y;
        }
    };
template<classT>classgreater    {
public:
booloperator()(constT&x, constT&y)
        {
returnx>y;
        }
    };
}
intmain()
{
my_functor::less<int>lessFunc;
//这样调用起来,看起来lessFunc是一个函数,但其实它是一个对象//因此,类less叫做仿函数,lessFunc叫做函数对象lessFunc(1, 2);
return0;
}

image.gif

仿函数的好处

还记得冒泡排序吗,在C语言中,一般情况下,我们写的排序算法,一般都是写死的,即只能用于降序,或用于升序,但这样显示不好,那么有没有办法让冒泡排序可以灵活点呢?站在上层的角度,每次调用冒泡排序的时候,我们可以多传一个参数进去,这个参数可以改变冒泡排序的排序形式!这时候,仿函数闪亮登场!

template<classT,classCompare>//添加泛型和仿函数voidBubbleSort(T*a, intn,Comparecom)//多添加一个函数对象{
for (intj=0; j<n; ++j)
    {
intexchage=0;
for (inti=1; i<n-j; ++i)
        {
//if (a[a[i] < a[i-1])if(com(a[i-1] , a[i])//不需要写死,排序方式交给函数对象即可!            {
swap(a[i-1], a[i]);
exchage=1;
            }
        }
if (exchage==0)
        {
break;
        }
    }
}

image.gif

此时调用的时候,就应该是这样的:

要升序是吧,简单!

inta[] = { 6,2,5,4,8,9,3,1,7 };
my_functor::less<int>lessFunc;
BubbleSort(a, sizeof(a) /sizeof(int), lessFunc);//有名对象BubbleSort(a, sizeof(a) /sizeof(int), my_functor::less<int>());//匿名对象

image.gif

要降序是吧,好说!

my_functor::greater<int>greaterFunc;
BubbleSort(a, sizeof(a) /sizeof(int), greaterFunc);

image.gif

总的来说仿函数的好处就是,可以将函数改造成函数对象,有了这个函数对象,我们可以把它写进类模板里面,为代码程序添加更多功能。

优先级队列priority_queue

优先级队列不是队列,它跟普通的队列不一样,普通的队列是先进先出,但是优先级队列是具有优先级的数据先出。因此优先级队列本质上就是一个堆,它是一种容器适配器(用vector来适配),根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的(即默认是大的数据优先级高,大堆)。

模拟实现优先级队列priority_queueq

优先级队列是一个堆,因此其核心是会建堆,以及调整堆排序。关于堆排序的相关知识,已经在二叉树的这篇文章中有了。二叉树----堆排序。因此这里就不再进行解析。

需要说明的是:因为我们在建堆的时候,可能会需要用到它的迭代器区间构造,因此需要实现一下,这就导致我们需要写一个无参构造,但这个无参构造并不需要写任何东西,因为适配器的对象会调用自己的初始化列表。

需要注意的是,根据文档:优先级队列中less表示的是大堆,即升序,greater表示的小堆,即降序。

R69GHQ3S_Q9$IL[GB_YZ00O.png

namespacemy_priority_queue{
//less类,仿函数template<classT>classless    {
public:
booloperator()(constT&x, constT&y)
        {
returnx<y;
        }
    };
//greater类仿函数template<classT>classgreater    {
public:
booloperator()(constT&x, constT&y)
        {
returnx>y;
        }
    };
template<classT,classContainer=vector<T>,classCompare=less<T>>classpriority_queue    {
public:
//迭代器区间构造,不需要实现迭代器template<classInputIterator>priority_queue(InputIteratorfirst,InputIteratorlast)
            :_con(first,last)//调用vector或其它适配器的迭代器区间构造        {
//建堆,采用向下调整比较好for (inti= (_con.size() -2) /2; i>=0; --i)
            {
adjust_down(i);
            }
        }
//无参构造,会去调用适配器的初始化列表priority_queue()
        {}
//向下调整voidadjust_up(size_tchild)
        {
Comparecom;
size_tparent= (child-1) /2;
while (child>0)
            {
//less-->x < y -->parent < child  默认大堆 if(com(_con[parent],_con[child]))  
                {
swap(_con[child], _con[parent]);
child=parent;
parent= (child-1) /2;
                }
else                {
break;
                }
            }
        }
//向上调整voidadjust_down(size_tparent)
        {
Comparecom;
size_tchild=parent*2+1;
while (child<_con.size())
            {
//默认左孩子大于右孩子  less-->x < y -->child < child+1  if (child+1<_con.size() &&com(_con[child] , _con[child+1]))
                {   
child++;
                }
//less-- > x < y-->child < parentif (com(_con[parent] , _con[child]))
                {
swap(_con[parent], _con[child]);
parent=child;
child=parent*2+1;
                }
else                {
break;
                }
            }
        }
//插入数据voidpush(constT&x)
        {
_con.push_back(x);
//向上调整adjust_up(_con.size() -1);
        }
//将堆顶元素pop掉voidpop()
        {
swap(_con[0], _con[_con.size() -1]);
_con.pop_back();
//向下调整adjust_down(0);
        }
//取堆顶元素constT&top() const        {
return_con[0];
        }
boolempty()
        {
return_con.empty();
        }
size_tsize() const        {
return_con.size();
        }
private:
Container_con;
    };
}

image.gif


相关文章
|
7月前
|
存储 设计模式 算法
stack和queue及优先级队列和适配器(包括deque)的介绍
stack和queue及优先级队列和适配器(包括deque)的介绍
70 1
|
5月前
|
存储 算法 Java
【C++】优先级队列priority_queue模拟实现&&仿函数
【C++】优先级队列priority_queue模拟实现&&仿函数
42 1
|
7月前
|
存储 算法 C语言
从C语言到C++_19(容器适配器+stack和queue模拟实现+优先级队列priority_queue)(下)
从C语言到C++_19(容器适配器+stack和queue模拟实现+优先级队列priority_queue)
56 2
|
7月前
|
存储 设计模式 算法
【C++/STL】stack和queue(容器适配器、优先队列、双端队列)
【C++/STL】stack和queue(容器适配器、优先队列、双端队列)
48 1
|
7月前
|
缓存 算法 C语言
从C语言到C++_19(容器适配器+stack和queue模拟实现+优先级队列priority_queue)(上)
从C语言到C++_19(容器适配器+stack和queue模拟实现+优先级队列priority_queue)
43 0
|
7月前
|
算法 C语言 C++
从C语言到C++_19(容器适配器+stack和queue模拟实现+优先级队列priority_queue)(中)
从C语言到C++_19(容器适配器+stack和queue模拟实现+优先级队列priority_queue)
49 0
|
7月前
|
存储 算法 C++
C++:stack、queue、priority_queue增删查改模拟实现、deque底层原理
C++:stack、queue、priority_queue增删查改模拟实现、deque底层原理
64 0
|
算法 C++ 容器
【C++】priority_queue使用和模拟实现——仿函数(下)
【C++】priority_queue使用和模拟实现——仿函数(下)
【C++】priority_queue使用和模拟实现——仿函数(下)
|
算法 Serverless C++
【C++】STL使用仿函数控制优先级队列priority_queue
【C++】STL使用仿函数控制优先级队列priority_queue
|
存储 C++ 容器
C++:deque的概念以及stack和queue的模拟实现
C++:deque的概念以及stack和queue的模拟实现