重生之C++学习:stack and queue

在我们完成基础的几个数据结构,顺序表,链表后,STL容器里面也有栈,队列,还有一个新的容器deque,与栈,队列容器的实现有关。那么我们开始吧

目录

栈的实现​编辑

队列的实现

浅谈deque

deque迭代器简图

 deque与vector和list的区别

deque的应用场景

优先级队列priority_queue

优先级队列的实现

仿函数 


栈的实现

这里我们看到一个词叫做“适配器容器”,那适配器容器是什么呢?按照我的理解,适配器容器就是一个类里面可以放置多种容器作为底层实现,就是适配器容器是一个大框架,底层实现就是跟vector,list,map等等的容器

 我们来看看cpp文档

适配器容器本质就是泛型编程思想,通过一个模版变量Container,来套入多种容器,在通过容器,调用容器的成员函数,适配则是可以满足多种容器。但是对于栈这个容器适配器来说,所适配的底层容器要能够满足“先入后出”的特性。

  如果还听的云里雾里,我们在之前数据结构的学习中,也知道栈实际上不是自己重新实现的一个容器,本质上是可以通过数组,链表,那么栈的底层就是vector list之类的,所以栈可以成为适配器容器也不奇怪了

template<class T,class Container = deque<T>>	// 可以通过模版参数Container控制底层容器,泛型思想

class my_stack {	// 直接可以对应多个容器类型的栈,叫做容器适配器

public:

	void push(const T& val) {    // 通过容器来调用底层的push_back

		_con.push_back(val);
	}
	void pop() {

		_con.pop_back();
	}
	const T& top() {

		return _con.back();	// 栈顶就是最上面的数据
	}
	bool empty() {

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

		return _con.size();
	}
     
    void swap(Container& con){
        
        _con.swap(con);
    }


private:
	Container _con;    // 定义容器来提供成员函数
};

那么我们测试一下my_stack,也可以用list来实现,这里我们就暂时vector来实现

void test_stack_Container() {	// 测试容器适配器

	my_stack<int>  s1;
	my_stack<int, vector<int>> s2;
	cout << "before push ifEmpty: " << s2.empty() << endl;
	s2.push(1);
	s2.push(2);
	cout << "top: " << s2.top() << endl;
	cout << "size: " << s2.size() << endl;
	cout << "after push ifEmpty: " << s2.empty() << endl;
	s2.pop();
	s2.pop();
	cout << "after pop ifEmpty: " << s2.empty() << endl;

}

队列的实现

队列的实现跟栈的实现类似,不过需要注意的是,队列是先进先出,也就是尾插,头删,但是我们在cpp文档中可以知道,vector不支持头删,所以这时候就体现出了,适配器的适配性

template<class T,class Container = deque<T>>	// 可以通过模版参数Container控制底层容器,泛型思想

class my_queue {	// 直接可以对应多个容器类型的栈,叫做容器适配器

public:

	void push(const T& val) {    // 通过容器来调用底层的push_back

		_con.push_back(val);
	}
	void pop() {

		_con.pop_front();
	}
	const T& top() {

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

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

		return _con.size();
	}
     
    void swap(Container& con){
        
        _con.swap(con);
    }


private:
	Container _con;    // 定义容器来提供成员函数
};

 这里也回答了我们的一个疑惑,为什么我们在之前vector和list的实现是,我们需要准备一个front和back来返回头值或者尾值,这时候就是给容器适配器使用的

浅谈deque

deque是一种双端队列,可以在队列的两端进行插入和删除操作。deque的名称来自于“double-ended queue”的缩写。

并且deque具有链表和顺序标的功能,能够进行下标访问

这个是deque的简易示意图,通过放置指针的中控数组,然后中控数组中的每一个下标对应一个放置着数据的buffer数组,这个数组可以恒定长短也可以变长。如图这里对应着定长buffer,在满数据时,头插会新开一个buffer,进行尾插。满数据后进行尾插,则会变为新buffer的头插。这时候有的人会想为什么不直接扩容,而是选择在开辟一个数组呢,这样不会看起来麻烦吗?实际上扩容需要创造一个更大的数组tmp然后将原有的数据拷贝构造,当数据量多的时候这样子,效率过低!

deque迭代器简图

 具体实现我们在未来再解决

 deque与vector和list的区别
  1. 内部实现:vector和deque都是通过数组实现的,而list则是通过双向链表实现的。
  2. 内存分配:vector和deque都是在内存中连续分配空间,而list则是动态分配空间。
  3. 随机访问效率:vector支持随机访问,时间复杂度为O(1),而deque也支持随机访问,但是由于其内部实现的原因,访问效率比vector略低。list不支持随机访问,只能通过遍历来访问元素,时间复杂度为O(n)。
  4. 插入和删除效率:vector在尾部插入和删除元素的效率较高,但在其他位置插入和删除元素的效率较低;deque在两端插入和删除元素的效率较高,但在其他位置插入和删除元素的效率较低;list在任意位置插入和删除元素的效率都很高。
  5. 空间占用:vector和deque在内存中连续分配空间,因此它们的空间占用比list更紧凑。

那么我们通过随机数大样本,插入deque,list,vector然后再对他排序,通过clock()函数在排序前后计时,计算时间差,来表示这三个组件的效率。另外为了更加深刻的对比,deque和vector,我们将deque拷贝在vector中排序,再转回deque来探讨它的效率 

效率的代码实现:

void test_efficiency()
{
    srand(time(0));
    const int N = 1000000;

    deque<int> dq1;
    deque<int> dq2;
    vector<int> v1;
    list<int> lt;

    for (int i = 0; i < N; ++i)
    {
        auto e = rand() + i;    // 插入随机数
        dq1.push_back(e);
        dq2.push_back(e);
        v1.push_back(e);
        lt.push_back(e);
    }

    
    // deque的效率
    int begin1 = clock();
    sort(dq1.begin(), dq1.end());
    int end1 = clock();
    printf("deque sort:%d\n", end1 - begin1);


    // deque转vector通过vector的排序再转为deque
    vector<int> v(dq2.begin(), dq2.end());
    int begin2 = clock();
    sort(v.begin(), v.end());
    dq2.assign(v.begin(), v.end());
    int end2 = clock();

    printf("deque copy vector sort, copy back deque:%d\n", end2 - begin2);
   
    
    // vector的效率
    int begin3 = clock();
    sort(v1.begin(), v1.end());
    int end3 = clock();

    printf("vector sort:%d\n", end3 - begin3);


    // list的效率
    int begin4 = clock();
    lt.sort();
    int end4 = clock();

    printf("list sort:%d\n", end4 - begin4);


}

如图:vector > deque copy vector >>deque sort  (这里不加入list,因为它的sort是经过优化的) 

所以我们可以看出vector的效率远高于deque,并且就算算上deque转为vector在转回deque的空间拷贝效率,这时的效率也远高于deque,所以deque的效率也不太好。所以deque在能用vector或者list的场景中,我们一般不会使用deque

deque的应用场景

deque(双端队列)是一种具有队列和栈的性质的数据结构,可以在队列两端进行插入和删除操作。deque的引用场景包括但不限于以下几种:

  1. 实现滑动窗口:deque可以在O(1)时间内在队列两端进行插入和删除操作,因此可以很方便地实现滑动窗口。

  2. 实现BFS算法:BFS算法需要使用队列来存储待访问的节点,而deque可以在队列两端进行插入和删除操作,因此可以很方便地实现BFS算法。

  3. 实现LRU缓存淘汰算法:LRU缓存淘汰算法需要在缓存满时删除最近最少使用的元素,而deque可以在队列两端进行插入和删除操作,并且支持O(1)时间复杂度的查找操作,因此可以很方便地实现LRU缓存淘汰算法。

  4. 实现单调队列:单调队列是一种特殊的队列,它的元素按照一定的单调性排列。deque可以在队列两端进行插入和删除操作,并且支持O(1)时间复杂度的查找操作,因此可以很方便地实现单调队列。

优先级队列priority_queue

priority_queue是C++ STL中的一个容器,它是一个优先队列,可以用来实现堆。它的特点是每次取出的元素都是当前队列中优先级最高的元素。

在priority_queue中,元素的优先级是通过元素类型的比较函数来确定的。默认情况下,priority_queue使用std::less作为比较函数,也就是说,元素类型必须支持小于操作符(operator<)。

priority_queue提供了以下几个常用的操作:

  1. push(x):将元素x插入到队列中。
  2. pop():弹出队列中优先级最高的元素。
  3. top():返回队列中优先级最高的元素。
  4. size():返回队列中元素的个数。
  5. empty():判断队列是否为空。

优先级队列的本质是堆

通过下面的代码和我们在数据结构中对堆的学习,我们也可以知道优先级队列和堆的关系,所以优先级队列的优先级实现,首先是分为大小堆,接着就是通过堆元素的删除,来删除堆顶的元素,这样就实现了优先级

void test_priority() {

    // 优先级队列,默认为大堆
    priority_queue<int> q_big;

    q_big.push(3);
    q_big.push(5);
    q_big.push(1);
    q_big.push(4);

    while (!q_big.empty()) {

        cout << q_big.top() << " ";
        q_big.pop();
    }
    cout << endl;

    // 这个为小堆
    priority_queue<int, vector<int>, greater<int>> q_small;

    q_small.push(3);
    q_small.push(5);
    q_small.push(1);
    q_small.push(4);
    
    while (!q_small.empty()) {

        cout << q_small.top() << " ";
        q_small.pop();
    }
    cout << endl;
}

控制台输出: 

 

优先级队列的实现
// 这两个类实现了小根堆,大根堆两个不同的构造函数
template<class T>
class Less {

	bool operator()(T x, T y) {

		return x < y;
	}
};
template<class T>
class Greater {

	bool operator()(T x, T y) {

		return x > y;
	}
};
// priority_queue实质上就是一个大堆

template<class T, class Container = vector<T>, class Compare = Less<T>>
class my_priority_queue {

public:
	// 容器适配器的拷贝与析构会调用容器的拷贝与析构

	void adjust_up(int child) {	

		Compare com;

		int parent = (child - 1) / 2;
		while (child > 0) {		// 当child恰好到下标为0退出循环

			// if ( _con[parent] < _con[child]) 写死了

			if ( com(_con[parent], _con[child]) ){

				std::swap(_con[child], _con[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else {

				break;
			}
		}
	}
	void adjust_down(int parent) {

		size_t child = 2 * parent + 1;
		while (child < _con.size()) {

			if (child + 1 < _con.size() // 保证右孩子不越界
				&& com(_con[child], _con[child + 1]) ) {	 
				// 找到节点值小的来比较,大根堆需要把最大的换上去
				++child;
			}
			// if (_con[parent] > _con[child])

			if ( com(_con[parent], _con[child]) ) {

				std::swap(_con[parent], _con[child]);
				parent = child;
				child = 2 * parent + 1;
			}
			else {

				break;
			}
		}
	}

	void push(const T& val) {

		_con.push_back(val);
		// 因为是大堆,插入后需要按照堆的排列
		adjust_up(_con.size() - 1);
	}
	void pop() {

		std::swap(_con[0], _con[_con.size() - 1]);
		_con.pop_back();
		adjust_down(0);

	}

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

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

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

private:
	Container _con;
};

仿函数 

仿函数(Functor)是一种重载了函数调用运算符 operator() 的类或结构体,它可以像函数一样被调用。通常用于泛型编程中,可以作为函数对象传递给算法或容器等函数,以实现更加灵活的操作。在优先级队列实现代码中,我们看到仿函数的使用,和仿函数的创造。

在这里通过仿函数,我们只用改变模版参数 Less 为 Greater 从大堆转为小堆,类似与函数重载,传入的参数不同进入不同的部分,所以这也体现出“仿函数”。再通过这两句代码的区别,第一行只能对于大堆的向上调整来实现(因为仅仅对应<)如果改为仿函数的话就可以进入不同的部分,这样子就更加合理。比如生活当中,我们通过大众点评,查找最好的餐厅排名后,可能也会去查差的餐厅排名,总不能让程序员随着相反需求的变化,来改代码吧!

	// if ( _con[parent] < _con[child]) 写死了

	if ( com(_con[parent], _con[child]) ){

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值