数据结构与算法(3)-栈和队列
Author:Liedra
https://www.cnblogs.com/LieDra/
Ch3 栈和队列
0x01 栈的定义和特点
栈:限定仅在表尾(栈顶)进行插入和删除操作的线性表。又称为 后进先出(Last In First Out) 的线性表,简称 LIFO 结构。
- 栈顶:把允许插入和删除的一端称为栈顶
- 栈底:栈顶相对的另一端为栈底
- 空栈:不含有任何数据元素的栈称为空栈
常见操作集合:初始化堆栈S,入栈,出栈,取栈顶元素,判断堆栈是否为非空等。
0x02 栈的表示和实现
顺序栈
可以使用线性表的顺序存储结构(即数组)实现栈,将之称之为 顺序栈
结构如下:
a0, a1, a2, a3, a4表示顺序堆栈中已存储的数据元素,stack表示存放数据元素的数组,MaxStackSize-1表示最大存储单元个数,top表示当前栈顶存储下标。
顺序栈的定义
//顺序栈类的定义SeqStack
class SeqStack
{
private:
DataType data[MaxStackSize]; //顺序堆栈数组
int top; //栈顶位置指示器
public:
SeqStack(void) { top = 0; } //构造函数
~SeqStack(void) {} //析构函数
void Push(const DataType item); //入栈
DataType Pop(void); //出栈
DataType GetTop(void)const; //取栈顶数据元素
int NotEmpty(void)const { return(top != 0); }; //堆栈非空否
};
顺序栈的实现
void SeqStack::Push(const DataType item) //入栈
//把元素item入栈;堆栈满时出错退出
{
if (top == MaxStackSize)
{
cout << "堆栈已满!" << endl;
exit(0);
}
data[top] = item; //先存储item
top++; //然后top加1
}
DataType SeqStack::Pop() //出栈
//出栈并返回栈顶元素;堆栈空时出错退出
{
if (top == 0)
{
cout << "堆栈已空!" << endl;
exit(0);
}
top--; //top先减1
return data[top]; //然后取元素返回
}
DataType SeqStack::GetTop(void)const //取栈顶数据元素
//取当前栈顶数据元素并返回
{
if (top == 0)
{
cout << "堆栈空!" << endl;
exit(0);
}
return data[top - 1]; //返回当前栈顶元素
}
程序测试
#include <iostream>
using namespace std;
typedef int DataType; //定义具体问题元素的数据类型
const int MaxStackSize = 100; //定义问题要求的元素数目的最大值
#include"SeqStack.h"
int main()
{
SeqStack myStack; //构造函数无参数时,定义的对象后不带括号
DataType test[] = { 1,3,5,7,9 };
int n = 5;
for (int i = 0; i < n; i++)
myStack.Push(test[i]);
while (myStack.NotEmpty())
cout << myStack.Pop() << " ";
cout << endl;
system("pause");
return 0;
}
结果如下:
9 7 5 3 1
请按任意键继续. . .
链栈
可以使用单链表结构实现栈,将之称之为 链栈
存储结构:以头指针为栈顶,在头指针处插入或删除。
结构如下:
链式栈结点类的定义和实现
template <class T> class LinStack; //前视定义,否则友元无法定义
//结点类定义和实现
template <class T> //模板类型为T
class StackNode
{
friend class LinStack <T>; //定义类LinStack<T>为友元
private:
T data; //数据元素
StackNode <T>* next; //指针
public:
//构造函数1,用于构造头结点
StackNode(StackNode <T>* ptrNext = NULL)
{
next = ptrNext;
}
//构造函数2,用于构造其他结点
StackNode(const T& item, StackNode <T>* ptrNext = NULL)
{
data = item;
next = ptrNext;
}
~StackNode() {};
};
链式栈的定义
//链式堆栈类的定义
template <class T>
class LinStack {
private:
StackNode <T>* head; //头指针
int size; //数据元素个数
public:
LinStack(void); //构造函数
~LinStack(void); //析构函数
void Push(const T& item); //入栈
T Pop(void); //出栈
T GetTop(void) const; //取栈顶元素
int NotEmpty(void) const; //堆栈非空否
};
链式栈的实现
//链式堆栈类的实现
template <class T>
LinStack <T>::LinStack() //构造函数
{
head = new StackNode <T>; //头指针指向头结点
size = 0; //size的初值为0
}
template <class T>
LinStack <T>::~LinStack(void) //析构函数
//释放所有动态申请的结点空间
{
StackNode <T>* p, * q;
p = head; //p指向头结点
while (p != NULL) //循环释放结点空间
{
q = p;
p = p->next;
delete q;
}
}
template <class T>
int LinStack <T>::NotEmpty(void) const //堆栈非空否
{
if (size != 0) return 1;
else return 0;
}
template <class T>
void LinStack <T>::Push(const T& item) //入栈
{
//新结点newNode的data域值为item,next域值为head->next
StackNode <T>* newNode = new StackNode <T>(item, head->next);
head->next = newNode; //新结点插入栈顶
size++; //元素个数加1
}
template <class T>
T LinStack <T>::Pop(void) //出栈
{
if (size == 0) {
cout << "堆栈已空无元素可删!" << endl; exit(0);
}
StackNode <T>* p = head->next; //p指向栈顶元素结点
T data = p->data;
head->next = head->next->next; //原栈顶元素结点脱链
delete p; //释放原栈顶结点空间
size--; //结点个数减1
return data; //返回原栈顶结点的data域值
}
template <class T>
T LinStack <T>::GetTop(void) const //取栈顶元素
{
return head->next->data;
}
程序测试
#include <iostream>
using namespace std;
typedef int DataType; //定义具体问题元素的数据类型
//const int MaxStackSize = 100; //定义问题要求的元素数目的最大值
//#include"SeqStack.h"
#include"LinStack.h"
int main()
{
LinStack <int> myStack; //构造函数无参数时,定义的对象后不带括号
DataType test[] = { 1,3,4,7,5 };
int n = 5;
for (int i = 0; i < n; i++)
myStack.Push(test[i]);
while (myStack.NotEmpty())
cout << myStack.Pop() << " ";
cout << endl;
system("pause");
return 0;
}
结果如下:
5 7 4 3 1
请按任意键继续. . .
一般不会出现栈满情况;除非没有空间导致malloc分配失败。
链栈的入栈、出栈操作就是栈顶的插入与删除操作,修改指针即可完成。
采用链栈存储方式的优点是,可使多个栈共享空间;当栈中元素个数变化较大,且存在多个栈的情况下,链栈是栈的首选存储方式。
顺序栈和链栈的异同
顺序栈
需要事先确定一个固定的长度(数组长度)
可能存在内存空间浪费问题,但优势是存取时定位很方便。
链栈
要求每个元素都要配套一个指向下个结点的指针域。
增大了内存开销,但好处是栈的长度(几乎)无限。
如果栈的使用过程中元素变化有时很大有时很小,那么用链栈较好,否则使用顺序栈即可。
堆栈应用
括号匹配问题
假设一个算法表达式中包含圆括号、方括号和花括号三种类型的括号,编写一个判别表达式中括号是否正确配对的函数。
思路:用栈暂时存左括号,每次读取时如果是左括号则入栈,是右括号则判断栈是否是空的,是空的则说明右括号多,return;不是空,则判断是否是与栈顶匹配的右括号;字符最后全部读完后,判断栈中是否还有元素,有元素则说明有多余的左括号。
详细代码如下:
#include <iostream>
using namespace std;
typedef char DataType; //定义具体问题元素的数据类型
const int MaxStackSize = 100; //定义问题要求的元素数目的最大值
#include"SeqStack.h"
//#include"LinStack.h"
void ExpIsCorrect(char exp[], int n)
//判断有n个字符的字符串exp左右括号是否配对正确
{
SeqStack myStack; //定义顺序堆栈类对象myStack
int i;
for (i = 0; i < n; i++)
{
if ((exp[i] == '(') || (exp[i] == '[') || (exp[i] == '{'))
myStack.Push(exp[i]); //入栈
else if (exp[i] == ')' && myStack.NotEmpty()
&& myStack.GetTop() == '(')
myStack.Pop(); //出栈
else if (exp[i] == ')' && myStack.NotEmpty()
&& myStack.GetTop() != '(')
{
cout << "左、右括号配对次序不正确!" << endl;
return;
}
else if (exp[i] == ']' && myStack.NotEmpty()
&& myStack.GetTop() == '[')
myStack.Pop(); //出栈
else if (exp[i] == ']' && myStack.NotEmpty()
&& myStack.GetTop() != '[')
{
cout << "左、右括号配对次序不正确!" << endl;
return;
}
else if (exp[i] == '}' && myStack.NotEmpty()
&& myStack.GetTop() == '{')
myStack.Pop(); //出栈
else if (exp[i] == '}' && myStack.NotEmpty()
&& myStack.GetTop() != '{')
{
cout << "左、右括号配对次序不正确!" << endl;
return;
}
else if (((exp[i] == ')') || (exp[i] == ']') || (exp[i] == '{'))
&& !myStack.NotEmpty())
{
cout << "右括号多于左括号!" << endl;
return;
}
}
if (myStack.NotEmpty())
cout << "左括号多于右括号!" << endl;
else
cout << "左、右括号匹配正确!" << endl;
}
int main() {
char a[] = "((())){}(][";
char b[] = "((())){}{}()[";
char c[] = "((())){}{})[]]";
char d[] = "((())){}{}()[]";
int aa = strlen(a), bb = strlen(b), cc = strlen(c), dd = strlen(d);
cout << "((())){}(][ ";
ExpIsCorrect(a, aa);
cout << "((())){}{}()[ ";
ExpIsCorrect(b, bb);
cout << "((())){}{})[]] ";
ExpIsCorrect(c, cc);
cout << "((())){}{}()[] ";
ExpIsCorrect(d, dd);
system("pause");
return 0;
}
结果如下:
((())){}(][ 左、右括号配对次序不正确!
((())){}{}()[ 左括号多于右括号!
((())){}{})[]] 右括号多于左括号!
((())){}{}()[] 左、右括号匹配正确!
请按任意键继续. . .
表达式计算问题
表达式计算是堆栈的一个典型利用,通常需要变换表达式的表示序列,如下图。
整个计算过程分为两步,一是中缀表达式转为后缀表达式,二是利用后缀表达式计算。
表达式转换
中缀表达式:A+(B-C/D)E
对应的后缀表达式:ABCD/-E+
中缀表达式转为后缀表达式的算法步骤如下:
(1)设置一个堆栈,初始时将栈顶元素置为“#”。
(2)顺序读入中缀表达式,当读到的为操作数时就将其输出,并接着读下一个单词。
(3)令x1为当前栈顶运算符的变量,x2为当前扫描读到运算符的变量,当顺序从中缀表达式中读入的单词为运算符时就赋予x2,然后比较x1的优先级与x2的优先级,若x1的优先级高于x2的优先级,将x1退栈并作为后缀表达式的一个单词输出,然后接着比较新的栈顶运算符x1的优先级与x2的优先级。若x1的优先级低于x2的优先级,将x2进栈然后接着读下一个单词;若x1的优先级等于x2的优先级,且x1为(,x2为)时,将进行退栈操作,然后接着读下一个单词;若x1的优先级等于的x2优先级,且x2为#时,算法结束。
下面是一个中缀表达式变换成后缀表达式具体过程:
计算后缀表达式的值
设置一个堆栈存放操作数,从左到右依次扫描后缀表达式,每读到一个操作数就将其进栈;每读到一个运算符就从栈顶取出两个操作数施以该运算符所代表的运算操作,并把该运算结果作为一个新的操作数入栈;此过程一直进行到后缀表达式读完,最后栈顶的操作数就是该后缀表达式的运算结果
这个后缀表达式可以通过字符串形式,也可以通过堆栈等来进行扫描,只是通过不同方式,需要进行的预处理操作不同。
0x03 队列的定义和特点
队列:限定仅在一端(队尾)进行插入操作,而在另一端(队头)进行删除操作的线性表。
- 队头:允许删除的一端
- 队尾:允许插入的一端
常见操作集合:初始化队列,入队列,出队列,取队头数据元素,判断队列是否非空等。
队列是一种特殊的线性表,因此也存在着顺序存储和链式存储两种存储方式。 - 顺序队列
顺序队列是顺序存储结构的队列。下面是顺序队列的一个示例
顺序队列存在“假溢出”问题:
顺序队列因多次入队列和出队列操作,出现的有存储空间但不能进行入队列操作的情况称之为假溢出。(因为实际还有空间)
解决顺序队列的假溢出问题:
四种方法:
- 采用循环队列。(把队列所使用的存储空间构造成一个逻辑上首尾相连的循环队列。当rear和front达到MaxQueueSize-1后,再前进一个位置就自动到位置0。)
- 按最大可能的进队操作次数设置顺序队列的最大元素个数
- 修改出队算法,使每次出队列后都把队列中剩余数据元素向队头方向移动一个位置。
- 修改入队算法,增加判断条件,当出现假溢出时,把队列中的数据元素向队头移动,然后完成入队操作。
循环队列的队空和队满判断问题:
虽然解决假溢出,但是又引出新的问题,即在循环队列中,空队特征是front=rear;队满时也会有front=rear,出现了二义性。解决方法如下三种:
- 使用计数器
判队满:count>0&&rear==front
判队空:count==0
- 加设标志位
判队满:tag==1&&rear==front
判队空:tag==0 && rear==front
- 少用一个存储单元
判队满: front==(rear+1)%MaxQueueSize
判队空: rear==front
0x04 队列的表示和实现
顺序循环队列
类定义
采用设置计数器方法来判断队空状态和队满状态,类定义如下:
SeqQueue.h
class SeqQueue {
private:
DataType data[MaxQueueSize]; //顺序队列数组
int front; //队头指示器
int rear; //队尾指示器
int count; //元素个数计数器
public:
SeqQueue(void) //构造函数
{front=rear=0;count=0;}
~SeqQueue(void){}; //析构函数
void Append(const DataType& item); //入队列
DataType Delete(void); //出队列
DataType GetFront(void)const; //取队头数据元素
int NotEmpty(void)const //非空否
{return count!=0;}
};
类实现
void SeqQueue::Append(const DataType& item) //入队列
//把数据元素item插入队列作为当前的新队尾
{
if (count > 0 && front == rear)
{
cout << "队列已满!" << endl;
exit(0);
}
data[rear] = item; //把元素item加在队尾
rear = (rear + 1) % MaxQueueSize; //队尾指示器加1
count++; //计数器加1
}
DataType SeqQueue::Delete(void) //出队列
//把队头元素出队列,出队列元素由函数返回
{
if (count == 0)
{
cout << "队列已空!" << endl;
exit(0);
}
DataType temp = data[front]; //保存原队头元素
front = (front + 1) % MaxQueueSize; //队头指示器加1
count--; //计数器减1
return temp; //返回原队头元素
}
DataType SeqQueue::GetFront(void)const //取队头数据元素
//取队头元素并由函数返回
{
if (count == 0)
{
cout << "队列已空!" << endl;
exit(0);
}
return data[Front]; //返回队头元素
}
链式队列类
链式存储结构的队列。链式队列的队头指针指向队列的当前队头结点;队尾指针指在队列的当前队尾结点。不带头结点的链式队列如下:
结点类的实现
template <class T> class LinQueue;//前视定义,否则友元无法定义
template <class T>
class QueueNode
{
friend class LinQueue <T>; //定义类LinQueue<T>为友元
private:
QueueNode <T> *next; //指针
T data; //数据元素
public: //构造函数
QueueNode(const %& item,QueueNode <T> *ptrNext=NULL):data(item), next(ptrNext){}
~QueueNode() {}; //析构函数
};
注意这里QueueNode(const %& item,QueueNode <T> *ptrNext=NULL):data(item), next(ptrNext){}
直接利用构造函数的参数初始化表。
链式队列类的定义与实现:
定义
template <class T>
class LinQueue {
private:
QueueNode <T>* front; //队头指针
QueueNode <T> *rear; //队尾指针
int count; //计数器
public:
LinQueue(void); //构造函数
~LinQueue(void); //析构函数
void Append(const T& item); //入队列
T Delete(void); //出队列
T GetFront(void)const; //取队头数据元素
int NotEmpty(void)const //非空否
{return count!=0;}
};
实现,这里也是增加一个count域来计算当前的元素个数。
template <class T>
LinQueue <T>::LinQueue() //构造函数
{
front = rear = NULL; //链式队列无头结点
count = 0; //count的初值为0
}
template <class T>
LinQueue <T>::~LinQueue(void) //析构函数
{
QueueNode <T>* p, * q;
p = front; //p指向第一个结点
while (p != NULL) //循环直至全部结点空间释放
{
q = p;
p = p->next;
delete q;
}
count = 0; //置为初始化值0
front = rear = NULL;
}
template <class T>
void LinQueue <T>::Append(const T& item) //入队列
//把数据元素item插入队列作为新队尾结点
{
//构造新结点newNode,newNode的data域值为item,next域值为NULL
QueueNode <T>* newNode = new QueueNode <T>(item, NULL);
if (rear != NULL) rear->next = newNode; //新结点链入
rear = newNode; //队尾指针指向新队尾结点
//若队头指针原先为空则置为指向新结点
if (front == NULL) front = newNode;
count++; //计数器加1
}
template <class T>
T LinQueue <T>::Delete(void) //出队列
//把队头结点删除并由函数返回
{
if(count==0)
{
cout<<"队列已空!"<<endl;
exit(0);
}
QueueNode <T>* p = front->next; //p指向新的队头结点
T data = front->data; //保存原队头结点的data域值
delete front; //释放原队头结点空间
front = p; //front指向新的对头结点
count--; //计数器减1
return data; //返回原队头结点的data域值
}
template <class T>
T LinQueue <T>::GetFront(void)const //取队头数据元素
{
if (count == 0)
{
cout << "队列已空!" << endl;
exit(0);
}
return front->data;
}
队列的应用
编写判断一个字符序列是否是回文的函数
思路:把字符数组中的字符逐个分别存入队列和堆栈,然后逐个出队列和退栈并比较出队列的字符和退栈的字符是否相等,若全部相等则该字符序列是回文,否则就不是回文。(仅仅作为一个应用案例,实际上判断是否是回文有更简单方法,比如直接前后循环比较即可)
void HuiWen(char str[]) {
LinStack <char> myStack;
LinQueue <char> myQueue;
int n = strlen(str); //求字符串长度
for(int i=0;i<n;i++)
{
myQueue.Append(str[i]);
myStack.Push(str[i]);
}
while(myQueue.NotEmpty()&&myStack.NotEmpty())
{
if(myQueue.Delete()!=myStack.Pop())
{
cout<<"不是回文!"<<endl;
return;
}
}
cout<<"是回文!"<<endl;
}
0x05 其他
优先级队列
带有优先级的队列。
与一般队列的区别:优先级队列的出队列操作不是把队头元素出队列,而是把队列中优先级最高的元素出队列。其数据元素的结构体不仅包括数据元素,还包括元素的优先级。除出队列操作外的其他操作的实现方法与前边讨论的顺序队列操作的实现方法相同。
可以利用优先级队列,模仿操作系统的进程管理,可以按优先级高的先服务、优先级相同的先到先服务的原则管理。