1. 栈和队列
栈最优实现是数组。
- 优点:随机访问,cpu缓存利用率高
- 缺点:头部和中间,删除,插入效率较低。且需要增容
队列最优实现是链表。
- 优点:任意位置,插入和删除,O(1)
- 缺点:不支持随机访问,缓存利用率低
2. 栈的模拟实现
#pragma once
//适配器模式
namespace zjn{
template <class T, class Container = deque<T> >
class stack{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()const
{
return _con.back();
}
bool empty()const
{
return _con.empty();
}
size_t size()const
{
return _con.size();
}
private:
Container _con;
};
}
3. 队列的模拟实现
#pragma once
namespace zjn{
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()const
{
return _con.front();
}
const T& back()const
{
return _con.back();
}
bool empty()const
{
return _con.empty();
}
size_t size()const
{
return _con.size();
}
private:
Container _con;
};
}
4. 认识双端队列
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。队列只能先进先出,他和队列没有关系,更像一个vector和list的融合体。
与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
vector和list现在依旧存在,证明了其实他是一个最终失败的设计,但是队列和栈依旧用它来适配,队列和栈主要就是对头尾进行操作,说明对于他们两个来说,用双端队列来实现是个不错的选择。
一个个分散的空间无法随机访问,连续的空间需要增容,所以双端队列采取折中的方式,将数据存在一段空间里。假如空间满了,不是增容而是转入下一个缓冲区。
这些缓冲区可以看做一个个数组,使用一个指针数组来管理他(即一个个指针指向缓冲区)
T* arr[]
或者
T** arr=(T**)malloc(sizeof(int*));
push_back,当前空间满了不扩容,转向下一个数组。
那中间插入呢,挪动数据吗,插入删除效率低,但单个扩容呢,【】可以使用是因为他根据每个数组的大小是一致的,相除算出你在那一个数组,单个扩容就会让数组大小不一致,那还要记录他增了多少,来计算。
迭代器是比较复杂的
有两个迭代器,一个start,一个finish。
first指向起始位置,last指向末尾的下一个位置。
cur指向当前位置。
node是下一个buf的首地址。
一般用迭代器的场景,
iterator it=begin();
while(it!=end())
{
*it;
it++;
}
*it实际就是对start中的cur进行解引用
it++就是让cur++。但是end()是finish中last的位置
cur++到自己buf的end()的时候,通过node找到下一个数组。
然后将找到的数组的开始和结束,给到start的开始结束。node++在指向下一个buf。
然后cur再次++。所以这个指针数组就像一个中控器。
所以总结一下,对于双端队列头删头插效率还可以,但是无法替代的原因就是,【】使用太频繁了,所以比较鸡肋
5. 优先级队列
对于入数据没有要求,但是出数据是按默认优先级高的来出,优先级是否高视场景而定。
由于他底层是堆,可以看到默认大堆,每次用的时候,取堆顶数据输出,然后删除堆顶,就完成了堆排序,所以他的pop接口里面必定有调堆的过程。
template很好理解,因为他底层是完全二叉树,使用vector来适配堆。Compare是一个仿函数,当less的时候是大堆,当传入greater的时候是小堆。
5.1 优先级队列实现
5.1.1 push
尾插数据,向上调整
大堆为例:
5.1.2 pop
删除堆顶。堆顶和堆尾交换,删除堆尾,向下调整
#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
#include<queue>
using namespace std;
namespace zjn{
template<class T, class Container = vector<T>>
class priority_queue
{
public:
//写了有区间的,不会默认生成了,所以要再写一个无参的
priority_queue()
{};
//对于已有的一段区间进行构造优先级队列,两种方法
template<class iterator>
priority_queue(iterator first, iterator last)
:_Con(first,last)
{
//向下调整法
for (int i = ((_Con.size() - 1-1)/2); i >=0; i--)
{
AdjustDown(i);
}
//向上调整法建堆
//for (int i = 1; i < _Con.size(); i++)
//{
// AdjustUp(i);
//}
}
//从下往上,和父亲开始比较
void AdjustUp(int child)
{
//找出父亲
int parnet = (child - 1) / 2;
//本来应该是当孩子小于等于0时,父亲小于0停止,但是由于父亲是除法算出来的不会小于0,那就让孩子小于等于0时停止(孩子大于0继续)
while (child>0)
{
//如果孩子比父亲大,那么就交换
if (_Con[child]>_Con[parnet])
{
swap(_Con[child], _Con[parnet]);
//向上继续迭代
child = parnet;
parnet = (child - 1) / 2;
}
//孩子小于父亲说明插入的数据刚好满足,退出
else
{
break;
}
}
}
void push(const T& x)
{
//堆尾插入x
_Con.push_back(x);
//传下标,把孩子调整上去
AdjustUp(_Con.size() - 1);
}
//从上往下,父亲和大的那一个孩子比较
void AdjustDown(int parent)
{
//默认是左孩子
size_t child = parent * 2 + 1;
while (child<_Con.size())
{
//左右孩子大的那一个和父亲比较,注意当堆尾只有一个左孩子时,虽然child<size进入循环,但是他没有右孩子会导致越界
if (child + 1<_Con.size() && _Con[child] < _Con[child + 1])
{
child++;
}
if (_Con[child]>_Con[parent])
{
swap(_Con[child], _Con[parent]);
//向下继续迭代
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除堆顶
void pop()
{
assert(size() > 0);
//堆顶和堆尾交换
swap(_Con[0], _Con[_Con.size() - 1]);
//尾删
_Con.pop_back();
//堆顶是原先堆尾的数据,除了他其他结构并未变化,可以使用向下调整
AdjustDown(0);
}
const T& top()const
{
/*return _Con.top();*/
return _Con[0];
}
size_t size()const
{
return _Con.size();
}
bool empty()const
{
return _Con.empty();
}
private:
Container _Con;
};
}
5.2 仿函数
在上述实现中,固定的是个大堆,再次实现一个小堆,就需要我们改变里面的逻辑关系。仿函数可以帮我们解决这个问题。
先来看一下仿函数
可以看出仿函数其实是一个类,里面重载了运算符(),看起来有点像函数调用的()。
那么怎么运用到刚才的代码中呢,不得不佩服实现者是真的很厉害。
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=less<T> >
class priority_queue
{
.........
};
上面用一个模板,表示这是一个比较的类,这个类里面得成员方法是x>y,还是x<y,不知道,我就传进来一个模板。根据你实例化对象时所用的参数,再来决定。怎么用起来呢?
Compare com;
//if(_Con[child]>_Con[parent])
if (com(_Con[child],_Con[parent]))
这个不确定的模板,定义一个不确定的对象,里面是不确定的关系,取决于你传入的是less类还是greater类,生成对应的对象调用他们自己的成员函数。实现比较关系,不过默认给了less类。关系是x>y,体现为_Con[child]>_Con[parent]
默认实现为大堆。
假如队列里面存的是自定义类型,里面没有重载大于小于,那么可以把仿函数声明成友元,从而访问他的私有数据,从而对自定义类型的基本成员进行比较,来达到比较自定义类型。至于比较哪个成员变量,视场景而定。
来看一个应用场景,比如比较一个货物,按销量排序,按价格排序。
通过调用sort函数,里面传入仿函数对象,进行自定义比较
也直接匿名对象,很类似于c语言中的函数指针
假如面试官问起什么是仿函数,仿函数的作用,该怎么表述呢?
仿函数就是一个类,里面的成员函数里重载了(),这个()函数表示针对对象的比较关系。假如是自定义对象(日期,货物?),还可以根据场景,自己实现对对象的成员变量的比较。当然原本对象对于不同场景,大于,小于等具有相同意义,所以直接重载了比较运算符,直接用即可。
6. 优先级队列最终代码
#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
#include<queue>
using namespace std;
namespace zjn{
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=less<T>>
class priority_queue
{
public:
//写了有区间的,不会默认生成了,所以要再写一个无参的
priority_queue()
{};
//对于已有的一段区间进行构造优先级队列,两种方法
template<class iterator>
priority_queue(iterator first, iterator last)
:_Con(first,last)
{
for (int i = ((_Con.size() - 1-1)/2); i >=0; i--)
{
AdjustDown(i);
}
//for (int i = 1; i < _Con.size(); i++)
//{
// AdjustUp(i);
//}
}
//从下往上,和父亲开始比较
void AdjustUp(int child)
{
//传的是模板,从而实现,实例化对象时传入不同的比较类,实现大堆小堆。
//greater com,less com
Compare com;
//找出父亲
int parent = (child - 1) / 2;
//本来应该是当孩子小于等于0时,父亲小于0停止,但是由于父亲是除法算出来的不会小于0,那就让孩子小于等于0时停止(孩子大于0继续)
while (child>0)
{
//如果孩子比父亲大,那么就交换
//if(_Con[child]>_Con[parent])
if (com(_Con[child],_Con[parent]))
{
swap(_Con[child], _Con[parent]);
//向上继续迭代
child = parent;
parent = (child - 1) / 2;
}
//孩子小于父亲说明插入的数据刚好满足,退出
else
{
break;
}
}
}
void push(const T& x)
{
//堆尾插入x
_Con.push_back(x);
//传下标,把孩子调整上去
AdjustUp(_Con.size() - 1);
}
//从上往下,父亲和大的那一个孩子比较
void AdjustDown(int parent)
{
Compare com;
//默认是左孩子
size_t child = parent * 2 + 1;
while (child<_Con.size())
{
//左右孩子大的那一个和父亲比较,注意当堆尾只有一个左孩子时,虽然child<size进入循环,但是他没有右孩子会导致越界
if (child + 1<_Con.size() && com(_Con[child + 1], _Con[child]))
{
child++;
}
if (com(_Con[child], _Con[parent]))
{
swap(_Con[child], _Con[parent]);
//向下继续迭代
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//删除堆顶
void pop()
{
assert(size() > 0);
//堆顶和堆尾交换
swap(_Con[0], _Con[_Con.size() - 1]);
//尾删
_Con.pop_back();
//堆顶是原先堆尾的数据,除了他其他结构并未变化,可以使用向下调整
AdjustDown(0);
}
const T& top()const
{
/*return _Con.top();*/
return _Con[0];
}
size_t size()const
{
return _Con.size();
}
bool empty()const
{
return _Con.empty();
}
private:
Container _Con;
};
}