Swift数据结构-队列Queue

声明:算法和数据结构的文章均是作者从github上翻译过来,为方便大家阅读。如果英语阅读能力强的朋友,可以直接到swift算法俱乐部查看所有原文,以便快速学习。作者同时也在学习中,欢迎交流

排队序列就是一个你只能在队伍的最后面加入新的值,从最前面取出旧的值的一串序列。这样的机制保证了最早进入序列的值,同时也会是最早出来的,就像我们平时排队办理业务一样,先到先服务!

为什么我们需要这样的机制呢?实际上,很多算法都会出现类似情况,在某一时刻你只想添加某些元素到一个临时列表里,然后过一会儿又将它们移除出去。这时候,你添加或者移除元素的顺序会影响整个算法。

队列可以提供先进先出的顺序(FIFO)。它每一次移除的元素,都是你最先放进去的元素。这里有一个非常类似的数据结构,堆栈Stack,属于后进先出的顺序(LIFO)。

比如,我们将一个数字放进队列里。

queue.enqueue(10)

现在队列的内容为 [ 10 ]。 我们再放进下一个数字:

queue.enqueue(3)

现在队列的内容变为 [ 10, 3 ]。我们继续放入新的数字:

queue.enqueue(57)

现在队列的内容变为[10,3,57]。这回,我们将队列里最先端的数字移除:

queue.dequeue

这里我们得到的返回值为10,因为它是我们最后放进去的数字。现在队列的内容变为[3,57]。

queue.dequeue

这一次我们得到的返回值为3,我们可以继续移除队列的数据。如果队列变为空,移除方程返回结果为nil,在一些执行语句里面会返回错误信。

注意: 通常情况下队列并不是最好的选择。如果对于一个数组来说,元素的添加和移除过程不是很重要的话,这里我们建议使用堆栈Stack会是更好的选择,因为堆栈Stack更简单也更高效。

代码

在swift中,队列很容易创建。简单的说它就是对一个数组进行包装,让你能够对数组添加,移除,查看数据。

public struct Queue { 
    fileprivate var array = [T]() 
    public var isEmpty: Bool { 
        return array.isEmpty 
    } 
    public var count: Int { 
        return array.count 
    }
    public mutating func enqueue(_ element: T) { 
        array.append(element) 
    } 
    public mutating func dequeue() -> T? { 
        if isEmpty {
           return nil 
        } else { 
           return array.removeFirst() 
        }
     } 
    public var front: T? { 
        return array.first 
    }
}

队列算法运算效率良好,但大多数情况下并不是最优选择。

插入队列过程是一个O(1)运算过程,因为每一次在队列的最后一位插入新的数值消耗的时间是固定的,与队列当前的大小无关,这是队列算法最棒的地方。

你可能会好奇,为什么添加数值到数组里面是一个O(1)运算过程?因为在swift里,在一个数组的最末尾,swift都预留了一些空位备用。比如我们执行以下代码:

var  queue = Queue()
queue.enqueue("Ada")
queue.enqueue("Steve")
queue.enqueue("Tim")

我们得到的数组应该是这样的:

["Ada", "Steve", "Tim", xxx, xxx , xxx]

这里的xxx是尚未填入数值的预留内存。添加新的数值会重写最近的一个xxx。

["Ada", "Steve", "Tim", "Grace", xxx , xxx]

这就相当于将一部分内存从一个地方复制到另一个地方,这是一个恒定运算过程。

当然,数组中的xxx个数是有限的。当最后一个xxx被使用,你还想继续添加数值,这个数组就需要重新调整大小,获取更多的空间。

调整大小包括获取新的内存,同时将现有的数据复制到新的数组里面。这是一个O(n)过程,所以这个过程有点慢。但是,因为这种情况偶尔发生,所以总体上来说,队列在添加新的数值到数组里的过程,所消耗的平均时间仍然接近O(1).

至于队列取出过程,就不太一样了。在取出数值的过程中,我们移除的是队列最前端的数值,而不是最末端。这个过程基本上一直是O(n)运算因为它需要将剩余的数据在内存上进行保留和移动。

在我们的例子中,取出第一个元素"Ada"其实是将"Steve"复制到"Ada"的位置,同时"Tim"被复制到原来"Steve"的位置,"Grace"被复制到运来"Tim"的位置:

取出前      ["Ada", "Steve", "Tim", "Grace", xxx , xxx]
                                 /        /            /
                               /        /            / 
                             /        /            /
                           /        /            /
取出后      ["Steve", "Tim", "Grace", xxx , xxx,xxx]

将所有数据在内存上进行移动一直是O(n)运算。所以在执行队列算法时候,插入数值的过程是简单高效的,但是取出的过程却有待提高。

更有效的队列算法

为了让队列算法更加有效,我们可以在预留内存位置上做文章,但是这次我们选择在队列的最前头进行内存预留。这里我们需要自己写代码去实现,因为swift本身的逻辑并没有支持我们提出来的想法。

想法如下:当我们需要从队列中取出元素的时候,我们不做数组元素的移动(慢),而是记住这个元素的位置,并且把它重写为xxx(快)。在取出"Ada"之后,数组内容如下:

[xxx, "Steve", "Tim", "Grace", xxx , xxx]

我们继续取出"Steve",数组变成:

[xxx, xxx, "Tim", "Grace", xxx , xxx]

因为在队列算法中,最前端的预留内存永远不会被用到,为了防止内存浪费,我们可以偶尔对数组重新进行大小调整,即删除前端预留内存,将"Tim"等元素往前放:

["Tim", "Grace", xxx , xxx]

这个调整过程包含了内存移动,所以这里是一个O(n)运算。但是因为它只是偶尔发生,所以,总体来说,取出过程也变成了O(1)运算。

一下代码实现了上述所说的新队列算法:

public struct Queue{
   fileprivate var array = [T?]()
   fileprivate var head = 0

   public var isEmpty : Bool {
        return count == 0
   }
   public var count: Int {
    return array.count - head
  }

  public mutating func enqueue(_ element: T) {
    array.append(element)
  }

  public mutating func dequeue() -> T? {
    guard head < array.count, let element = array[head] else { return nil }

    array[head] = nil
    head += 1

    let percentage = Double(head)/Double(array.count)
    if array.count > 50 && percentage > 0.25 {
      array.removeFirst(head)
      head = 0
    }

    return element
  }

  public var front: T? {
    if isEmpty {
      return nil
    } else {
      return array[head]
    }
  }
}

因为我们需要将数组的元素重写为空,所以现在这里的数组储存对象类型为T?,而不是T。变量head是数组里面最前面的对象的索引。

对比原来的代码,我们对dequeuq()进行了修改。当我们取出数值的时候,我们首先将array[head]改为nil,同时将head的数值增加,保证其表示下一个元素的索引。

取出前:

["Ada", "Steve", "Tim", "Grace", xxx , xxx]
  head

取出后:

[xxx, "Steve", "Tim", "Grace", xxx , xxx]
        head

当然,如果我们从来不将前头的这些空点移除掉,而是不停的添加新的数值,移除前端的数值,那数组的大小将会不断变大,消耗的内存跟着增加。所以我们必须周期性的对数组进行调整。代码为:

    let percentage = Double(head)/Double(array.count)
    if array.count > 50 && percentage > 0.25 {
      array.removeFirst(head)
      head = 0
    }

在数组大小超过50的前提下,我们计算了空点在数组中所占的比例,当占比超过25%,我们就对数组进行一次调整,避免内存浪费。这里的50可以自己调整。

你可能感兴趣的:(Swift数据结构-队列Queue)