计算机程序离不开算法和数据结构。本文简介栈(Stack)和队列(Queue)的实现,.NET中与之相关的数据结构,典型应用等,希望能加深自己对这两个简单数据结构的理解。

1. 基本概念

概念非常easy,栈 (Stack)是一种后进先出(last in first off。LIFO)的数据结构,而队列(Queue)则是一种先进先出 (fisrt in first out。FIFO)的结构,例如以下图:

数据结构(1)栈与队列_.net

2. 实现

如今来看怎样实现以上的两个数据结构。

在动手之前。Framework Design Guidelines这本书告诉我们,在设计API或者实体类的时候。应当环绕场景编写API规格说明书。

1.1 Stack的实现

栈是一种后进先出的数据结构。对于Stack 我们希望至少要对外提供下面几个方法:

数据结构(1)栈与队列_数据结构_02

Stack<T>() 创建一个空的栈
void Push(T s) 往栈中加入一个新的元素
T Pop() 移除并返回近期加入的元素
boolean IsEmpty() 栈是否为空
int Size() 栈中元素的个数

要实现这些功能,我们有两中方法,数组和链表,先看链表实现:

栈的链表实现:

我们首先定义一个内部类来保存每一个链表的节点,该节点包含当前的值以及指向下一个的值,然后建立一个节点保存位于栈顶的值以及记录栈的元素个数;

1
2
3
4
5
class Node
{
    public T Item{get;set;}
    public Node Next { get; set; }
}
1
2
private Node first = null;
private int number = 0;

如今来实现Push方法。即向栈顶压入一个元素,首先保存原先的位于栈顶的元素,然后新建一个新的栈顶元素。然后将该元素的下一个指向原先的栈顶元素。整个Pop步骤例如以下:

数据结构(1)栈与队列_函数调用_03

实现代码例如以下:

1
2
3
4
5
6
7
8
void Push(T node)
{
    Node oldFirst = first;
    first = new Node();
    first.Item= node;
    first.Next = oldFirst;
    number++;
}

Pop方法也非常easy。首先保存栈顶元素的值,然后将栈顶元素设置为下一个元素:

数据结构(1)栈与队列_.net_04

1
2
3
4
5
6
7
T Pop()
{
    T item = first.Item;
    first = first.Next;
    number--;
    return item;
}

基于链表的Stack实现,在最坏的情况下仅仅须要常量的时间来进行Push和Pop操作。

栈的数组实现:

我们能够使用数组来存储栈中的元素Push的时候。直接加入一个元素S[N]到数组中,Pop的时候直接返回S[N-1].

数据结构(1)栈与队列_函数调用_05

首先,我们定义一个数组,然后在构造函数中给定初始化大小,Push方法实现例如以下,就是集合里加入一个元素:

1
2
3
4
5
6
7
T[] item;
int number = 0;
 
public StackImplementByArray(int capacity)
{
    item = new T[capacity];
}
1
2
3
4
5
public void Push(T _item)
{
    if (number == item.Length) Resize(2 * item.Length);
    item[number++] = _item;
}

Pop方法:

1
2
3
4
5
6
7
public T Pop()
{
    T temp = item[--number];
    item[number] = default(T);
    if (number > 0 && number == item.Length / 4) Resize(item.Length / 2);
    return temp;
}

在Push和Pop方法中。为了节省内存空间。我们会对数组进行整理。Push的时候,当元素的个数达到数组的Capacity的时候,我们开辟2倍于当前元素的新数组。然后将原数组中的元素复制到新数组中。

Pop的时候,当元素的个数小于当前容量的1/4的时候,我们将原数组的大小容量降低1/2。

Resize方法基本就是数组复制:

1
2
3
4
5
6
7
8
9
private void Resize(int capacity)
{
    T[] temp = new T[capacity];
    for (int i = 0; i < item.Length; i++)
    {
        temp[i] = item[i];
    }
    item = temp;
}

当我们缩小数组的时候,採用的是推断1/4的情况。这样效率要比1/2要高。由于能够有效避免在1/2附件插入。删除。插入。删除,从而频繁的扩大和缩小数组的情况。下图展示了在插入和删除的情况下数组中的元素以及数组大小的变化情况:

数据结构(1)栈与队列_函数调用_06

分析:1. Pop和Push操作在最坏的情况下与元素个数成比例的N的时间。时间主要花费在扩大或者缩小数组的个数时,数组拷贝上。

2. 元素在内存中分布紧凑,密度高,便于利用内存的时间和空间局部性,便于CPU进行缓存。较LinkList内存占用小。效率高。

2.2 Queue的实现

Queue是一种先进先出的数据结构,和Stack一样。他也有链表和数组两种实现,理解了Stack的实现后,Queue的实现就比較简单了。

数据结构(1)栈与队列_链表_07

Stack<T>() 创建一个空的队列
void Enqueue(T s) 往队列中加入一个新的元素
T Dequeue() 移除队列中最早加入的元素
boolean IsEmpty() 队列是否为空
int Size() 队列中元素的个数

首先看链表的实现:

Dequeue方法就是返回链表中的第一个元素,这个和Stack中的Pop方法相似:

1
2
3
4
5
6
7
8
9
public T Dequeue()
{
    T temp = first.Item;
    first = first.Next;
    number--;
    if (IsEmpety())
        last = null;
    return temp;
}

Enqueue和Stack的Push方法不同,他是在链表的末尾添加新的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void Enqueue(T item)
{
    Node oldLast = last;
    last = new Node();
    last.Item = item;
    if (IsEmpety())
    {
        first = last;
    }
    else
    {
        oldLast.Next = last;
    }
    number++;
}

相同地,如今再来看怎样使用数组来实现Queue,首先我们使用数组来保存数据,并定义变量head和tail来记录Queue的首尾元素。

数据结构(1)栈与队列_链表_08

和Stack的实现方式不同,在Queue中,我们定义了head和tail来记录头元素和尾元素。

当enqueue的时候,tial加1,将元素放在尾部,当dequeue的时候,head减1。并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void Enqueue(T _item)
{
    if ((head - tail + 1) == item.Length) Resize(2 * item.Length);
    item[tail++] = _item;
}
 
public T Dequeue()
{
    T temp = item[--head];
    item[head] = default(T);
    if (head > 0 && (tail - head + 1) == item.Length / 4) Resize(item.Length / 2);
    return temp;
}
 
private void Resize(int capacity)
{
    T[] temp = new T[capacity];
    int index = 0;
    for (int i = head; i < tail; i++)
    {
        temp[++index] = item[i];
    }
    item = temp;
}
3. .NET中的Stack和Queue

在.NET中有Stack和Queue泛型类,使用Reflector工具能够查看其详细实现。先看Stack的实现,以下是截取的部分代码。仅列出了Push,Pop方法。其它的方法希望大家自己使用Reflector查看:

数据结构(1)栈与队列_.net_09

能够看到.NET中的Stack的实现和我们之前写的几乎相同,也是使用数组来实现的。.NET中Stack的初始容量为4,在Push方法中,能够看到当元素个数达到数组长度时,扩充2倍容量,然后将原数组复制到新的数组中。Pop方法和我们之前实现的基本上同样,以下是详细代码,仅仅截取了部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
[Serializable, ComVisible(false), DebuggerTypeProxy(typeof(System_StackDebugView<>)), DebuggerDisplay("Count = {Count}"), __DynamicallyInvokable]
public class Stack<T> : IEnumerable<T>, ICollection, IEnumerable
{
    // Fields
    private T[] _array;
    private const int _defaultCapacity = 4;
    private static T[] _emptyArray;
    private int _size;
    private int _version;
 
    // Methods
    static Stack()
    {
        Stack<T>._emptyArray = new T[0];
    }
 
    [__DynamicallyInvokable]
    public Stack()
    {
        this._array = Stack<T>._emptyArray;
        this._size = 0;
        this._version = 0;
    }
 
    [__DynamicallyInvokable]
    public Stack(int capacity)
    {
        if (capacity < 0)
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNumRequired);
        }
        this._array = new T[capacity];
        this._size = 0;
        this._version = 0;
    }
 
    [__DynamicallyInvokable]
    public void CopyTo(T[] array, int arrayIndex)
    {
        if (array == null)
        {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
        }
        if ((arrayIndex < 0) || (arrayIndex > array.Length))
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
        }
        if ((array.Length - arrayIndex) < this._size)
        {
            ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
        }
        Array.Copy(this._array, 0, array, arrayIndex, this._size);
        Array.Reverse(array, arrayIndex, this._size);
    }
 
    [__DynamicallyInvokable]
    public T Pop()
    {
        if (this._size == 0)
        {
            ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyStack);
        }
        this._version++;
        T local = this._array[--this._size];
        this._array[this._size] = default(T);
        return local;
    }
 
    [__DynamicallyInvokable]
    public void Push(T item)
    {
        if (this._size == this._array.Length)
        {
            T[] destinationArray = new T[(this._array.Length == 0) ?

4 : (2 * this._array.Length)];

            Array.Copy(this._array, 0, destinationArray, 0, this._size);
            this._array = destinationArray;
        }
        this._array[this._size++] = item;
        this._version++;
    }
 
    // Properties
    [__DynamicallyInvokable]
    public int Count
    {
        [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
        get
        {
            return this._size;
        }
    }
 
}

以下再看看Queue的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
[Serializable, DebuggerDisplay("Count = {Count}"), ComVisible(false), DebuggerTypeProxy(typeof(System_QueueDebugView<>)), __DynamicallyInvokable]
  public class Queue<T> : IEnumerable<T>, ICollection, IEnumerable
  {
      // Fields
      private T[] _array;
      private const int _DefaultCapacity = 4;
      private static T[] _emptyArray;
      private int _head;
      private int _size;
      private int _tail;
      private int _version;
      // Methods
      static Queue()
      {
          Queue<T>._emptyArray = new T[0];
      }
 
      public Queue()
      {
          this._array = Queue<T>._emptyArray;
      }
 
      public Queue(int capacity)
      {
          if (capacity < 0)
          {
              ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNumRequired);
          }
          this._array = new T[capacity];
          this._head = 0;
          this._tail = 0;
          this._size = 0;
      }
 
      public T Dequeue()
      {
          if (this._size == 0)
          {
              ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue);
          }
          T local = this._array[this._head];
          this._array[this._head] = default(T);
          this._head = (this._head + 1) % this._array.Length;
          this._size--;
          this._version++;
          return local;
      }
 
      public void Enqueue(T item)
      {
          if (this._size == this._array.Length)
          {
              int capacity = (int)((this._array.Length * 200L) / 100L);
              if (capacity < (this._array.Length + 4))
              {
                  capacity = this._array.Length + 4;
              }
              this.SetCapacity(capacity);
          }
          this._array[this._tail] = item;
          this._tail = (this._tail + 1) % this._array.Length;
          this._size++;
          this._version++;
      }
 
      private void SetCapacity(int capacity)
      {
          T[] destinationArray = new T[capacity];
          if (this._size > 0)
          {
              if (this._head < this._tail)
              {
                  Array.Copy(this._array, this._head, destinationArray, 0, this._size);
              }
              else
              {
                  Array.Copy(this._array, this._head, destinationArray, 0, this._array.Length - this._head);
                  Array.Copy(this._array, 0, destinationArray, this._array.Length - this._head, this._tail);
              }
          }
          this._array = destinationArray;
          this._head = 0;
          this._tail = (this._size == capacity) ? 0 : this._size;
          this._version++;
      }
 
      public int Count
      {
          [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
          get
          {
              return this._size;
          }
      }
  }

能够看到.NET中Queue的实现也是基于数组的,定义了head和tail。当长度达到数组的容量的时候,使用了SetCapacity方法来进行扩容和拷贝。

4. Stack和Queue的应用

Stack这样的数据结构用途非常广泛,比方编译器中的词法分析器、Java虚拟机、软件中的撤销操作、浏览器中的回退操作,编译器中的函数调用实现等等。

4.1 线程堆 (Thread Stack)

线程堆是操作系型系统分配的一块内存区域。通常CPU上有一个特殊的称之为堆指针的寄存器 (stack pointer) 。在程序初始化时,该指针指向栈顶,栈顶的地址最大。

CPU有特殊的指令能够将值Push到线程堆上,以及将值Pop出堆栈。每一次Push操作都将值存放到堆指针指向的地方。并将堆指针递减。每一次Pop都将堆指针指向的值从堆中移除,然后堆指针递增。堆是向下增长的。Push到线程堆,以及从线程堆中Pop的值都存放到CPU的寄存器中。

当发起函数调用的时候,CPU使用特殊的指令将当前的指令指针(instruction pointer),如当前运行的代码的地址压入到堆上。

然后CPU通过设置指令指针到函数调用的地址来跳转到被调用的函数去运行。当函数返回值时。旧的指令指针从堆中Pop出来。然后从该指令地址之后继续运行。

当进入到被调用的函数中时,堆指针减小来在堆上为函数中的局部变量分配很多其它的空间。假设函数中有一个32位的变量分配到了堆中,当函数返回时。堆指针就返回到之前的函数调用处,分配的空间就会被释放。

假设函数有參数。这些參数会在函数调用之前就被分配在堆上,函数中的代码能够从当前堆往上訪问到这些參数。

线程堆是一块有一定限制的内存空间。假设调用了过多的嵌套函数,或者局部变量分配了过多的内存空间,就会产生堆栈溢出的错误。

下图简单显示了线程堆的变化情况。

数据结构(1)栈与队列_数据结构_10

4.2 算术表达式的求值

Stack使用的一个最经典的样例就是算术表达式的求值了,这当中还包含前缀表达式和后缀表达式的求值。E. W. Dijkstra发明了使用两个Stack,一个保存操作值。一个保存操作符的方法来实现表达式的求值,详细过程例如以下:

1) 当输入的是值的时候Push到属于值的栈中。

2) 当输入的是运算符的时候,Push到运算符的栈中。

3) 当遇到左括号的时候。忽略

4) 当遇到右括号的时候,Pop一个运算符,Pop两个值,然后将计算结果Push到值的栈中。

以下是在C#中的一个简单的括号表达式的求值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// <summary>
/// 一个简单的表达式运算
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
    Stack<char> operation = new Stack<char>();
    Stack<Double> values = new Stack<double>();
    //为方便。直接使用ToChar对于两位数的数组问题
    Char[] charArray = Console.ReadLine().ToCharArray();
 
    foreach (char s in charArray)
    {
        if (s.Equals('(')) { }
        else if (s.Equals('+')) operation.Push(s);
        else if (s.Equals('*')) operation.Push(s);
        else if (s.Equals(')'))
        {
            char op = operation.Pop();
            if (op.Equals('+'))
                values.Push(values.Pop() + values.Pop());
            else if (op.Equals('*'))
                values.Push(values.Pop() * values.Pop());
        }
        else values.Push(Double.Parse(s.ToString()));
    }
    Console.WriteLine(values.Pop());
    Console.ReadKey();
}

执行结果例如以下:

数据结构(1)栈与队列_.net_11

下图演示了操作栈和数据栈的变化。

数据结构(1)栈与队列_数据结构_12

在编译器技术中,前缀表达式,后缀表达式的求值都会用到堆。

4.3 Object-C中以及OpenGL中的图形绘制

在Object-C以及OpenGL中都存在”画图上下文”,有时候我们对局部对象的画图不希望影响到全局的设置,所以须要保存上一次的画图状态。

以下是Object-C中绘制一个圆形的典型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)drawGreenCircle:(CGContextRef)ctxt {
    UIGraphicsPushContext(ctxt);
    [[UIColor greenColor] setFill];
    // draw my circle
    UIGraphicsPopContext();
}
 
- (void)drawRect:(CGRect)aRect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    [[UIColor redColor] setFill];
    // do some stuff
    [self drawGreenCircle:context];
    // do more stuff and expect fill color to be red
}

能够看到,在drawGreenCircle方法中,在设置填充颜色之前,我们Push保存了画图上下文的信息,然后在设置当前操作的一些环境变量,绘制图形,绘制完毕之后,我们Pop出之前保存的画图上下文信息。从而不影响后面的画图。

4.4 一些其它场景

有一个场景是利用stack 处理多余无效的请求,比方用户长按键盘,或者在非常短的时间内连续按某一个功能键,我们须要过滤到这些无效的请求。一个通常的做法是将全部的请求都压入到堆中。然后要处理的时候Pop出来一个。这个就是最新的一次请求。

Queue的应用

在现实生活中Queue的应用也非常广泛,最广泛的就是排队了。”先来后到” First come first service ,以及Queue这个单词就有排队的意思。

还有。比方我们的播放器上的播放列表,我们的数据流对象,异步的传输数据结构(文件IO,管道通讯,套接字等)

另一些解决对共享资源的冲突訪问,比方打印机的打印队列等。

消息队列等。交通状况模拟,呼叫中心用户等待的时间的模拟等等。

5. 一点点感悟

本文简介了Stack和Queue的原理及实现,并介绍了一些应用。

最后一点点感悟就是不要为了使用数据结构而使用数据结构。

举个样例,之前看到过一个数组反转的问题,刚学过Stack可能会想。这个简单啊。直接将字符串挨个的Push进去,然后Pop出来就能够了,完美的解决方式。

可是,这是不是最有效地呢,事实上有更有效地方法,那就是以中间为对折。然后左右两边替换。

1
2
3
4
5
6
7
8
9
10
11
12
public static void Reverse(int[] array, int begin, int end)
{
    while (end > begin)
    {
        int temp = array[begin];
        array[begin] = array[end];
        array[end] = temp;
 
        begin++;
        end--;
    }
}