C++:stack和queue的使用以及底层实现

stack和queue的使用以及底层实现

    • 1.适配器模式
    • 2.stack的介绍和使用
      • 2.1stack的介绍
      • 2.2stack的使用
    • 3.queue的介绍和使用
      • 3.1queue的介绍
      • 3.2queue的使用
    • 4.仿函数介绍
    • 5.priority_queue的介绍和使用
      • 5.1priority_queue的介绍
      • 5.2priority_queue的使用
    • 6.deque的介绍
      • 6.1deque的实现原理
      • 6.2deque的缺陷
      • 6.3选择deque作为stack和queue的底层默认容器的原因
    • 7.模拟实现
      • 7.1stack的模拟实现
      • 7.2queue的模拟实现
      • 7.3priority_queue的模拟实现

1.适配器模式

适配器是一种设计模式(代码设计经验),该种模式是把一个类的接口转换成用户希望的另一个接口。像本文介绍的容器底层其实是用其它容器实现的,提供给用户的接口只是对其它容器接口的简单封装


2.stack的介绍和使用

2.1stack的介绍

  1. stack是栈。

  2. stack是一种容器适配器,特点是后进先出,其删除只能从容器的一端进行元素的插入与提取操作

  3. stack是一个类模板(template > class stack),stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
    (1)empty:判空操作
    (2)back:获取尾部元素操作
    (3)push_back:尾部插入元素操作
    (4)pop_back:尾部删除元素操作

  4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque(双端队列,后面再讲)。

  5. 使用要包含头文件
    C++:stack和queue的使用以及底层实现_第1张图片

2.2stack的使用

常用接口说明:

函数 说明
stack() 构造函数,构造空栈
empty() 判断栈是否为空,空返回真,否则返回假
size() 返回栈中的元素个数
top() 取到栈顶数据的引用
push(val) 把元素val压入栈中
pop() 弹出栈顶元素

练习:
最小栈

//这个题目的要点是记录最小值,但出栈后可能最小值可能变化,只用一个变量记录不够
//我们可以设计两个栈:
//(1)s栈,正常出入数据
//(2)min栈,在s栈入栈时进行判断,如果自身为空或者新入栈的元素小于等于min栈顶就入栈
//在s栈出栈时也进行判断,如果出栈的元素等于min栈顶,min也要出栈

//不过这个题目还有优化的空间,那就是[1,1,1,1,1]这样多个相同数的情况
//为避免空间浪费,可以设计一个count计数记录每个数,多次出现的数入、出栈只需要减计数
//计数归0才真正的出栈
class MinStack {
public:
    MinStack() 
    {}
    
    void push(int val) 
    {
        if(min.empty() || min.top() >= val)
        {
            min.push(val);
        }
        s.push(val);
    }
    
    void pop() 
    {
        if(s.top() == min.top())
        {
            min.pop();
        }
        s.pop();
    }
    
    int top() 
    {
        return s.top();
    }
    
    int getMin() 
    {
        return min.top();
    }
private:
    stack<int> s;
    stack<int> min;
};

栈的弹出压入序列

//这个题目最好的办法就是模拟栈的压入弹出过程
//比如pushv[1,2,3,4,5]这个序列得到popv[4,5,3,2,1]
//先入栈s,1,1 != popV.top(),继续入栈
//入栈s,2, 2 != popV.top(),继续入栈
//入栈s,3,3 != popV.top(),继续入栈
//入栈s,4, 4 == popV.top(),同时出栈,popV是一个数组,下标加1视为出栈
//出栈结束栈s.top() = 3 != popV.top() = 5,继续入栈
//入栈, 5, 5 == popV.top(),同时出栈
//出栈结束s.top() = 3 == popV.top() = 3,同时出栈
//………………………………………………………………………………
//最后pushV的所有元素都入栈,并且s栈刚好出空,说明pushV可以得到popV
//如果pushV所有元素入栈,s栈没法出空,说明pushV无法得到popV
class Solution {
public:
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
        size_t pushi = 0;
        //popi下标加1视为popV出栈
        size_t popi = 0;
        stack<int> s;

        while (pushi < pushV.size()) 
        {
            s.push(pushV[pushi++]);
            while(!s.empty() && s.top() == popV[popi])
            {
                s.pop();
                popi++;
            }
        }

        return s.empty();
    }
};

逆波兰表达式求值

//这个题目思路并不难,借助一个栈s即可
//(1)遇到数字:直接入s栈
//(2)遇到运算符,把栈中的两个左右数出栈,计算完把结果入s栈即可
//重复上面的步骤,一直到遍历完tokens即可,最后s栈顶即为结果
class Solution {
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> s;
        for(auto str : tokens)
        {
            if(!(str == "+" || str == "-" || str == "*" || str == "/"))
            {
               s.push(atoi(str.c_str()));
            }
            else
            {
                int right = s.top();  s.pop();
                int left = s.top();  s.pop();
                switch(str[0])
                {
                    case '+':
                        s.push(left + right);
                        break;
                    case '-':
                        s.push(left - right);
                        break;
                    case '*':
                        s.push(left * right);
                        break;
                    case '/':
                        s.push(left / right);
                        break;
                }
            }
        }
        return s.top();
    }
};

3.queue的介绍和使用

3.1queue的介绍

  1. queue是队列。

  2. queue是一种容器适配器,特点为先进先出,其中从容器一端插入元素,另一端提取元素。

  3. queue是一个类模板(template > class queue;),底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
    (1)empty:检测队列是否为空
    (2)size:返回队列中有效元素的个数
    (3)front:返回队头元素的引用
    (4)back:返回队尾元素的引用
    (5)push_back:在队列尾部入队列
    (6)pop_front:在队列头部出队列

  4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。

  5. 使用要包含头文件

C++:stack和queue的使用以及底层实现_第2张图片

3.2queue的使用

常用接口说明:

函数 说明
queue() 构造函数,构造一个空队列
empty() 判断队列是否为空,空返回真,否则返回假
size() 返回队列的元素个数
front() 返回队头元素的引用
back() 返回队尾元素的引用
push(val) 队尾入元素val
pop() 队头元素出队

练习:
用队列实现栈

//这个题目的思路是有两个队列实现栈(其实也可以单队列实现,这里主要不是讲题目)
//需要始终有一个队列为空,比如[1,2,3]这个顺序队列
//队列q1为1 -> 2 - > 3,q2为空,top()只需要返回不为空的队列的队尾即可
//难点在出栈,出栈的话把1 -> 2转移到另一个队列q2,剩余一个元素即为栈顶,保存了再出栈即可
class MyStack {
public:
    MyStack() {}
    
    void push(int x) {
        if(q2.empty())
            q1.push(x);
        else
            q2.push(x);
    }
    
    int pop() {
        //默认q1为空栈
        queue<int>* empty_q = &q1;
        queue<int>* non_empty_q = &q2;
        if(q2.empty())
        {
            empty_q = &q2;
            non_empty_q = &q1;
        }
        while(non_empty_q->size() > 1)
        {
            empty_q->push(non_empty_q->front());
            non_empty_q->pop();
        }
        int ret = non_empty_q->front();
        non_empty_q->pop();
        return ret;
    }
    
    int top() {
        if(q1.empty())
            return q2.back();
        else
            return q1.back();
    }
    
    bool empty() {
        return q1.empty() && q2.empty();
    }
    queue<int> q1;
    queue<int> q2;
};

4.仿函数介绍

后面需要用到,就先简单介绍一下,以后会详细的讲各种用途。

//仿函数,又称函数对象,其实就是一个类,重载了operator()
// 使得类对象可以像函数一样使用
// 相比传函数指针,传仿函数要更加灵活一些
//开一个命名空间,和库里面的区分开
namespace my_compare
{
	template<class T>
	class less
	{
	public:
		bool operator()(const T& x, const T& y)const
		{
			return x < y;
		}
	};

	template<class T>
	class  greater
	{
	public:
		bool operator()(const T& x, const T& y) 
		{
			return x > y;
		}
	};
}

5.priority_queue的介绍和使用

5.1priority_queue的介绍

  1. priority_queue(优先级队列)是堆。

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

  3. 在优先级队列中可以随时插入元素,并且只能检索最大(小)元素(优先队列中位于顶部的元素)

  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该支持随机访问(下标加[]),并支持以下操作:
    (1)empty():检测容器是否为空
    (2)size():返回容器中有效元素个数
    (3)front():返回容器中第一个元素的引用
    (4)push_back():在容器尾部插入元素
    (5)pop_back():删除容器尾部元素

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

  6. 需要支持随机访问,以便始终在内部保持堆结构。

  7. priority_queue是一个模板类,template , class Compare = less< typename Container::value_type>> class priority_queue;

  8. 使用前要包含头文件

5.2priority_queue的使用

常用接口说明:

函数 说明
priority_queue() 构造一个空的优先级队列
priority_queue(begin(), end()) 传迭代器区间初始化
empty() 判断优先级队列是否为空,空返回真,否则返回假
top() 返回优先级队列中最大(小)元素,即堆顶元素
push(val) 在优先级队列中插入元素val
pop() 删除优选级队列中最大(小)元素,即堆顶元素

注意:

  1. 默认情况下priority_queue为大堆。
#include 
#include 
#include  // greater算法的头文件
void TestPriorityQueue()
{
	 // 默认情况下,创建的是大堆,其底层按照小于号比较
	 vector<int> v{3,2,7,6,0,4,1,9,8,5};
	 priority_queue<int> q1;
	 for (auto& e : v)
	 	q1.push(e);
	 cout << q1.top() << endl;
	 // 如果要创建小堆,将第三个模板参数换成greater比较方式
	 priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
	 cout << q2.top() << endl;
}
  1. 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。

练习:

数组中的第K个最大元素

//对于取数组中前K个最大的元素(TOPK),很容易就能想到利用堆
//先把原数组构造一个堆,要求第K大的元素
//只需要pop() K - 1次即可,最后堆顶一定是第k个大的元素
//其中建堆的时间复杂度为O(N),K - 1次的删除后调整为 (K-1) * logN
//在 K比较小的情况下,时间复杂度接近于O(N)
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k)
    {
        //其实这个题目也可以利用快速排序,不过必须选择性的排序区间
        priority_queue<int> p(nums.begin(), nums.end());
        for(int i = 0; i < k - 1; i++)
        {
            p.pop();
        }
        return p.top();
    }
};

6.deque的介绍

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

6.1deque的实现原理

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

C++:stack和queue的使用以及底层实现_第3张图片

6.2deque的缺陷

  1. 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list。deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构
    (实在要使用的话包含头文件< deque >)
  2. 头插尾插入效率高,中间插入还是要移动数据的(子数组长度恒定),并且频繁的下标加[]访问有很大的计算消耗,这个时候效率远远不如vector和list。

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

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

7.模拟实现

7.1stack的模拟实现

namespace my_std
{
	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();
		}

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

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

	private:
		container _con;
	};
}

7.2queue的模拟实现

namespace my_std
{
	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& front()
		{
			return _con.front();
		}

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

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

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

	private:
		container _con;
	};
}

7.3priority_queue的模拟实现

本文主要讲的不是堆相关的算法,如果大家对建堆和调整有疑问,可以看往期的堆讲解:
堆的实现
堆实际应用和时间复杂度分析

namespace my_std
{
	//优先级队列
	template<class T, class container = vector<T>, class comparation = less<T>>
	class priority_queue
	{
	public:
		//向上调整
		void adjust_up(size_t child)
		{
			comparation com; //用于比较的仿函数
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				// _con[parent]  < _con[child]
				//默认大堆,如果孩子大,就交换和父亲的值
				//然后更新孩子和父亲的下标
				if (com(_con[parent], _con[child]))
				{
					std::swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

		void push(const T& x)
		{
			_con.push_back(x);
			adjust_up(_con.size() - 1);
		}

		//向下调整
		void adjust_down(size_t parent)
		{
			size_t child = parent * 2 + 1;
			comparation com;
			while (child < _con.size())
			{
				//_con[child] < _con[child + 1]
				//默认左孩子大,如果右孩子大就让孩子下标加1,注意右孩子不存在的情况
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
				{
					child++;
				}
				//_con[parent] < _con[child]
				//默认大堆。如果孩子大就和父亲交换,然后更新孩子下标和父亲下标
				if (com(_con[parent], _con[child]))
				{
					std::swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;	 
				}
				else
				{
					break;
				}
			}
		}

		void pop()
		{
			std::swap(_con.front(), _con.back());
			_con.pop_back();
			adjust_down(0);
		}


		//无参构造,这个必须写,不然就没有默认构造用了
		priority_queue()
		{}

		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				_con.push_back(*first);
				++first;
			}

			// 建堆,自下而上建堆
			for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
			{
				adjust_down(i);
			}
		}

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

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

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

你可能感兴趣的:(C++初阶,c++,开发语言,stl,学习方法,笔记)