实验一
题目要求
用C#构造一个队列Queue。要求此队列是循环队列,并进行入队、出队的测试
题意分析
-
将循环队列封装为一个类,在类内部实现入队、出队、展示等操作
-
选择数组作为存储队列的数据结构
-
在Main()方法内编写测试用函数
数据结构
队列
是一种先进先出(FIFO)的线性表,只允许在队尾(rear)进行入队操作(enqueue, push),在队头(front)进行出队操作(dequeue, pop)。其中,初始化时rear == front
,有元素之后rear指向尾元素的下一个位置。此时,当rear与**队列容量上限(MAX_SIZE)**相同时,表示队列达到上限。
在实际表示队列中,往往采用数组或者是链表进行存储。在使用数组,或是规定了队列容量上限的时候,往往会出现以下问题:**队尾不断向后(下标变大,下同)增加(enqueue),队头也会不断向后增加(dequeue),**然后:
同学们就会发现“呀这不浪费吗,明明还有位置却报告队列装不下了”。
有一个解决上述问题的方法:每次出队之后,不断将所有的元素向前挪。很容易得知这个方法的时间复杂度为O(n),比较浪费时间。
于是有人就说:“我们可不可以在逻辑上把表头表尾相接起来,变成一个循环的表,rear到顶了之后再从下标0开始存起。这样,空间是不断循环重复利用的,就不用担心有剩余空间了。”
循环队列
循环队列应运而生!
这样的数据结构很好地解决了空间时间浪费的问题,但是同时也带来了新的问题:怎么样判断队列是空还是满?
在原有的朴素队列中,判断队空只需要if (front == rear)
,而判断队满只需要if (rear - front == MAX_SIZE)
,而在循环队列中,队头和队尾在队空和队满的时候都会碰面,给大家造成困扰。有人提出了这样的解决方法:新增一个位置表示队列的元素个数。但是更加快捷,省空间的解决方法是:牺牲一个空间并用rear指向它。这样子,队空的判断条件依然是if (front == rear)
,而队满的条件变为if (rare + 1 == front)
。
而队满的条件变为
if (rare + 1 == front)
(有没有感觉这话有点问题?)
我们说循环队列只是逻辑上的循环队列,物理上存储依然是数组(一段连续的内存地址),所以完全有可能出现这样的情况:front == 0
而rear == MAX_SIZE
,这时候就出问题了,rear+1依然是一个大的数,而非0。因此,我们需要将判断语句稍加改动:
(rare + 1) % q.Length == front
这样子,当rear达到或超过了数组的最大值时,就会通过取模回到开头,从而达到了循环的效果。
完整代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace C_Sharp_test
{
public class Queue
{
// 宏定义
private const int OVERFLOW = -1;
private const int QNULL = -2;
// 私有成员定义
private double[] q;
private uint front = 0;
private uint rear = 0;
// 方法
public Queue() { q = new double[100]; } // 默认队列大小为100
public Queue(uint size)
{
while (size == 0)
{
Console.WriteLine("队列容量不可为0!");
Console.WriteLine("请输入正确的队列容量: ");
size = Convert.ToUInt32(Console.ReadLine().Trim());
}
q = new double[size + 1];
}
public int enqueue(double ele)
{
if ((rear + 1) % q.Length == front)
return OVERFLOW;
q[rear] = ele;
rear = (uint)((rear + 1) % q.Length);
return 1;
}
public int dequeue(ref double ele)
{
if (is_empty())
return QNULL;
ele = q[front];
front = (uint)((front + 1) % q.Length);
return 1;
}
public void show_queue()
{
Console.WriteLine("Queue: ");
if (is_empty())
{
Console.WriteLine("队列已空!");
return;
}
uint sub_rear = (uint)((rear + q.Length - 1) % q.Length);
while (sub_rear != front)
{
Console.Write("{0} ", q[sub_rear]);
sub_rear = (uint)((sub_rear + q.Length - 1) % q.Length);
}
Console.Write(q[sub_rear]);
Console.WriteLine();
return;
}
public uint get_len()
{
return (uint)((q.Length + rear - front) % q.Length);
}
public bool is_empty()
{
return rear == front;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("请输入队列容量: ");
uint size = Convert.ToUInt32(Console.ReadLine().Trim());
Queue q = new Queue(size);
// 入队测试
Console.WriteLine("请输入入队数据: ");
string[] lst = Console.ReadLine().Trim().Split(' ');
if (lst.Length > size)
{
Console.WriteLine("超出队列容量!");
goto end;
}
for (int i = 0; i < lst.Length; i++)
q.enqueue(Convert.ToDouble(lst[i]));
q.show_queue();
// 出队测试
double element = 0;
uint len = q.get_len();
Console.WriteLine("出队的数据: ");
while (len != 0)
{
q.dequeue(ref element);
Console.Write("{0} ", element);
len--;
}
Console.WriteLine();
q.show_queue();
// 循环性测试
Console.WriteLine("队列循环性测试: ");
for (int i = 0; i < lst.Length; i++)
q.enqueue(Convert.ToDouble(lst[i])); // 队列中先塞入一些元素
q.show_queue();
for (int i = 0; i < size * 2; i += 2)
{
q.enqueue(i + 1);
q.enqueue(i + 2);
q.dequeue(ref element);
q.dequeue(ref element);
q.show_queue();
}
// 结束
end:
Console.WriteLine("请按任意键继续...");
Console.ReadLine();
}
}
}
代码片段分析
类Queue中
// 宏定义
private const int OVERFLOW = -1;
private const int QNULL = -2;
防止异常的出现,事先定义好错误的返回值(溢出为-1,队空为-2)
C#是纯面向对象编程的语言,所有的“宏定义”都要写在类中。
size = Convert.ToUInt32(Console.ReadLine().Trim());
Console类的ReadLine()方法返回一个字符串,而不是像C/C++中将读入的数据直接赋给变量
String类中的Trim()方法表示将头尾的空白字符删去,它的兄弟有TrimStart(),TrimEnd(),分别表示删去开头的空白字符和删去末尾的空白字符
Convert类中的ToUInt32()方法表示将一个字符串转化为32位无符号整型(即uint)
这样子,就通过一步代码将读入的字符串转化为无符号整数。
学过Python的同学就会对以上的操作感觉到非常熟悉,行云流水。它等效于以下的python代码:
size = int(input().strip()) // Python
我们接着往下看
q = new double[size + 1];
由于牺牲了一个空间,所以在开辟空间时多开辟一个元素的容量。
public int enqueue(double ele)
{
if ((rear + 1) % q.Length == front)
return OVERFLOW;
q[rear] = ele;
rear = (uint)((rear + 1) % q.Length);
return 1;
}
public int dequeue(ref double ele)
{
if (is_empty())
return QNULL;
ele = q[front];
front = (uint)((front + 1) % q.Length);
return 1;
}
将上面说的数据结构翻译成C#语言来表示,结果就是这样了。
在C++中,会出现比如dequeue(double &ele)
中double &
这样的类型,被称为引用。在变量名前(或是类型之后,如果变量只有一个的话)加上&表示成引用类型,作用是给变量起一个别名,它和原变量指向同一块内存地址。也就是说,更改了它的值就相当于更改了原有那个变量的值(因为是在对应的内存位置上直接改动)。
在C#中,也有称为ref的关键字,作用和C++中的引用是一样的,即将参数按引用传值。只不过在C#中,调用参数含ref引用的方法时,必须将ref加上(如下所示),而在C++中没有相应的要求。
q.dequeue(ref element);
使用引用的原因是,需要将队中要弹出的元素弹出给某个变量,否则它会丢失(当然,你也可以再写一个get_front这样的函数)。而使用return的话,则会将值和返回的错误信息混淆。
我们接着往下看
在方法show_queue()中:
uint sub_rear = (uint)((rear + q.Length - 1) % q.Length);
在实际测试中发现当q.Length和它右边的-1位置对调之后程序会出bug,原因应该是rear为uint型,-1有可能会造成溢出问题,因而需要对调。
public uint get_len()
{
return (uint)((q.Length + rear - front) % q.Length);
}
此方法的功能是返回队列长度。笔者第一次在书写这个程序的时候写出来的是 return q.Length
,千万别犯这样的错误。这里要返回的并不是队列数组的容量,而是循环队列的“长度”。
Main()方法中
// 入队测试
Console.WriteLine("请输入入队数据: ");
string[] lst = Console.ReadLine().Trim().Split(' ');
Split()方法将字符串按照指定参数“分裂”,返回一个字符串数组,这个字符串数组的元素为为分裂过后的字符串
学过Python的同学依然会对以上的操作感觉到非常熟悉,行云流水。它等效于以下的python代码:
lst = input().strip().split() // Python
我们接着往下看
// 循环性测试
这个测试就是测试队列的循环功能是否正常,而非朴素的线性表就能达到的功能
// 结束
end:
Console.WriteLine("请按任意键继续...");
Console.ReadLine();
在VS中按下F5或者是点击上面的“启动”时,VS会运行代码,但是在运行之后他会直接退出程序,而不是像之前的C/C++一样等待用户按键。所以我们需要手动制造一个结束暂停,就像上面的代码一样。另外一种可行的方法是按下Ctrl+F5
,这样便会在程序运行之后等待用户按键。
经过一系列辛苦的理解和编码,我们的循环队列终于做完了!😃
总结
通过实验一的第一题,我们学习到了队列这一种数据结构以及他的一种表示方法——循环队列。当然,我们的队列是通过数组存储的,接下来还能继续学习使用链表来表示的队列。我们还学习到了C#的读入和处理,Convert转换台,ref引用等知识点。
参考文献
严蔚敏,吴伟民.数据结构(C语言版):清华大学出版社,2012
李春葆,曾平,喻丹丹.C#程序设计教程(第3版):清华大学出版社,2015
Copyright @ 2021, CSDN: ForeverMeteor, all rights reserved.