2-4 队列 - 图1

1. 队列的定义

队列,顾名思义,就是一种类似于日常生活中排队的数据结构。假设我们到银行柜台排队办理业务,柜台的服务人员必定先处理队头人员的业务,当有新人排队时,一般都排在队尾。

类似于排队,遵循先进先出,后进后出的数据结构,我们成为队列。

2. 队列的实现

队列可以用链表、数组实现。使用数组实现的队列称为顺序队列,使用链表实现的队列称为链式队列。下面我们一一介绍。

2.1 顺序队列

当我们使用数组实现队列时,首先要考虑的问题是:数组的哪一端作为队头,哪一端作为队尾?
显而易见,应该使用数组头部作为队头,使用数组尾部作为队尾,为什么呢?
若我们采用数组头部作为队尾,那么每次入队时,就必须要进行数据搬移工作,效率非常低下。
虽然将数组头部作为队头也会进行数据搬移工作,但我们可以通过一系列的优化手段,降低其时间复杂度。

  • 我们准备两个指针,表明队头、队尾的位置;
  • 当入队时,队尾指针后移,当出队时,队头指针后移,这样就实现了时间复杂度为2-4 队列 - 图2的入队、出队操作。

那么有同学会问了,这样的话不就会造成一部分内存浪费了吗?事实确实如此,因此我们需要继续优化:

  • 当队尾指针指向数组尾部时,此时若是数组中仍有空闲位置,则将head~tail间的元素搬运到0~(tail-head)位置来,这样就可以继续插入了。

094ba7722eeec46ead58b40c097353c7.webp


接下来我们分析一下时间复杂度:

  • 出队操作因不会触发数据搬移,因此其时间复杂度为2-4 队列 - 图4
  • 入队时,若不涉及数据搬运,则时间复杂度为<最好时间复杂度>
  • 入队时,若不涉及数据搬运,则时间复杂度为2-4 队列 - 图5<最坏时间复杂度>

那么平均时间复杂度呢?
分析整个入队过程,假设数组容量为n,那么在n-1次的时间复杂度为入队操作后,第n次时间复杂度会提升到,此后会一直循环这个过程,直到队满,那么将时间复杂度为的这一次操作均摊到其他n-1次操作后,我们可以得出平均时间复杂度为,也就是最好时间复杂度。


具体代码实现如下:

  1. class CArrayQueue
  2. {
  3. public:
  4. CArrayQueue(const int &ncount = 10) : m_ncount(ncount)
  5. {
  6. if (nullptr != m_strData)
  7. {
  8. delete m_strData;
  9. m_strData = nullptr;
  10. }
  11. m_strData = new std::string[ncount];
  12. if (nullptr == m_strData)
  13. {
  14. return;
  15. }
  16. }
  17. ~CArrayQueue()
  18. {
  19. if (nullptr != m_strData)
  20. {
  21. delete[] m_strData;
  22. m_strData = nullptr;
  23. }
  24. }
  25. int size()
  26. {
  27. return m_ntail - m_nhead;
  28. }
  29. bool empty()
  30. {
  31. return m_nhead == m_ntail;
  32. }
  33. bool full()
  34. {
  35. return size() == m_ncount;
  36. }
  37. bool enqueue(const std::string &strElem)
  38. {
  39. if (full())
  40. {
  41. return false;
  42. }
  43. if (m_ntail == m_ncount)
  44. {
  45. int ntemp = 0;
  46. // 执行一次数据搬移操作
  47. for (int i = m_nhead; i < m_ntail; ++i)
  48. {
  49. m_strData[ntemp++] = m_strData[i];
  50. }
  51. m_ntail = m_ntail - m_nhead;
  52. m_nhead = 0;
  53. }
  54. m_strData[m_ntail++] = strElem;
  55. return true;
  56. }
  57. bool dequeue(std::string &strElem)
  58. {
  59. if (empty())
  60. {
  61. return false;
  62. }
  63. strElem = m_strData[m_nhead++];
  64. return true;
  65. }
  66. void print()
  67. {
  68. for(int i = m_nhead; i < m_ntail; ++i)
  69. {
  70. std::cout << "队列元素_" << i << "内容:" << m_strData[i] << std::endl;
  71. }
  72. }
  73. private:
  74. std::string *m_strData = nullptr; // 队列内容
  75. int m_ncount = 0; // 队列容量
  76. int m_nhead = 0; // 队首指针
  77. int m_ntail = 0; // 队尾指针
  78. };

2.2 链式队列

链式队列采用链表实现,我们使用head、tail两个指针分别记录队头,队尾,当入队时,tail->next=new_node,tail=new_node即可,当出队时,new_node->next=head->next,head=new_node
下面时整个流程示意图:
c916fe2212f8f543ddf539296444d393.webp
代码如下:

  1. struct SNode
  2. {
  3. std::string m_strData;
  4. SNode *m_next;
  5. };
  6. class CListQueue
  7. {
  8. public:
  9. CListQueue()
  10. {
  11. m_head = new SNode;
  12. if (nullptr == m_head)
  13. {
  14. return;
  15. }
  16. m_head->m_next = nullptr;
  17. m_first = m_last = nullptr;
  18. }
  19. ~CListQueue()
  20. {
  21. SNode *node = nullptr;
  22. while (nullptr != m_head->m_next)
  23. {
  24. node = m_head;
  25. m_head = m_head->m_next;
  26. delete node;
  27. node = nullptr;
  28. }
  29. }
  30. int size()
  31. {
  32. SNode *temp = m_first;
  33. int nb = 0;
  34. while (nullptr != temp)
  35. {
  36. nb++;
  37. temp = temp->m_next;
  38. }
  39. return nb;
  40. }
  41. bool empty()
  42. {
  43. return 0 == size();
  44. }
  45. bool enqueue(const std::string &strElem)
  46. {
  47. SNode *node = new SNode;
  48. if (nullptr == node)
  49. {
  50. return false;
  51. }
  52. node->m_strData = strElem;
  53. node->m_next = nullptr;
  54. if (0 == size())
  55. {
  56. m_first = m_last = node;
  57. m_head->m_next = node;
  58. return true;
  59. }
  60. m_last->m_next = node;
  61. m_last = m_last->m_next;
  62. return true;
  63. }
  64. bool dequeue(std::string &strElem)
  65. {
  66. if (empty())
  67. {
  68. return false;
  69. }
  70. strElem = m_first->m_strData;
  71. SNode *temp = m_first;
  72. m_first = m_first->m_next;
  73. m_head->m_next = m_first;
  74. if (nullptr != temp)
  75. {
  76. delete temp;
  77. temp = nullptr;
  78. }
  79. }
  80. void print()
  81. {
  82. SNode *temp = m_first;
  83. int nb = 0;
  84. while (nullptr != temp)
  85. {
  86. std::cout << "队列内容:" << temp->m_strData << std::endl;
  87. temp = temp->m_next;
  88. }
  89. }
  90. private:
  91. SNode *m_head; // 头节点
  92. SNode *m_first; // 第一个节点
  93. SNode *m_last; // 最后一个节点
  94. };

3. 循环队列

上述的顺序队列虽然经过优化之后,效率已经可以了,但是在某些情况下时间复杂度还是退化为2-4 队列 - 图7。那么我们如何继续优化呢?
我们注意到,时间复杂度退化为2-4 队列 - 图8的时候,恰好是搬运数据的时候,那么我们如何避免数据搬运呢?
答案就是我们将数组头部与尾部连接起来,这样就可以省去数据搬运的过程。
具体结构如下图所示:
58ba37bb4102b87d66dffe7148b0f990.webp
看上图可知,当队尾指针达到数组末尾时,并不会增长到8,而是会变为0,这样数据就可以继续入队,我们也通过这一方法成功的避免了数据搬移工作。
循环队列的原理看起来很简单,但是其代码实现很复杂,首先我们需要明确,如何判断队满还是队空?还是依赖于head=tail这一条件判断吗?显示不是!!因为我们发现当队满或队空是都会满足这一条件。因此需要做一些取舍:

  • 采用head=tail 这一条件作为循环队列为空的判定条件
  • 牺牲一个存储单元,当满足(tail + 1) % count = head时,判定循环队列满。

3d81a44f8c42b3ceee55605f9aeedcec.webp
经过上述分析可知,队尾、队头指针可能会由8变为0,那么如何实现这一过程呢?我们仅需要对自增后的指针取模即可,即tail=(tail+1)%ncount head=(head+1)%ncount,同理,计算循环队列元素个数的公式为:size=(tail - head + ncount) % ncount

代码如下:

  1. class CLoopQueue
  2. {
  3. public:
  4. CLoopQueue(const int &ncount = 10) : m_ncount(ncount)
  5. {
  6. if (nullptr != m_strData)
  7. {
  8. delete m_strData;
  9. m_strData = nullptr;
  10. }
  11. m_strData = new std::string[ncount];
  12. if (nullptr == m_strData)
  13. {
  14. return;
  15. }
  16. }
  17. ~CLoopQueue()
  18. {
  19. if (nullptr != m_strData)
  20. {
  21. delete[] m_strData;
  22. m_strData = nullptr;
  23. }
  24. }
  25. int size()
  26. {
  27. return (m_ntail - m_nhead + m_ncount) % m_ncount;
  28. }
  29. bool empty()
  30. {
  31. return m_nhead == m_ntail;
  32. }
  33. bool full()
  34. {
  35. return (m_ntail + 1) % m_ncount == m_nhead;
  36. }
  37. bool enqueue(const std::string &strElem)
  38. {
  39. if (full())
  40. {
  41. return false;
  42. }
  43. m_strData[m_ntail] = strElem;
  44. m_ntail = (m_ntail + 1) % m_ncount;
  45. return true;
  46. }
  47. bool dequeue(std::string &strElem)
  48. {
  49. if (empty())
  50. {
  51. return false;
  52. }
  53. strElem = m_strData[m_nhead];
  54. m_nhead = (m_nhead + 1) % m_ncount;
  55. return true;
  56. }
  57. void print()
  58. {
  59. if (m_nhead < m_ntail)
  60. {
  61. for(int i = m_nhead; i < m_ntail; ++i)
  62. {
  63. std::cout << "队列元素_" << i << "内容:" << m_strData[i] << std::endl;
  64. }
  65. }
  66. else
  67. {
  68. for(int i = m_nhead; i < m_nhead + m_ncount; ++i)
  69. {
  70. std::cout << "队列元素_" << i % m_ncount << "内容:" << m_strData[i % m_ncount] << std::endl;
  71. }
  72. }
  73. }
  74. private:
  75. std::string *m_strData = nullptr; // 队列内容
  76. int m_ncount = 0; // 队列容量
  77. int m_nhead = 0; // 队首指针
  78. int m_ntail = 0; // 队尾指针
  79. };