编程基础 - 栈(Stack)
本文意指简明扼要的描述栈,并用C++从零到有的实现栈。
需要一定的数据结构与程序语言的基础,尤其是要了解什么是顺序表。
文章目录
1 栈的简介(Introduction of Stack)
栈(Stack)又名堆栈。
-
定义:栈作为一种数据结构,它是一种只能在一端进行插入和删除操作的特殊线性表
- 允许插入和删除的一端称为栈顶(TOP)
- 不允许插入和删除的一端称为栈底(BOTTOM)
- 插入操作称为进栈或入栈(PUSH)
- 删除操作称为退栈或出栈(POP)
-
限制:只能在栈顶插入和删除
-
存储结构:顺序结构或链式结构
-
运算规则(特点):后进先出(Last-In/First-Out,LIFO)或先进后出(First-In/Last-Out,FILO)
-
与一般线性表的区别:仅运算规则不同
2 栈的主要方法(Main Methods of Stack)
栈的主要操作包括:
-
初始化(Initialize)
-
进栈(Push):插入元素
-
退栈(Pop):如果栈中有元素,删除栈顶元素;
-
获取栈顶元素(GetTop):如果栈不为空,获取栈顶元素;
-
清空栈(Clear):如果栈不为空,则删除所有元素;
-
判断栈空(IsEmpty):栈空返回
true
,否则返回false
; -
判断栈满(IsFull):栈满返回
true
,否则返回false
;
3 栈的实现(C++ Code)
虽然在各种高级语言中,栈已经都被实现了:
-
在C++中,使用栈需要加入
#include <stack>
。 -
在C#中,使用栈需要加入
using System.Collection.Generic;
。
但在这里为了更好的理解它,我们将用C++自己实现栈。
提示:以下代码编译器为VC++,属性__declspec(property…)代码在其它编译器可能不通用,如果用其它编译器可删除这一行,直接使用方法代替。
3.1 栈的抽象类(Abstract Class)
首先,我们新建一个头文件起名为Stacks.h
。它将包含如下内容:
-
一些需要的常量(主要是顺序结构需要):
- 栈的默认最大长度
- 栈的默认长度增长率
-
一个栈的模板抽象类:包含了栈的主要方法的虚函数。
-
一些需要的包含库(
#include
)
我们同时为这些内容放入 命名空间Stacks
(之后所有代码都在它之中) 中:
#pragma once
#include <stdexcept> // Build-in exceptions
namespace Stacks
{
constexpr int DEFAULT_MAX_COUNT = 255; // Max Count of Sequential Structure
constexpr int DEFAULT_INCREAMENT = 16; // Increament of Sequential Structure
template<typename T>
class AbstractStack
{
public: // Constructor
virtual ~AbstractStack()
{
};
public: // public Properties
inline virtual int GetCount() = 0;
__declspec(property(get = GetCount)) int count;
public: // public Methods
virtual void Push(T item) = 0;
virtual T Pop() = 0;
virtual T Top() = 0;
virtual T Peek(); // in C#, `Top()` Method is called `Peek()`
virtual void Clear() = 0;
virtual bool Empty() = 0;
};
template<typename T>
T AbstractStack<T>::Peek()
{
return Top();
}
}
-
#include <stdexcept>
:程序异常库,里面有许多常用的异常错误信息 -
DEFAULT_MAX_COUNT
:顺序结构的默认长度; -
DEFAULT_INCREAMENT
:顺序结构的默认增长长度; -
template<typename T> class AbstractStack
:栈的模板抽象类(T
为元素的类型)int GetCount()
与int count
:是一个属性方法,获取栈内元素个数void Push(T item)
:入栈T Pop()
:出栈T Top()
与T Peek()
:获取栈顶元素()void Clear()
:清空栈bool Empty()
:栈是否为空
Tips
constexpr int
可以替换成#define
:
#define DEFAULT_MAX_COUNT 255
#define DEFAULT_INCREAMENT 16
有了基类之后,我们将按照栈的结构来分别写代码。
3.2 顺序结构(Sequential Structure)
在顺序结构中,数组是存储数据的主要类型。而我们在进行栈操作时,需要一个变量指向栈顶元素,从而顺序结构需要的变量:
-
元素数组:存储元素
- 数组初始化的长度
- 当数组满时,需要增长的长度
-
栈顶元素下标:标识当前栈顶元素的位置
而在方法上,除了继承下来的方法,我们需要增加一些顺序结构的必要方法:
-
扩充栈:当栈满时,扩充栈的大小
-
栈是否满
所以,我们建立一个新的头文件SequentialStack.h
来写顺序结构,并将顺序栈命名为SequentialStack
,
即,我们的最终结构为:
#pragma once
#include "Stacks.h"
namespace Stacks
{
template<typename T>
class SequentialStack : virtual public AbstractStack<T>
{
public: // Constructor
SequentialStack();
virtual ~SequentialStack() override;
protected: // protected Fields
T* m_Items; // 元素数组
int m_TopIndex; // 栈顶下标
int m_MaxCount; // 元素数组长度
int m_Increament; // 元素数组增长长度
public: // public Properties
inline virtual int GetCount() override;
inline int GetMaxCount();
inline int GetIncreament();
inline void PutIncreament(int value);
__declspec(property(get = GetCount)) int count;
__declspec(property(get = GetMaxCount)) int maxCount;
__declspec(property(get = GetIncreament, put = PutIncreament)) int increament;
protected: // protected Methods
virtual void Overflow(); // 扩充栈,每次扩充`m_Increament`的长度
public: // public Methods
virtual void Push(T item) override;
virtual T Pop() override;
virtual T Top() override;
virtual void Clear() override;
virtual bool Empty() override;
virtual bool Full(); // 栈是否满
};
}
3.2.1 初始化与销毁(Initialize and Destroy)
在构造函数,我们主要是进行数组的初始化:
template<typename T>
inline SequentialStack<T>::SequentialStack()
{
m_TopIndex = -1;
m_MaxCount = DEFAULT_MAX_COUNT;
m_Increament = DEFAULT_INCREAMENT;
m_Items = new T[m_MaxCount];
if (m_Items == 0) // == NULL
{
throw std::bad_alloc(); // 分配内存失败
}
}
类似的,在析构函数中,我们主要是对数组的销毁:
template<typename T>
inline SequentialStack<T>::~SequentialStack()
{
if (m_Items != 0)
{
delete[] m_Items;
}
m_TopIndex = 0;
m_MaxCount = 0;
m_Increament = 0;
}
3.2.2 属性方法(Properties)
这里只是给内部字段对外的一个属性方法:
template<typename T>
inline int SequentialStack<T>::GetCount()
{
return m_TopIndex + 1;
}
template<typename T>
inline int SequentialStack<T>::GetMaxCount()
{
return m_MaxCount;
}
template<typename T>
inline int SequentialStack<T>::GetIncreament()
{
return m_Increament;
}
template<typename T>
inline void SequentialStack<T>::PutIncreament(int value)
{
if (value < 1) // 栈的增长率不能小于1
{
value = 1;
}
m_Increament = value;
}
3.2.3 主要方法(Main Methods)
我们逐个添加,要注意每个方法的前置条件。
-
入栈(PUSH):
template<typename T> void SequentialStack<T>::Push(T item) { if (Full()) // 如果栈满 { Overflow(); // 扩充栈 } m_TopIndex++; // 栈顶位置+1(后移) m_Items[m_TopIndex] = item; // 赋值数据 }
-
出栈(POP):
template<typename T> T SequentialStack<T>::Pop() { if (Empty()) // 如果栈空 { throw std::underflow_error("no element."); // 溢出错误 } T item = m_Items[m_TopIndex]; // 取出元素 m_Items[m_TopIndex] = 0; // 将此位置设置为空 m_TopIndex--; // 栈顶位置-1(前移) return item; // 返回取出的元素 }
-
获取栈顶元素(TOP):
template<typename T> T SequentialStack<T>::Top() { if (Empty()) // 如果栈空 { throw std::underflow_error("no element."); // 溢出错误 } return m_Items[m_TopIndex]; // 返回栈顶元素 }
-
清空栈(CLEAR):
template<typename T> void SequentialStack<T>::Clear() { while (!Empty()) // 如果栈非空 { m_Items[m_TopIndex] = 0; // 将栈顶元素设置成空 m_TopIndex--; // 栈顶位置-1(前移) } }
-
判断栈是否空(EMPTY):由于我们有栈顶位置,只要栈顶位置为
-1
,即栈空template<typename T> bool SequentialStack<T>::Empty() { return m_TopIndex == -1; }
-
判断栈是否满(FULL):如果栈顶位置达到了数组最后的位置(
m_MaxCount - 1
),即栈满template<typename T> bool SequentialStack<T>::Full() { return m_TopIndex + 1 == m_MaxCount; }
-
扩充栈(OVERFLOW):当栈满时,我们为栈增加存储空间
template<typename T> void SequentialStack<T>::Overflow() { int newSize = m_MaxCount + m_Increament; // 新的长度 = 旧长度 + 增长率 T* items = new T[newSize]; // 用新的长度初始化新的元素数组 if (items == 0) { throw std::bad_alloc(); // 分配内存失败 } // 将旧数组中的元素复制到新数组 for (int i = 0; i < m_MaxCount; i++) { items[i] = m_Items[i]; } delete[] m_Items; // 删除旧数组 m_Items = items; // 将新数组赋值 m_MaxCount = newSize; // 更新长度 }
3.3 双栈共享的顺序结构(Shared Sequential Structure)
这种顺序结构是指两个栈共享一个数组空间。
这种结构我们并不过多的阐述,只是需要注意以下几点:
-
栈顶标识应有两个
int m_TopIndex[2]
:- 自下而上(数组从左到右),进栈时栈顶位置递增
- 自上而下(数组从右到左),进栈时栈顶位置递减;
-
判断条件要分别判断,或需要更改:
- 栈满:两个栈顶下标相差1
- 栈空:左栈空且右栈空(数组左右)
-
扩充栈时,要注意复制右栈元素的下标和新数组元素下标的关系
如果是继承前面的类,可能需要添加一些方法(主要是区分左右栈):
void PushRight(T item);
T PopRight();
T TopRight();
void ClearLeft();
void ClearRight();
bool EmptyLeft();
bool EmptyRight();
// and so on
如果不继承,也可以重新编写,主要方法可以加入位置参数,例如:
void Push(T item, int position = 0); // 0表示左,1表示右
3.4 链式结构(Linked Structure)
在链式结构中,链表用于存储数据。
我们首先建立一个头节点(HEAD),将它指向的第一个元素作为栈顶。
则我们进栈的方式:
- 将新栈顶(New)链接到旧栈顶(Old)
- 将头(Head)链接到新栈顶(New)
我们建立一个新的头文件LinkedStack.h
来写链式结构,并将链式栈命名为LinkedStack
,
即,我们的链式结构最终为:
#pragma once
#include "Stacks.h"
namespace Stacks
{
template<typename T>
struct LinkedNode
{
T item;
struct LinkedNode<T>* next;
};
template<typename T>
class LinkedStack : virtual public AbstractStack<T>
{
public: // Constructor
LinkedStack();
virtual ~LinkedStack() override;
protected: // protected Fields
LinkedNode<T>* m_Head; // 头结点
int m_Count;
public: // public Properties
inline virtual int GetCount() override;
__declspec(property(get = GetCount)) int count;
public: // public Methods
virtual void Push(T item) override;
virtual T Pop() override;
virtual T Top() override;
virtual void Clear() override;
virtual bool Empty() override;
};
template<typename T>
inline int LinkedStack<T>::GetCount()
{
return m_Count;
}
}
3.4.1 初始化与销毁(Initialize and Destroy)
类似顺序结构,我们这里主要是初始化与销毁链表的头节点。
template<typename T>
inline LinkedStack<T>::LinkedStack()
{
m_Head = new LinkedNode<T>(); // 初始化头结点
m_Head->item = 0;
m_Head->next = 0;
m_Count = 0; // 初始化数量
}
template<typename T>
inline LinkedStack<T>::~LinkedStack()
{
Clear();
delete m_Head;
}
3.4.2 主要方法(Main Methods)
我们依然逐个添加。
-
入栈(PUSH):
template<typename T> void LinkedStack<T>::Push(T item) { LinkedNode<T>* newTop = new LinkedNode<T>(); // 初始化新栈顶 newTop->item = item; newTop->next = 0; LinkedNode<T>* oldTop = m_Head->next; // 获取当前栈顶指针 m_Head->next = newTop; // 断开旧栈顶,链接新栈顶 newTop->next = oldTop; // 链接新栈顶与旧栈顶 m_Count++; // 数量+1 }
-
出栈(POP):
template<typename T> T LinkedStack<T>::Pop() { if (Empty()) // 如果栈空 { throw std::underflow_error("no element."); // 溢出错误 } LinkedNode<T>* pop = m_Head->next; // 取出栈顶,作为旧栈顶 m_Head->next = pop->next; // 链接新栈顶 pop->next = 0; // 将旧栈顶链接断开 T item = pop->item; // 取出栈顶数据 delete pop; // 删除旧栈顶 m_Count--; // 数量-1 return item; }
-
获取栈顶元素(TOP):
template<typename T> T LinkedStack<T>::Top() { if (Empty()) // 如果栈空 { throw std::underflow_error("no element."); // 溢出错误 } return m_Head->next->item; }
-
清空栈(CLEAR):
template<typename T> void LinkedStack<T>::Clear() { LinkedNode<T>* top = m_Head->next; // 获取栈顶 while (top != 0) // 如果栈顶有元素 { m_Head->next = top->next; // 断开栈顶,并设置新栈顶 top->next = 0; // 断开旧栈顶链接 delete top; // 删除旧栈顶 top = m_Head->next; // 循环获取栈顶 } m_Count = 0; // 更新长度 }
-
判断栈是否空(EMPTY):
template<typename T> bool LinkedStack<T>::Empty() { return m_Head->next == 0; // 或者 m_Count == 0 }
3.5 测试栈(Stack Testing)
到目前为止,我们完成了三种栈的编写,我们来稍微测试一下它们。
创建一个主函数main.cpp
,内容如下:
#include <iostream>
#include "SequentialStack.h"
#include "SharedSequentialStack.h"
#include "LinkedStack.h"
using namespace std;
int main()
{
Stacks::SequentialStack<char> sStack;
Stacks::SharedSequentialStack<char> ssStack(1000);
Stacks::LinkedStack<char> lStack;
cout << "为每个栈添加[a, n]字母(共享是左右都添加)(PUSH)" << endl;
for (char ch = 'a'; ch <= 'n'; ch++)
{
sStack.Push(ch);
ssStack.Push(ch);
ssStack.PushRight(ch);
lStack.Push(ch);
}
cout << "顺序栈数量:" << sStack.count << " / " << sStack.maxCount << endl;
cout << "共享栈数量:" << ssStack.count << " / " << ssStack.maxCount << endl;
cout << "链式栈数量:" << lStack.count << endl;
cout << endl;
cout << "出栈打印(POP):" << endl;
// 打印结果应为:"nnnn mmmm .... aaaa" (以换行隔开)
while (sStack.count > 0)
{
cout << sStack.Pop();
cout << ssStack.Pop() << ssStack.PopRight();
cout << lStack.Pop();
cout << endl;
}
cout << "顺序栈数量:" << sStack.count << " / " << sStack.maxCount << endl;
cout << "共享栈数量:" << ssStack.count << " / " << ssStack.maxCount << endl;
cout << "链式栈数量:" << lStack.count << endl;
cout << endl;
for (int i = 0; i < 300; i++)
{
sStack.Push('a');
}
cout << "加入300个`a`后,顺序栈正确数量(OVERFLOW):默认255 + 3次 x 增长16 = 303" << endl;
cout << "测试实际顺序栈数量:" << sStack.count << " / " << sStack.maxCount << endl;
cout << endl;
cout << "清除顺序栈(CLEAR)" << endl;
sStack.Clear();
cout << "顺序栈数量:" << sStack.count << " / " << sStack.maxCount << endl;
cout << endl;
system("pause");
return 0;
}
测试结果如下:
为每个栈添加[a, n]字母(共享是左右都添加)(PUSH)
顺序栈数量:14 / 255
共享栈数量:28 / 1000
链式栈数量:14
出栈打印(POP):
nnnn
mmmm
llll
kkkk
jjjj
iiii
hhhh
gggg
ffff
eeee
dddd
cccc
bbbb
aaaa
顺序栈数量:0 / 255
共享栈数量:0 / 1000
链式栈数量:0
加入300个`a`后,顺序栈正确数量(OVERFLOW):默认255 + 3次 x 增长16 = 303
测试实际顺序栈数量:300 / 303
清除顺序栈(CLEAR)
顺序栈数量:0 / 303
请按任意键继续. . .
4 栈的应用实例 (Stack Examples)
以下常见应用,将在后续更新,更新后将变成链接。
-
逆波兰表达式(后缀表达式) (Reverse Polish Notation)