队列——数组实现和链式实现
/***********************************************************************/ /*队列和栈一样也是一个受限线性表 ,栈要求的是在同一端进行插入和删除操作*/ /*而队列,它要求在一端进行插入(入队),另一端进行删除(出队的操作) */ /*我们把允许入队操作的一端称为队尾,把允许出队列的一端称为队首 */ /*栈是一个后进先出的结构,而队列则是一个先进先出的数据结构 */ /*任何数据结构都可以有链式存储和数组存储两种存储结构 */ /*本次采用的实现方式是数组实现,队列寻找元素的方式也有三种 */ /*第一种:将第i个元素放在i位置,但是这样虽然能够很的利用空间 */ /*但是删除的时间复杂度将会是O(n),因为每删除一个圆度都要将首元素后面的 */ /*元素向前移动n-1次(n是队列的大小),所以删除的时间复杂度为O(n) */ /*这种方式暗示我们始终把数组的最左端视为队列的首部 */ /*而将数组的右端size - 1位置视为队列的尾部(size为队列的长度) */ /*其队列的大小就是 队尾索引值 + 1 */ /*第二种方式:队列的首部是数组左边的一个游标,每次删除操作都动态更新 */ /*初始话的时候它指向一个无效位置 */ /*队列的尾部是数组右边的一个游标,每次插入都动态更新它,初始化时指向 */ /*一个无效位置 */ /*所以元素的定位公式是 location(i) = location(0) (队首元素的位置)+ i */ /*队列的大小是: 队尾索引 - 队首索引 +1 */ /*使用这种方式,我们需要一个3元组才能描述这个队列,即 */ /*(队首位置,队尾位置,存储元素的一维数组) */ /*其ADT大致如下: */ /* class ArrayQueue */ /* { * public: * int size() const * { * if( -1 = m_iBack ) * { * return 0; * } * return m_iBack - m_iFront +1; * } * int empty() const * { * if( -1 == m_iBack || m_iFront > m_iBack) //在连续删除不插入的时候就有可能出现 m_iFront > m_iBack的时候 * return true; * return false; * } * void pop() //删除队首元素 * { * if(true == empty()) * return; * else * { * m_pArray[m_iFront].~T(); * ++m_iFront; //将数组的左端当做队列首部,这样能使得插入和删除的时间复杂度都是O(1) * } * } * void push() * { * //将数组的右端视为队列的尾部 * if( m_iBack >= m_iMaxSize-1 ) //说明此队列已经满了 * { * return; //其实应该是扩大数组容量, 迁移元素,但是这个类要讨论的不是这种实现方式,或者将队列整体左移 * } * ++m_iBack; * m_pArray[m_iBack] = T; * } * private: * int m_iMaxSize{100}; //队列的最大大小 * int m_iFront{ -1 }; //队首元素的索引 * int m_iBack{ -1 }; //队尾元素的索引 * T *m_pArray{nullptr}; * } * 通过上面的简单实现,可以看出一个问题,当逐步执行删除操作时,会造成较大的空间浪费 */ /*方式三:循环队列,就是将一维数组首尾相连视为一个环,每个元素的位置由公式 */ /*现在队列是一个环形,那么意味着每一个元素都有其下一个位置和前一个位,此时确定第i个 */ /*元素的位置,使用公式:location(i) = (location(0)(队首元素的位置) + i ) % arrayLength */ /*公式中arrayLength是你为这个队列分配的一维数组的大小,而location(0)则是队首元素的位置 */ /*由公式可知要知道第i个元素的位置,必须要知道第0个元素的位置,假定和队首元素位置相关的 */ /*变量其名字为queueFront,现在我们约定,queueFront指向的不是队首元素,而是沿逆时针方向 */ /*指向队首元素的下一位置 */ /*另外,依然使用一个变量来记录队尾元素的位置,假设名字为queueBack,它直接指向队尾元素 */ /***************************************************************************************/ #ifndef ARRAYQUEUE_H_ #define ARRAYQUEUE_H_ //本例中使用环形数组来描述这个队列 #include <stdexcept> #include <cmath> #include <new> #include <algorithm> template <typename T> class ArrayQueue { public: bool empty() const { //这个判断条件意味着,在这个数组中,我们永远留着一个空位,确保m_iTheFront和m_iTheRear除了初始情况之外,永远不会存在相等的情况 //所以在每一次的插入操作之前,都需要先判断一下本次操作会不会让队列变满,如果会,那么就扩充数组容量,然后再执行插入操作, //按照这种策略,队列中的元素个数最多只能是 m_iArraySize - 1 if(m_iTheFront == m_iTheRear) return true; return false; } void push(const T &element); //在队尾插入元素 void pop(); //在队首删除元素 T& front(); //返回队首元素 T& back(); //返回队尾元素 int size() const; //返回队列中元素的个数 int maxsize() const; //返回队列能容纳的最大元素个数 ArrayQueue(); ~ArrayQueue(); private: int m_iTheFront{0}; //指向队列首元素的游标,重要,初始情况下,m_iTheFront和m_iTheRear的值都是0 int m_iTheRear{0}; //指向队列尾元素的游标 int m_iArraySize{10}; T *m_pArray{nullptr}; void expandCapacity(); //扩充一维数组的容量 }; template <typename T> void ArrayQueue<T>::push(const T &element) { if( (m_iTheRear + 1) % m_iArraySize == m_iTheFront ) { expandCapacity(); //如果下一个插入位置刚好和首元素逆时针方向上的下一位置游标相等,说明这个队列已经满了 } m_iTheRear = (m_iTheRear + 1) % m_iArraySize; m_pArray[m_iTheRear] = element; } template <typename T> void ArrayQueue<T>::pop() { if(true == empty()) return; m_iTheFront = (m_iTheFront + 1) % m_iArraySize; m_pArray[m_iTheFront].~T(); } template <typename T> T& ArrayQueue<T>::front() { if(true == empty()) throw std::runtime_error("Empty Queue"); //先计算出队列首元素的位置,然后返回它的值 return m_pArray[(m_iTheFront + 1) % m_iArraySize]; } template <typename T> T& ArrayQueue<T>::back() { //返回队列的尾部元素,m_iTheRear这个游标直接指向队尾元素 if(true == empty()) throw std::runtime_error("Empty Queue"); return m_pArray[m_iTheRear]; } template <typename T> int ArrayQueue<T>::size() const { //循环队列的大小计算分为两种情况 //情形一:m_iTheFront < m_iTheRear,那么队列的大小就是 m_iTheRear - m_iTheFront //情形二:m_iTheFront > m_iTheRear,那么计算起来就分为两部分了,一部分是 0 + m_iTheRear,另一部分是 m_iArraySize - m_iTheFront,所以合起来就是 //m_iTheRear + m_iArraySize - m_iTheFront //把上述两种情况合在一起,那么计算大小的公式就是 (m_iTheRear - m_iTheFront + m_iArraySize) % m_iArraySize return (m_iTheRear - m_iTheFront + m_iArraySize) % m_iArraySize; } template <typename T> int ArrayQueue<T>::maxsize() const { return m_iArraySize - 1; //数组中最多只能存储最大大小减去一个位置的元素 } template <typename T> ArrayQueue<T>::ArrayQueue() { //如果分配内存失败的话,会抛出一个bad_alloc的异常,记得使用时捕获它 m_pArray = new T[m_iArraySize * sizeof(T)]; } template <typename T> ArrayQueue<T>::~ArrayQueue() { for(int i = 0; i < m_iArraySize; ++i) { //析构函数,要把数组中的每一对象释放了,因为new的时候会调用默认构造函数,如果是使用了内存分配器allocate,那么要进行更加精细的操作 m_pArray[i].~T(); } if(nullptr != m_pArray) { delete[] m_pArray; m_pArray; } } template <typename T> void ArrayQueue<T>::expandCapacity() { //首先计算出队列首元素的在数组中所在的索引,现在我们把这个一维数组视为一个环形,这里的m_iTheFront指向的是自队列首元素结点处逆时针方向的下一位置 //但是它本质上其实还是一个一维数组,一维数组它在内存中的索引从0开始,那么怎么判断目前的结构是不是形成了一个环呢? //只要m_iTheRear(尾元素游标)和 m_iTheFront(首元素处逆时针下一位置游标)都处于0以及0之后的索引,那么就认为它形成了一个环。 //求出队列首元素实际的存储位置,因为我们是把一维数视为环形的,并且m_iTheFront是首元素逆时针方向的下一位置,将它想象成一个环,可以得到求首元素位置的公式为: // (m_iTheFront + 1) % m_iArraySize(数组大小),要对数组大小取模,是因为,你的环,每一个位置上的索引是固定的,不是无限增长的。 int iStart = (m_iTheFront + 1) % m_iArraySize; //队列首元素的位置索引 T *newQueue = new (std::nothrow) T[2 * m_iArraySize]; if(iStart < 2) { //没有形成环 std::copy(m_pArray + iStart , m_pArray + iStart + m_iArraySize - 1,newQueue); } else { //形成环 std::copy(m_pArray+iStart,m_pArray+m_iArraySize,newQueue); std::copy(m_pArray,m_pArray + m_iTheRear + 1,newQueue + m_iArraySize - iStart); } //设置新的队列的首元素和尾元素的位置 m_iTheFront = 2*m_iArraySize - 1; m_iTheRear = m_iArraySize - 2; // 这里是m_iArraySize -2 ; -2 的原因是满队列的情况下还要留出一个位置,然后索引最大是m_iArraySize -2.综合起来就得到了-2这个结论 m_iArraySize *= 2; delete [] m_pArray; m_pArray = newQueue; } template <typename T> class Node { public: Node() = default; Node(const T& element) : m_Element(element){}; Node(const T& element,const Node<T> *pNext):m_Element(element),m_pNextNode(pNext) {}; T m_Element; Node<T> *m_pNextNode{nullptr}; }; template <typename T> class LinkedQueue { public: bool empty() const { if(nullptr == m_pFront) return true; return false; } int size() const { return m_iSize; } T& front() const; //返回队列的队首元素 T& back() const; //返回队列的队尾元素 void push(const T& element); //从队列的尾部插入元素 void pop(); //删除队列首部的元素 LinkedQueue() = default; ~LinkedQueue(); private: Node<T> *m_pFront{nullptr}; //队列的首部 int m_iSize{0}; //队列大小 Node<T> *m_pBack{nullptr}; //队列的尾部 void clear(); //清理数据结构 }; template <typename T> T& LinkedQueue<T>::front() const { if(false == empty()) return m_pFront->m_Element; } template <typename T> T& LinkedQueue<T>::back() const { if(false == empty()) return m_pBack->m_Element; } template <typename T> void LinkedQueue<T>::push(const T &element) { Node<T> *pNewNode = new(std::nothrow) Node<T>(element); if(nullptr == pNewNode) return; if(m_iSize == 0) { //如果大小等于0,那么说明,这是第一个被插入链表的元素 m_pFront = pNewNode; //这时候时头指针指向新的结点 } else { //队列不为空,此时链表的生成方向有两种,一种是从头部开始链接,另一种从尾部开始链接,当你写到pop函数的时候就能发现如果从尾部开始链接,删除会变的很复杂,因为我们的链表是单向的 m_pBack->m_pNextNode = pNewNode; //令新结点的指针域指向记录尾结点位置的m_pBack } m_pBack = pNewNode; //更新尾结点 ++m_iSize; } template <typename T> void LinkedQueue<T>::pop() { if(true == empty()) return; Node<T> *pTemp = m_pFront; //保存一下队首元素的位置,这个待会要删除结点用 m_pFront = pTemp->m_pNextNode; pTemp->m_Element.~T(); //销毁对象 if(pTemp != nullptr) { delete pTemp; pTemp = nullptr; } --m_iSize; if( 0 == m_iSize) m_pFront = m_pBack = nullptr; } template <typename T> void LinkedQueue<T>::clear() { for(int i=0; i < m_iSize; ++i) { Node<T> *pTempNode = m_pFront; m_pFront = pTempNode->m_pNextNode; if(nullptr != pTempNode) { delete pTempNode; pTempNode = nullptr; } } m_pFront = m_pBack = nullptr; } template <typename T> LinkedQueue<T>::~LinkedQueue() { clear(); } #endif
以上代码均在gcc4.8.5上跑过了。欢迎指正。
/***********************************************************************//*队列和栈一样也是一个受限线性表 ,栈要求的是在同一端进行插入和删除操作*//*而队列,它要求在一端进行插入(入队),另一端进行删除(出队的操作) *//*我们把允许入队操作的一端称为队尾,把允许出队列的一端称为队首 *//*栈是一个后进先出的结构,而队列则是一个先进先出的数据结构 *//*任何数据结构都可以有链式存储和数组存储两种存储结构 *//*本次采用的实现方式是数组实现,队列寻找元素的方式也有三种 *//*第一种:将第i个元素放在i位置,但是这样虽然能够很的利用空间 *//*但是删除的时间复杂度将会是O(n),因为每删除一个圆度都要将首元素后面的 *//*元素向前移动n-1次(n是队列的大小),所以删除的时间复杂度为O(n) *//*这种方式暗示我们始终把数组的最左端视为队列的首部 *//*而将数组的右端size - 1位置视为队列的尾部(size为队列的长度) *//*其队列的大小就是 队尾索引值 + 1 *//*第二种方式:队列的首部是数组左边的一个游标,每次删除操作都动态更新 *//*初始话的时候它指向一个无效位置 *//*队列的尾部是数组右边的一个游标,每次插入都动态更新它,初始化时指向 *//*一个无效位置 *//*所以元素的定位公式是 location(i) = location(0) (队首元素的位置)+ i *//*队列的大小是: 队尾索引 - 队首索引 +1 *//*使用这种方式,我们需要一个3元组才能描述这个队列,即 *//*(队首位置,队尾位置,存储元素的一维数组) *//*其ADT大致如下: *//* class ArrayQueue *//* { * public: * int size() const * { * if( -1 = m_iBack ) * { * return 0; * } * return m_iBack - m_iFront +1; * } * int empty() const * { * if( -1 == m_iBack || m_iFront > m_iBack) //在连续删除不插入的时候就有可能出现 m_iFront > m_iBack的时候 * return true; * return false; * } * void pop() //删除队首元素 * { * if(true == empty()) * return; * else * { * m_pArray[m_iFront].~T(); * ++m_iFront; //将数组的左端当做队列首部,这样能使得插入和删除的时间复杂度都是O(1) * } * } * void push() * { * //将数组的右端视为队列的尾部 * if( m_iBack >= m_iMaxSize-1 ) //说明此队列已经满了 * { * return; //其实应该是扩大数组容量, 迁移元素,但是这个类要讨论的不是这种实现方式,或者将队列整体左移 * } * ++m_iBack; * m_pArray[m_iBack] = T; * } * private: * int m_iMaxSize{100}; //队列的最大大小 * int m_iFront{ -1 }; //队首元素的索引 * int m_iBack{ -1 }; //队尾元素的索引 * T *m_pArray{nullptr}; * } * 通过上面的简单实现,可以看出一个问题,当逐步执行删除操作时,会造成较大的空间浪费 *//*方式三:循环队列,就是将一维数组首尾相连视为一个环,每个元素的位置由公式 *//*现在队列是一个环形,那么意味着每一个元素都有其下一个位置和前一个位,此时确定第i个 *//*元素的位置,使用公式:location(i) = (location(0)(队首元素的位置) + i ) % arrayLength *//*公式中arrayLength是你为这个队列分配的一维数组的大小,而location(0)则是队首元素的位置 *//*由公式可知要知道第i个元素的位置,必须要知道第0个元素的位置,假定和队首元素位置相关的 *//*变量其名字为queueFront,现在我们约定,queueFront指向的不是队首元素,而是沿逆时针方向 *//*指向队首元素的下一位置 *//*另外,依然使用一个变量来记录队尾元素的位置,假设名字为queueBack,它直接指向队尾元素 *//***************************************************************************************/
#ifndef ARRAYQUEUE_H_#define ARRAYQUEUE_H_//本例中使用环形数组来描述这个队列#include <stdexcept>#include <cmath>#include <new>#include <algorithm>template <typename T>class ArrayQueue{public: bool empty() const { //这个判断条件意味着,在这个数组中,我们永远留着一个空位,确保m_iTheFront和m_iTheRear除了初始情况之外,永远不会存在相等的情况 //所以在每一次的插入操作之前,都需要先判断一下本次操作会不会让队列变满,如果会,那么就扩充数组容量,然后再执行插入操作, //按照这种策略,队列中的元素个数最多只能是 m_iArraySize - 1 if(m_iTheFront == m_iTheRear) return true; return false; } void push(const T &element); //在队尾插入元素 void pop(); //在队首删除元素 T& front(); //返回队首元素 T& back(); //返回队尾元素 int size() const; //返回队列中元素的个数 int maxsize() const; //返回队列能容纳的最大元素个数 ArrayQueue(); ~ArrayQueue();private: int m_iTheFront{0}; //指向队列首元素的游标,重要,初始情况下,m_iTheFront和m_iTheRear的值都是0 int m_iTheRear{0}; //指向队列尾元素的游标 int m_iArraySize{10}; T *m_pArray{nullptr}; void expandCapacity(); //扩充一维数组的容量};
template <typename T>void ArrayQueue<T>::push(const T &element){ if( (m_iTheRear + 1) % m_iArraySize == m_iTheFront ) { expandCapacity(); //如果下一个插入位置刚好和首元素逆时针方向上的下一位置游标相等,说明这个队列已经满了 } m_iTheRear = (m_iTheRear + 1) % m_iArraySize; m_pArray[m_iTheRear] = element;}
template <typename T>void ArrayQueue<T>::pop(){ if(true == empty()) return; m_iTheFront = (m_iTheFront + 1) % m_iArraySize; m_pArray[m_iTheFront].~T();}
template <typename T>T& ArrayQueue<T>::front(){ if(true == empty()) throw std::runtime_error("Empty Queue"); //先计算出队列首元素的位置,然后返回它的值 return m_pArray[(m_iTheFront + 1) % m_iArraySize];}
template <typename T>T& ArrayQueue<T>::back(){ //返回队列的尾部元素,m_iTheRear这个游标直接指向队尾元素 if(true == empty()) throw std::runtime_error("Empty Queue"); return m_pArray[m_iTheRear];}
template <typename T>int ArrayQueue<T>::size() const{ //循环队列的大小计算分为两种情况 //情形一:m_iTheFront < m_iTheRear,那么队列的大小就是 m_iTheRear - m_iTheFront //情形二:m_iTheFront > m_iTheRear,那么计算起来就分为两部分了,一部分是 0 + m_iTheRear,另一部分是 m_iArraySize - m_iTheFront,所以合起来就是 //m_iTheRear + m_iArraySize - m_iTheFront //把上述两种情况合在一起,那么计算大小的公式就是 (m_iTheRear - m_iTheFront + m_iArraySize) % m_iArraySize return (m_iTheRear - m_iTheFront + m_iArraySize) % m_iArraySize;}
template <typename T>int ArrayQueue<T>::maxsize() const{ return m_iArraySize - 1; //数组中最多只能存储最大大小减去一个位置的元素}
template <typename T>ArrayQueue<T>::ArrayQueue(){ //如果分配内存失败的话,会抛出一个bad_alloc的异常,记得使用时捕获它 m_pArray = new T[m_iArraySize * sizeof(T)];}
template <typename T>ArrayQueue<T>::~ArrayQueue(){ for(int i = 0; i < m_iArraySize; ++i) { //析构函数,要把数组中的每一对象释放了,因为new的时候会调用默认构造函数,如果是使用了内存分配器allocate,那么要进行更加精细的操作 m_pArray[i].~T(); } if(nullptr != m_pArray) { delete[] m_pArray; m_pArray; }}
template <typename T>void ArrayQueue<T>::expandCapacity(){ //首先计算出队列首元素的在数组中所在的索引,现在我们把这个一维数组视为一个环形,这里的m_iTheFront指向的是自队列首元素结点处逆时针方向的下一位置 //但是它本质上其实还是一个一维数组,一维数组它在内存中的索引从0开始,那么怎么判断目前的结构是不是形成了一个环呢? //只要m_iTheRear(尾元素游标)和 m_iTheFront(首元素处逆时针下一位置游标)都处于0以及0之后的索引,那么就认为它形成了一个环。
//求出队列首元素实际的存储位置,因为我们是把一维数视为环形的,并且m_iTheFront是首元素逆时针方向的下一位置,将它想象成一个环,可以得到求首元素位置的公式为: // (m_iTheFront + 1) % m_iArraySize(数组大小),要对数组大小取模,是因为,你的环,每一个位置上的索引是固定的,不是无限增长的。
int iStart = (m_iTheFront + 1) % m_iArraySize; //队列首元素的位置索引 T *newQueue = new (std::nothrow) T[2 * m_iArraySize]; if(iStart < 2) { //没有形成环 std::copy(m_pArray + iStart , m_pArray + iStart + m_iArraySize - 1,newQueue); } else { //形成环 std::copy(m_pArray+iStart,m_pArray+m_iArraySize,newQueue); std::copy(m_pArray,m_pArray + m_iTheRear + 1,newQueue + m_iArraySize - iStart); } //设置新的队列的首元素和尾元素的位置 m_iTheFront = 2*m_iArraySize - 1; m_iTheRear = m_iArraySize - 2; // 这里是m_iArraySize -2 ; -2 的原因是满队列的情况下还要留出一个位置,然后索引最大是m_iArraySize -2.综合起来就得到了-2这个结论 m_iArraySize *= 2; delete [] m_pArray; m_pArray = newQueue;}
template <typename T>class Node{public: Node() = default; Node(const T& element) : m_Element(element){}; Node(const T& element,const Node<T> *pNext):m_Element(element),m_pNextNode(pNext) {}; T m_Element; Node<T> *m_pNextNode{nullptr};};
template <typename T>class LinkedQueue{public: bool empty() const { if(nullptr == m_pFront) return true; return false; } int size() const { return m_iSize; } T& front() const; //返回队列的队首元素 T& back() const; //返回队列的队尾元素 void push(const T& element); //从队列的尾部插入元素 void pop(); //删除队列首部的元素 LinkedQueue() = default; ~LinkedQueue();private: Node<T> *m_pFront{nullptr}; //队列的首部 int m_iSize{0}; //队列大小 Node<T> *m_pBack{nullptr}; //队列的尾部 void clear(); //清理数据结构};
template <typename T>T& LinkedQueue<T>::front() const{ if(false == empty()) return m_pFront->m_Element;}
template <typename T>T& LinkedQueue<T>::back() const{ if(false == empty()) return m_pBack->m_Element;}
template <typename T>void LinkedQueue<T>::push(const T &element){ Node<T> *pNewNode = new(std::nothrow) Node<T>(element); if(nullptr == pNewNode) return; if(m_iSize == 0) { //如果大小等于0,那么说明,这是第一个被插入链表的元素 m_pFront = pNewNode; //这时候时头指针指向新的结点 } else { //队列不为空,此时链表的生成方向有两种,一种是从头部开始链接,另一种从尾部开始链接,当你写到pop函数的时候就能发现如果从尾部开始链接,删除会变的很复杂,因为我们的链表是单向的 m_pBack->m_pNextNode = pNewNode; //令新结点的指针域指向记录尾结点位置的m_pBack } m_pBack = pNewNode; //更新尾结点 ++m_iSize;}
template <typename T>void LinkedQueue<T>::pop(){ if(true == empty()) return; Node<T> *pTemp = m_pFront; //保存一下队首元素的位置,这个待会要删除结点用 m_pFront = pTemp->m_pNextNode; pTemp->m_Element.~T(); //销毁对象 if(pTemp != nullptr) { delete pTemp; pTemp = nullptr; } --m_iSize; if( 0 == m_iSize) m_pFront = m_pBack = nullptr;}
template <typename T>void LinkedQueue<T>::clear(){ for(int i=0; i < m_iSize; ++i) { Node<T> *pTempNode = m_pFront; m_pFront = pTempNode->m_pNextNode; if(nullptr != pTempNode) { delete pTempNode; pTempNode = nullptr; } } m_pFront = m_pBack = nullptr;}
template <typename T>LinkedQueue<T>::~LinkedQueue(){ clear();}#endif