数据结构和算法:队列(Queue)

一、什么是队列

队列是一种表(list),但是你只能把最新的元素插到最后,或者移除最前面的元素。

我们把这种特征成为先进先出(FIFO),先加入队列的元素会最先被移出队列。

类似的数据结构是栈,与队列不同的是,栈的元素操作顺序是后进先出(LIFO)

                              ^
                              |
┏━━━━━━┓ ┏━━━━━━┓ ┏━━━━━━┓ ┏━━━━━━┓ ┏━━━━━━┓
┃      ┃ ┃  10  ┃ ┃  10  ┃ ┃  10  ┃ ┃   3  ┃
┃      ┃ ┃      ┃ ┃  3   ┃ ┃  3   ┃ ┃  57  ┃
┃      ┃ ┃      ┃ ┃      ┃ ┃  57  ┃ ┃      ┃
┗━━━━━━┛ ┗━━━━━━┛ ┗━━━━━━┛ ┗━━━━━━┛ ┗━━━━━━┛
   ^         ^       ^
   |         |       |
   10        3       57  

图 1. 队列

队列的种类:

  • 双端队列(Deque):两端都可以入队、出队
  • 优先队列(Priority queue):一种有序队列,最重要的元素始终放在最前面

二、为什么使用队列(队列的特点)

当你在处理一组数据时,需要考虑添加和移除元素的顺序,而且需要先进先出这种处理机制,此时就可以用到队列。

三、复杂度

1. enqueue 的复杂度:O(1)

因为每次添加一个元素到队列时,都是添加到数组的最后,而不管数组有多大,添加元素到数组最后的耗时都是一个常量。

延伸:为什么添加元素到数组后面的复杂度是 O(1)或者一个耗时为常量的操作?

因为在 Swift 的实现中,数组的末尾总是会有空余的空间,比如,现在有这样一个数组:

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

在内存中,它实际上是这样的:

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

xxx 就是暂时分配了内存但是没有用到的单元,如果添加了一个新元素,新添加的元素就会占用一个空白的内存单元:

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

边界情况:但是数组末尾分配的空余空间是有限的,当最后一个 xxx 也被用了的话,如果你还要添加新的元素到数组中,你就需要重新调整数组的大小。调整数组的大小意味着需要分配新的内存空间给一个新数组,并且将原来的数组元素全部拷贝到新数组中。这个时候的操作就是一个复杂度为 O(n) 的操作,但是这只是一种极端情况,所以可以说一般情况下,添加新元素到数组的最后还是 O(1) 的操作。

2. dequeue 的复杂度为 O(n)

因为每次移除元素时,都是将数组中的第一个元素移除,然后再将后面剩余的元素往前移
比如在上面的例子中,当将 Ada 从队列中移除时,后面的 Steve 将会被拷贝到原来 Ada 的位置,Tim 将会被拷贝到原来 Steve 的位置 …, 以此类推。

before   [ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
                   /       /      /
                  /       /      /
                 /       /      /
                /       /      /
after    [ "Steve", "Tim", "Grace", xxx, xxx, xxx ]

遗留问题

  • 数组在内存中是如何布局的?
  • 从数组中移除第一个元素后,数组后面的元素发生了什么变化?

四、队列的实现

实现队列的方式有多种:

  • 基于数组(Array)
  • 基于链表(Linked List)
  • 基于环形缓存器(Circular Buffer)
  • 基于堆(Heap)

这里我们只讨论基于最基本的数组来实现。

1. 基本实现

我们可以基于数组来实现一个基本的队列结构,这个队列支持以下几种接口:

  • count:元素个数
  • isEmpty:是否为空
  • front:第一个元素
  • enqueue:加入元素到队列
  • dequeue:从队列中移除元素

(1) Swift 版本的实现

/// 基于数组封装的队列
public struct Queue<T> {
    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 array.isEmpty {
            return nil
        } else {
            return array.removeFirst()
        }
    }
    
    // 首元素
    public var front: T? {
        return array.first
    }
    
}

(2) Objective-C 版本的实现

Queue.h

@interface Queue<ObjectType> : NSObject

@property (assign, nonatomic, readonly) NSUInteger count;
@property (assign, nonatomic, readonly) BOOL isEmpty;
@property (strong, nonatomic, readonly) ObjectType front;

- (ObjectType)dequeue;
- (void)enqueueObject:(ObjectType)object;

@end

Queue.m

@interface Queue ()

@property (strong, nonatomic) NSMutableArray *array;

@end

@implementation Queue

- (instancetype)init {
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
    }
    return self;
}

- (BOOL)isEmpty {
    return (self.count == 0);
}

- (NSUInteger)count {
    return self.array.count;
}

- (id)front {
    if (self.count == 0) {
        return nil;
    }
    
    return self.array.firstObject;
}

- (id)dequeue {
    if (self.array.count == 0) {
        return nil;
    }
    
    id objectToRemove = self.array.firstObject;
    [self.array removeObjectAtIndex:0];
    
    return objectToRemove;
}

- (void)enqueueObject:(id)object {
    [self.array addObject:object];
}

- (NSString *)description {
    
    NSMutableString *string = [NSMutableString string];
    [string appendString:@"Queue: ["];
    for (id <NSObject> element in self.array) {
        if (element != self.array.firstObject) {
            [string appendString:@", "];
        }
        
        [string appendString:element.description];
    }
    
    [string appendString:@"]"];
    
    return string;
}

@end

2. 更高效的队列

借鉴 Swift 中数组末尾预留内存的思路,我们可以 dequeue 时通过在数组前面保留一些空余内存空间,来提高队列的 dequeue 操作的效率。

其核心思想是:当我们从队列中移除一个元素后,我们会通过将这个元素在数组中的位置标记为空,来保证后面的元素在内存中的位置不发生改变。

比如,我们有这样一个数组:

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

移除第一个元素 Ada 后,变成这样:

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

移除第二个元素 Steve 后,就变成这样

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

因为数组前面的空白单元都没有被重用,所以我们可以在某个时间点,再将数组中所有的元素都往前移,然后就变成这样

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

这种操作的复杂度为 O(n),但是因为只是偶尔才出现一次,所以总的复杂度平均下来还是 O(1)。

(1) Swift 版本的实现

/// 更高效的队列
public struct EfficientQueue<T> {
    fileprivate var array = [T?]()   // 数组中的数据类型为 optional,因为会有 nil
    fileprivate var headIndex = 0    // 标记第一个真实元素的位置
    
    
    // 是否为空
    public var isEmpty: Bool {
        return (count == 0)
    }
    
    // 元素个数
    public var count: Int {
        return (array.count - headIndex)
    }
    
    // 加入队列
    public mutating func enqueue(_ element: T) {
        array.append(element)
    }
    
    
    // 移除队列
    public mutating func dequeue() -> T? {
        // 异常处理,记录要移除的元素
        guard array.count > headIndex, let elementToRemove = array[headIndex] else {
            return nil
        }
        
        // 移除元素
        array[headIndex] = nil // 将要移除的元素的空间设为 nil
        headIndex += 1         // head 索引加 1
        
        // 定期调整元素位置
        let percentage = Double(headIndex) / Double(array.count)
        let triggerCount = 50
        if array.count > triggerCount, percentage > 0.25 {  //    或者其他条件:    if headIndex > 2 {
            array.removeFirst(headIndex)    // 移除前 headIndex 个元素
            headIndex = 0                   // 重置真实 head 索引值
        }
        
        return elementToRemove
    }
    
    // 首元素
    public var front: T? {
        // 先判断是否为空
        if isEmpty {
            return nil
        } else {
            return array[headIndex]
        }
    }
    
}

(2) Objective-C 版本的实现

EfficientQueue.h

@interface EfficientQueue<ObjectType> : NSObject

@property (assign, nonatomic, readonly) NSUInteger count;
@property (assign, nonatomic, readonly) BOOL isEmpty;
@property (strong, nonatomic, readonly) ObjectType front;

- (ObjectType)dequeue;
- (void)enqueueObject:(ObjectType)object;

@end

EfficientQueue.m


@interface EfficientQueue ()

@property (strong, nonatomic) NSMutableArray *array;
@property (assign, nonatomic) NSInteger headIndex;

@end

@implementation EfficientQueue

- (instancetype)init {
    self = [super init];
    if (self) {
        _array = [NSMutableArray array];
        _headIndex = 0;
    }
    return self;
}

- (BOOL)isEmpty {
    return (self.count == 0);
}

- (NSUInteger)count {
    return self.array.count - self.headIndex;
}

- (id)front {
    if (self.isEmpty) {
        return nil;
    }
    
    return self.array[self.headIndex];
}

- (id)dequeue {
    if (self.isEmpty) {
        return nil;
    }
    
    // 移除
    id objectToRemove = self.front;
    self.array[self.headIndex] = [NSNull null];
    self.headIndex++;
    
    // 定期清理空值
    double percentage = self.headIndex / (double)(self.array.count);
    if (percentage > 0.25 && self.count > 100) {
        [self.array removeObjectsInRange:NSMakeRange(0, self.headIndex)];
        self.headIndex = 0;
    }
    
    return objectToRemove;
}

- (void)enqueueObject:(id)object {
    [self.array addObject:object];
}

- (NSString *)description {
    
    NSMutableString *string = [NSMutableString string];
    [string appendString:@"EfficientQueue: ["];
    
    for (id <NSObject> element in self.array) {
        if ([element isKindOfClass:[NSNull class]]) {
            continue;
        }
        
        if (element != self.front) {
            [string appendString:@", "];
        }
        
        [string appendString:element.description];
    }
    
    [string appendString:@"]"];
    
    return string;
}

@end

五、队列的应用

在经典的 iOS 图片下载框架 SDWebImage 中,SDWebImageDownloader 类里面就通过一个 executionOrder 来决定下载队列中下载任务的先后顺序,是 FIFO,还是 LIFO。

六、我所理解的队列

  • 队列是一种表
  • 队列的元素操作顺序是先进先出(FIFO)

七、相关面试题(TODO)

八、参考资料

如果你也喜欢交流技术、喜欢阅读、积极践行,欢迎关注我的公众号:祥龙Shannon写字的地方,一起成长。

《数据结构和算法:队列(Queue)》 qrcode_for_gh_cc686217be41_344.jpg

    原文作者:ShannonChenCHN
    原文地址: https://www.jianshu.com/p/62dcd5657413
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞