【c++】stack和queue的接口使用以及底层实现

前言

在前面了解了string,vector,list的相关接口的使用以及底层实现。这期我首先会先将探明他们的优缺点进行讲解,然后介绍一下deque。由于stack与queue接口太过简单,所以我直接通过一些题目进行介绍。在最后来分析底层。

string,vector,list优缺点介绍

因为string就是vector<char>就不单独介绍了。

std::vector

底层:一段连续堆内存,容量不足时整块重新分配、拷贝迁移

优点

1、随机访问 O (1):veci、at() 直接下标访问,速度极快。

2、缓存友好(最重要):内存连续,CPU 预取生效,遍历速度远快于 list。

3、尾部增删高效:push_back()/pop_back() 均摊 O (1)。

4、内存紧凑,额外开销极小(只保存指针、大小、容量三个成员)。

5、支持迭代器加减:it + 5。

缺点

1、中间 / 头部插入、删除很慢 O (n):需要移动后面所有元素。

2、扩容代价高:到达容量上限,重新开辟更大内存,拷贝全部元素,释放旧内存。

3、删除元素不会自动释放多余内存;capacity() >= size()。

4、迭代器容易失效:扩容、中间插入删除会导致全部迭代器失效。

适用场景: 需要频繁遍历、随机访问;增删大多在尾部;数据量变化不大。

std::list

底层:双向循环链表,每个节点独立分配内存,节点存【元素 + 前驱指针 + 后继指针】

优点

1、任意位置插入 / 删除 O (1)(前提:已经拿到迭代器),只修改指针,不需要移动数据。

2、迭代器几乎不会失效:删除一个元素,仅被删元素的迭代器失效,其他迭代器保持有效。

3、没有扩容问题,按需分配节点。

缺点

1、不支持随机访问,不能 listi;查找只能顺序遍历 O (n)。

2、缓存极差:节点散落在堆上,CPU 无法预取,遍历性能远低于 vector。

3、内存开销大:每个节点额外两个指针,大量小元素时内存占用显著变高。

4、不支持迭代器算术运算(it+1可以,it+5不行)。

适用场景: 频繁在任意位置插入删除;极少随机访问;元素较大、拷贝成本极高。

deque

了解了string vector的优缺点不得不来认识一下deque。deque相当于是string和vector缝合到一起的,其即支持高效率的头插尾插,头删尾删。也支持下标访问。

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个

动态的二维数组,其底层结构如下图所示

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其"整体连续"以及随机访问

的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示

这里一眼看过去,是真复杂,但是掌握他的逻辑还是可以理顺的。

接下来我对这幅图片进行简单的解释,并不涉及其他的。

图中的map所指的数组是中控数组,在这个数组里面还有缓冲数组,缓冲数组的多少与数据量有关。从图中可以看出这里有start和finish迭代器分别指向开始的缓冲数组和最后的缓冲数组。每个迭代器中都含有cur,first,last和node指针。各自的指向看图即可理解。

优点

1、与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。

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

缺点

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

为什么选择deque作为stackqueue的底层默认容器

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

  2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。结合了deque的优点,而完美的避开了其缺陷

所以在接下来我要通过一下题目来带你们认识stack和queue。

stack

最小栈(题目)

这里可以用辅助栈来进行解决,也就是说搞两个栈,辅助栈是用来存储另一个栈中的最小数据。右边的是我做的其他笔记不用管他。

复制代码
class MinStack {
public:
    MinStack() {
        
    }
    
    void push(int value) {
        _st.push(value);
        if(_minst.empty()||value<=_minst.top())
        {
            _minst.push(value);
        }
    }
    
    void pop() {
        if(_st.top()==_minst.top())
        {
            _minst.pop();
        }
        _st.pop();
    }
    
    int top() {
        return _st.top();
    }
    
    int getMin() {
       return  _minst.top();
    }
private:
    stack<int> _st;
    stack<int> _minst;
};

栈的压入,弹出序列(题目)

复制代码
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        size_t popi=0;
        stack<int> s;
        for(auto e:pushV)
        {
            s.push(e);
            while(!s.empty()&&s.top()==popV[popi])
            {
                s.pop();
                ++popi;
            }
        }
        return s.empty();
    }

queue

二叉树的层序遍历(题目)

这里的题解在代码的注释部分有所体现

复制代码
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
//层序遍历的原理在于使用一个辅助的队列,根结点先入队,再将其数值存入tmp数组中
//再根结点出队,将其左右结点都入队,进行第二层遍历,每一次遍历的结尾标志为队列内所有已有结点都出队完成,其对应的左右结点都入队完成,每一层的遍历结果都存入tmp中,在每一层的遍历结尾将tmp的内容存入到res中去
queue<TreeNode*>que;//设置辅助队列
vector<vector<int>>res;//用来存储最终结果
if(root!=nullptr)que.push(root);//根结点先入队
while(!que.empty()){
    vector<int>tmp;//每一层遍历以前都初始化tmp
    for(int i=que.size(); i>0; i--){
//这里需注意由于在for循环的执行过程中que.size()的值会进行更改,因此que.size()只能作为i的起点而不是终点
        TreeNode* p=que.front();
        que.pop();
        tmp.push_back(p->val);//在出队列的时刻进行打印,出队列的同时左右结点都入队列
        if(p->left!=nullptr)que.push(p->left);
        if(p->right!=nullptr)que.push(p->right);
    }
    res.push_back(tmp);
}
return res;
    }
};

到这里铺垫的就差不多了,就可以进行底层的讲解

容器适配器

在介绍底层之前先了解一下这个东西。他是一种被反复使用的,多数人知晓的,经过分类编目的代码设计经验总结,该种模式是将一个类的接口转化成客户希望的另一个接口。下面的container就是代表了的容器适配器。

stack和queue实现

仔细想一下stack需要实现尾插,尾删,取头数据,数据个数,判空.在前面也有提到过deque,在这里deque用作stack和queue的容器适配器。前面也有介绍原因。

复制代码
template<class T,class container=deque<T>>
class stack
{
	void push(const T&x)
	{
		_con.push_back(x);
	}

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

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

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

	bool empty()
	{
		return _con.empty();
	}
private:
	container _con;
};

而queue功能与stack大部分一样,由于其时先进先出,所以其是头删,并且也就多了取头和取尾。

这里的取尾与satck中的top一个原理。

复制代码
void pop()
{
	_con.pop_front();
}
const T& front()
{
	return _con.front();
}

也就是将pop改一下,再加一个成员函数。

优先级队列

了解了queue又怎么能少了priority queue,它也是一个适配器底层是堆,默认大的优先级高。

简介

  1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素
    中最大的。
  2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶
    部的元素)。
  3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue
    提供一组特定的成员函数来访问其元素。元素从特定容器的"尾部"弹出,其称为优先队列的
    顶部。
  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过
    随机访问迭代器访问,并支持以下操作:
    empty():检测容器是否为空
    size():返回容器中有效元素个数
    front():返回容器中第一个元素的引用
    push_back():在容器尾部插入元素
    pop_back():删除容器尾部元素
  5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue
    类实例化指定容器类,则使用vector
  6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用
    算法函数make_heap、push_heap和pop_heap来自动完成此操作。

接下来可以使用代码简单的看一下。

在这里先看q1另一个先不管。从调试中可以看出优先级队列底层默认是以大堆,如果想要实现小堆也是可以的。这里就会用到仿函数,也就是q2中的greater<int>。

仿函数

本质是一个类,这个类重载operator()他的对象可像函数一样使用。

因为在priority_queue中放自定义类型的数据vector,并且大小堆是需要比大小才能搞出来因此用户需要在自定义类型中提供> 或者< 的重载。

复制代码
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;
	}
};

给一个简单的用例

复制代码
//因为这里的仿函数可以是任意的
template<class compare>
void Bubblesort(int* a, int n, compare con)
{
	for (int j = 0li < n; j++)
	{
		int flag = 0;
		for (int i = 1; i < n - j; i++)
		{
			if (com(a[i], a[i - 1]))
			{
				swap(a[i - 1], a[i])
					flag = 1;
			}
		}
        if (flag == 0)
	    {
		    break;
	    }

	}
	
}

在这里可以做一个题目来加强理解。

数组中第k个最大元素(题目)

这里也不需要解析,直接根据优先级队列的特性就写出来了。

复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 将数组中的元素先放入优先级队列中
        priority_queue<int> p(nums.begin(),nums.end());
        // 将优先级队列中前k-1个元素删除掉
        for(int i= 0; i < k-1; ++i)
        {
            p.pop();
        }
        return p.top();
    }
};

了解了那么多接下来开始底层实现

优先队列的实现

先将他的整体架构写出来

从上面的这么多铺垫就可以知道这些是怎么来的。

复制代码
namespace bit
{
	// 默认是大堆
	template<class T, class Container = vector<T>, class Compare = Less<T>>
	class priority_queue
	{
    public:
	


                //.............






    private:
        Container _con;
	};
}

根据优先级队列的特性就可以知道其底层要实现什么。

重中之重但是向上和向下调整,因为优先级队列底层是堆所以在插入删除是需要用到这两个调整。

复制代码
void AdjustUp(int child)
{
	Compare com;
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//if (_con[parent] < _con[child])
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void AdjustDown(int parent)
{
	// 先假设左孩子小
	size_t child = parent * 2 + 1;

	Compare com;
	while (child < _con.size())  // child >= n说明孩子不存在,调整到叶子了
	{
		// 找出小的那个孩子
		//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
		if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
		{
			++child;
		}

		//if (_con[parent] < _con[child])
		if (com(_con[parent], _con[child]))
		{
			swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

这里的逻辑在之前c堆的时候已经介绍过了在这不过多介绍。仅仅有一些符号变了,因为less<T>是大堆,所以之前的 > 和 < 要相反。

接着就是那些功能,与stack和queue实现几乎没有差别。

复制代码
	void push(const T& x)
	{
		_con.push_back(x);

		AdjustUp(_con.size() - 1);
	}

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

	const T& top()
	{
		return _con[0];
	}

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

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

那么这一期到这里也就结束了。

感谢支持!!!

相关推荐
松仔log2 小时前
Java中级——组合和继承
android·java·开发语言
(Charon)3 小时前
【C++】锁与原子操作(四):自旋锁的完整实现与性能优化
c语言·开发语言·c++
雨田言炎4 小时前
五、Qt控件使用说明大全
开发语言·qt
ShineWinsu4 小时前
对于Linux:五种IO模型以及非阻塞IO的详细解析
linux·c++·面试·io·阻塞·非阻塞·fcntl
bu_shuo5 小时前
MATLAB中的meshgrid和ndgrid
开发语言·matlab
我命由我123455 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
呜喵王阿尔萨斯5 小时前
extern “C“ in C++
c语言·c++·算法
深海呐5 小时前
仓颉语言是ArkTs的上层语言吗?就像kotlin和Java
java·开发语言·kotlin·仓颉
乐观勇敢坚强的老彭5 小时前
C++信奥静态数组和动态数组
数据结构·c++·算法