[翻译]C#数据结构与算法 – 第五章栈与队列(Part 2)
队列,Queue类及一个队列类的实现
队列是这样一种数据结构,数据由列表的尾部进入,并由列表的头部被移除。队列用于按项的出现顺序来存储它们。队列是先进先出(FIFO)数据结构的一个代表。队列常用于提交给操作系统的命令的处理或提交给一个打印池任务的处理,另外模拟程序使用队列模型模拟排队等候的顾客。
队列操作
与队列相关的两个主要操作是向队列中添加一个项目与由队列中移除一个项目。向队列中添加项的操作被称作入队(Enqueue),由队列移除一个项的操作被称作出队(Dequeue)。入队操作向队列的尾部添加一个项,出队操作由队列的头部移除一个项。图5.2展示了这些操作。
另一个在队列上执行的主要操作是查看头部的项目。Peek方法,类似Stack类中的同名方法,用于查看起始位置的项目。这个方法仅返回这个项目而不是将其由队列中移除。
Queue类中有许多其它属性可以用来辅助我们的编程。然而,在我们讨论它们之前,让我们看一下怎样实现一个队列类。
实现一个队列
使用ArrayList来实现一个队列类几乎是不用多想的,正如我们栈类的实现。由于其内置的特性,ArrayList是实现这些类型数据结构的一个极佳选择。当我们需要向队列插入一个项,ArrayList的Add方法将元素放置在列表中下一个空闲位置。当我们需要由队列中移除开始位置的项时,ArrayList将列表中剩余项目依次向前移动一个位置。我们不需要维护一个占位标志,其可能导致代码中出现隐蔽的错误。
下面这个队列类的实现包含了EnQueue,DeQueue,ClearQueue(清空队列),Peek方法与Count属性以及一个默认的构造函数:
1 public class CQueue 2 { 3 private ArrayList pqueue; 4 5 public CQueue() 6 { 7 pqueue = new ArrayList(); 8 } 9 10 public void EnQueue(object item) 11 { 12 pqueue.Add(item); 13 } 14 15 public void DeQueue() 16 { 17 pqueue.RemoveAt(0); 18 } 19 20 public object Peek() 21 { 22 return pqueue[0]; 23 } 24 25 public void ClearQueue() 26 { 27 pqueue.Clear(); 28 } 29 30 public int Count() 31 { 32 return pqueue.Count; 33 } 34 }
Queue类:一个示例程序
之前我们已经介绍了Queue类中的主要方法,并在实现一个自己的队列类的过程中学习了它们。我们在使用队列作为基础数据结构的一个特殊的编程问题中进一步探寻这些方法。首先,我们需要提几个Queue对象的基本属性。
当一个新的Queue对象被初始化,这个队列的默认容量为32。通过定义,当队列被填满,其大小将以2倍方式增长。这意味着当一个队列初始容量被填满,其容量将变为64.而且这个数字没有限制。当你初始化一个队列时,你可以指定一个不同的初始容量。如下所示:
1 Queue myQueue = new Queue(100);
这个语句将队列的容量设置为100。你也可以更改增长的倍数,其作为第二个参数传入构造函数,如下:
1 Queue myQueue = new Queue(32, 3);
这行语句在默认初始容量的基础上指定了一个3倍的增长速度。即使这种情况下需要的容量与默认初始容量相同你也需要手工指定,因为此时类构造器寻找一个与默认构造函数是不同签名的函数。
一个泛型的队列的初始化方式如下:
1 Queue<int> numbers = new Queue<int>();
正如之前提到的,队列常用来模拟人们排队的情况。一种场景是我们可以用队列模拟位于Elks Lodge的年度单身午夜舞会。男士女士各排成一队进入聚会地。舞台非常小同一时间仅可以供3对男女。由于舞台空间所限,当有空存在时,由男女各自的队列中请出第一个组成一对舞伴。当这一对被由队列中取出后,排在后一位男士与女士移动到队列的最前列。
当这个动作发生时,程序将显示这一对舞伴已经接下来队列中最前面的人。如果没有一对完整的舞伴,队列中下一个人将被宣布。如果队列中没有剩余的人,将宣布这种情况。
首先,让我们看一下我们用来模拟的数据:
F Jennifer Ingram
M Frank Opitz
M Terrill Beckerman
M Mike Dahly
F Beata Lovelace
M Raymond Williams
F Shirley Yaw
M Don Gundolf
F Bernica Tackett
M David Durr
M Mike McMillan
F Nikki Feldman
我们使用一个数据结构来表示每一个舞者,两个简单的String类的方法(Chars与Substring)被用构建一个舞者。如下是程序:
1 using System; 2 using System.Collections; 3 using System.IO; 4 5 namespace csqueue 6 { 7 public struct Dancer 8 { 9 public string name; 10 public string sex; 11 12 public void GetName(string n) 13 { 14 name = n; 15 } 16 17 public override string ToString() 18 { 19 return name; 20 } 21 } 22 23 class Class1 24 { 25 static void newDancers(Queue male, Queue female) 26 { 27 Dancer m, w; 28 m = new Dancer(); 29 w = new Dancer(); 30 31 if (male.Count > 0 && female.Count > 0) 32 { 33 m.GetName(male.Dequeue().ToString()); 34 w.GetName(female.Dequeue().ToString()); 35 } 36 else if ((male.Count > 0) && (female.Count == 0)) 37 Console.WriteLine("Waiting on a female dancer."); 38 else if ((female.Count > 0) && (male.Count == 0)) 39 Console.WriteLine("Waiting on a male dancer."); 40 } 41 42 static void headOfLine(Queue male, Queue female) 43 { 44 Dancer w, m; 45 m = new Dancer(); 46 w = new Dancer(); 47 48 if (male.Count > 0) 49 m.GetName(male.Peek().ToString()); 50 51 if (female.Count > 0) 52 w.GetName(female.Peek().ToString()); 53 54 if (m.name != " " && w.name != "") 55 Console.WriteLine("Next in line are: " + m.name + "\t" + w.name); 56 else if (m.name != "") 57 Console.WriteLine("Next in line is: " + m.name); 58 else 59 Console.WriteLine("Next in line is: " + w.name); 60 } 61 62 static void startDancing(Queue male, Queue female) 63 { 64 Dancer m, w; 65 m = new Dancer(); 66 w = new Dancer(); 67 68 Console.WriteLine("Dance partners are: "); 69 Console.WriteLine(); 70 71 for (int count = 0; count <= 3; count++) 72 { 73 m.GetName(male.Dequeue().ToString()); 74 w.GetName(female.Dequeue().ToString()); 75 Console.WriteLine(w.name + "\t" + m.name); 76 } 77 } 78 79 static void formLines(Queue male, Queue female) 80 { 81 Dancer d = new Dancer(); 82 StreamReader inFile; 83 inFile = File.OpenText("c:\\dancers.dat"); 84 string line; 85 while (inFile.Peek() != -1) 86 { 87 line = inFile.ReadLine(); 88 d.sex = line.Substring(0, 1); 89 d.name = line.Substring(2, line.Length - 2); 90 if (d.sex == "M") 91 male.Enqueue(d); 92 else 93 female.Enqueue(d); 94 } 95 } 96 97 static void Main(string[] args) 98 { 99 Queue males = new Queue(); 100 Queue females = new Queue(); 101 formLines(males, females); 102 startDancing(males, females); 103 104 if (males.Count > 0 || females.Count > 0) 105 headOfLine(males, females); 106 newDancers(males, females); 107 108 if (males.Count > 0 || females.Count > 0) 109 headOfLine(males, females); 110 newDancers(males, females); 111 112 Console.Write("press enter"); 113 Console.Read(); 114 } 115 } 116 }
使用展示的数据执行这段代码的输出如下:
使用队列排序数据
使用队列的另一个程序是排序数据的。在计算机出现的早期,通过使用包含一条单独的程序语句的打孔卡片来将程序输入主机。卡片使用是一个利用容器样结构的机械排序设备来完成排序。我们可以使用队列来模拟这个排序过程。这个排序技术被称作基数排序。在你掌握的编程技术中基数排序不是最快的一种,但是基数排序确实展示了队列另一种有趣的用途。
基数排序的工作方式对数据集进行两次遍历,在这个例子中,整数的范围是0-99。第一趟排序基于数字的个位数,第二趟排序基于数字的十位上的数字。每个数字都被放置在与其排序标识相同的编号的罐子中。
给出如下数字:
91 46 85 15 92 35 31 22
第一趟的结果为如下罐子配置方式:
Bin 0:
Bin 1: 91 31
Bin 2: 92 22
Bin 3:
Bin 4:
Bin 5: 85 15 35
Bin 6: 46
Bin 7:
Bin 8:
Bin 9:
现在将这些数字基于其所在的罐子进行排序:
91 31 92 22 85 15 35 46
下一步获取这个列表并按照十位上数字将其放置到适合的罐子进行排序:
Bin 0:
Bin 1: 15
Bin 2: 22
Bin 3: 31 35
Bin 4: 46
Bin 5:
Bin 6:
Bin 7:
Bin 8: 85
Bin 9: 91 92
由罐子中取出这些数字并将它们放回一个列表,其结果是一个排好序的整数集:
15 22 31 35 46 85 91 92
我们可以使用队列来表示罐子以实现这个算法。我们需要9个队列,每一个数字对应一个。我们使用模数与整除来得到一个数字的个人与十位数字。剩下的事情就是将数字添加到相应的队列,由队列取出这些数字,此时它们已是基于个位数字排序的,然后针对十位数字重复这个过程。最终结果是排好序的整数列表。
如下是代码:
1 using System; 2 using System.Collections; 3 using System.IO; 4 5 namespace csqueue 6 { 7 class Class1 8 { 9 enum DigitType 10 { 11 ones = 1, 12 tens = 10 13 } 14 15 static void DisplayArray(int[] n) 16 { 17 for (int x = 0; x <= n.GetUpperBound(0); x++) 18 Console.Write(n[x] + " "); 19 } 20 21 static void RSort(Queue[] que, int[] n, DigitType digit) 22 { 23 int snum; 24 for (int x = 0; x <= n.GetUpperBound(0); x++) 25 { 26 if (digit == DigitType.ones) 27 snum = n[x] % 10; 28 else 29 snum = n[x] / 10; 30 31 que[snum].Enqueue(n[x]); 32 } 33 } 34 35 static void BuildArray(Queue[] que, int[] n) 36 { 37 int y = 0; 38 for (int x = 0; x >= 9; x++) 39 while (que[x].Count > 0) 40 { 41 n[y] = Int32.Parse(que[x].Dequeue().ToString()); 42 y++; 43 } 44 } 45 46 static void Main(string[] args) 47 { 48 Queue[] numQueue = new Queue[10]; 49 int[] nums = new int[] { 91, 46, 85, 15, 92, 35, 31, 22 }; 50 int[] random = new Int32[99]; 51 52 // Display original list 53 for (int i = 0; i < 10; i++) 54 numQueue[i] = new Queue(); 55 RSort(numQueue, nums, DigitType.ones); 56 57 //numQueue, nums, 1 58 BuildArray(numQueue, nums); 59 Console.WriteLine(); 60 Console.WriteLine("First pass results: "); 61 DisplayArray(nums); 62 63 // Second pass sort 64 RSort(numQueue, nums, DigitType.tens); 65 BuildArray(numQueue, nums); 66 Console.WriteLine(); 67 Console.WriteLine("Second pass results: "); 68 69 // Display final results 70 DisplayArray(nums); 71 Console.WriteLine(); 72 Console.Write("Press enter to quit"); 73 Console.Read(); 74 } 75 } 76 }
RSort子函数接受一个队列数组,整数数组,以及一个区分按个位还是按十位来排序的描述符。如果是按个位数字进行排序,程序将计算个位数字 – 即数字与10进行模运算的余数。如果是按十位数字进行排序,程序将计算十位数字 – 数字与10的整除结果(整除即取整数部分)。
要重建数字列表,每一个队列上都连续执行Dequeue操作直到队列变为空。这个操作在BuildArray子函数中完成。因为我们由包含最小数字的数组开始,最终构建的数字列表是有序的。
优先队列:派生自Queue类
正如你现在所知道,队列这个数据结构中第一个被放置到队列中元素将首先被由队列中移除。这种行为的效果是队列中最旧的项目将最先被移除。对于许多程序,需要这样一种数据结构 – 一个高优先级的项目需要被首先移除,即使这个项目不是结构中"最旧"的项目。有一个特殊类型队列 – 优先队列适用于这种程序。
有许多程序在它们的队列中利用了优先队列。一个很好的列子是操作系统中的进程管理。特定的进程比其它如打印进程这样优先级明显较低的进程优先级要高。进程(或任务)通常按它们的优先级来编号,一个优先级为0的进程比优先级为20的进程优先级更高。
存储于优先队列的项目通常由键-值对构成,其中键为优先级,值指示了存储了项目。例如,一个操作系统可能按如下这样定义:
1 struct Process 2 { 3 int priority; 4 string name; 5 }
我们不能将一个未更改的队列对象用于优先队列。当DeQueue方法被调用时简单的将队列中的第一个项目移除。然而,我们可以派生Queue类来定义我们自己的优先队列,并重写Dequeue来完成我们自己的命令。
我们称这个类为PQueue。我们可以使用Queue类的所有方法,除了重写Dequeue方法来移除最高优先级的项目。要由队列中移除一个不是队列中最开始位置的项,我们不得不首先将队列项写入一个数组。然后我们可以迭代数组并找到优先级最高项目。最后,用这个标记过的数组重建队列,被标记的项目被留下不用。
如下是PQueue类的代码:
1 public struct pqItem 2 { 3 public int priority; 4 public string name; 5 } 6 7 public class PQueue : Queue 8 { 9 public PQueue() 10 : base() 11 { 12 } 13 14 public override object Dequeue() 15 { 16 object[] items; 17 int min, minindex = 0; 18 items = this.ToArray(); 19 min = ((pqItem)items[0]).priority; 20 21 for (int x = 1; x <= items.GetUpperBound(0); x++) 22 if (((pqItem)items[x]).priority < min) 23 { 24 min = ((pqItem)items[x]).priority; 25 minindex = x; 26 } 27 this.Clear(); 28 29 for (int x = 0; x <= items.GetUpperBound(0); x++) 30 if (x != minindex && ((pqItem)items[x]).name != "") 31 this.Enqueue(items[x]); 32 33 return items[minindex]; 34 } 35 }
(原文貌似有误,此处做了部分修正)
下列代码演示了PQueue类的一个简单实用。急诊室的等候室给前来接受治疗的患者指派一个优先级。一个有心脏病征兆的患者要比一个严重割伤的病人更早的接受治疗。下面的程序模拟了3个病人大致在一个时刻进入急诊室的情况。每个病人都被分拣护士来查看并被分配一个优先级然后加入队列。第一个被治疗的病人是由Dequeue方法首先由队列移除的那个。
1 static void Main() 2 { 3 PQueue erwait = new PQueue(); 4 pqItem[] erPatient = new pqItem[4]; 5 pqItem nextPatient; 6 erPatient[0].name = "Joe Smith"; 7 erPatient[0].priority = 1; 8 erPatient[1].name = "Mary Brown"; 9 erPatient[1].priority = 0; 10 erPatient[2].name = "Sam Jones"; 11 erPatient[2].priority = 3; 12 13 for (int x = 0; x <= erPatient.GetUpperBound(0); x++) 14 erwait.Enqueue(erPatient[x]); 15 16 nextPatient = (pqItem)erwait.Dequeue(); 17 Console.WriteLine(nextPatient.name); 18 }
程序的输出是"Mary Brown",因为她比其他患者有更高的优先级。
总结
学会有效与恰当的使用数据结构是区分专家程序员与一般程序员的技能之一。专家程序员知道组织一个程序的数据到一个适当的数据结构使操作数据的工作变得简单。事实上,一上来就使用数据抽象的方式来考虑一个计算机编程问题可以很容易的想出一个好的解决问题的方案。
在本章中我们讨论了2种很常见的数据结构:栈和队列。栈用来解决许多不同类型的计算机编程问题,尤其是在系统编程领域如解释器和编译器。我们同样看到如何使用栈解决更普遍的问题,如判断一个单词是否为回文。
队列同样由于许多程序。操作系统使用队列来管理进程(通过优先队列),并且队列同样常用于模拟现实生活中的流程。最后,我们派生自Queue类实现了一个优先队列。由.NET Framework类库中的类派生新类的能力是C#的.NET版本的主要能力之一。
练习
1. 你可以使用栈来检查一个程序语句或者一个表达式是否有对称的圆括号。编写一个Windows应用程序,其中提供一个文本框供用户输入一个带圆括号的表达式。提供一个检查小括号的按钮,当点击时,运行一个程序来检查表达式中圆括号的数目并将不对称的圆括号高亮显示。
2. 一个后缀表达式计算器接受这种格式 - op1 op2 operator…的算术语句。使用两个栈,一个用于操作数另一个用于表达式,设计与实现一个Calculator类将中缀表达式转化为后缀表达式,然后使用栈来计算表达式。
3. 这个联系包括设计一个服务台优先管理程序。帮助请求以以下格式存储在一个文本文件中:优先级,请求方ID,请求时间。优先级是一个范围1-5的整数:1表示最不重要,5表示最重要;ID是一个4位数的唯一雇员编号;时间是使用"TimeSpan.Hours,TimeSpan.Minutes,TimeSpan.Seconds"格式来表示的。编写一个Windows应用程序,在FormLoad事件处理过程中,由包含帮助请求的数据文件中读取5条记录,使用一个优先队列来处理列表的优先级,并将结果列表显示在一个列表框中。每次一个工作完成后,用户可以点击列表中工作并将其移除。当所有5个任务都被完成后,程序将自动读取下五条记录,将它们按优先级排序,并显示在列表中。