Bootstrap

c#数据结构06:队列

说明
(此系列帖子记录数据结构学习成果,如侵犯视频作者权益,立删)
视频链接:离忧夏天C#数据结构

本文实现队列需要用到动态数组ArrayList和单链表的相关知识,详细请看:C#数据结构专栏

一:队列的基本概念

队列是一种遵循先进先出原则的线性表
在这里插入图片描述

二:队列的基本操作

  • 入队(Enqueue)
  • 出队(DeQueue)
  • 获取队列的大小(Count)
  • 检查队列是否为空(IsEmpty)
  • 查看队头元素(GetFirst)

实现队列的接口

    interface IQueue<T>
    {
        //查看元素数量
        int Count { get; }
        //判空
        bool IsEmpty { get; }
        //入队
        void Enqueue(T t);
        //出队,并返回元素
        T Dequeue();
        //查看队首的元素
        T Peek();
    }

三:队列的实现

1.数组队列(由动态数组实现)

数组队列时间复杂度
入队(Enqueue)O(1)
出队(DeQueue)O(n)
获取队列的大小(Count)O(1)
检查队列是否为空(IsEmpty)O(1)
查看队头元素(GetFirst)O(1)
    class Array1Queue<T> : IQueue<T>
    {
        private Array1<T> arr;

        public int Count => arr.Count;
        public bool IsEmpty => arr.IsEmpty;
        //传入容量
        public Array1Queue(int capacity)
        {
            arr = new Array1<T>(capacity);
        }
        //无参
        public Array1Queue()
        {
            arr = new Array1<T>();
        }
        //入队
        public void Enqueue(T t)
        {
            //选择数组尾部作为队尾
            arr.AddLast(t);
        }
        //出队
        public T Dequeue()
        {
            return arr.RemoveFirst();
        }
        //查看队首的元素
        public T Peek()
        {
            return arr.GetFirst();
        }
        public override string ToString()
        {
            return "Queue: Head" + arr.ToString() + "Tail";
        }
    }

2.循环队列(由循环数组实现)

循环数组其中的元素可以在数组的末端重新改回到起始位置,其头尾相连形成一个闭环。

  1. 头部由fist指针标识
  2. 尾部由last指针标识
  3. 当到达数组的末尾时,last指针会重写指向数组的起始位置,从而充分利用整个数组空间,避免了不必要的内存浪费。
  4. 其中需要取余操作是必须的,这样可以保证数组的循环,即(last+1)% data.length

循环数组的实现

class Array2<T>
{
    private T[] data;  //静态数组存储元素
    private int first; //记录头部
    private int last;  //记录尾部
    private int N;     //当前元素个数

    public Array2(int capacity)
    {
        data = new T[capacity];

        first = 0;
        last = 0;
        N = 0;
    }
    public Array2() : this(10) { }

    public int Count => N;
    public bool IsEmpty => N == 0;
    //添加元素
    public void AddLast(T t)
    {
        if(N ==data.Length)
            ResetCapacity(2*data.Length);

        data[last] = t;

        last = (last+1)%data.Length;
        N++;
    }
    //删除元素
    public T RemoveFirst()
    {
        if (IsEmpty)
            throw new InvalidOperationException("数组为空");

        T ret = data[first];
        data[first] = default(T);

        first = (first + 1) % data.Length;
        N--;

        if(N == data.Length/4)
            ResetCapacity(data.Length/2);

        return ret;
    }
    //获取头部元素
    public T GetFirst()
    {
        if (IsEmpty)
            throw new InvalidOperationException("数组为空");
       
        return data[first];
    }
    //扩容
    private void ResetCapacity(int newCapacity)
    {
        //临时数组用于转移元素
        T[] newData = new T[newCapacity];

        for (int i = 0; i < N; i++)
            newData[i] = data[(first+i)%data.Length];

        data = newData;
        first = 0;
        last = N;
    }
    public override string ToString()
    {
        StringBuilder res = new StringBuilder();
        res.Append("[");
        for (int i = 0; i < N; i++)
        {
            res.Append(data[(first + i) % data.Length]);
            if ((first + i + 1) % data.Length != last)
                res.Append(",");
        }
        res.Append("]");
        return res.ToString();
    }
}

循环队列的实现

循环队列时间复杂度
入队(Enqueue)O(1)
出队(DeQueue)O(1)
获取队列的大小(Count)O(1)
检查队列是否为空(IsEmpty)O(1)
查看队头元素(GetFirst)O(1)
    //循环队列
    class Array2Queue<T> :IQueue<T>
    {
        //循环数组作为底层的数据结构
        private Array2<T> arr;

        //创建容量为capacity的队列
        public Array2Queue(int capacity)
        {
            arr =new Array2<T>(capacity);
        }
        //使用默认的容量创建队列
        public Array2Queue()
        {
            arr = new Array2<T>();
        }

        //获取队列元素的个数O(1)
        public int Count => arr.Count;

        //查看队列是否为空O(1)
        public bool IsEmpty => arr.IsEmpty;

        //入队,往队尾添加元素O(1)
        public void Enqueue(T t)
        {
            arr.AddLast(t);
        }

        //出队,删除队首的元素O(1)
        public T Dequeue()
        {
            return arr.RemoveFirst();
        }

        //查看队首的元素O(1)
        public T Peek()
        {
            return arr.GetFirst();
        }

        //打印循环队列信息
        public override string ToString()
        {
            return "Queue: front" + arr.ToString() + "tail";
        }
    }

3.链表队列(由只带头指针的链表实现)

因为在向单链表添加元素的过程中,每次添加元素,我们都需要用一层for循环遍历到链表尾部,然后再添加元素,因此入队的时间复杂度是O(n)

链表队列时间复杂度
入队(Enqueue)O(n)
出队(DeQueue)O(1)
获取队列的大小(Count)O(1)
检查队列是否为空(IsEmpty)O(1)
查看队头元素(GetFirst)O(1)
 class LinkedList1Queue<T>:IQueue<T>
 {
     //链表
     private LinkedList01<T> list;

     private LinkedList1Queue()
     {
         list = new LinkedList01<T>();
     }

     public int Count => list.Count;
     public bool IsEmpty => list.IsEmpty;

     //删除队列头部==删除链表头部O(1)
     public T Dequeue()
     {
         return list.RemoveFirst();
     }
     //添加到队列尾部==添加链表尾部O(n)
     public void Enqueue(T t)
     {
        list.AddLast(t);
     }
     //查看队列头部==查看链表头部O(1)
     public T Peek()
     {
         return list.GetFirst();
     }

     public override string ToString()
     {
         return "Queue front" + list.ToString() +"tail";
     }
 }

4.链表队列(由带头尾指针的链表实现)

和只有头指针的单链表类似,在尾指针添加元素,只需要让尾指针的下一个节点为newNode,并让newNode为尾指针即可
即tail.next = node; tail = node;

链表队列(带尾指针)时间复杂度
入队(Enqueue)O(1)
出队(DeQueue)O(1)
获取队列的大小(Count)O(1)
检查队列是否为空(IsEmpty)O(1)
查看队头元素(GetFirst)O(1)

带尾指针的单链表的实现

//带有尾指针的单链表
class LinkedList02<T>
{
    //链表的节点类
    private class Node
    {
        public T data;
        public Node next;
        public Node(T data, Node next)
        {
            this.data = data;
            this.next = next;
        }
        public Node(T data)
        {
            this.data = data;
            this.next = null;
        }
        public override string ToString()
        {
            return base.ToString();
        }
    }

    private Node head;
    private Node tail;
    private int N;

    public LinkedList02()
    {
        head = null;
        tail = null;
        N = 0;
    }

    public int Count => N;
    public bool IsEmpty => N == 0;
    //往链表尾部添加元素
    public void AddLast(T t)
    {
        Node node = new Node(t);
        if(IsEmpty) 
        {
            head = node;
            tail = node;
        }
        else
        {
            tail.next = node;
            tail = node;
        }
        N++;
    }
    //链表头部删除节点
    public T RemoveFirst()
    {
        if (IsEmpty)
            throw new InvalidOperationException("链表为空");
        //删除的元素
        T t = head.data;
        //移动指针
        head = head.next;

        N--;

        if (head == null)
        {
            tail = null;
        }

        return t;
    }
    //查看头部
    public T GetFirst()
    {
        if (IsEmpty)
            throw new InvalidOperationException("链表为空");


        return head.data;
    }
    public override string ToString()
    {
        StringBuilder sb = new StringBuilder();
        Node cur = head;
        while (cur != null)
        {
            sb.Append(cur.data + "->");
            cur = cur.next;
        }
        sb.Append("Null");
        return sb.ToString();
    }
}

通过带有尾指针的链表实现的队列

//带尾指针的链表队列
class LinkedList2Queue<T>:IQueue<T>
{
    //基于带尾指针的链表实现
    private LinkedList02<T> list;

    public LinkedList2Queue()
    {
         list = new LinkedList02<T>();
    }
    public int Count => list.Count;
    public bool IsEmpty => list.IsEmpty;

    //入队
    public void Enqueue(T t)
    {
        list.AddLast(t);
    }

    //出队,并返回元素
    public T Dequeue()
    {
        return list.RemoveFirst();
    }

    //查看队首的元素
    public T Peek()
    {
        return list.GetFirst();
    }
    public override string ToString()
    {
        return "Queue front" + list.ToString() +"tail";
    }
}

四:性能分析比较

对于队列的测试,编写TestQueue方法

 //测试队列的性能
 private static long TestQueue(IQueue<int> queue, int N)
 {
     Stopwatch t = new Stopwatch();
     t.Start();
     for (int i = 0; i < N; i++)
     {
         queue.Enqueue(i);
     }
     for (int i = 0; i < N; i++)
     {
         queue.Dequeue();
     }
     t.Stop();
     return t.ElapsedMilliseconds;
 }

数组队列 VS 循环队列

 static void Main(string[] args)
 {
     int N = 100000;
     //数组队列
     Array1Queue<int> array1Queue = new Array1Queue<int>();
     long t1 = TestQueue(array1Queue, N);
     Console.WriteLine("Array1Queue Time:" + t1+"ms");
     //循环队列
     Array2Queue<int> array2Queue = new Array2Queue<int>();
     long t2 = TestQueue(array2Queue, N);
     Console.WriteLine("Array2Queue Time:" + t2 + "ms");

     Console.ReadKey();
 }

在这里插入图片描述
我们可以清楚的看到,循环队列的性能对于数组队列是大幅度提升的。

由只有头指针链表实现的队列 VS 由具有头尾指针链表实现的队列

        static void Main(string[] args)
        {
            int N = 100000;
            //链表队列(头指针)
            LinkedList1Queue<int> linkedList1Queue = new LinkedList1Queue<int>();
            long t1 = TestQueue(linkedList1Queue, N);
            Console.WriteLine("linkedList1Queue Time:" + t1+"ms");
            //链表队列(头尾指针)
            LinkedList2Queue<int> linkedList2Queue = new LinkedList2Queue<int>();
            long t2 = TestQueue(linkedList2Queue, N);
            Console.WriteLine("linkedList2Queue Time:" + t2 + "ms");

            Console.ReadKey();
        }

在这里插入图片描述
我们可以清楚的看到,通过优化链表,使得具有尾指针,并通过尾指针添加元素,使得性能对于普通的单链表是大幅度提升的。

;