栈
概念
栈是线性表的特例,栈的顺序存储是线性表顺序存储的简化,简称顺序栈。
栈是限定在表尾进行插入和删除操作的线性表。
允许插入和删除的一端称为栈顶,另一端称为栈底。不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表,称为LIFO结构。
栈首先是一个线性表,即栈元素具有线性关系,即前驱后继关系,只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里的表尾是指栈顶,而不是栈底。
栈的特殊之处在于限制了这个线性表的插入和删除位置,它始终只在栈顶进项。这就使得,栈底是固定的,最先进栈的只能在栈底。栈的插入操作,叫做进栈。栈的删除操作,叫做出栈。
【注意】
- 顺序栈中元素用向量存放
- 栈底位置是固定不变的,可设置在向量两端的任意一个端点
- 栈顶位置是对着进栈和退栈操作而变化的,用一个整型量top(通常称top为栈顶指针)来指示当前栈顶位置
栈的实现
●简单数组实现栈的基本操作
首先基于简单数组实现栈的基本操作,如下图所示,从左至右向数组中添加所有的元素,并定义一个变量用来记录数组当前栈顶元素的下标。当数组存满了栈元素时,执行入栈操作时将抛出栈满异常。当对一个没有存储栈元素的数组执行出栈操作时,将抛出栈空异常。
下图所示为栈大小(mSize)取值为6的顺序栈s中,数组元素和栈栈顶指针的变化。
栈中元素是动态变化的,当栈中已有mSize个元素时,进栈操作会产生上溢(overflow)。在空栈上进行出栈操作会产生下溢(underflow)。为了避免溢出,在对栈进行push和pop操作之前都要检查是否栈空或栈满。
顺序栈操作的相关代码如下:
public class ArrayStack{
//以数组模拟堆栈的类声明
private int[] stack; //在类中声明数组
private int top; //指向堆栈顶端的索引
//StackByArray类构造函数
public ArrayStack(int stack_size){
stack=new int[stack_size]; //建立数组
top=-1;
}
//类方法:push
//存放顶端数据,并更新新堆栈的内容
public boolean push(int data){
if(top>stack.length){
System.out.println("堆栈已满,无法再加入");
return false;
}
else{
stack[++top]=data; //将数据存入堆栈
return true;
}
}
//类方法:empty
//判断堆栈是否为空栈,是则返回true,否则返回false
public boolean empty(){
if(top==-1) return true;
else return false;
}
//类方法:pop
//从堆栈取出数据
public int pop(){
if(empty()){ //判断堆栈是否为空 若是返回-1
return -1;
}
else{
return stack[top--]; //先将数据取出后,再将堆栈指针往下移
}
}
}
顺序栈的性能和局限性,具体说明如下:
○性能:假设n为栈中元素的个数。在基于简单数组的栈实现中,各种栈操作的算法复杂度如下:
push()和pop()的时间复杂度都为0(1),n次push()操作的空间复杂度为0(n)
○局限性:栈的最大空间必须预先声明且不能改变。试图对一个满栈执行入栈操作将产生一个针对简单这种特定实现栈方式的异常
●栈的链式实现
通过在链表的表头插入元素的方式实现push操作,删除链表的表头结点实现pop操作。链式栈本质是简化的链表。需要注意的是为了方便存储,栈顶元素应设置为链表头。如图所示为链式栈的一个简单示意图:
顺序栈和链式栈的比较如下
- 顺序栈和链式栈的基本操作都只需要常数时间,因此二者在时间效率上难分伯仲。从空间角度来看,初始时顺序栈必须说明一个固定的长度,当栈不够满时,势必浪费一些空间;链式栈的长度可根据需要而增减,但每个元素都需要一个指针域,从而产生结构性的开销。
- 在栈的实际应用中,有时需要访问栈的内部元素。顺序栈可以根据元素与栈顶的相对位置快速定位并读取内部元素,而链式栈则需要沿着指针遍历才能访问内部元素。
结论
栈是一种很重要,应用非常广泛的数据结构。常见的应用包括表达式转换和求值,函数的调用和递归实现、深度优化搜索等等。栈的一个重要的应用在于函数机制和递归实现的支持。
队列
概念
与栈相似,队列也是一种限制访问点的线性表。队列的元素只能从表的一端插入,另一端删除。按照习惯,通常会把只允许删除的一端称为队列的头,简称队头,把删除操作本身称为出队;而称表的另一端为队列的尾,简称队尾,这一段只能进行插入操作,简称入队。队列是先进先出的线性表。
如图所示:
队列的实现
○队列的顺序实现
定义:队列的顺序存储结构称为顺序队列,顺序队列实际上是运算受限的顺序表。用顺序存储结构来实现队列就形成顺序队列。与顺序表一样,顺序队列需要分配一块连续的区域来存储队列的元素,需要实现知道或估算队列大小。
顺序队列的表示:(1).与顺序表一样,顺序队列用一个向量空间来存放当前队列中的元素。
(2).由于队列的队头和队尾的位置是变化的,设置两个指针front和rear分别只是队头元素和队尾元素在向量空间中的位置,它们的初值在队列初始化时均应置为0
队列操作示意图如下:
首先,分析是否可以借鉴基于简单数组实现栈的方法来实现队列。由队列的定义可知,只能在队列的一端执行插入操作,而在另一端执行删除操作。当执行多次插入和删除操作就,就可以很容易发现使用简单数组来实现队列问题。
如图所示,可以清楚的看到数组中靠前的空间被浪费了,所以基于简单数组实现队列不是一个简单有效的方法。为了解决这个问题,假设数组是循环存储的方式,即将数组最后一个元素与第一个元素看作连续的。依据这个假设,如果数组前端有空闲的空间,指向队尾的指针就能够很容易的移动到下一个空闲的位置。
随着时间的推移,整个队列会向数组的尾部移动,一旦达到数组的最末端,即rear=mSize-1,即使数组前端可能还有空闲的位置,再进行入队操作也会发生溢出。这种数组实际上还有空闲位置而发生上溢的现象称为“假溢出”。解决假溢出的方法便是采用循环的方式来组织存放队列元素中的数组,在逻辑上把数组看成是一个环,即把数组中下标编号最低的位置(0位置)看成是编号位置最高的位置(mSize-1)的直接后继,这就可以通过取模运算实现,即数组位置x的后继位置为(x+1)%mSize,这样就形成了循环队列。
如图所示为一个循环队列,起初,队首存放在数组中编号较低的位置,队尾则存放在数组编号较高的位置,沿顺时针方向存放队列。这样,入队操作增加rest的值,出队操作增加front的值。
队列的顺序实现方式采用数组,在数组中,采用循环增加元素的方式,并使用两个变量分别记录队首元素和队尾元素,通常用front变量和rear变量表示队首元素和队尾元素。基于数组来存储队列中的元素,可能会出现数组被填满的情况。这时,若执行入队操作,则抛出队列满异常,反之,若对空队列执行元素删除操作,会出现队列空异常。
顺序队列的实现代码如下:
public class ArrayQueue{
private int front;
private int rear;
private int capacity;
private int[] array;
public ArrayQueue(int size){
capacity=size;
front=1;
rear=-1;
array=new int[size];
}
public static ArrayQueue createQueue(int size){
return new ArrayQueue(size);
}
//判断循环队列是否为空
public boolean isEmpty(){
return (front==-1);
}
//判断循环队列是否已满
public boolean isFull(){
return((rear+1)%capacity==front);
}
public int getQueueSize(){
return((capacity-front+rear+1)%capacity);
}
//方法enqueue:队列数据传入
public void enQueue(int data){
if(idFull()){
System.out.println("队列溢出");
}else{
rear=(rear)%capacity;
array[rear]=data;
if(front==-1){
front=rear;
}
}
}
//方法dequeue:队列数据删除
public int deQueue(){
int data=0;
if(isEmpty()){
System.out.println("队列为空");
}else{
data=array[front];
if(front==rear){
front=rear-1;
}else{
front=(front+1)%capacity;
}
}
return data;
}
}
○队列的顺序实现
定义:队列的链式存储结构简称为链队列。它是限制挤在表头删除和表尾插入的单链表。实现队列的另一种方式是使用链表。通过在链表末端插入元素的方法实现入队操作。通过删除链表表头元素的方法实现出队操作。
链队列的结构类型说明:链式队列是队式的链式实现,是对链表简化。如图所示,成员front和rear分别指向队首和队尾的指针。
链式队列的实现代码如下:
class QueueNode //队列结点类
{
int data; //结点数据
QueueNode next; //指向下一个结点
//构造函数
public QueueNode(int data){
this.data=data;
next=null;
}
}
public class LinkedQueue{
public QueueNode front; //队列的前端指针
public QueueNode rear; //队列的尾端指针
//构造函数
public LinkedQueue(){ front=null; rear=null; }
public boolean isEmpty(){
retrun(front==null);
}
//方法enqueue:队列数据的存入
public boolean enQueue(int value){
QueueNode node=new QueueNode(value); //建立结点
//检查是否为空队列
if(rear==null)
front=node; //新建立的结点成为第一个结点
else
rear.next=node; //将结点加入到队列的尾端
rear=node; //将队列的尾端指针指向新加入的结点
return true;
}
//方法dequeue:队列数据的取出
public int deQueue(){
int data=0;
if(isEmpty()){
System.out.println("队列为空");
}else{
data=front.data;
front=front.next;
}
return data;
}
}
顺序队列与链式队列的比较如下
- 由于存储空间固定,顺序队列无法满足队列规模变化很大且最大规模无法预测的情况,而链式队列则可以轻松应对这种类型的应用。另一方面,顺序队列在存取访问上很简单,可以适用那些对队列内部元素有访问需求的应用。
- 顺序队列和链式队列中常用的入队和出队操作都需要常数时间,两者在时间效率上没有优劣之分。在空间代价上与栈的情况类似。只是顺序队列不像顺序栈那样,不能在一个数组中存储两个队列,除非总有数据项从一个队列转入另一个队列。