C++STL初阶(13):双端队列deque和优先级队列priority_queue(包含详细计算)

目录

1. deque

设计deque

deque的设计与功能的联系

iterator

2. 优先级队列

2.1 优先级队列的使用 

3. 实现优先级队列(堆)

4. 仿函数 


1. deque

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

尽管名字叫双端队列,但是deque和队列FIFO的特性没有太多关系,我们抛开先进先出的特性进行学习。

deque是一个顺序容器(sequence container),而不是容器适配器。也就是说deque更像是list和vector,而非stack和queue


并且在功能上,deque就是list和vector的结合:

而deque什么都可以: 

                                    


       deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维 数组。有许多个buff数组的数组指针组成作为 中控 的指针数组。buff数组从中间位置开始存放,这样就能头插和尾差。其中buff数组的大小N会根据每一次使用的不同而调整,专门有一个成员函数计算buff的大小。

设计deque

虽然中控数组满了会扩容,但是中控数组是指针数组,拷贝代价很低。


deque的设计与功能的联系

方括号访问的方法:

                                      

但是下标访问不如vector的极致,速度会相对慢一些。

头尾插删是不错,但是中间位置的插入删除就很麻烦了。

可以选择全部挪动数据,但是插入效率就和vector一样低;

或选择对插入数据的buff数组扩容,这样就只扩容一个小数组,而不影响大环境,但是扩容就会导致上面蓝字的方括号访问方法行不通。

我们通过sort来测试vector和deque的下标访问的速度(快速排序一直都在使用下标访问) 

#include <iostream>
#include <deque>
#include <algorithm>
#include <vector>

using namespace std;

int main() {

	deque<int> dq;
	vector<int> v;
	srand(time(0));
	int N = 1000000;

	while (N) {
		dq.push_back(rand() + N);
		v.push_back(rand() + N);
		--N;
	}
	
	size_t begin1 = clock();
	sort(dq.begin(), dq.end());
	size_t end1 = clock();

	size_t begin2 = clock();
	sort(v.begin(), v.end());
	size_t end2 = clock();

	cout << "time of deque : " << end1 - begin1 << endl << "time of vector : " << end2 - begin2;

	return 0;
}

vector的速度是deque的两到三倍。 

 再来使用一下将deque拷贝给vector再排序:

有大量数据需要排序或者访问的时候是不建议用deque的,偶尔用用还行。

iterator

deque的iterator比较复杂:

                              

由四个指针封装构成。map_pointer是一个T** , 是中控数组的指针

start指向的是第一个buff数组的第一个位置(但是第一个数组不会放到中控数组的第一个,而是放到中间位置)

finish指向的是最后一个buff数组的最后一个数据的下一个位置


关于解引用,迭代器解引用*it, 也就是 *cur


在cur等于last之前,就直接对指针进行++即可,当cur == last时

就让cur指向下一个buff的first并返回。

                               

当前buff走完之后:

尾插的思路和遍历类似。

头插:

头插并非和我们所想到在start前的数组再从头加起,而是倒着加。

例如,在当前的deque下先头插-1 再头插-2

所以第一个buff不一定是满的。

为了让这样的头插和方括号的访问方法相匹配,我们需要调整一下下标访问:

比如我们要访问第n个元素:

那我们其实访问的是n+(cur-first)个元素 ,假装第一个buff是满的

综上所述,deque更适合用来适配stack和queue

vector 比较 deque 的优势是:头部插入和删除时, 不需要搬移元素,效率特别高 ,而且在 扩容时,也不 需要搬移大量的元素 ,因此其效率是必 vector 高的。
list 比较 ,其底层是连续空间, 空间利用率比较高 ,不需要存储额外字段。

缺点: 

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

2. 优先级队列

优先级队列是一种容器适配器,priority_queue的底层选择的是vector

目前为止我们已经了解了三种容器和三种容器适配器。分别是vector deque list和 stack queue priority_queue

优先级就是去取大的来优先进出队列或者取小的来优先进出队列。

回忆我们学过的数据结构,只有大堆和小堆符合要求。

优先级队列的本质就是堆(默认是大堆)。

  优先级队列也是被包含在头文件queue中的。并且在命名上,虽然优先级队列和队列关系不大,但我们还是多用q来表示它。

2.1 优先级队列的使用 

关于构造:

直接用迭代器区间就可以建堆(建堆就是建priority_queue),一个一个push就会一遍一遍调整。

一个一个push的时间复杂度:O(nlogn)

直接建堆的时间复杂度: O(n)

因为一个一个push是采用的向上调整算法;而直接将存在的数组建堆是采用的向上遍历的向下调整算法。后文会有具体的计算。

原生指针也可以用于建堆:

因为迭代器就是像指针一样的东西 ,指针支持解引用、++、--等一些列迭代器的功能。

但是不支持initializer_list

                 

默认是大堆:


可观察到有三个模版参数,第一个class T表示元素类型,第二个表示底层容器类型并默认是vector

其中第三个涉及新内容:仿函数

less默认是大堆,greater是小堆

如果要使用小堆,就要加入第三个模版参数。而根据半缺省的规则,写第三个参数就得写第二个。

将第三个参数传成greater<int>


3. 实现优先级队列(堆)

先不管仿函数的内容:

对于push的实现,先将新元素塞到容器的末尾,再向上调整:

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

 紧接着就需要实现向上调整算法

(本文为了方便理解,实现的都是只支持vector或者deque等可以使用下标访问的容器,但是真正的priority_queue一定是可以用list的,只是不推荐将list作为底层)

根据我们在C语言数据结构中对树的学习,实现出代码:C语言数据结构基础笔记——树、二叉树简介_c语言的树-CSDN博客

void adjust_up(int child) {
	assert(child >= 0);
	int parent = (child - 1) / 2;
	while (child != 0) {
		if (_con[parent] < _con[child]) {
			swap(_con[parent], _con[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else {
			break;
		}
	}
}

while后面的大条件只能用child > 0,因为根据int的特性,父亲无论如何都不会小于0

因为(0-1)/2=0;

           

同理向下调整:

void adjust_down(int parent) {
	assert(parent >= 0);

	int child = parent * 2 + 1;
	while (child < _con.size()) {
		if (child + 1 < _con.size() && _con[child] < _con[child + 1]) {
			++child;
		}
		if (_con[parent] < _con[child]) {
			swap(_con[parent], _con[child]);
			parent = child;
			child = child * 2 + 1;
		}
		else {
			break;
		}
	}
}

除了push还有pop

然后是几个直接封装vector即可的简单接口:

T& top() {
	return _con[0];
}
const T& top() const {
	return _con[0];
}
bool empty() {
	return _con.empty();
}
size_t size() {
	return _con.size();
}

迭代器区间构造:

实现方法一:一个一个push

本质就是向上调整,挨个插入。

因为push是调用向上调整,总体复杂度是O(nlogn)

并不是因为每次都是logn,一共有n次,而是因为

我们一般取N和h的关系为h = logN


而利用向上遍历的向下调整算法来直接建堆是O(n)

                    

而向上遍历的向下调整算法就是从 倒数的第一个非叶子节点开始建堆

也就是从(_con.size()-1-1)/2开始调整。

template<typename InputIterator>
priority_queue(InputIterator first, InputIterator last) {
	/*while (first != last) {
		*this->push(*first);
		++first;
	}*/
	while (first != last) {
		_con.push_back(*first);
		++first;
	}
	for (int i = (_con.size() - 1-1) / 2; i >= 0;--i) {
		adjust_down(i);
	}
}

最后,由于我们只实现了一个范围拷贝,还希望继续使用编译器提供的其他构造。

priority_queue = default();

这样建好了大堆。

如果想改成小堆呢?

以前的做法是改大小于符号,

而C++的处理方法是仿函数。


4. 仿函数 

仿函数,也叫函数对象,仿函数是一个类,本质是对operator()的重载

仿函数是一个重载了operator()的类,类的对象可以像函数一样使用

f1是一个类,f1()就是对于f1.operator() ()  的调用。

                                            

仿函数是class或者struct都可以,但是class需要使用public修饰,建议直接改成struct

我们以一个比较大小的函数为例:

      

   如果不加public,默认是private:

                  

所以一般建议使用struct来控制仿函数

为了比较不同的类型,我们也可以通过模版自己传数据类型

回到优先级队列中:

在prority_queue中刚刚的基础上再传一个参数:

也就是:

接着就可以使用了: 

                        

也可以不在函数中单独定义func,而是在private下直接新增这样一个变量。

                                        

想从大堆变小堆,就可以传mygreater而不是myless即可。


仿函数第一层作用:替代并加强函数指针的作用。仿函数被当作一个模版参数传入中,可以在类中使用。

再来观察一下priority_queue的优势:        

                 

每次运行都不一样。

因为传的是Date* ,存进去的是指针,比较的就是地址,而地址大小每一次new出来都不一样(没有规定先new的和后new的有什么不同)

但是我们可以自己完成一个由仿函数控制的新的比较逻辑

最后贴一下优先级队列的整体代码:

namespace lsnm {

	template<class T>
	struct myless {
		bool operator()(const T& x, const T& y) {
			return x < y;
		}
	};

	template<class T>
	struct mygreater {
		bool operator()(const T& x, const T& y) {
			return x > y;
		}
	};

template<typename T,typename Container = vector<int>,typename Compare = myless<int>>
class priority_queue {
public:
    priority_queue = default();
	template<typename InputIterator>
	priority_queue(InputIterator first, InputIterator last) {
		/*while (first != last) {
			*this->push(*first);
			++first;
		}*/
		while (first != last) {
			_con.push_back(*first);
			++first;
		}
		for (int i = (_con.size() - 1-1) / 2; i >= 0;--i) {
			adjust_down(i);
		}
	}
	//先都和默认一样,按照大堆实现
	void adjust_up(int child) {
		assert(child >= 0);
		//Compare func;

		int parent = (child - 1) / 2;
		while (child != 0) {
			//if (_con[parent] < _con[child]) {
			if (func(_con[parent],_con[child]) {
				swap(_con[parent], _con[child]);
				child = parent;
				parent = (parent - 1) / 2;
			}
			else {
				break;
			}
		}
	}

	void adjust_down(int parent) {
		assert(parent >= 0);

		int child = parent * 2 + 1;
		while (child < _con.size()) {
			if (child + 1 < _con.size() && func(_con[child],_con[child+1]) {
				++child;
			}
			//if (_con[parent] < _con[child]) {
			if (func(_con[parent], _con[child]) {
				swap(_con[parent], _con[child]);
				parent = child;
				child = child * 2 + 1;
			}
			else {
				break;
			}
		}
	}

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

	T& top() {
		return _con[0];
	}
	const T& top() const {
		return _con[0];
	}
	bool empty() {
		return _con.empty();
	}
	size_t size() {
		return _con.size();
	}

private:
	Container _con;
    Compare   _func;
};

}

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值