数据结构 C#描述 第五章 栈和队列
第五章:栈与队列
数据一般都以线性表的存在的。我们已经用了Array和ArrayList类来将数据组成线性表。虽然这些数据结构帮助我们在处理的时候把数据分成了很方便的形式,但是没有一种结构为实际的设计和解决问题的方案提供一个真的方法。
两个面向线性表并且很容易理解的数据结构就是栈和队列。在栈中的数据的添加和移除都是线性表的尾端来进行的,然而在队列中的数据在线性表的一端添加,移除数据却在表的另外一端。栈在程序设计语言中被广泛的应用,从计算表达式到函数的调用。队列被用在操作系统的进程和真实世界中的事务提交。
C#为这些数据结构提供了两个类:Stack和Queue类。我们将会讨论怎样去用这些类,而且实现一些操作。
Stacks,一个栈的实现,Stack类
栈在数据结构中是很常用的。我们定义将数据元素只在尾端访问的出线性表为一个栈,尾端就是栈顶。标准的栈的模型就是食堂的一堆盘子。盘子总是从最上面拿走,就像出栈,当洗碗工洗碗盘后有把盘子放到最上面,就像入栈。栈是后进先出的数据结构。
栈的操作
栈的主要两种操作就是把元素添加到栈中和把元素从栈中取出。Push操作就是向栈中添加元素,Pop操作就是把元素从栈中取出。操作如图5.1
另外的一种操作就是查看栈顶的元素。Pop操作返回栈顶的元素,同时也将元素从栈顶移除。当我们只是想查看栈顶的元素,而不想把元素从栈中移除时,在C#中我们可以用Peek操作,在其他的语言中这个操作是用其他不同的名字和不同的实现(还有的语言中Peek就是Pop)。
Pushing,popoing,peeking都是当我们用一个栈时的主要的操作。当然,还有我们还要去执行其他的一些操作。把所有的元素一次从栈中移除的操作是很有用的。把栈清空的操作成为Clear方法。同时,知道栈中有多少个元素也是很有用的操作,我们将之称为Count属性。在很多的实现中都有一个StackEmpty的方法通过返回true或者false来判断栈的状态,但是我们可以用Count属性来达到同样的目的。
在.NET Framework中的Stack类实现了所有的这些操作,但是在我们用他们之前,让我们来看看当没有Stack类的时候,你怎样去实现一个Stack类。
Stack类的实现
栈的实现要用一个基本的数据结构来保存数据。我们将会选中ArrayList,因为当我们添加元素进栈的时候不用担心线性表的大小的问题。
因为C#有很好的面向对象的特点,我们将会把栈实现为一个类,称为CStack。这个类有一个构造器和上述提到过的操作。Count被实现为一个属性。
我们需要的最重要的变量是一个ArrayList对象来存储栈的元素。我们还要一个变量index来跟踪栈顶,当栈初始化时变量初始化为-1。每次有元素添加到栈时,变量就加1.
类的构造器除了把变量初始化wi-1,就不做其他的事情了。第一个方法是Push。代码将会调用ArrayList的Add方法来将元素添加到ArrayList。Pop方法做三件事:调用RemoveAt方法移除栈定的元素,将index的值减1,最后返回移除的元素对象。
Peek方法通过调用以index变量为参数的Item方法来实现的。Clear方法简单的调用ArrayList的identical方法。Count属性在ArrayList中是一个只读的,因为我们不想意外的改变栈中元素的个数。
代码入下:
using System;
using System.Collections;
public class CStack
{
private int p_index;
private ArrayList list;
public CStack()
{
list = new ArrayList();
p_index = -1;
}
public int Count
{
get
{
return list.Count;
}
}
public void Push(object item)
{
list.Add(item);
p_index++;
}
public object Pop()
{
object obj = list[p_idnex];
list.RemoveAt(p_index);
p_index--;
return obj;
}
public void Clear()
{
list.Clear();
p_index = -1;
}
public object Peek()
{
return list[p_index];
}
}
我们现在来使用这些代码来写一段程序。
回文字符串就是从前向后看个从后向前看都是一样的字符串。例如,“dad”,madam,”sees”都是回文串,但是”hello“就不是的。检查回文字符串的一个方法就是用栈。算法大概思路就是读一个字符串的时候都是先读一个个的字符,然后把每个已读字符放入栈中。这样字符串在栈中就是反的,然后把一个个的字符从栈中弹出,同时和原字符串的开头比较。如果任何位置的两个比较的字符串都相同,那么字符串就是回文,否则就不是。
代码如下:
static void Main()
{
CStack alist = new CStack();
string ch;
string word = "sees";
bool isPalindrome = true;
for (int x = 0; x < word.Length; x++)
{
alist.Push(word.Substring(x, 1));
}
int pos = 0;
while (alist.Count > 0)
{
ch = alist.Pop().ToString();
if (ch != word.Substring(pos, 1))
{
isPalindrome = false;
break;
}
pos++;
}
if (isPalindrome)
Console.WriteLine(word + " is a Palindrome");
else
Console.WriteLine(word + " is not a Palindrome");
Console.Read();
}
Stack类
Stack类是实现了ICollection接口的一种后进先出的集合。
Stack类的构造器
有三种方式来初始化一个栈对象。默认的构造器在初始化栈是分配10个空间。默认构造器如下:
Stack mystic=new Stack();
一个泛型版的构造器如下:
Stack<string> myStack=new Stack<string>();
每当栈满时,栈的容量就自动的加倍。
栈的第二种构造器允许你从另外的数组对象来创建一个栈。例如创给构造器一个数组,栈就以这些已经存在的数组创立元素。
String[] names=new Strinf[]{“Rayond”,”David”,”Mike”};
Stack nameStack=new Stack(names);
当执行Pop操作时,“Mike”就首先出栈。
你还可以通过申明栈的容量来初始化栈。当你想要知道栈可以存多少元素时,这种初始化方法就很好。用种方式使得程序更加高效。
初始化代码如下:
Stack myStack=new Stack();
栈的操作
操作一个栈时的主要操作是哦Push,Pop。把数据添加到栈中用Push方法。把数据中栈中移除用Pop操作。让我们来看看用栈的这些方法来进行算术运算。
算术表达式用了两个栈:一个操作数的栈,一个操作符的栈。算术表达式以字符串的形式存储。我们将字符串分割为单个的字符,用一个For循环来读取表达式中的每一个字符。如果字符是一个数字,便把它放入操作数栈中;如果字符是一个操作符,就放入操作符栈中。
算法如下:
Peek方法
Peek方法允许我们在不弹出元素的前提下查看栈定的元素值。没有这个方法,为了知道一个元素的值,你可能就要把这个元素弹出栈。
If(IsNumberic(Nums.Peek))
Num=Nums.Pop();
Clear方法
Clear方法把栈中的所有元素都移除,并且将栈的count设置为0。我们很难说,Clear方法是否对栈的容量有影响,因为我们不能检查栈的实际的容量,所以我们最好就假设栈的容量回到了我们当时初始化的那个大小――10个元素。
当在处理的过程中有错误产生时,用Clear方法就非常有用了。例如,在计算算术表达式时,当除数是0的时候,错误产生,我们就想把栈清空:
If(oper2==0)
Nums.Cleat();
Contains方法
Contains方法用来确定一个指定的元素是否在栈中。这个方法返回true或者false。我们可以用这个方法来判断一个值是否在栈中,这个值不一定是栈顶的元素,例如,在某些情况下,栈中包含某些字符就会产生错误:
,if(myStack.Contains(“ “))
StopProcessing();
,else
ContinueProcessing();
CopyTo和ToArray方法
CopyTo方法把一个栈中的内容复制到一个数组中。这个数组必须是Object类型的,因为栈中的元素的类型是Object的。这个方法需要两个参数:一个数组和数组中放入栈元素的起始索引。元素都以后进先出的顺序存放,就好像把元素从栈中一个个的弹出一样的。下面的代码描述了方法:
Stack myStack = new Stack();
for (int i = 20; i > 0; i--)
myStack.Push(i);
object[] myArray = new object[myStack.Count];
myStack.CopyTo(myArray, 0);
ToArray方法和上面的差不多。只是你不能指定插入位置的索引,如下:
Stack myStack = new Stack();
for (int i = 20; i > 0; i--)
myStack.Push(i);
object[] myArray = new object[myStack.Count];
myArray=myStack.ToArray();
栈的例子:十进制到多进制的转换
虽然十进制经常用在商业应用,但是一些科学计算,技术应用中也要求其他进制的数据。很多的计算机系统应用中都要求八进制或者二进制。
我们可以用栈来实现进制转换的问题,算法步骤如下:
获取数据
获取要转换的进制
循环
把数据整除进制后入栈
数据成为了整除后的数组
直到数据不为0
一旦循环完成,你就已经把数据转换了,直到把数据从栈中弹出就得到结果了。实现代码如下:
using System;
using System.Collections;
public class ccStack
{
static void Main()
{
int num, baseNum;
Console.Write("输入一个十进制数:");
num = Convert.ToInt32(Console.ReadLine());
Console.Write("输入转换的进制:");
baseNum = Convert.ToInt32(Console.ReadLine());
Console.Write (num+"converts to");
MulBase(num,baseNum);
Console.WriteLine("bsae" + baseNum);
Console.Read();
}
static void MulBase(int n, int b)
{
Stack Digits = new Stack();
do
{
Digits.Push(n % b);
n /= b;
}
while (n != 0);
while (Digits.Count > 0)
Console.Write(Digits.Pop());
}
}
这段程序说明了为为什么栈在很多的计算问题中都是有用的数据结构。
虽然栈是很有用的数据结构,但是一些程序还用了一些其他的线性的数据结构。例如,从你家到商品的这条线上,货物从商品发出,到你家就受到了。不像栈,现在的这条线是先进先出的。另外的一个例子就打印机的工作,先进出的东西先打印。这些例子都是队列的模型。
队列,队列类以及队列类的实现
队列是一种数据在尾端进入,在前端出去的的数据结构。队列是先进先出的数据结构。队列经常用在对提交给操作系统的进程排序,提交的程序用队列把用户的等待排列。
队列的操作
队列主要有两种操作:从队尾添加元素,从对头删除元素。添加元素称为入队,删除元素称为出队。如图:
另外的一个重要的操作就是查看队头的元素。和栈一样,Peek方法用来查看队头元素。这个方法简单的返回元素,而不将其从队列中移除。
在Queue类中还有其他的属性,可以使得我们更好的编程。和之前一样,我们首先来自己实现一个Queue类。
Queue类的实现
就像我们实现Stack类一样,我们也用ArrayList来实现队列。用ArrayList来实现这些数据结构是很好的选择,因为ArrayList可以自动的增长。。当我们把元素入队列时,ArrayList的Add方法把元素添加到一个空闲的位置。当我们把元素出队列时,ArrayList的Remove方法就从已经存在的元素中移除元素。 下面的实现Queue的类包含了EnQueue,DeQueue,ClearQueue,Peek,Count方法,也包含一个构造器方法:
using System;
using System.Collections;
public class CQueue
{
private ArrayList pqueue;
public CQueue()
{
pqueue = new ArrayList();
}
public void EnQueue(object item)
{
pqueue.Add(item);
}
public void DeQueue()
{
pqueue.RemoveAt(0);
}
public object Peek()
{
return pqueue[0];
}
public void ClearQueue()
{
pqueue.Clear();
}
public int Count()
{
return pqueue.Count;
}
}
Queue类的一个简答的应用
我们已经在上面的Queue类中实现了主要的一些方法,下面就来看看这样利用这个Queue类。我们可以以现有的Queuel类为基本,为某个程序而对类的一些方法做进一步的开发。首先,我们来顺义县Queue类一些基本的属性。
当一个Queue类初始化时,队列的默认的容量是32个元素。通过定义,可以使的当队列满的时候,他的容量就扩大因子为2。这就意味这当初始的那个队列满了的时候,他的新的容量就是64。你不必设置这些容量的数据。当你要初始申明一个特定容量的时候,你可以这样:
Queue myQueue=new Queue(100);
那么这个队列的初始的容量就是100.你也可以改变容量的增涨的因子。如下,第二个参数就是增涨的因子:
Queue myQueue=new Queue(32,3);
这行代码就申明了容量的增涨的因子是3.
泛型版的队列如下:
Queue<int> numbers=new Queue<int>();
正如之前所提的,队列用在对排队的用户请求进行提交。
用队列存储数据
队列的另外一个应用就是存储数据。回溯计算机的早期,通过纸带把程序输入计算机中,每一条纸带就是一句程序语句。我们用队列将纸带上的处理程序排队。我们称这种技术为基数排序。基本排序在程序设计中不是很快的排序,但是它却说明了队列的另外的一种用法。
优先队列:演化的Queue类
如你所知,队列是个先进先出的数据结构。操作的结果是使得最早进入的数据最先出去。但是在很多的应用中,需要一种数据结构来使得优先级高的项先出去,即使这个项不是最先进入的数据集合的。这就是优先级队列应用的一种情况。
有很多利用优先操作的应用。计算机的操作系统就是一个很好的例子。某一个进程比另外的一个进程的优先级高,例如,打印进程就是一个相对优先级较低的进程。进程或者作业通常通过他们的优先级来编号,例如,编号为0 的优先级就比20 的要高
存储在一个优先级队列中的元素一般是键值形式的,键就是元素的优先级,而值就是元素的真实值。例如,在操作系统中的进程可能是这样定义的:
struct Process
{
int priority;
string name;
}
我们不能用一个没有修改的Queue对象来做优先级队列。当调用DeQueue方法时,它只是简单的把第一个元素移除。所以我们要从已有的Queue类中继承,并且重写DeQueue方法来达到我们的目的。
我们将这个类称为PQueue。我们可以用Queue的所有方法,并且重写 DeQueue方法使得优先级高的元素先移除。为了把优先级高且不在队列第一个的元素移除,我们首先要将队列的元素写到一个数组中。然后我们遍历数组,找到优先级最高的元素项。最后,将那个元素项标号,再重建队列,并且移除标号的元素项。