C#实际案例分析(第一弹)

实验一

题目要求

用C#构造一个队列Queue。要求此队列是循环队列,并进行入队、出队的测试

题意分析

  1. 将循环队列封装为一个类,在类内部实现入队、出队、展示等操作

  2. 选择数组作为存储队列的数据结构

  3. 在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 == 0rear == 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.

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值