stack与queue的模拟实现、对deque的了解性学习

目录

stack

基础框架

push和pop

top

empty和size

基础测试和容器适配器的相关说明

重制版的stack

stack的整体代码

对容器deque的了解性学习

queue

基础框架

push和pop

back和front

size和empty

queue的整体代码

测试代码


stack

基础框架

    template<class T>
	class stack
	{
	public:


	private:
		vector<T>v;
	};

因为没有其他的内置类型,因此stack使用编译器自动生成的默认构造,拷贝构造,析构函数,赋值运算符函数即可,这些函数对于自定义类型又会去调用自定义类型对应的成员函数,因此我们不必在stack中编写这些成员函数。

push和pop

    template<class T>
	class stack
	{
	public:
		void push(const T& x)
		{
			v.push_back(x);
		}

		void pop()
		{
			v.pop_back();
		}


	private:
		vector<T>v;
	};

因为当前stack的底层是vector,而vector头插和头删的效率需要将vector中所有数据都挪动一次,效率是特别低的,所以我们选择vector的尾作为栈顶,因此stack的push和pop操作就对应vector的尾插和尾删。

top

    template<class T>
	class stack
	{
	public:
		
		T& top()
		{
			return v.back();
		}

        const T& top()const
		{
			return v.back();
		}

	private:
		vector<T>v;
	};

empty和size

    template<class T>
	class stack
	{
	public:
		
		bool empty()const
        {
            return v.empty();
        }
        
        size_t size()const
		{
			return v.size();
		}

	private:
		vector<T>v;
	};

加上const,让普通对象和const对象都能调用这些接口。

基础测试和容器适配器的相关说明

可以看到正常运行,符合我们的预期。

但走到这里有一个问题,上面通过这么多代码所实现的stack是不是容器适配器呢?

答案:并不是容器适配器。拿电源适配器比如我们手机的充电器来说明,手机的电源适配器只负责充电的功能,而不管你电压是多少,我可以插在家里的插座上,经过适配器将220v的电压转化成合适的大小给手机充电,也可以插在车上,经过适配器将车载电压转化成合适的大小给手机充电,甚至可以插在蓝牙耳机上给手机充电,也就是说电源适配器能够适应不同的电压,能灵活的转化和控制。

既然电源适配器能够适配不同的电源(电压),那我们容器适配器是不是也应该适配不同的容器呀?没错,应该适配。而反观我们上面模拟实现的stack,虽然能实现栈(对应充电)的功能,但并不能适配不同的容器(对应电压),因为上面模拟实现时,把stack底层的容器定死成了vector,导致我们的stack只能在容器vector下才能发挥栈的功能,这就不符合适配器能灵活转化和控制的特性,因此我们要对自己实现的stack做出修改。

重制版的stack

上文中也说过,之前实现的stack虽然能跑,但该stack顶多只算是一层vector的封装,并不能算是适配器,因此接下来咱们要对模拟实现的适配器stack进行修正,代码如下所示。

    template<class T,class Container>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_back();
		}

		T& top()
		{
			return _con.back();
		}

		const T& top()const
		{
			return _con.back();
		}

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

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

	private:
		//vector<T>v;
		Container _con;
	};

修正后的代码加了一个类型模板参数Container,表示我不管你是什么类型的容器,只要你Container类型支持push_back、pop_back、back、empty、size等操作,那我stack就能适配你这个容器来发挥我栈该具有的功能。

修正代码后,定义stack对象时就得传一个具体的容器类型给stack的第二个模板参数了,如下两图所示。

底层为vector。

因为list也支持push_back、pop_back、back等操作,因此stack也可以适配list。

可以看到虽然list和vector底层数据的结构和组织已经是千差万别了,但对上层stack的使用是没有一点影响的,我栈依然保持着后进先出等等的一些栈该具有的性质。

stack的整体代码

在之前重制版stack的代码的基础上,给类型模板参数Container加上缺省值deque<T>后,就是stack的整体代码了,如下所示。

    template<class T,class Container=deque<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_back();
		}

		T& top()
		{
			return _con.back();
		}

		const T& top()const
		{
			return _con.back();
		}

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

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

	private:
		//vector<T>v;
		Container _con;
	};

对容器deque的了解性学习

走到这里,我们已经成功地模拟实现了一个容器适配器stack。看看STL标准库中的stack,如上图红框处,我们和它一样,也是定义了一个模板参数Container,但STL中的Container是有缺省值的,默认给的是deque<T>这个类模板,那么它是一种什么样的容器呢?

deque的学名叫做双端队列,但它并不是一个队列,因为没有对它要求先进先出。

如下图红框处,更神奇的是,deque既支持push_front和pop_front(vector不持支,list支持),也支持operator[](vector持支,list不支持),它好像就是list和vector的合体版,注意虽然听上去好像很厉害,但实际效率是有点外强中干的。 

deque的逻辑结构图如下所示。

deque通过管理一个中控指针数组对deque内的数据进行管理,中控数组表示数组从中间开始赋值。中控指针数组上的每一个值都是地址,是某个小数组buffer的地址。

deque内的数据是通过一个个小数组buffer组织起来的,每个小数组的容量都是固定的,比如上图中容量就为8。小数组buffer分为头插小数组buffer、尾插小数组buffer、其他都称之为中间插入小数组。头插数组最多只能存在一个,尾插数组也最多只能存在一个,中间插入数组可以存在多个。举个例子,调用deque的头插函数,则数据只会赋给头插小数组A,如果从右向左赋值插满了,则新开辟一个头插小数组bufferB,头插数组A将不再是头插数组,转而成为中间插入数组,头插数组B就会是唯一的头插数组了。

在头插数组和尾插数组中插入或者删除都不需要挪动数据,拿上图的环境举个例子,第一行的头插数组中只有-1和0,如果头插,则把数据赋给-1左边的值,然后更改记录【头插数组中有效数据】的变量即可。如果头删,则只需修改对应的记录【头插数组中有效数据】的变量即可。

如果是往中间插入数组中插入数据,因为既不是头插,也不是尾插,所以一定得指定插入到哪。如果该数组没有插满,则直接在对应位置上赋值即可,如果该数组已经插满,则需要挪动数据,在插入位置后的所有元素都得往后挪动一次,在这过程中,溢出的数据会挪动到其他小数组buffer当中。拿上图的环境举个例子,当往第二行的中间插入数组中插入数据时,因为第二行的中间插入数组已经插满,则位于插入位置后的所有数据都要往后挪动,第二行的8将会挪动到第三行的1位置上,第三行的8则会移到第四行的尾插小数组的9位置上。

deque的优势与设计缺陷如下。

deque的迭代器如下图所示。

总结:

对deque的介绍走到这里,我们可以大致想象出deque会在哪些场景中被使用。

如果尾插尾删多,特别是随机访问多,比如需要排序,则应该用vector;

如果任意位置,特别是中间插入删除多,则应该用list;

如果头部和尾部的插入删除都多,则在当前场景下,比起list或者vector,此时就更应该使用deque,因为vector不支持头插头删,对于尾插,vector比起deque也不占优势,因为vector尾插如果要扩容是整体扩2倍,还要将大量数据拷贝到新空间上,然后尾插新数据,而deque只要扩一个小数组buffer,无需拷贝数据到新空间上,只需尾插新数据即可。list对比deque每次插入删除都得申请空间释放空间,效率低下。而stack需要频繁尾插尾删,queue需要频繁的尾插和头删,所以deque就很适合去做他俩的底层容器。

queue

<< stack与queue的介绍和使用>> 一文中也说过,stack和queue在性质上的唯一区别在于queue是先进先出,而stack是后进先出,其余所有性质,他俩都是完全一样的,比如上图所示,queue和stack一样,也是一个容器适配器,并且他俩默认适配的都是deque这个容器;而stack和queue在操作函数上的唯一区别在于除了queue没有stack的top函数,以及stack没有queue的back和front函数,其余接口的使用方法是和stack完全一致的。

因为queue和stack具有相似性,所以上面模拟实现完stack后,接下来的工作就简单不少了。

基础框架

    template<class T, class Container>
	class queue
	{
	public:
			
	
	private:
		Container _con;
	};

和stack一样,也无需手写默认构造、拷贝构造、赋值运算符函数等等接口,编译器默认生成的就够用了。

push和pop

对于一个队列,是有队头和队尾之分的,既然要插入进行排队,理所应当的就是从队尾开始排队,所以queue是从队尾进,队头出,先进先出。模拟实现queue时,我们让底层容器的尾部充当队列的尾部,头部充当队列的头部。

    template<class T, class Container>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_front();
		}

	private:
		Container _con;
	};

back和front

    template<class T, class Container>
	class queue
	{
	public:
	    T& back()
		{
			return _con.back();
		}

		const T& back()const
		{
			return _con.back();
		}

		T& front()
		{
			return _con.front();
		}

		const T& front()const
		{
			return _con.front();
		}
    
	private:
		Container _con;
	};

size和empty

    template<class T, class Container>
	class queue
	{
	public:
	    bool empty()const
		{
			return _con.empty();
		}

		size_t size()const
		{
			return _con.size();
		}
	private:
		Container _con;
	};

让成员函数加上const,让普通对象和const对象都能调用这些接口。

queue的整体代码

给类型模板参数Container加上缺省值deque<T>后,就是queue的整体代码了,如下所示。

    template<class T, class Container=deque<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			_con.pop_front();
		}

		T& back()
		{
			return _con.back();
		}

		const T& back()const
		{
			return _con.back();
		}

		T& front()
		{
			return _con.front();
		}

		const T& front()const
		{
			return _con.front();
		}

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

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

	private:
		Container _con;
	};

测试代码

如下图,当给queue的第二个模板参数传list<int>时正常运行。

如下图,当给queue的第二个模板参数传vector<int>时编译不通过。 

这也很好的符合了我们的预期,如下图,想要queue能适配一个容器是有条件的,容器需要提供pop_front函数,而STL的vector因为头删的效率太低所以没有提供pop_front这个成员函数,所以queue无法适配vector,所以编译不通过。注意如果vector提供了pop_front反而是个祸端,因为提供后,queue就可以适配vector,导致queue的效率会极低,对于学习了数据结构底层实现的人还好说,如果不学习底层实现,连出现效率低下问题的原因都找不到。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值