【C++栈和队列:数据结构中的经典组合,高效处理先进先出与后进先出问题的最佳方案】

[本节目标]

  • 1. stack的介绍和使用

  • 2. queue的介绍和使用

  • 3. priority_queue的介绍和使用

  • 4. 容器适配器

1. stack的介绍和使用

1.1 stack的介绍

1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。

2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。

3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下 操作:

  • empty:判空操作
  • back:获取尾部元素操作
  • push_back:尾部插入元素操作
  • pop_back:尾部删除元素操作

4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用deque。

1.2 stack的使用

函数说明 接口说明
stack() 构造空的栈
empty() 检测stack是否为空
size() 返回stack中元素的个数
top() 返回栈顶元素的引用
push() 将元素val压入stack中
pop() 将stack中尾部的元素弹出

1.3 stack题目的练习

​​​​​​155. 最小栈 - 力扣(LeetCode)

我们来看看这个题目的解题思路,我们首先来看一种错误的解题方法,看看它为什么错误。

错误思路:将数组中的值依次入栈,此时定义一个变量min去记录最小值,当入栈的值比min小,更新min的值。

我们来看看这个思路为什么错误,如果栈中途没有元素出栈,那么这个思路完全正确,因为入栈的过程相当于把这个栈遍历了一遍,此时就可以找到最小值,可是当我们有元素出栈呢?我们看看上面的图片,此时已经找到最小值了3,可是随后3出栈了,但是此时的min该如何变化呢?此时找min就需要再遍历一次栈,所以这个思路是行不通的。

正确思路:建立两个栈,一个普通栈正常入数据,另外一个最小栈入栈最小值,当普通栈为空的时候,此时该值就直接入最小栈,当普通栈入栈的数据大于上次最小值入栈的值时,此时再次将上次的最小值入最小栈,当普通栈入栈的数据小于上次最小值入栈的值时,此时将这个小值入最小栈,如果要取出最小值的时候,只需要取最小栈的栈顶值即可。

我们能不能再进一步优化呢?我们上面的最小栈没必要存储那么多的元素5,当普通栈栈顶元素 > 最小栈栈顶元素的时候,此时就不需要入栈,但是普通栈栈顶元素 = 最小栈栈顶元素的时候,我们需要入最小栈。

解题代码:

class MinStack {
public:
    MinStack() {
        //这里可以不用写
        //自定义类型会去调用自己的默认构造
    }
    
    void push(int val) {
        st.push(val);
        if(minst.empty() || st.top() <= minst.top())
        {
            minst.push(val);
        }
    }
    
    void pop() {
        if(st.top() == minst.top()) minst.pop();
        st.pop();
    }
    
    int top() {
        return st.top();
    }
    
    int getMin() {
        return minst.top();
    }
    stack<int> st;
    stack<int> minst;
};

此时面试官又会提一个问题,如果入普通栈的数据全部都是3呢?按照我们上面的逻辑,此时最小栈也要全部存3,那么我们上面的优化就相当于没有优化,此时我可以通过计数器来解决,当普通栈有多个相同值时,此时最小栈入栈还可以存储一个计数器,每次入最小栈的时候++count;栈每次pop的时候只需要--count,当减到0的时候就可以删除3。

栈的压入、弹出序列_牛客题霸_牛客网 (nowcoder.com)

解题思路:题目要我们判断两个序列是否符合入栈出栈的次序,我们就可以用一个栈来模拟。对于入栈序列,我们让它入栈,只要栈为空,序列肯定要依次入栈。那什么时候出来呢?自然是遇到一个元素等于当前的出栈序列的元素,那我们就放弃入栈,让它先出来。

💡强调一下:我们这里不是拿的入栈序列和出栈序列依次比较,而是构建一个辅助栈,让的入栈序列再次入栈,然后再比较的!!!因为我们是想判断出栈顺序是否匹配,且中途有元素出栈,所以需要入栈序列入栈来模拟出栈过程。

具体做法:

  • step 1:准备一个辅助栈st,两个下标curpush和curpop分别访问两个序列。
  • step 2:辅助栈st为空或者栈顶不等于出栈数组当前元素,就持续将入栈数组加入栈中。
  • step 3:辅助栈st不为空并且栈顶等于出栈数组当前元素,就出栈。
  • step 4:当入栈数组访问完,出栈数组无法依次弹出,就是不匹配的,否则两个序列都访问完就是匹配的。
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
    stack<int> st;
    int curpush = 0;
    int curpop = 0;
    while(curpush < pushV.size())
    {
        //栈为空或者栈顶元素不等于出栈数组当前元素,直接入栈
        st.push(pushV[curpush++]);
        //这里需要写成while,因为可能会有多次匹配
        //当栈为空时,就不需要再出栈了
        while(!st.empty() && st.top() == popV[curpop])
        {
            st.pop();
            ++curpop;
        }
    }  
    //根据出栈序列的下标即可判断是否匹配
    return curpop == popV.size();
}

150. 逆波兰表达式求值 - 力扣(LeetCode)

💡逆波兰式是一种数学表达式的表示方法,也称为后缀表达式。与传统的中缀表达式不同,逆波兰式将操作符置于操作数的后面,从而无需使用括号来指定运算顺序。

💡在逆波兰式中,每个运算符都紧跟在其相关的操作数之后。例如,中缀表达式 " (2 + 1) * 3" 在逆波兰式中表示为 "2 1 + 3 * "。计算逆波兰式时,从左到右扫描表达式,遇到操作数就将其压入栈中,遇到操作符就从栈中弹出相应数量的操作数进行运算,并将结果压回栈中。最终,栈中的唯一元素即为整个表达式的结果。

💡逆波兰式的优势在于不需要考虑运算符优先级和括号的使用,使得计算机能够更容易地处理和解析数学表达式。这种表示方法在计算器和编程语言中得到广泛应用。

解题思路:

使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:

  • step 1:如果遇到操作数,则将操作数入栈;
  • step 2:如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。
  • step 3:整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。

代码实现:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        int right = 0;
        int left = 0;
        for(auto& e : tokens)
        {
            if(e == "+" || e == "-" || e == "*" || e == "/")
            {
                right = st.top();
                st.pop();
                left = st.top();
                st.pop();
                switch(e[0])//返回的char类型
                {
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right);
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        //题目明确不发生为除数为0清空
                        st.push(left / right);
                        break;
                }
            }
            else
            {
                //转为数字
                st.push(stoi(e));
            }
        } 
    return st.top();
    }
};

这道题目只要清除栈的规则很容易做出来,因为题目已经把后缀表达式给我们了,导致题目非常容易,如果这道题给我们的是中缀表达式呢?我们就需要先转为后缀表达式,然后再按照上述步骤进行计算,转话的步骤也需要栈来完成。

  • step 1:如果遇到操作数,则将操作数输出;
  • step 2:如果遇到运算符,栈为空,操作符入栈;当前操作符比栈顶的操作符优先级高,操作符也入栈。
  • step 3:如果遇到运算符,操作符比栈顶的操作符优先级低或者相等,代表栈定的操作符可以运算了,出栈顶操作符

可是带括号呢???

  • step 1:如果遇到操作数,则将操作数输出;
  • step 2:如果遇到运算符,栈为空,操作符入栈;当前操作符比栈顶的操作符优先级高,操作符也入栈。
  • step 3:如果遇到运算符,操作符比栈顶的操作符优先级低或者相等,代表栈定的操作符可以运算了,出栈顶操作符
  • step4:如果遇到运算符是括号,找到左括号和右括号的位置,然后让这段区间重复上面的step1- step3过程,也就是递归,如果当前元素是左括号,则递归处理括号内的子表达式,将子表达式的后缀形式输出到后缀表达式。如果当前元素是右括号,则返回上一级递归。

这里就不实现了,仅仅为本题的扩展。

1.4 stack的模拟实现

我们先来看一下我们传统的栈的写法。

namespace yu
{
	template<class T>
	class stack
	{
	public:
		push(const T& x)
		{
			//...
		}
	private:
		T* _a;
		size_t _top;
		size_t _capacity;
	};
}

但是从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack。

namespace yu
{
	//新增一个模板参数
	template<class T, class Contanier>
	class stack
	{
	public:
        //元素入栈
		void push(const T& x)
		{
			_con.push_back(x);
		}
        //元素出栈
		void pop() 
		{ 
			_con.pop_back(); 
		}
        //普通栈 返回栈顶元素
		T& top() 
		{ 
			return _con.back(); 
		}
        //const栈 返回栈顶元素
		const T& top()const 
		{ 
			return _con.back(); 
		}
        //返回栈内元素的个数
		size_t size()const 
		{ 
			return _con.size();
		}
        //判断栈是否为空
		bool empty()const 
		{ 
			return _con.empty();
		}
	private:
		Contaner _con;
	};
}

我们上面stack的模拟实现新增了一个模板,它是一个容器,容器这个概念对于我们来说并不陌生,我们之前就已经学过vector和list容器了,那是不是意味着这里需要传容器呢?比如vector<int>,然后里面_con的类型就是vector<int>,里面的尾插,尾删等其他函数就会调用vector的尾插,尾删等其他函数,这样就轻松的实现了我们的stack,我们来测试一下。

int main()
{
	yu::stack<int, vector<int>> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);

	while (!st.empty())
	{
		cout << st.top() << " "; 
		st.pop();
	}
	cout << endl;
	return 0;
}

运行结果:

我们发现使用将vector容器传入就可以直接模实现拟栈,并且也能符合栈的先进后出的特点。如果我们传入的容器是list呢?

int main()
{
	yu::stack<int, list<int>> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);

	while (!st.empty())
	{
		cout << st.top() << " "; 
		st.pop();
	}
	cout << endl;
	return 0;
}

 运行结果:

我们发现此时也能实现栈,并且也能符合出栈的先进后出的特点。并且我们上面的stack还不用写构造函数,因为_con是自定义类型,如果我们没显示的写构造函数,自定义类型会去调用它自己的构造函数去初始化,比如传入的容器时vector,那么就会去调用vector的构造函数。不过需要注意一下,如果我们传入的容器没有尾插、尾删等函数,那么模拟实现的栈就会报错啦!!!但是我们发现库里面的stack的模拟实现容器给是缺省值,但缺省值不是vector<int>,也不是我们的list<int>,而是deque<int>。

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。deque它包含了vector和list的功能,同时还综合了vector和list的优点,我们后面的会介绍,这里我们先来使用它。

#include "stack.h"
#include <deque>
#include <stack>

int main()
{
	//stack<int, deque<int>> st;
	//这里也可以不用传入deque<int>
	//因为库里面实现是deque<int>作为缺省值
	//但是我们自己实现的需要带上
    //如果我们也不想带呢?用下面的语句
    //template<class T, class Container = deque<T>>
	yu::stack<int, deque<int>> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);

	while (!st.empty())
	{
		cout << st.top() << " "; 
		st.pop();
	}
	cout << endl;
	return 0;
}

 运行结果:

2. queue的介绍和使用

2.1 queue的介绍

1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端 提取元素。

2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的 成员函数来访问其元素。元素从队尾入队列,从队头出队列。

3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操 作:

  • empty:检测队列是否为空
  • size:返回队列中有效元素的个数
  • front:返回队头元素的引用
  • back:返回队尾元素的引用
  • push_back:在队列尾部入队列
  • pop_front:在队列头部出队列

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

2.2 queue的使用

函数声明 接口说明
queue() 构造空的队列
empty() 检测队列是否为空,是返回true,否则返回false
size() 返回队列中有效元素的个数
front() 返回队头元素的引用
back() 返回队尾元素的引用
push() 在队尾将元素val入队列
pop() 将队头元素出队列

2.3 queue题目的练习

225. 用队列实现栈 - 力扣(LeetCode)

102. 二叉树的层序遍历 - 力扣(LeetCode)

这个题目初看很简单,但是题目的要求逐层遍历所有结点,即在输出的时候我们必须要知道那个数字是那一个层的。

2.4 queue的模拟实现

有了之前的stack,我们这里实现queue就非常简单.


namespace yu
{
	template<class T,class Container = deque<T>>
	class queue
	{
	public:
		//元素入队列
		void push(const T& x)
		{
			_con.push_back(x);
		}
		//元素出队列
		void pop()
		{
			_con.pop_front();
		}
		//取队头元素
		const T& front()
		{
			return _con.front();
		}
		//取队尾数据
		const T& back()
		{
			return _con.back();
		}
		//返回队列内元素的个数
		size_t size()const
		{
			return _con.size();
		}
		//判断队列是否为空
		bool empty()const
		{
			return _con.empty();
		}
	private:
		Contaner _con;
	};
}

然后我们来测试一下

int main()
{
	yu::queue<int, deque<int>> q;
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);

	while (!q.empty())
	{
		cout << q.front() << " ";
		q.pop();
	}
	cout << endl;
	return 0;
}

运行结果:

同样的我们这里给上list容器都能是实现队列的先进进出的特点,但是vector却不行,因为vector没有pop_front接口,所以会报错,但是我们这里可以使用erase接口,但是由于队列是出队头数据,而vector的erase头删效率比较低,需要挪动数据,故一般很少传入vector容器去适配。

3. priority_queue的介绍和使用

3.1 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来自动完成此操作。

3.2 priority_queue的使用

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

默认情况下,priority_queue是大堆。

#include <iostream>
#include <vector>
#include <queue>
#include <functional> // greater算法的头文件

using namespace std;

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);
	while (!q1.empty())
	{
		cout << q1.top() << " ";//建大堆,每次取堆顶数据,降序
		q1.pop();
	}
	cout << endl;
	// 如果要创建小堆,将第三个模板参数换成greater比较方式
	priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
	while (!q2.empty())
	{
		cout << q2.top() << " ";//建小堆,每次取堆顶数据,升序
		q2.pop();
	}
}

int main()
{
	TestPriorityQueue();
	return 0;
}

运行结果:

3.3 priority_queue题目的练习

215. 数组中的第K个最大元素 - 力扣(LeetCode)

思路一:利用算法库的sort进行降序排序,然后再通过下标[]直接返回元素。

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        sort(nums.begin(),nums.end(),greater<int>());
        return nums[k-1];
    }
};

这里要提一点,之前的priority_queue我们想建小堆传入的参数是priority_queue<int, vector<int>, greater<int>> q2;而这里sort排序却是sort(nums.begin(),nums.end(),greater<int>());sort这里还多一个(),为什么呢?

我们上面的程序虽然能运行,但是不符合题目要求,我们的一个排序时间复杂度就是O(N*logN)。

思路二:通过建大堆 + pop掉前k个数据,此时堆顶的数据就是第K个大的数据

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int> pq(nums.begin(),nums.end());//大堆
        while(--k)//循环k-1次
        {
            pq.pop();//把前k-1最大的数据pop掉
        }
        return pq.top();
    }
};
  1. 构建堆: 使用 priority_queue 构建堆的时间复杂度为 O(n),其中 n 是数组 nums 的长度。

  2. 循环 k-1 次的 pop 操作: 这一部分的时间复杂度为 O(k * log(n)),因为每次 pop 操作都涉及到对堆的调整,而每次调整的时间复杂度是堆的高度,即 log(n)。

总的时间复杂度为 O(n + k * log(n))。

需要注意的是,当 k 远小于 n 时,时间复杂度为 O(n),即构建堆的时间。当 k 较大时,可能需要进行多次的 pop 操作,对应的时间复杂度为 O(k * log(n)),此时就接近O(n * log(n))。

思路三:如果N和K非常大,利用TopK解决问题,前k个数据建小堆,当有数据大于堆顶,然后pop堆顶数据,大于的那个数据入堆,最后走完堆顶的数就是第K个大的数。

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        //左闭右开,前k个数据建小堆,包括第k个数据
        priority_queue<int, vector<int>, greater<int>> pq(nums.begin(),nums.begin() + k);
        
        for(int i = k;i < nums.size(); ++i)
        {
            if(nums[i] > pq.top())
            {
                pq.pop();
                pq.push(nums[i]);
            }
        }
        return pq.top();
    }
};
  1. 构建小顶堆: 使用小顶堆的构建时间复杂度为 O(k),其中 k 是输入参数。

  2. 遍历数组: 从第 k 个元素开始,遍历数组,并在小顶堆中维护前 k 个最大的元素。这一部分的时间复杂度为 O((n - k) * log(k)),其中 n 是数组 nums 的长度。

总的时间复杂度为 O(k + (n - k) * log(k))。此时就能完美接近O(n)。当k远小于n,此时时间复杂度就是O(n),当k很大,n - k此时也就可以忽略不计,此时时间复杂度就是O(logk)。但是呢?如果k是n的一半,此时效率也就不太行。

3.4 priority_queue的模拟实现

通过对priority_queue的底层结构就是堆,因此此处只需对对进行通用的封装即可。

这里容器给的缺省值是vector<int>,并没有给我们的deque<int>,说明deque<int>也是有缺点的,至于缺点是啥,我们后面会提到。这里priority默认实现的是大堆,因此我们也先来实现一下大堆。

我们这里的堆插入逻辑是尾插,然后再向上调整,注意这里不能头擦向下调整,因为头插后父子之间的关系就不对了,并且此时不满足向下调整的前提:左右子树是堆。

堆的删除逻辑是

  1. 将堆顶元素与堆中最后一个元素进行交换。
  2. 删除堆中最后一个元素。
  3. 将堆顶元素向下调整到满足堆特性为止。
  4. 向下调整的结束条件是child等于叶子结点。

namespace yu
{
	template<class T,class Container = vector<T>>
	class priority_queue
	{
	public:
		void adjust_up(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_con[parent] < _con[child])
				{
					swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void adjust_down(int parent)
		{
			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() && _con[child] < _con[child + 1])
				{
					child = child + 1;
				}
				if (_con[parent] < _con[child])
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		void push(const T& x)
		{
			_con.push_back(x);

			adjust_up(_con.size() - 1);
		}
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}
		const T& top()
		{
			return _con[0];
		}
		//返回堆内元素的个数
		size_t size()const
		{
			return _con.size();
		}
		//判断堆是否为空
		bool empty()const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

我们来测试一下:

int main()
{
	yu::priority_queue<int> q;
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);

	while (!q.empty())
	{
		cout << q.top() << " ";
		q.pop();
	}
	cout << endl;
	return 0;
}

运行结果:

如果是小堆呢,我们就要修改上面的代码。

namespace yu
{
	template<class T,class Container = vector<T>>
	class priority_queue
	{
	public:
		void adjust_up(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_con[parent] > _con[child])
				{
					swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void adjust_down(int parent)
		{
			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() && _con[child] > _con[child + 1])
				{
					child = child + 1;
				}
				if (_con[parent] > _con[child])
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		void push(const T& x)
		{
			_con.push_back(x);

			adjust_up(_con.size() - 1);
		}
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}
		const T& top()
		{
			return _con[0];
		}
		//返回堆内元素的个数
		size_t size()const
		{
			return _con.size();
		}
		//判断堆是否为空
		bool empty()const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

运行结果:

注意:我们这里不能使用list去适配,因为list不支持[ ]去访问元素,而我们堆需要大量的[ ]去访问元素!我们上面通过修改堆里面的代码去实现大小堆的切换,但是这样的做法很不好,有的使用者如果不清楚堆的逻辑,就不知道怎么去修改,我们应该提供一个方法传递大于就是大堆,小于就是小堆,能完成大小堆的切换,而不是通过去修改堆的代码,c语言一般通过函数指针 + 回调函数去解决,但是比较复杂,C++就通过仿函数/函数对象解决。

//仿函数/函数对象
namespace yu
{
	class less
	{
	public:
		bool operator()(int x, int y)
		{
			return x < y;
		}
	};
}
int main()
{
	yu::less lessFunc;
	cout << lessFunc.operator()(1, 2) << endl;//运算符重载
	cout << lessFunc(2, 1) << endl;//有点像函数调用 - 仿函数
	//lessFunc像函数名 - 实际上是一个对象 - 函数对象
	return 0;
}

运行结果:

然后我们再来看一下函数指针的缺陷

bool lessfunc(int x, int y)
{
	return x < y;
}
bool greaterfunc(int x, int y)
{
	return x > y;
}
// A这个类要回调lessfunc
//构成函数的函数指针是一个对象,不能在类模板的参数传递
//函数指针只能通过函数传递,光有类型还不够,我们要找到指针指向的函数
class A
{
public:
	A(bool(*pf)(int, int))
		:_pf(pf)
	{}

	void func(int x, int y)
	{
		cout << _pf(x, y) << endl;;
	}
private:
	bool(*_pf)(int, int);//类型为bool(*)(int,int),变量名为pf
};


int main()
{
	yu::less lessFunc;
	cout << lessFunc.operator()(1, 2) << endl;//运算符重载
	cout << lessFunc(2, 1) << endl;//有点像函数调用 - 仿函数
	//lessFunc像函数名 - 实际上是一个对象 - 函数对象

	//函数指针
	A aa1(lessfunc);
	aa1.func(1, 2);
	A aa2(greaterfunc);
	aa2.func(1, 2);
	return 0;
}

函数指针的类型比较复杂,并且函数指针只能通过函数参数传递,比较复杂,我们来看一下仿函数实现回调呢?

//仿函数/函数对象
namespace yu
{
	class less
	{
	public:
		bool operator()(int x, int y)
		{
			return x < y;
		}
	};
}
//仿函数/函数对象
namespace yu
{
	class greater
	{
	public:
		bool operator()(int x, int y)
		{
			return x > y;
		}
	};
}

template<class T, class Compare>
class A
{
public:
	void func(const T& x, const T& y)
	{
		Compare com;
		cout << com(x, y) << endl;;
	}
};

int main()
{
	A<int, yu::less> aa1;
	aa1.func(1, 2);
	A<int, yu::greater> aa2;
	aa2.func(1, 2);
	return 0;
}

这样通过一个仿函数就也实现了类似函数指针的功能,并且实现更简单。所以我们现在也要实现一个仿函数,能让我们的堆结构实现大小堆的切换。

namespace yu
{
	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 T,class Container = vector<T>,class Compare = yu::less<T>>
	class priority_queue
	{
	public:
		void adjust_up(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				//这里的less是默认建大堆 - 使用小于
				//if (_con[parent] < _con[child])
				//com是less类的对象,可以调用内部成员函数
				if (com(_con[parent],_con[child]))
				{
					swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void adjust_down(int parent)
		{
			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
				{
					child = child + 1;
				}
				//if (_con[parent] < _con[child])
				if (com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
		void push(const T& x)
		{
			_con.push_back(x);

			adjust_up(_con.size() - 1);
		}
		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}
		const T& top()
		{
			return _con[0];
		}
		//返回堆内元素的个数
		size_t size()const
		{
			return _con.size();
		}
		//判断堆是否为空
		bool empty()const
		{
			return _con.empty();
		}
	private:
		Container _con;
		Compare _com;//实现比较
	};
}

我们来测试一下:

int main()
{
	yu::priority_queue<int,vector<int>, yu::less<int>> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	q1.push(5);

	while (!q1.empty())
	{
		cout << q1.top() << " ";
		q1.pop();
	}
	cout << endl;

	yu::priority_queue<int, vector<int>, yu::greater<int>> q;
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);

	while (!q.empty())
	{
		cout << q.top() << " ";
		q.pop();
	}
	cout << endl;

	return 0;
}

运行结果:

我们再来看一下这张图

priority_queue是只能传递仿函数,如果也传入函数指针类型的话,它就找不到那个要回调的函数,而sort里面的不仅可以传入仿函数对象,还可以传入函数指针,不仅能推导出函数指针的类型,同时还拿到了指向要回调函数的指针。

4. 容器适配器

4.1 什么是适配器

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

💡适配器的本质就是封装复用!!!

4.2 STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:

为什么stack和queue都使用我们的deque去做适配器,而priority_queue却选择我们的verctor去做适配器,这里我们就需要了解一下deque容器。

4.3 deque的简单介绍(了解)

我们首先来看一下vector和list的优缺点

而deque的功能既包括了vector和list,同时还具有vector和list的优点,两者兼得!!!

4.3.1 deque的原理介绍

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

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

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

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

那deque如何尾插和头插呢?如果cur不等于last,就可以直接插入数据,如果cur等于last,就表示buff已经满了,此时就需要新开一个buff,让finish指向新开的buff,通过node找到这个新开的位置,直接插入即可。如果是头插呢?也是新开一个buff,然后修改start中的node指针指向中控位置,再让cur指向插入的数据,first指向新开buff的开始,last指向新开buff的结束。

那如何判断第一个buff是不是从头开始的呢?如果是从头开始,那么cur一定是和first是相等的。我们再来看一下迭代器如何遍历。cur里面存入的是数据,在一个buff中,我们只需要让cur如果不等于last,就可以遍历到这一个buff里面的全部数据,当这个buff遍历完,让node++,找到下一个buff继续遍历数据。我们开一下源码里面的迭代器++。

4.3.2 deque的缺陷

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

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

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

结论:

  • 1.deque下标随机访问,效率不错,但是和vector仍有差距。
  • 2.deque中间插入删除,效率较差。
  • 使用场景:
  • 大量头插头删尾插尾删:deque
  • 大量下标访问元素:vector
  • 大量中间插入:list

4.4 为什么选择deque作为stack和queue的底层默认容器

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

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

结合了deque的优点,而完美的避开了其缺陷。

​​​​​​​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/368077.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c语言--求第n个斐波那契数列(递归、迭代)

目录 一、概念二、用迭代求第n个斐波那契数1.分析2.完整代码3.运行结果4.如果求第50个斐波那契数呢&#xff1f;看看会怎么样。4.1运行结果&#xff1a;4.2画图解释 三、用迭代的方式求第n个斐波那契数列1.分析2.完整代码3.运行结果4.求第50个斐波那契数4.1运行结果4.2运行结果…

2024/2/3 牛客寒假算法基础集训营1

目录 why买外卖 G-why买外卖_2024牛客寒假算法基础集训营1 (nowcoder.com) 要有光 L-要有光_2024牛客寒假算法基础集训营1 (nowcoder.com) why买外卖 G-why买外卖_2024牛客寒假算法基础集训营1 (nowcoder.com) 题目要求&#xff1a;这道题要求计算鸡排最贵为多少 思路&a…

C语言经典面试题——翻转单词顺序VS左旋转字符串

目录 1. 翻转单词顺序1.1 题目描述1.2 解法1.3 完整代码 2. 左旋转字符串2.1 题目描述2.1.1 解法一&#xff1a;2.1.2 解法二&#xff1a;2.1.2.1 strcpy2.1.2.2 strcat2.1.2.3 完整代码 2.1.3 解法三&#xff1a; 1. 翻转单词顺序 1.1 题目描述 输入一个英文句子&#xff0c;…

AI专题:2023年AI和标准化网络安全报告

今天分享的是AI系列深度研究报告&#xff1a;《AI专题&#xff1a;2023年AI和标准化网络安全报告》。 &#xff08;报告出品方&#xff1a;enisa&#xff09; 报告共计&#xff1a;37页 文件目的和目标 本文件的总体目标是概述与人工智能(AI)网络安全有关的标准(现有的、正在…

作业2024/2/3

第二章 引用内联重载 一&#xff0e;选择题 1、适宜采用inline定义函数情况是&#xff08;C&#xff09; A. 函数体含有循环语句 B. 函数体含有递归语句 C. 函数代码少、频繁调用 D. 函数代码多、不常调用 2、假定一个函数为A(int i4, int j0) {;}, 则执行“A (1);”语句…

三路快排解决TopK问题

前言&#xff1a; 我们首先要明白什么是三路快排&#xff0c;什么是topk问题。 三路快排&#xff1a; 思想&#xff1a; 三路快排就是数组分3块&#xff0c;三个指针&#xff0c;先随机取一个基准值key&#xff0c;然后将数组划分为3个部分&#xff1a; 【小于key】【等于…

【八大排序】冒泡排序 | 快速排序 + 图文详解!!

&#x1f4f7; 江池俊&#xff1a; 个人主页 &#x1f525;个人专栏&#xff1a; ✅数据结构冒险记 ✅C语言进阶之路 &#x1f305; 有航道的人&#xff0c;再渺小也不会迷途。 文章目录 交换排序一、冒泡排序1.1 算法步骤 动图演示1.2 冒泡排序的效率分析1.3 代码实现1.4 …

【Vue】组件间通信的7种方法(全)

目录 组件之前的通信方法 1. props/$emit 2.parent/children 3.ref 4.v-model 5.sync 6.attrs,attrs,attrs,listeners 7.provide/inject 7.eventBus 组件之前的通信方法 1. props/$emit 父传子 props 这个只能够接收父组件传来的数据 不能进行修改 可以静态传递 也可…

【计算机视觉】目标检测 |滑动窗口算法、YOLO、RCNN系列算法

一、概述 首先通过前面对计算机视觉领域中的卷积神经网络进行了解和学习&#xff0c;我们知道&#xff0c;可以通过卷积神经网络对图像进行分类。 如果还想继续深入&#xff0c;会涉及到目标定位(object location)的问题。在图像分类的基础上(Image classification)的基础上…

Maven快速入门——基础篇

本篇对Maven基础进行总结&#xff0c;主要对Maven的定义、作用、Maven坐标、依赖管理、依赖配置、依赖传递特性以及Maven的生命周期进行总结&#xff0c;后面会对springboot以及Maven高级进行总结。 文章目录 目录 一、Maven是什么&#xff1f; 二、Maven的作用&#xff1a; 三…

基于YOLOv8深度学习的水稻叶片病害智能诊断系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

openGauss学习笔记-213 openGauss 性能调优-总体调优思路

文章目录 openGauss学习笔记-213 openGauss 性能调优-总体调优思路213.1 调优思路概述213.2 调优流程 openGauss学习笔记-213 openGauss 性能调优-总体调优思路 213.1 调优思路概述 openGauss的总体性能调优思路为性能瓶颈点分析、关键参数调整以及SQL调优。在调优过程中&…

python使用两个栈实现队列

这里主要是使用两个栈来实现一个队列,并实现队列的入队和出队函数。 对于一个单词hello,如果正常情况下按照队列中先进先出的特点,会按照hello的顺序入队,同样也会按照hello的顺序出队。 添加图片注释,不超过 140 字(可选) 因此如果想要利用两个栈来形成队列,就要将后…

基于SpringBoot+Vue的高校在线答疑管理系统

末尾获取源码作者介绍&#xff1a;大家好&#xff0c;我是墨韵&#xff0c;本人4年开发经验&#xff0c;专注定制项目开发 更多项目&#xff1a;CSDN主页YAML墨韵 学如逆水行舟&#xff0c;不进则退。学习如赶路&#xff0c;不能慢一步。 目录 一、项目简介 二、开发技术与环…

前端学习笔记 | 响应式网页+Boostrap

一、响应式网页 一套代码适应多端 1、媒体查询media(条件){css} max-width 小于等于max-width生效min-width 【案例】左侧隐藏 因为CSS的层叠性&#xff0c;书写顺序&#xff1a;max-width从大到小&#xff1b;min-width从小到大。 【媒体查询完整写法】 在html中link用于不同…

JSP和JSTL板块:第二节 JSP的指令和动作 来自【汤米尼克的JAVAEE全套教程专栏】

JSP和JSTL板块&#xff1a;第二节 JSP的指令和动作 一、page指令&#xff1a;页面设置&#xff08;1&#xff09;导入包&#xff1a;import属性&#xff08;2&#xff09;设定字符集&#xff1a;pageEncoding属性&#xff08;3&#xff09;设定错误页面&#xff1a;errorPage/i…

Docker上安装配置tomcat

目录 1. 拉取镜像 2. 创建运行镜像 3. 查看是否创建成功 ps&#xff1a;如果出现404错误 tomcat目录结构 1. 拉取镜像 这里使用 tomcat:8.5.40 版本作为安装 docker pull tomcat:8.5.40 2. 创建运行镜像 docker run -d --name tomcat -p 8080:8080 \--privilegedtrue …

day07-CSS高级

01-定位 作用&#xff1a;灵活的改变盒子在网页中的位置 实现&#xff1a; 1.定位模式&#xff1a;position 2.边偏移&#xff1a;设置盒子的位置 left right top bottom 相对定位 position: relative 特点&#xff1a; 不脱标&#xff0c;占用自己原来位置 显示模…

题目:有1,2,3,4共四个数字,能组成多少个不相同而且无重复数字的三位数有多少个,都是多少?lua

这是作者的思路&#xff0c; 创建三个表&#xff0c; 第一个数是从四个数遍历&#xff0c; 第二个是数剔除第一个数进行遍历 第三个是剔除第一第二个数遍历 脚本如下 local a{1,2, 3, 4} local b{} local c{} local d{} local function copy(tbl) local ctbl{} for k,v in…

【JS】基于node-media-server搭建流媒体服务器示例

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍基于node-media-server搭建流媒体服务器示例。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&…