学习栈我们要知道栈是什么,有什么特点?
首先栈是一种特殊的线性表,其特殊在栈仅能在线性表的一端进行操作,其中栈顶(Top)是允许操作的一端,栈底(Bottom)是不允许操作的一端。
栈的唯一特性就是Last In First Out,即后进先出,也可以说栈 的特性是先进后出。看一个进栈和出栈的图理解后进先出的意义:
知道了栈 的特性后就该进行栈操作了,那栈有哪些操作?
创建栈(Stack())
销毁栈(~Stack())
清空栈(Clear())
进栈(push())
出栈(pop())
获取栈顶元素(top())
获取栈大小(size())
栈的设计
栈的实现分为顺序栈和链式栈,那么这样就可以对两种类型进行设计实现了,因为都是栈,都具有相同的栈操作,所以设计一个栈的抽象类,其中包含栈的基本操作,都设置成纯虚函数,其具体实现根据继承抽象类的栈的类型不同而不同。
那么这个抽象类该怎么实现?
使用模块编程
继承自顶层父类Object
将栈的操作函数设计成纯虚函数
push函数的形参为const,防止意外修改。
将top函数设计成const类型以供const对象调用。
将size函数设计成const类型以供const对象调用。
具体实现如下:
template <typename T>
class Stack : public Object
{
public:
virtual void push(const T& obj) = 0;
virtual void pop() = 0;
virtual int size()const = 0;
virtual T top()const = 0;
virtual void clear() = 0;
};
顺序栈
栈的实现可以为顺序栈或者链式栈,今天我们先学习顺序栈,顺序栈就是先预留一片固定空间,在这片空间里创建栈,然后在创建的栈上进行增删改查的操作。
从图中我们可以看见当top指向的是0位置之前时表示此时的栈为空栈,当push一个元素然后top就往后移动一个位置直到top指向了栈的最后一个位置,此时栈的状态已经被填满了,继续push就会产生异常了。
直观的看了栈的实现后我们在程序中应该如何进行顺序栈设计实现。
类模块编程。
使用原生数组作为栈的存储空间。
使用模块参数决定栈的最大容量。
继承自Stack父类。
需要对栈顶和栈的当前大小进行标识。
最后就是将Stack里的对栈的操作进行具体实现。
当操作异常时我们选择抛出异常。
实现栈的过程中要对栈保证异常安全。
由于不需要进行new和delete操作,可以省掉析构函数。
StaticStack顺序栈类的具体实现如下:
template <typename T,int N>
class StaticStack : public Stack<T>
{
protected:
T m_space[N];
int m_top;
int m_size;
public:
StaticStack()
{
m_top = -1;//为什么?因为创建栈时栈是空的,而不是指向0位置处。
m_size = 0;
}
int capacity()
{
return N;
}
void push(const T& obj)
{
if(m_size < N)
{
m_space[m_top + 1] = obj;//保持异常安全,当obj为类对象时可能赋值操作符没有重载而导致失败,此时就可能发生异常返回,但返回后对栈本身并没有影响
m_top++;
m_size++;
}
else//操作不合法时抛出异常
{
THROW_EXCEPTION(InvalidOperationException,"no memory to push...");
}
}
void pop()
{
if(m_size > 0)//栈不为空时出栈才有意义
{
m_size--;
m_top--;
}
else
{
THROW_EXCEPTION(InvalidOperationException,"InvalidOperation to pop...");
}
}
int size()const//const对象也能调用
{
return m_size;
}
T top()const//const 对象也能调用
{
if(m_size > 0)
{
return m_space[m_top];
}
else
{
THROW_EXCEPTION(InvalidOperationException,"InvalidOperation to top...");
}
}
void clear()
{
m_size = 0;
m_top = -1;
}
//最后需不需要析构函数?
//个人认为不需要,因为并没有涉及到内存资源分配和释放,所以可以省去这一步,让系统自动调用默认的析构函数
};
StaticStack顺序栈类实现其实还是挺简单的,而且算法复杂度都是O(1)。
值得注意的有两点:
1。构造函数里对栈的初始化,因为栈创建时并没有元素,而栈的元素又是从第0个位置开始push的,所以将m_top赋值为-1。
2。push操作里的异常安全,由于是泛型编程,所以T为类类型是有可能的,当将一个类对象赋值给数组成员时如果并未进行相应的赋值操作符重载就可能产生异常,如果此时抛出异常就不会影响到栈原先的状态,所以我们在赋值成功后再改变栈的大小和栈顶位置。
最后对这一节进行一个总结:
栈是一个特殊的顺序表。
他只允许在其中一端进行操作。
StaticStack类使用原生数组作为内部存储空间,其最大容量由模板参数N决定。、
保证了在进栈时的异常安全。
顺序栈有一个缺点:当泛指类型T是类类型时,创建栈的时候就会多次调用构造函数影响效率,所以我们还需要实现另一种栈--链式栈,具体内容请看下一讲。
学习软件就是要自己多敲多写多想多思考,在此感谢狄泰唐老师。