1. 假溢出与循环队列

队列的特点是先进先出(FIFO),只能从队列头读取元素,插入元素追加到队列尾

队列中存在两个指针,front表示队列头,rear表示队列尾

1.1 假溢出

顺序队列的操作问题:
image.png

  • 假设队列长度为5,当frontrear都指向位置0,表示当前队列为空,当front == rear,表示当前队列已满

  • C1C2C3相继入队,此时front位置不变,rear指向位置3

  • C1C2相继出队,此时front指向位置2rear指向位置3

  • C4C5C6相继入队,然后C3C4相继出队,此时front指向位置4,rear指向位置5

  • 此时的问题,队列中位置0 ~ 位置3都是空位,但元素无法入队

当出队速度大于入队速度,就会出现上述问题,我们称之为“假溢出”

1.2 循环队列

解决顺序队列的假溢出问题,可以将顺序队列理解成一个环
image.png

此时我们构成了一个循环队列,里面的向量称之为循环向量

在循环队列中,头尾指针和向量的关系是不变的,只是读取方式发生了一些变化
image.png

  • 使用(rear + 1) % MAXSIZE表示队尾

  • front == rear,表示当前队列为空

  • 当队列存满时,frontrear也会指向相同位置,这会导致我们无法区分当前队列是满还是空。所以对于一个非空队列,我们永远不能让它的头尾相遇。我们可以将队列中预留一个空位,当(rear + 1) % MAXSIZE == front,表示当前队列已满

示例:
image.png

  • 当循环队列为空时,frontrear都指向位置0

  • 当三个元素入队,front位置不变,rear指向位置3

  • 当一个元素出队,front指向位置1rear位置不变

  • 如果队列存满,就会导致头尾相遇,此时无法判断队满还是队空

  • 正确的做法,牺牲一个存储单元。当(rear + 1) % MAXSIZE == front,表示当前队列已满

2. 顺序队列

顺序队列为了避免“假溢出”问题,从逻辑上需要设计成循环队列的形式。为了区别队空和队满的情况,需要牺牲一个存储单元,永远不能让它的头尾相遇

2.1 创建

  1. class LinearQueue<Element>{
  2. fileprivate var _data : UnsafeMutablePointer<Element>;
  3. fileprivate var _capacity : Int;
  4. fileprivate var _front : Int;
  5. fileprivate var _rear : Int;
  6. var length : Int {
  7. get{
  8. return (_rear - _front + _capacity) % _capacity;
  9. }
  10. }
  11. init(capacity : Int){
  12. _capacity = capacity + 1;
  13. _data = UnsafeMutablePointer<Element>.allocate(capacity: _capacity);
  14. _front = 0;
  15. _rear = 0;
  16. }
  17. func clear(){
  18. _front = 0;
  19. _rear = 0;
  20. }
  21. func isEmpty() -> Bool{
  22. return _front == _rear;
  23. }
  24. func isFull() -> Bool{
  25. let last = (_rear + 1) % _capacity;
  26. return last == _front;
  27. }
  28. fileprivate func move(n : inout Int){
  29. n = (n + 1) % _capacity;
  30. }
  31. }

2.2 获取元素

  1. class LinearQueue<Element>{
  2. func getHead() -> Element?{
  3. if(isEmpty()){
  4. return nil;
  5. }
  6. return _data.advanced(by: _front).pointee;
  7. }
  8. }

2.3 入队 & 出队

  1. class LinearQueue<Element>{
  2. func enter(element : Element) -> Int{
  3. if(isFull()){
  4. return ERROR;
  5. }
  6. _data.advanced(by: _rear).pointee = element;
  7. move(n: &_rear);
  8. return OK;
  9. }
  10. func out() -> Element?{
  11. let data = getHead();
  12. if(data == nil){
  13. return data;
  14. }
  15. move(n: &_front);
  16. return data;
  17. }
  18. }

2.4 元素遍历

  1. class LinearQueue<Element>{
  2. func traverse() -> String {
  3. var str : String = "";
  4. if(isEmpty()){
  5. return str;
  6. }
  7. var n = _front;
  8. while(n != _rear){
  9. let val = _data.advanced(by: n).pointee;
  10. str += "\(val),"
  11. move(n: &n);
  12. }
  13. return str;
  14. }
  15. }

3. 链式队列

链式队列不存在“假溢出”问题,因为链式队列没有存储长度的限制

链式队列本质上就是一个单向链表,逻辑比单向链表更简单,因为它只能删除队头,只能在队尾插入
image.png

  • front指向头结点,rear指向尾结点

  • 创建链式队列,默认frontrear都指向头结点

  • frontrear都指向头结点时,表示当前链式队列为空

  • 出队时,删除的永远是首元结点,也就是front的后继

  • 入队时,将rear的后继指向新结点,然后将rear指向新结点

2.1 创建

  1. class LinkedQueue<Element>{
  2. fileprivate class Node<T> {
  3. var next : Node<T>?;
  4. var data : T?;
  5. init(){
  6. }
  7. init(data : T){
  8. self.data = data;
  9. self.next = nil;
  10. }
  11. deinit {
  12. print(data == nil ? "头结点释放" : "值为\(data!)的Node释放");
  13. }
  14. }
  15. fileprivate var _front : Node<Element>?;
  16. fileprivate var _rear : Node<Element>?;
  17. fileprivate var _count : Int;
  18. var length : Int {
  19. get{
  20. return _count;
  21. }
  22. }
  23. init(){
  24. _front = Node();
  25. _rear = _front;
  26. _count = 0;
  27. }
  28. func clear(){
  29. _front?.next = nil;
  30. _rear = _front;
  31. _count = 0;
  32. }
  33. func destory(){
  34. _front = nil;
  35. _rear = nil;
  36. _count = 0;
  37. }
  38. func isEmpty() -> Bool {
  39. return _front?.next == nil;
  40. }
  41. }

2.2 获取元素

  1. class LinkedQueue<Element>{
  2. func getHead() -> Element?{
  3. if(isEmpty()){
  4. return nil;
  5. }
  6. return _front?.next?.data;
  7. }
  8. }

2.3 入队 &出队

  1. class LinkedQueue<Element>{
  2. func enter(element : Element) -> Int {
  3. let node = Node(data: element);
  4. _rear?.next = node;
  5. _rear = node;
  6. _count += 1;
  7. return OK;
  8. }
  9. func out() -> Element?{
  10. let data = getHead();
  11. if(data == nil){
  12. return data;
  13. }
  14. _front?.next = _front?.next?.next;
  15. _count -= 1;
  16. if(isEmpty()){
  17. _rear = _front;
  18. }
  19. return data;
  20. }
  21. }

2.4 元素遍历

  1. class LinkedQueue<Element>{
  2. func traverse() -> String {
  3. var str : String = "";
  4. if(isEmpty()){
  5. return str;
  6. }
  7. var node = _front?.next;
  8. while(node != nil){
  9. str += "\(node!.data!),"
  10. node = node?.next;
  11. }
  12. return str;
  13. }
  14. }

总结

假溢出:

  • 顺序队列有存储长度的限制。当出队速度大于入队速度,即使队列中有空位,元素也无法入队。这种问题我们称之为“假溢出”

循环队列:

  • 解决顺序队列的假溢出问题,可以将顺序队从逻辑上设置成循环队列

  • 为了区分队空和队满,需要牺牲一个存储单元,永远不能让它的头尾相遇

  • front == rear,表示当前队列为空

  • (rear + 1) % MAXSIZE == front,表示当前队列已满

链式队列:

  • 链式队列不存在“假溢出”问题,因为链式队列没有存储长度的限制

  • 链式队列本质上就是一个单向链表,逻辑比单向链表更简单,因为它只能删除队头,只能在队尾插入

  • frontrear都指向头结点时,表示当前链式队列为空

  • 出队时,删除的永远是首元结点,也就是front的后继


  • 入队时,将rear的后继指向新结点,然后将rear指向新结点