目录
一丶栈的概念和类型定义
1:栈的基本概念
2:抽象数据类型层面的栈
1)栈的数据元素
2)栈的基本操作
3)C#中的栈类
二丶栈的存储结构及实现
1:栈的顺序存储结构和操作实现
1)栈的初始化
2)返回栈中元素个数
3)判断栈为空或满
4)入栈
5)出栈
6)获取栈顶元素的值
7)显示栈中所有数据元素的值
2:栈的链式存储结构及操作实现
1)栈的初始化
2)返回栈的元素个数
3)判断栈状态为空或满
4)入栈
5)出栈
6)获取栈顶元素值
3:栈的应用举例
1)基于栈结构的函数嵌套调用
2)应用栈结构的典型例子
三丶队列的概念及类型应用
1:队列的基本概念
2:抽象数据类型层面的队列
1)队列的数据元素
2)队列的基本操作
3:C#中的队列类
四丶队列的存储结构及实现
1:队列的顺序存储结构及操作实现
1)队列的顺序存储结构
2)顺序循环队列的定义及操作
1)队列初始化
2)返回队列的元素个数
3)判断队列的状态是否为空或满
4)入队
5)出队
6)获取队首对象,但不移除
7)输出队列中所有数据元素
2:队列的链式存储结构及操作实现
1)队列初始化
2)返回队列的元素个数
3)判断队列的状态是否为空或满
4)入队
5)出队
6)获取队首对象,但不移除
3:队列的应用举例
栈和队列是两种特殊的线性结构,其数据元素之间也具有顺序的逻辑关系。与线性表可以在表中任意位置进行插入和删除操作不同,栈和队列的插入和删除操作限制在特殊的位置。
栈具有后进先出的特性,队列具有先进先出的特性。在实现方式上,栈和队列都可以采用顺序存储结构和链式存储结构。
栈(stack)是一种特殊的线性数据结构,栈结构中的数据元素之间具有顺序的逻辑结构,但栈只允许在数据集合的一断进行插入和删除数据的操作。
向栈中插入数据元素的操作称为入栈(push),从栈中删除元素称为出栈(pop)
栈就像某种只有单个出入口的仓库,每次只允许一件件地往里面堆货物,然后一件件地往外取货物,不允许从中间进行操作
栈作为一种特殊地线性结构,可以如同线性表一样采用顺序存储结构和链式存储结构实现。
采用顺序存储结构实现的栈称为顺序栈(sequenced stack),采用链式存储结构实现的栈称为链式栈(Linked stack)
Initialize:栈的初始化
Count:数据元素计数
Empty:判断栈的状态是否为空
Push:入栈,该操作将一个数据元素插入栈中作为新的栈顶元素。在入栈操作之前必须判断栈的状态是否已满,不满则直接入栈,否则会出现栈上溢错误(stack overflow exception)
Pop:出栈,该操作将取出栈顶元素
Peek:探测栈顶,获取栈顶数据元素,但不移除
在C#类库中定义了一个非泛型栈stack类和一个泛型栈stack
例:利用栈进行数制转换
数制转换是计算机实现计算的一个基本问题。十进制数N和其他d进制数的转换具有以下关系:
数制转换就是要确定序列{},解决方法很多,其中一种算法如下:
N=(N/d)×d+N%d
例如,其运算如下:
N | N/d | N%d |
2468 | 308 | 4 |
308 | 38 | 4 |
38 | 4 | 6 |
4 | 0 | 4 |
现编写一个程序:用户输入任意一个非负十进制数,程序打印输出与其等值的八进制数
程序如下:
using System;
using System.Collections.Generic;
namespace stackqueuetest
{
public class DecOctConversion
{
public static void Main(string[] args)
{
int n=2468;
if(args.Length>0)
{
n=int.Parse(args[0]);
}
Stack s=new Stack(20);
Console.WriteLine("十进制数:{0}->八进制:",n);
while(n!=0)
{
s.Push(n%8);
n=n/8;
}
int i=s.Count;
while(i>0)
{
Console.Write(s.Pop());
i--;
}
Console.WriteLine();
}
}
}
输出结果为:
十进制数:2468->八进制:4644
上述程序中,我们使用了泛型栈类Stack
顺序栈采用一组连续的存储空间存放栈的数据元素,可以用下面声明的sequenced stack类来实现栈:
public class SequencedStack
{
private T[] items;
private const int empty=-1;
private int top= empty;//top为栈顶元素下标,简称栈顶指针
....
}
SequencedStack类中的成员变量items定义为数组类型,即准备用数组存储栈的数据元素。
成员变量top指示当前栈顶数据元素在数组items中的下标,起栈顶指针的作用。
成员变量empty起符号常量的作用
用类的构造方法初始化一个栈对象,在构造方法中为items数组变量申请指定大小的存储空间,以备用于存放栈的数据元素,通过使用top值为empty来设置栈初始状态为空(empty则定义为常量-1)
public SequencedStack(int n)
{
inems=new T[n];
top=empty; //设置栈初始状态为空
}
//缺省构造方法,不带参数时,构造具有16个存储单元的空栈
public SequencedStack():this(16){}
通过count来实现
public int Count
{
get{return top+1;}
}
分别通过bool类型的属性Empty和Full来实现,编码如下:
public bool Empty
{
get{return top==empty;}
}
public bool Full
{
get{return Top>=item.Length-1;}
}
通过Push来实现入栈操作。当栈实例当前预分配的存储空间已满则需要调用本类中定义的私有方法DoubleCapcity重新分配空间,编码如下:
public void Push(T k)
{
if(Full)DoubleCapacity();
top++; 顶端位置+1
items[top]=k 顶端加元素k
}
//扩充顺序栈的容量
private void DoubleCapacity()
{
int c=Count;
int capacity =2*items.Length;
T[]copy=new T[capacity]; //按照新容量构造一个数组
for (int i=0;i
如果栈顶分配的空间大小合理,栈处于非满的状态,则Push操作的时间复杂度为O(1),如果经常需要增加存储容量,则复杂度为O(n);
Push方法的形参k声明为T类型,即入栈的数据元素声明为T类型,在调用该操作时,实参的类型要与栈实例定义时声明的类型保持一致。例如定义s为SequencedStack
定义Pop方法实现出栈操作,取出栈顶元素,并设定下一个元素为新的栈顶元素
public T Pop()
{
T k=default(T); //k初始化为缺省值
if(!Empty)
{
k=items[top];
top--;
return k;
}
else //栈为空
{
throw new InvalidOperationException("Stack is Empty:"+this.GetType());
}
}
定义Peek方法实现探测栈顶元素值的操作.该操作获取栈顶元素,但不移除该数据元素,栈顶指针top不变。
public T Peek()
{
if(!Empty)
return items[top];
else
throw new InvalidOperationException("Stack is empty"+this.GetType());
}
当栈非空。从栈顶结点开始,直到栈底结点,依次显示各结点的值
public void Show(bool showTypeName=false)
{
if(showTypeName)
{
Console.WriteLine("SequencecStack:");
}
if(!Empty)
{
for(int i=this.top;i>=0;i--)
{
Console.Write(items[i]+" ");
}
Console.WriteLine();
}
}
比起在控制台显示信息,更为一般的操作,是以字符串的形式返回对栈对象而言有意义的值,这可以通过在顺序栈SequencedStack类重写(override)从object类继承的ToString的方法来实现。
public override string ToString()
{
StringBuilder s=new StringBuilder();
for(int i=this.top;i>=0;i--)
{
a.Append(items[i]);
s.Append(";");
}
return s.ToString();
}
例:调用栈的顺序栈的基本操作,测试SequencedStack类
namespace stackqueuetest
{
class SequencedStackTest
{
public static void Main(string[] args)
{
int i =0;
SequencedStack s1=new SequencedStack(20);
Console.Write("Push:");
while(i
声明LinkedStack类实现栈的链式存储
namespace DSA
{
public class LinkedStack:SingleLinkedList
{
private SingleLinkedNodetop;
...
}
}
这里的新类LinkedStack是作为前一章定义的类SIngleLinkedList的子类来定义的
用构造方法创建栈对象并对它进行初始化,它首先用jilei(SingleLinkedList)类的构造方法创建一条单向链表,接着将成员变量top指向第一个数据节点,设置栈的状态初始为空
由基类SingleLinkedList继承的公共属性Count即可完成返回栈的元素个数的功能
用布尔属性Empty来实现判断栈是否为空的功能,实现代码如下:
public override bool Empty{get{return top==null;}}
定义Push方法实现入栈操作
为将插入的数据元素值k构造一个新结点q,在top指向的栈顶结点之前插入结点q作为新的栈顶结点,并使成员变量top指向它。入栈的数据元素是T类型,在调用该操作时,实参的类型要与栈实例定义时声明的元素类型保持一致。
采用动态分配方式为每一个新结点分配内存空间,方法的运算复杂度为O(1)
public void Push(T k)
{
SingleLinkedNodeq= new SingleLinkedNode(k);
q.next=top;
top=q;
base.Head.Next=top;
}
定义pop方法实现出栈操作
public T Pop()
{
T k=default(T); //置变量k为T类型的缺省值
if(!Empty) //栈不空
{
k=top.Item; //取得栈顶数据元素
top=top.Next; //删除栈顶元素
base.Head.Next=top;
return k;
}
else
{
throw new InvalidOperationException("stack is Empty:"+this.GetType());
}
}
定义peek操作来获取栈顶数据元素,运算复杂度为O(1);
public T Peek()
{
if(!Empty)
{
return top.Item;
}
else //栈为空时产生异常
{
throw new InvalidOperationException("stack is Empty:"+this.GetType());
}
}
程序中函数的嵌套调用是指在程序运行中,一个函数的执行语句序列中存在队另一个函数的调用,每个函数在执行完与函数调用后再返回到调用它的函数中继续执行,对于多层嵌套调用来说,函数返回的次序和函数调用的次序刚好相反,整个过程具有后进先出的特征,系统通过建立一个栈结构来协助实现这种函数嵌套调用机制。
例如:
执行函数A时,函数A中的某语句又调用了函数B,系统要做如下一系列的入栈操作:
①将调用语句后的下一条语句作为返回地址信息保存在栈中,,该过程为保存现场
②将函数A调用函数B的实参保存在栈中,该过程为实参压栈
③控制交给函数B,在栈中分配函数B的局部变量,然后开始执行函数B内的其他语句
函数B执行完毕后,系统要做一系列的如下出栈操作才能保证将系统控制返回到调用函数B的函数A中:
①退回栈中的函数B的局部分配的空间
②退回栈中为函数B的参数分配的空间
③取出保存在栈中的返回地址信息,该过程为恢复现场
判断C#表达式中括号是否匹配
在高级编程语言的表达式中,括号一般都是要求左右匹配的,对于一个给定的表达式,可用栈来辅助判断其中括号是否匹配。
假设在C#语言的表达式中,只能出现圆括号用以改变运算次序,而且圆括号是左右匹配的。
本例声明MatchExpBracket类,对于一个字符串expstr,方法MatchingBracket(expstr)判断字符串expstr中的括号是否匹配,例如,当字符串expstr保存表达式((9-1)*(3+4))时,MatchingBracket()方法的算法描述:第一个"("入栈,第二个"("入栈,遇到")"时,出栈一个"(",表达式检测完后,栈为空即可。
方法MatchingBracket()的实现算法描述如下:
-》1:设NextToken是待检测字符串expstr的当前字符,s1是算法设置的一个栈:
若NextToken是左括号,则NextToken入栈;
若NextToken是右括号,则从s1中出栈一个符号,若为左括号,表示括号匹配
-》2:重复上一步,检测结束后,若栈为空,则表示括号匹配,否则缺少右括号。
代码段如下:
using System;
namespace stackqueuetest
{
public class MathExpBracket
{
public static void Main(string[] args)
{
string expstr1="((9-1)*(3+4))";
Console.WriteLine(expstr1);
Console.WriteLine("Matching Bracket: "+MatchingBracket(expstr1));
}
public static string MatchingBracket(string expstr)
{
SequencedStack s1= new SequencedStack(30);
char NextToken,outToken;
int i=0;bool LlrR=true;
while(LlrR&&i
程序运行结果:
((9-1)*(3+4))
Matching Bracket:期望)!
与栈一样,队列(queue)也是一种常用的线性数据结构,它的元素之间具有顺序的逻辑关系。
它具有“先进先出(first in first out)”特性的数据结构
队列作为一种特殊的线性结构,可以如同线性表及栈一样,采用顺序存储结构和链式存储结构实现。
顺序存储结构实现的队列称为顺序队列(Sequenced queue),链式存储结构实现的队列称为链式队列(Linked Queue)
Initializi:队列的初始化。创建一个队列实例,并进行初始化操作,例如设置队列状态为空
Count:队列元素计数
Empty:判断队列是否为空
Full:判断队列的状态是否已满
Enqueue:入队。该操作将新的数据元素从队尾加入对列。在入队以前必须进行判断队列是否已满,如果队列不满,则接受,如果已满则会产生上溢错误(queue overflow exception),或者为队列先分配更大的空间,然后接受新元素
Dequeue:出队。该操作取出队头的数据元素,下一个数据元素为新的队头元素。
在出队前必须进行判断队列是否为空,队列为空则会产生下溢错误(queue underflow exception)
Peek:探测队首。获得队首数据元素,但不移除,队头指针保持不变
在C#的类库中定义了一个非泛型队列Queue类和一个泛型队列Queue
例:创建字符串类型的队列对象并向其添加若干值,打印出队列的内容
using System;
using System.Collections.Generic;
namespace stackqueuetest
{
public class SamplesQueue
{
public static void Main()
{
Queue q= new Queue();
q.Enqueue("F");
q.Enqueue("S");
q.Enqueue("T");
Console.WriteLine("queue");
Console.WriteLine("\tConunt :{0}",q.Count);
Console.WriteLine("\tValue: {0}", q);
foreach(string o in q)
{
Console.Write("{o}\t",o);
}
Console.WriteLine();
}
}
}
顺序队列用一组连续的存储结构存放队列的数据结构,可以用SequencedQueue类来实现顺序队列,SequencedQueue类中的成员变量items定义为数组,用以存储加入队列的数据元素;成员变量front和rear分别作为队首元素在数组items中的位置下标和下一个将入队的数据元素将占据的存储单元的位置下标,构成了队首指针和队尾指针
用该类定义和构造的对象就是一个个具体的队列实例
public class SequencedQuene
{
private T[]items;
private int front,rear;
...
}
front是队头的数据元素的下标,简称队头下标;rear是下一个入队的数据元素将占据的存储单元的位置下标。
元素入队或者出队时,需要相应修改front和rear变量的值:一个元素入队时,rear+1,而一个元素出队时,front+1
假设现在有三个数据元素(a,b,c)已入队,那么front=0,rear=3
接着ab两个元素出队,此时front=2,rear=3
接着两个新元素d和e入队,此时front=2,rear=5
假设数组items的长度为5,此时如果有新的数据元素入队,那么应该存放在rear指示的位置,但是此时rear为5,数组下标越界而产生溢出,这是一种假溢出
要解决假溢出,应该将顺序队列设计成逻辑上的“环形”结构,看似是一种顺序循环队列
为实现顺序队列循环利用存储空间,进行入队和出队操作时,front和rear不是简单的+1,而是应该+1后再作取模运算
rear=(rear+1)%items.Length;
front=(front+1)%items.Length;
顺序循环队列的操作为SequencedQueue类的方法和属性成员予以实现
①队列初始化
用类的构造方法初始化一个队列对象,在构造方法中首先为items数组变量申请指定大小的存储空间,以备用来存放队列的数据元素;接着设置队列初始状态为空,front=0,rear=0
public SequencedQueue(int n)
{
items=new T[n+1];
front=rear=0;
}
public sequencedQueue():this(16){}
②返回队列的元素个数
使用Count来实现
public int Count
{
get{return(rear-front+items.Length)%items.Length;}
}
③判断队列的状态是否为空或满
使用Empty和Full来实现,当front==rear时,表明其中没有数据元素,队列为空,编码如下:
public bool Full
{
get{return front==(rear+1)%items.Length;}
}
④入队
定义Enqueue方法来实现,如果队列当前预分配的存储空间已装满数据元素,在后续的操作前,需要调用本类中定义的私有方法DoubleCapacity重新分配空间,将原数组中的数据元素逐个拷贝到新数组,并相应调整队首和队尾指针,实现代码如下:
public void Enqueue(T k)
{
if(Full)DoubleCapacity();
items[rear]=k;
raer=(rear+1)%items.Length;
}
private void DoubleCapacity()
{
int i,j;
int capacity=2*items.Length-1;
int count=Count;
T[]copy=new T[capacity] //按照新容量构建一个数组
for(i=0;i
⑤出队
定义Dequeue方法来实现出队操作。需要先测试队列是否为空,当队列不为空时,取走front位置上的队首数据元素,front+1,新front位置上的数据元素成为新的队首数据元素,此方法的时间复杂度为O(1),编码如下:
public T Dequeue()
{
T k=default(T);
if(!Empty)
{
k=items[front];
front=(front+1)%items.Length;
return k;
}
else
{
throw new InvalidOperationException("Queue is Empty:"+this.GetType())
}
}
⑥获取队首对象,但不移除
使用peek方法来进行该操作,当队列不为空时,取走front位置上的队首数据元素,front位置保持不变,运算复杂度为O(1),编码如下:
public T Peek()
{
if(!Empty)return items[front]
else
{
throw new IndexOutOfRangeException("Queue is empty:"+this.GetType)
}
}
⑦输出队列中所有数据元素
当队列非空,从队首结点开始,直到队尾结点,依次输出结点值,编码如下:
public void Show(bool showTypeName=false)
{
if(showTypeName)
Console.Write("SequencedQueue: ");
int i=this.front;
int n=i;
if(!Empty)
{
if(i
由此可见:顺序循环队列相比原始顺序队列的设计,有两方面的改进:
-①入队时只改变下标rear,出队时只改变下标front,它们都做“循环”移动,取值范围是0-items.Length-1,这样可以重复的使用队列内部的存储空间,避免“假溢出”
-②在队列中设立一个空位置,如果不设立一个空位置,则队列空和队列满两种状态的条件都是队头指针和队尾指针相等
例:测试顺序循环队列的操作实现
using System;
namespace stackqueuetest
{
class SequencedQueueTest
{
public static void Main(string[] args)
{
int i=0,n=2;
SequencedQueue q1=new SequencedQueue();
while(i
在控制台窗口可以用如下命令进行编译:
csc SequenedQueueTest.cs/r:...\stackqueue\bin\Debug\stackqueue.dll
从命令行输入参数运行SequencedQueueTest程序:
SequencedQueueTest HelloWorld
程序结果如下:
Enqueue:1 SequeueQueue:1
....
声明LinkedQueue类实现链式队列:
public class LinkedQueue
{
private SingleLinkedListitems;
private SingleLinkedNodefront,rear;
}
用构造方法创建一条准备用以存储队列数据的单向链表,设置队列的初始状态为空
public LinkedQueue()
{
items=new SingleLinkedList();
front=items.Head.Next;
rear=items.Head;
}
使用Count来实现,编码如下:
public int Count;
{
get{return items.Count;}
}
内嵌成员items的数据结点的个数(items.Count)也就是队列的元素个数
定义属性Empty来实现
public bool Empty
{
get{return(front==null)&&(rear==items.Head);}
}
定义Enqueue方法实现入队操作
public void Enqueue(T k)
{
SingleLinkedNodeq=new SingleLinkedNode(k);
rear.Next=q;
front=items.Head.Next;
rear=q;
}
定义Dequeue方法来实现出队操作,方法的运算复杂度为O(1).
public T Dequeue()
{
T k=default(T); //置变量k为T类型的缺省值
if(!Empty)
{
k=front.Item; //取得队头结点数据元素
front=front.Next; //删除队头结点
items.Head.Next=front;
}
if(front==null)
{
rear=items.Head;
}
return k;
else
{
throw new InvalidOperationException("Queue is Empty: "+this.GetType());
}
}
当队列不为空时,取走front位置上的队首数据元素,运算复杂度为O(1).
public T Peek()
{
if(!Empty)
return front.Item;
else
throw new InvalidOperationException("Queue is Empty: "+GetType());
}
队列是一种先进先出的特殊线性结构,可以作为求解具有“先进先出”特性的数学模型,因此队列结构成为解决相应问题算法设计的有力工具。
例:(解素数环问题)将1,2,...,n个数排成环形,使得每相邻两数之和为素数,构成一个素数环。
解决思路:依次试探每个数,用一个线性表存放素数环的数据元素,用一个队列存放等待检查的数据元素,依次从队列取出一个数据元素k与素数环最后一个数据元素相加,如果两个数之和为素数,则将k加入素数环中,否则暂时无法进入素数环,必须让他再次放入队列等待处理,重复上述操作,直到列表为空
本例应用顺序表SequencedList类和顺序队列SequencedQueue类。创建SequencedList类的一个线性表实例ring1,用以存放素数环的数据元素,创建SequecedQueue类的一个实例q1作为队列,存放待检测的数据元素,静态方法IsPrime(k)判断k是否为素数。
using System;
using System.Collections;
using System.Collections.Generic;
namespace stackqueuetest
{
public class PrimeRing
{
public static bool IsPrime(int k)
{
int j=2;
if(k==2)return true;
if(k<2||k>2&&k%2==0)return false;
else
{
j=3;
while(jk)return true;
else return false;
}
}
public static void Main(string[] args)
{
int i,j,k,n=10;
SequencedQueue q1=new SequencedQueue(); //创建队列q1
SequencedList ring1=new SequencedList(); //创建线性表ring1表示素数环
ring1.Add(1);
for(i=2;i