【C++】揭秘STL:stack与queue的底层实现

前言

在上篇讲解了list的模拟实现,遇到了迭代器失效、拷贝构造、指针指向逻辑等问题。

string、vector、list中运用过许多函数接口,而这篇会跳过函数接口的使用,直接向下解决栈和队列的模拟实现

stack与queue

一、deque的简单介绍

1.1deque的原理介绍

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

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其"整体连续"以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

那deque是如何借助其迭代器维护其假想连续的结构呢?

1.2deque的缺陷

对比 vector:

  1. 头插 / 头删不需要移动元素,效率更高
  2. 扩容时不需要移动大量元素,效率更高

对比 list:

底层是分段连续空间,空间利用率更高
缺陷:

不适合频繁遍历场景:遍历时 deque 的迭代器需要频繁检测是否到达某段小空间的边界,导致遍历效率低于 vector/list

序列式场景中通常需要频繁遍历,因此实际开发中,需要线性结构时优先选择 vector 和 list,deque 的直接应用场景较少;但 STL 选择 deque 作为 stack 和 queue 的底层默认容器,核心原因如下:

1.3deque作为stack和queue的底层默认容器

  1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
  2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。

二、stack和queue的整体框架

stack、queue均基于容器适配器 实现(默认底层容器为 deque),核心区别是:stack(栈)遵循先进后出(LIFO)规则,queue(队列)遵循先进先出规则。

cpp 复制代码
// Stack.h
#pragma once
#include<deque>

namespace yuuki
{
   // 容器适配器:默认以deque为底层容器实现stack
   template<class T, class Container = deque<T>>
   class stack
   {
   public:
   	// 插入接口(支持const引用和移动语义)
   	void push(const T& x)
   	{
   		_con.push_back(x);
   	}

   	void push(T&& x)
   	{
   		_con.push_back(std::move(x));
   	}
   	
   	// 删除接口
   	void pop()
   	{
   		_con.pop_back();
   	}
   	
   	// 返回栈顶元素(const/非const版本)
   	const T& top() const
   	{
   		return _con.back();
   	}

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

   	// 有效元素个数
   	size_t size() const
   	{
   		return _con.size();
   	}

   	// 判空
   	bool empty() const
   	{
   		return _con.empty();
   	}

   private:
   	Container _con; // 底层容器(默认deque)
   };
}
cpp 复制代码
// queue.h
#pragma once
#include<deque>

namespace yuuki
{
	// 容器适配器:默认以deque为底层容器实现queue
	template<class T, class Container = deque<T>>
	class queue
	{
	public:
		// 插入接口(支持const引用和移动语义)
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void push(T&& x)
		{
			_con.push_back(std::move(x));
		}
		
		// 删除接口
		void pop()
		{
			_con.pop_front();
		}

		// 返回队头元素(const/非const版本)
		const T& front() const
		{
			return _con.front();
		}

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

		// 返回队尾元素(const/非const版本)
		const T& back() const
		{
			return _con.back();
		}

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

		// 有效元素个数
		size_t size() const
		{
			return _con.size();
		}

		// 判空
		bool empty() const
		{
			return _con.empty();
		}

	private:
		Container _con; // 底层容器(默认deque)
	};
}

三、priority_queu的介绍和使用

3.1priority_queu的介绍

  1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
  2. 底层逻辑等价于 "堆":可随时插入元素,仅能访问 / 删除堆顶(优先队列顶部)元素;
  3. 优先队列基于容器适配器实现,封装特定容器作为底层存储,通过堆算法维护堆结构;
  4. 底层容器需支持随机访问迭代器,并提供以下接口:
  • empty():检测容器是否为空
  • size():返回容器中有效元素个数
  • front():返回容器中第一个元素的引用
  • push_back():在容器尾部插入元素
  • pop_back():删除容器尾部元素
  1. 标准容器vector/deque满足上述需求,默认使用vector作为底层容器;
  2. 容器适配器自动调用make_heap、push_heap、pop_heap算法维护堆结构。

3.2priority_queu的使用

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

函数声明 接口说明
priority_queue()/priority_queue(first,last) 构造空优先级队列 / 迭代器区间构造优先级队列
empty( ) 检测是否为空,空返回 true,否则返回 false
top( ) 返回堆顶元素(默认大堆返回最大,小堆返回最小)
push(x) 插入元素 x 并调整堆结构
pop() 删除堆顶元素并调整堆结构
注意:
  1. 默认情况下,priority_queue是大堆
cpp 复制代码
#include <vector>
#include <queue>
#include <functional> // greater算法头文件
#include <iostream>
using namespace std;

void Test()
{
	// 默认大堆:底层按小于号比较,堆顶为最大值
	vector<int> v{3,6,3,8,0,9,5,1};
	priority_queue<int> q1;
	for(auto& e : v)
	{
		q1.push(e);
	}
	cout << q1.top() << endl; // 输出:9
	
	// 小堆:指定底层容器为vector,比较规则为greater<int>
	priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
	cout << q2.top() << endl; // 输出:0
}
  1. 存储自定义类型时,需重载<(大堆)或>(小堆)运算符。
cpp 复制代码
#include <vector>
#include <queue>
#include <functional>
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	
	// 大堆需要重载<
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	
	// 小堆需要重载>
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
	
	friend ostream& operator<<(ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

void TestPriorityQueue()
{
	// 大堆:依赖<运算符重载
	priority_queue<Date> q1;
	q1.push(Date(2018, 10, 29));
	q1.push(Date(2018, 10, 28));
	q1.push(Date(2018, 10, 30));
	cout << q1.top() << endl; // 输出:2018-10-30
	
	// 小堆:依赖>运算符重载
	priority_queue<Date, vector<Date>, greater<Date>> q2;
	q2.push(Date(2018, 10, 29));
	q2.push(Date(2018, 10, 28));
	q2.push(Date(2018, 10, 30));
	cout << q2.top() << endl; // 输出:2018-10-28
}

3.3priority_queu的模拟实现

cpp 复制代码
#pragma once
#include<vector>
#include<utility> // std::swap
#include<iostream>
using namespace std;

// 大堆比较规则(默认)
template<class T>
class Less
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

// 小堆比较规则
template<class T>
class Greater
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};

namespace yuuki
{
	// 优先队列(堆)模拟实现:默认大堆,底层容器为vector
	template<class T, class Container = vector<T>, class Compare = Less<T>>
	class priority_queue
	{
	public:
		// 默认构造
		priority_queue() = default;

		// 迭代器区间构造(建堆)
		template <class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				_con.push_back(*first);
				++first;
			}
			// 从最后一个非叶子节点开始向下调整,构建堆
			for (int i = (_con.size() - 2) / 2; i >= 0; --i)
			{
				AdjustDown(i);
			}
		}

		// 向上调整:维护堆结构(插入元素后调用)
		void AdjustUp(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				// 根据比较规则调整(大堆/小堆)
				if (com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

		// 插入元素
		void push(const T& x)
		{
			_con.push_back(x);
			AdjustUp(_con.size() - 1);
		}

		// 向下调整:维护堆结构(删除堆顶后调用)
		void AdjustDown(size_t parent)
		{
			size_t child = parent * 2 + 1; // 左孩子
			while (child < _con.size())
			{
				// 找左右孩子中优先级更高的那个
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
				{
					++child;
				}

				// 父节点与优先级更高的孩子比较
				if (com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}

		// 删除堆顶元素
		void pop()
		{
			if (empty())
			{
				cout << "priority_queue is empty!" << endl;
				return;
			}
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			AdjustDown(0);
		}

		// 返回堆顶元素(const/非const版本)
		const T& top() const
		{
			return _con.front();
		}

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

		// 有效元素个数
		size_t size() const
		{
			return _con.size();
		}

		// 判空
		bool empty() const
		{
			return _con.empty();
		}

	private:
		Container _con;    // 底层容器
		Compare com;       // 比较规则对象
	};
}
相关推荐
weixin_425023002 小时前
Java开发高频实用技巧汇总(List操作/多线程/反射/监控等)
java·windows·list
草莓熊Lotso3 小时前
Python 进阶核心:字典 / 文件操作 + 上下文管理器实战指南
数据结构·c++·人工智能·经验分享·笔记·git·python
alonewolf_993 小时前
深入Spring核心原理:从Bean生命周期到AOP动态代理全解析
java·后端·spring
天远Date Lab3 小时前
Python实现用户消费潜力评估:天远个人消费能力等级API对接全攻略
java·大数据·网络·python
没有bug.的程序员10 小时前
服务安全:内部服务如何防止“裸奔”?
java·网络安全·云原生安全·服务安全·零信任架构·微服务安全·内部鉴权
一线大码11 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
YJlio11 小时前
VolumeID 学习笔记(13.10):卷序列号修改与资产标识管理实战
windows·笔记·学习
weixin_4407305011 小时前
java数组整理笔记
java·开发语言·笔记
weixin_4250230011 小时前
Spring Boot 实用核心技巧汇总:日期格式化、线程管控、MCP服务、AOP进阶等
java·spring boot·后端