下载文件:第03章

Download Report

Transcript 下载文件:第03章

第3章 堆栈和队列
主
要
知
识
点
堆栈
堆栈应用
队列
优先级队列
3.1 堆 栈
1、堆栈的基本概念
(1)定义:限定只能在固定一端进行插入和删除操作的线性表。
特点:后进先出。
(2)允许进行插入和删除操作的一端称为栈顶,另一端称为
栈底。
2、堆栈抽象数据类型
数据集合: {a0,a1,…,an-1}ai的数据类型为DataType。
操作集合:(1)Initiate(S) 初始化堆栈S
(2)Push(S,x) 入栈
(3)Pop(S,d)
出栈
(4)GetTop(S) 取栈顶数据元素
(5)NotEmpty(S) 堆栈S非空否
3、顺序堆栈类
(1)顺序堆栈
顺序存储结构的堆栈。
(2)顺序栈的存储结构
它是利用一组地址连续的存储单元依次存放自栈底到栈
顶的数据元素,其结构如图所示:
栈底
栈顶
stack a0 a1 a2 a3 a4
0
1
2
3
4
5
MaxStackSize-1
=
top
其中,a0, a1, a2, a3, a4表示顺序堆栈中已存储的数据元素,
stack表示存放数据元素的数组,MaxStackSize-1表示最大存储
单元个数,top表示当前栈顶存储下标。
类的定义:
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);};
};
//入栈
(3)顺序栈类的操作实现
一、void SeqStack::Push(const DataType item)
//把元素item入栈;堆栈满时出错退出
{
if(top==MaxStackSize)
{
cout<<"堆栈已满!"<<endl;
exit(0);
}
data[top]=item;
top++;
}
//先存储item
//然后top加1
//入栈
二、DataType SeqStack::Pop()
//出栈
//出栈并返回栈顶元素;堆栈空时出错退出
{
if(top==0)
{
cout<<"堆栈已空!"<<endl;
exit(0);
}
top--;
return data[top];
}
//top先减1
//然后取元素返回
三、DataType SeqStack::GetTop(void)const
//取栈顶数据元素
//取当前栈顶数据元素并返回
{
if(top==0)
{
cout<<"堆栈空!"<<endl;
exit(0);
}
return data[top-1];
}
//返回当前栈顶元素
测试主程序如下:
#include <iostream.h>
#include <stdlib.h>
const int MaxStackSize=100;
typedef int DataType;
//定义问题要求的元素数目的最大值
//定义具体问题元素的数据类型
void main(voie)
{
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());
while(myStack.NotEmpty())
cout<<myStack.Pop()<<" ";
}
程序运行输出结果为:9 7 5 3 1
4、链式堆栈类
(1)链式堆栈
顺序存储结构的堆栈。
(2)链式栈的存储结构
它是以头指针为栈顶,在头指针处插入或删除,其结构
如图所示:
头结点
h
栈顶
an-1
an-2
…
栈底
a0
∧
链栈中每个结点由两个域构成:data域和next域,其结点
类和类定义分别如下:
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;
LinStack(void);
~LinStack(void)
void Push(const T& item);
//入栈
T Pop(void);
//出栈
T GetTop(void) const;
//取栈顶元素
int NotEmpty(void) const;
};
//头指针
//数据元素个数public:
//构造函数
//析构函数
//堆栈非空否
(3)链式栈类的操作实现
一、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--;
return data;
}
//原栈顶元素结点脱链
//释放原栈顶结点空间
//结点个数减1
//返回原栈顶结点的data域值
六、template <class T>
T LinStack <T>::GetTop(void) const
//取栈顶元素
{
return head->next->data;
}
说明:
1)在链栈中的头结点对操作的实现影响不大,栈顶(表头)操作频繁,
可不设头结点链栈。
2)一般不会出现栈满情况;除非没有空间导致malloc分配失败。
3)链栈的入栈、出栈操作就是栈顶的插入与删除操作,修改指针即可
完成。
4)采用链栈存储方式的优点是,可使多个栈共享空间;当栈中元素个
数变化较大,且存在多个栈的情况下,链栈是栈的首选存储方式。
3.2 堆栈应用
1、括号匹配问题
例:假设一个算法表达式中包含圆括号、方括号和花括号三
种类型的括号,编写一个判别表达式中括号是否正确配对的
函数。
设计思路:用栈暂存左括号
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;
}
2、表达式计算问题
表达式计算是编译系统中的基本问题,其实现方法是堆
栈的一个典型应用。在编译系统中,要把便于人理解的表达
式翻译成能正确求值的机器指令序列,通常需要先把表达式
变换成机器便于理解的形式,这就要变换表达式的表示序列。
假设计算机高级语言中的一个算术表达式为
A+(B-C/D)*E
这种表达式称为中缀表达式,写成满足四则运算规则的相应
的后缀表达式即为
ABCD/-E*+
中缀表达式变换为后缀表达式的算法步骤可以总结
为:
(1)设置一个堆栈,初始时将栈顶元素置为“#”。
(2)顺序读入中缀表达式,当读到的单词为操作数时
(3)令x1为当前栈顶运算符的变量,x2为当前扫描读
到运算符的变量,当顺序从中缀表达式中读入的单词为
运算符时就赋予x2,然后比较x1的优先级与x2的优先级,
若x1的优先级高于x2的优先级,将x1退栈并作为后缀表
达式的一个单词输出,然后接着比较新的栈顶运算符x1
的优先级与x2的优先级。
利用堆栈计算后缀表达式值的函数编写如下:
void PostExp(LinStack <T> &s)
{
char ch;
//ch为char类型变量
T x,x1,x2;
cout<<"输入后缀表达式(表达式以#符号结束):";
while(cin>>ch&&ch!='#') //循环直到输入为'#'
{
if(isdigit(ch))
//ch为数字类型
{
cin.putback(ch); //回退一位
}
cin>>x;
//按数值类型重新输入
s.Push(x);
//x入栈
else
{
x2=s.Pop();
//退栈得操作数
x1=s.Pop();
//退栈得被操作数
switch(ch)
{
case'+':{x1+=x2;break;}
case'-':{x1-=x2;break;}
case'*':{x1*=x2;break;}
case'/':
if(x2==0.0)
{
cout<<"除数为0错!";
exit(0);
}
else
{
x1/=x2;
break;
}
}
s.Push(x1);
//运算结果入栈
}
}
cout<<"后缀表达式计算结果为:"<<s.Pop()<<endl;
}
3.3 队 列
队尾插
入
1、队列的基本概念
(1)定义:只能在表的一端进行插入操作,在表的另一端进行
删除操作的线性表。一个队列的示意图如下:
队头
a0
队尾
a1
a2
…
an-1
队头删
除
2、队列抽象数据类型
数据集合:{a0,a1,…,an-1},ai的数据类型为DataType。
操作集合:(1)Initiate(Q) 初始化队列Q
(2)Append(Q,x) 入队列
(3)Delete(Q) 出队列
(4)GetFront(Q) 取队头数据元素
(5)NotEmpty(Q) 队列Q非空否
3、顺序队列
(1)顺序队列
顺序存储结构的队列。
(2)顺序队列的存储结构
下图是一个有6个存储空间的顺序队列的动态示意图
5
4
3
2
1
front
rear =0
(a)空队列
5
4
3
2 C
1 B
front =0 A
5
4
rear = 3
front= 2 C
1
0
rear = 5
4 E
3 D
front= 2 C
1
0
(b)入队列A、
(c)出队列A、
(d)入队列D、
B、C后的状态
B后的状态
E后的状态
(3)顺序队列的“假溢出”问题
①假溢出
顺序队列因多次入队列和出队列操作后出现的有存储空间
但不能进行入队列操作的溢出。
②如何解决顺序队列的假溢出问题?
可采取四种方法:
1)采用循环队列;
2)按最大可能的进队操作次数设置顺序队列的最大元素个数;
3)修改出队算法,使每次出队列后都把队列中剩余数据元素
向队头方向移动一个位置;
4)修改入队算法,增加判断条件,当假溢出时,把队列中的
数据元素向对头移动,然后方完成入队操作。
(4)顺序循环队列的基本原理
把顺序队列所使用的存储空间构造成一个逻辑上首尾相连
的循环队列。当rear和front达到MaxQueueSize-1后,再前进
一个位置就自动到0。
顺序队列 0
循环队列
N-1
1
front
rear
2
a1
3
a2
.
a3
.
N-1
0
1
a1
rear
a3
a2
3
front
2
(5)顺序循环队列的队空和队满判断问题
新问题:在循环队列中,空队特征是front=rear;队满时也会有
front=rear;判决条件将出现二义性!解决方案有三:
①使用一个计数器记录队列中元素个数(即队列长度);
判队满:count>0 && rear==front
判队空:count==0
②加设标志位,出队时置0,入队时置1,则可识别当前front=rear
属于何种情况
判队满:tag==1 && rear==front
判队空:tag==0 && rear==front
③ 少用一个存储单元
判队满: front==(rear+1)%MaxQueueSize
判队空: rear==front
4、顺序循环队列类
采用设置计数器方法来判断队空状态和队满状态,类定义如下:
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;
cout++;
}
//计数器加1
//队尾指示器加1
DataType SeqQueue::Delete(void)
//出队列
//把队头元素出队列,出队列元素由函数返回
{
if(count==0)
{
cout<<"队列已空!"<<endl;
exit(0);
}
DataType temp=data[front];
//保存原队头元素
front=(front+1) & MaxQueueSize; //队头指示器加1
count--;
return temp;
}
//计数器减1
//返回原队头元素
DataType SeqQueue::GetFront(void)const //取队头数据元素
//取队头元素并由函数返回
{
if(count==0)
{
cout<<"队列已空!"<<endl;
exit(0);
}
return data[Front];
}
//返回队头元素
5、链式队列类
(1)链式队列
顺序存储结构的队列。
(2)链式队列的存储结构
链式队列的队头指针指向队列的当前队头结点;队尾指针
指在队列的当前队尾结点,下图是一个不带头结点的链式队列
的结构:
rear
front
a0
a1
…
an-1
an-1
∧
(3)链式队列类的定义及实现
结点类的定义和实现如下:
template <class T> class LinQueue;//前视定义,否则友元无法定义
template <class T>
class QueueNode
{
friend class LinQueue <T>;
private:
QueueNode <T> *next;
T data;
public:
//定义类LinQueue<T>为友元
//指针
//数据元素
//构造函数
QueueNode(const %& item,QueueNode <T> *ptrNext=NULL)
:data(item),next(ptrNext){}
~QueueNode(){};
//析构函数
};
为了方便设计,增加了一个count域用来计算当前的元素个数
链式队列类的定义如下:
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;}
};
//队头指针
//队尾指针
//计数器
//构造函数
//析构函数
//入队列
//出队列
//取队头数据元素
//非空否
链式队列类的实现如下:
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;
front=rear=NULL;
}
//置为初始化值0
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;
T data=front->data;
delete front;
front=p;
count--;
return data;
}
//p指向新的队头结点
//保存原队头结点的data域值
//释放原队头结点空间
//front指向新的对头结点
//计数器减1
//返回原队头结点的data域值
template <class T>
T LinQueue <T>::GetFront(void)const
//取队头数据元素
{
if(count==0)
{
cout<<"队列已空!"<<endl;
exit(0);
}
return front->data;
}
6、队列的应用
例:编写判断一个字符序列是否是回文的函数。
编程思想:
设字符数组str中存放了要判断的字符串。把字符数组中
的字符逐个分别存入队列和堆栈,然后逐个出队列和退栈并比
较出队列的字符和退栈的字符是否相等,若全部相等则该字符
序列是回文,否则就不是回文。
设计函数如下:
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;
}
3.4 优先级队列
1、优先级队列
带有优先级的队列。
2、顺序优先级队列 用顺序存储结构存储的优先级队列。
3、优先级队列和一般队列的主要区别
优先级队列的出队列操作不是把队头元素出队列,而是把
队列中优先级最高的元素出队列。
它的数据元素定义为如下结构体:
struct DataType
{
ElemType elem;
int priority;
//数据元素
//优先级
};
注:顺序优先级队列除出队列操作外的其他操作的实现方法与
前边讨论的顺序队列操作的实现方法相同。
出队列操作(把优先级最高的元素出队列并由函数返回,优先级相
同时按先进先出的原则出队列),核心语句如下:
DataType min=data[0];
//初始选data[0]为优先级最高的元素
int minIndex=0;
//minIndex为优先级最高元素的下标
for(int i=1;i<count;i++)//寻找优先级最高的元素及对应下标
if(data[i].priority<min.priority)
{
min=data[i];
minIndex=i;
}
例:设计一个程序模仿操作系统的进程管理进程。进程服务按优先级高
的先服务、
优先级相同的先到先服务的原则管理。
模仿数据包括两部分:进程编号和优先级。一个模仿数据集合如下,其
中第一列表示
进程编号,第二列表示进程优先级:
1 30
2 20
3 40
4 20
5 0
用顺序优先级队列可直接完成,核心语句如下:
for(int i=0;i<n;i++)
myQueue.Append(test[i]);
cout<<"任务号 优先级"<<endl;
for(i=0;i<n;i++)
{
if(myQueue.NotEmpty())
{
temp=myQueue.Delete();
cout<<temp.elem<<" ";
cout<<temp.priority<<endl;
}