队列基于线性表存储结构而实现的存取结构。

1 栈

是一种LIFO结构,后进先出,只能从线性结构的一端对数据进行操作。

1.1 顺序栈

  • 存储结构定义 ```c // 定义初始大小和增量

    define STACK_INIT_SIZE 150

    define STACK_INCREMENT 20

    // 定义返回状态及返回码

    define Status int

    define OK 1

    define ERROR 0

    // 定义存储的元素类型

    define ElementType int

// 顺序栈的结构定义 typedef struct SqStack { // 存储元素的数组 ElementType* elem; // 栈顶元素的索引(这里称为栈顶指针) int top; // 栈容量 int stackSize; // 每次扩容的大小 int incrementSize; }SqStack;

  1. - 常用操作
  2. - **初始化** -> 对一个栈类型的结构体变量,分配数据部分存储空间,给定结构维护部分变量的初始值。
  3. ```c
  4. /* 01_顺序栈——初始化*/
  5. Status initStack_Sq(SqStack& sS)
  6. {
  7. sS.elem = (ElementType*)malloc(STACK_INIT_SIZE * sizeof(ElementType));
  8. if (!sS.elem)
  9. {
  10. return ERROR;
  11. }
  12. sS.top = -1;
  13. sS.stackSize = STACK_INIT_SIZE;
  14. sS.incrementSize = STACK_INCREMENT;
  15. return OK;
  16. }
  1. - 栈顶指针**直接指向**当前栈顶的元素。
  • 需要注意的是,栈为空时,栈顶指针top == -1
    • 获取栈顶元素 -> 获取栈顶元素的值,不出栈。
      1. /* 02_顺序栈——取栈顶元素*/
      2. Status getTop_Sq(SqStack sS, ElementType& e)
      3. {
      4. // 如果栈空,返回错误
      5. if (stackEmpty_Sq(sS))
      6. {
      7. return ERROR;
      8. }
      9. // 赋值
      10. e = *(sS.elem + sS.top);
      11. return OK;
      12. }
  • 注意栈为空的情况。
  • 扩容

    1. /* 08_顺序栈——扩容*/
    2. Status incrementStack_Sq(SqStack& sS)
    3. {
    4. ElementType* newElem;
    5. // 进行扩容操作,当前容量加上增量
    6. newElem = (ElementType*)realloc(sS.elem, (sS.stackSize + (size_t)sS.incrementSize) * sizeof(ElementType));
    7. // 扩容失败,返回错误
    8. if (!sS.elem)
    9. {
    10. return ERROR;
    11. }
    12. sS.elem = newElem;
    13. // 扩容不要忘记修改容量
    14. sS.stackSize += sS.incrementSize;
    15. return OK;
    16. }
    • realloc函数第一个参数是待扩容的空间首地址。
    • 扩容完成别忘记栈容量。
  • 入栈

    1. /* 03_顺序栈——入栈*/
    2. Status push_Sq(SqStack& sS, ElementType e)
    3. {
    4. // 达到栈大小,进行扩容
    5. if (sS.top + 1 == sS.stackSize)
    6. {
    7. // 扩容失败,返回失败
    8. if (!incrementStack_Sq(sS))
    9. {
    10. return ERROR;
    11. }
    12. }
    13. // 压入栈顶
    14. *(sS.elem + ++sS.top) = e;
    15. // 成功
    16. return OK;
    17. }
  • 出栈

    1. /* 04_顺序栈——出栈*/
    2. Status pop_Sq(SqStack& sS, ElementType& e)
    3. {
    4. // 如果栈为空,返回结果
    5. if (stackEmpty_Sq(sS))
    6. {
    7. return ERROR;
    8. }
    9. // 赋值并移动栈顶索引
    10. e = *(sS.elem + sS.top--);
    11. return OK;
    12. }
  • 清空栈

    1. /* 07_顺序栈——清空栈*/
    2. Status clearStack_Sq(SqStack& sS)
    3. {
    4. // 只需要改变栈顶索引,因为对元素的存取都只对栈顶操作,其他位置的值无法读到
    5. sS.top = -1;
    6. return OK;
    7. }
    • 只是清空数据,结构还在。
  • 销毁栈
    1. /* 05_顺序栈——销毁*/
    2. Status destroyStack_Sq(SqStack& sS)
    3. {
    4. // 不存在,则标识已经销毁过了
    5. if (!sS.elem)
    6. {
    7. return ERROR;
    8. }
    9. // 删除指针
    10. delete sS.elem;
    11. // 结构定义字段置默认值
    12. sS.top = -1;
    13. sS.stackSize = 0;
    14. sS.incrementSize = 0;
    15. return OK;
    16. }

1.2 链栈

  • 存储结构定义 ```c // 定义返回状态及返回码

    define Status int

    define OK 1

    define ERROR 0

    // 定义存储的元素类型

    define ElementType int

// 采用单链表的结点定义,由于操作简单,所以不带头结点即可 // 定义指向该单链栈结点的指针类型 typedef SL_Node S_Node, *LS;

  1. - 和单链表的结点结构一样,数据域 + 指针域。
  2. ```c
  3. /* 定义单链表结点*/
  4. typedef struct SL_Node
  5. {
  6. // 存储的元素
  7. ElementType data;
  8. // 指向下一个结点的指针
  9. SL_Node* next;
  10. }HNode, *SL;
  • 由于栈只能在一端操作数据,结合单链表的特性,链表头指针一段即可以轻松对数据进行增加和删除。
    • 常用操作
  • 初始化 -> 不带头结点,只需将头指针赋值为NULL
  • 取栈顶元素 -> 同顺序栈,不出栈。

    1. /* 02_单链栈——取栈顶*/
    2. Status getTop_LS(LS& Ls, ElementType& elem)
    3. {
    4. if (stackEmpty_LS(Ls))
    5. {
    6. return ERROR;
    7. }
    8. elem = Ls->data;
    9. return OK;
    10. }
  • 入栈

    1. /* 03_单链栈——入栈*/
    2. Status push_LS(LS& Ls, ElementType elem)
    3. {
    4. // 类似于单链表的头插法
    5. // 指针p用于新创建结点
    6. LS p = (LS)malloc(sizeof(S_Node));
    7. // 指针q用来保存
    8. LS q = Ls;
    9. // 创建结点失败,返回
    10. if (!p)
    11. {
    12. return ERROR;
    13. }
    14. p->data = elem;
    15. // 进行头插
    16. p->next = q;
    17. Ls = p;
    18. return OK;
    19. }
    • 和链表头插法一样,需要用一个指针来保存上一个结点,才能将新结点作为它的前驱。
  • 出栈

    1. /* 04_单链栈——出栈*/
    2. Status pop_LS(LS& Ls, ElementType& elem)
    3. {
    4. // 栈为空,返回错误
    5. if (stackEmpty_LS(Ls))
    6. {
    7. return ERROR;
    8. }
    9. LS p = Ls;
    10. // 赋值
    11. elem = p->data;
    12. // 指针后移
    13. Ls = Ls->next;
    14. // 销毁出栈结点
    15. delete p;
    16. return OK;
    17. }
    • 出栈,可以顺着出栈元素的next域,找到新的栈顶元素,所以不需要额外指针。
  • 栈长度

    1. /* 05_单链栈——栈长度*/
    2. int stackLength_LS(LS Ls)
    3. {
    4. int length = 0;
    5. // 直接用s做迭代,所以不能取引用类型
    6. while (Ls)
    7. {
    8. length++;
    9. Ls = Ls->next;
    10. }
    11. return length;
    12. }
    • 由于没有设置维护结点,这里直接用遍历方式来求长度。
  • 判空

    1. /* 06_单链栈——判空*/
    2. Status stackEmpty_LS(LS Ls)
    3. {
    4. // 为空返回非0,否则返回0
    5. return Ls == NULL ? OK : ERROR;
    6. }
    • 不带头结点,直接判断头指针是否为空
  • 清空

    1. /* 07_单链栈——清空*/
    2. Status clearStack_LS(LS& Ls)
    3. {
    4. // 指针p,保存栈顶元素
    5. LS p;
    6. while (Ls)
    7. {
    8. // 保存栈顶元素
    9. p = Ls;
    10. // 栈顶指针后移
    11. Ls = Ls->next;
    12. // 销毁栈顶元素
    13. delete p;
    14. }
    15. delete Ls;
    16. return OK;
    17. }
    • 与单链表清空一致。

2 队列

队列是一种FIFO结构,先进先出,从线性表的两端对数据进行操作。 队头:允许删除的一端; 队尾:允许插入的一端。

  • 队列操作从两端操作数据,如果以顺序表的方式实现,那么不可能每次入队和出队都移动元素,这将极大影响性能。
  • 通过队头索引front和队尾索引rear之间即是队列的所有数据,入队和出队时,除了操作数据,只需要修改这两个值,即可维护顺序队列的结构。
  • 顺序队列存在的最大问题是入队过程需要扩容,而出队过程中空出来的小地址空间却无法得到利用。于是,优化后的顺序队列——循环队列产生。

2.1 循环队列

  • 存储结构定义

    1. // 循环队列的存储结构
    2. typedef struct CircularQueue
    3. {
    4. // 存储数据的数组
    5. ElementType* elem;
    6. // 队列的当前元素个数
    7. int count;
    8. // 队头的索引
    9. int front;
    10. // 队尾索引,需要注意rear始终指向队尾元素的下一个位置
    11. int rear;
    12. // 队列容量,需要注意【实际容量】是比这个值小1的,为了区别队满和队空的情况
    13. int queueSize;
    14. // 循环队列扩容的增量
    15. int incrementSize;
    16. }CQueue;
    • 顺序队列的定义没有不同, 只是实现的时候对队首及队尾的处理不同
    • 顺序存储的方式,队列的数据在这个结构的数组elem中。
    • 用一个变量count记录当前队列的元素个数,避免遍历方式获取元素个数。
    • 队列容量queueSize用于记录当前数组的大小,但实际容量比这个值小1,为了区分队满和队空的情况。
    • 队头索引front直接指向队头元素,而队尾索引rear指向的是队尾元素的下一个位置。
  • 常用操作

    • 初始化

      1. /* 01_循环队列——初始化*/
      2. Status initQueue_Cq(CQueue& cQ)
      3. {
      4. // 数据部分的存储空间申请
      5. cQ.elem = (ElementType*)malloc((Queue_INIT_SIZE) * sizeof(ElementType));
      6. // 空间分配失败,返回错误
      7. if (!cQ.elem)
      8. {
      9. return ERROR;
      10. }
      11. // 初始状态,队首和队尾重合
      12. // 有元素时候,需要注意rear始终指向队尾元素的下一个位置
      13. cQ.front = 0;
      14. cQ.rear = 0;
      15. cQ.count = 0;
      16. // 容量和增量初始化
      17. cQ.queueSize = Queue_INIT_SIZE;
      18. cQ.incrementSize = Queue_INCREMENT;
      19. return OK;
      20. }
      • 初始为空,队首和队尾指针重合。(区分于队满的情况,队尾下一个元素是队首)
    • 获取队首元素

      1. /* 08_循环队列——获取队首元素*/
      2. Status getFront_Cq(CQueue cQ, ElementType& elem)
      3. {
      4. // 如果为空,返回NULL
      5. if (queueEmpty_Cq(cQ))
      6. {
      7. elem = NULL;
      8. return ERROR;
      9. }
      10. elem = *(cQ.elem + cQ.front);
      11. return OK;
      12. }
      • 注意队列为空的情况
    • 队列长度

      1. /* 02_循环队列——队列长度*/
      2. int queueLength_Cq(CQueue cQ)
      3. {
      4. int length;
      5. // 1. 方法1,通过front和rear
      6. length = cQ.front > cQ.rear ? cQ.rear - cQ.front + cQ.queueSize : cQ.rear - cQ.front;
      7. // 2. 方法2,直接通过的元素个数计数变量
      8. length = cQ.count;
      9. return length;
      10. }
      • 可通过count变量直接获取;
      • 若通过队首front队尾rear指针方式来获取的话注意两种情况。
        • front > rear,则队列经规了循环,采用了出队列后的小地址空间。
        • front < rear,则可以直接求差得出长度。
  • 队空

    1. /* 07_循环队列——队空*/
    2. Status queueEmpty_Cq(CQueue cQ)
    3. {
    4. // 队首指针与队尾指针重合即表示队列为空
    5. return cQ.front == cQ.rear;
    6. }
    • 队满

      1. /* 06_循环队列——队满*/
      2. Status queueFull_Cq(CQueue cQ)
      3. {
      4. // 判断队满条件是循环【队首元素】和【队尾】中间隔一个,由于rear指向队尾的下一个,所以rear相邻的下一个是front
      5. if ((cQ.rear + 1) % cQ.queueSize == cQ.front)
      6. {
      7. return OK;
      8. }
      9. else
      10. {
      11. return ERROR;
      12. }
      13. }
    • 扩容

      • 如果队列满时候 rear > front,即满队状态没有进行循环。则realloc可以直接使用。
      • 如果队列满时候 rear < front,即满队状态有元素越过 size - 1的索引,从0再存,
        • 那么此时直接使用realloc会导致获取队列元素,队列中会有很多空的位置。
        • 这种情况是需要处理的。
          • a. 先进行realloc重新分配,再进行元素移动。本函数采用该方法实现。
          • b. 创建新数组,依次出队列,入队到新数组,毁原数组。
  1. Status incrementQueue_Cq(CQueue& cQ)
  2. {
  3. ElementType* newElem;
  4. newElem = (ElementType*)realloc(cQ.elem, (cQ.queueSize + (size_t)cQ.incrementSize) * sizeof(ElementType));
  5. // 扩容失败返回错误
  6. if (!newElem)
  7. {
  8. return ERROR;
  9. }
  10. cQ.elem = newElem;
  11. cQ.queueSize += cQ.incrementSize;
  12. printf("\n扩容后,移动前\n");
  13. testTraverse(cQ);
  14. // 当有用到循环,从数组0处再入队时候,需要对元素的位置进行操作,设置rear索引
  15. if (cQ.rear < cQ.front)
  16. {
  17. // 当数组索引 rear 之前元素个数小于等于扩容的大小
  18. if (cQ.rear <= cQ.incrementSize)
  19. {
  20. // 队头的元素移动部分或全部到队尾
  21. for (int i = 0; i < cQ.rear; i++)
  22. {
  23. *(cQ.elem + cQ.queueSize - cQ.incrementSize + i) = *(cQ.elem + i);
  24. }
  25. // 设置rear索引
  26. cQ.rear += cQ.queueSize - cQ.incrementSize;
  27. }
  28. else
  29. {
  30. for (int i = 0; i < cQ.rear; i++)
  31. {
  32. // 前increment个元素全部移到后面
  33. if (i < cQ.incrementSize)
  34. {
  35. *(cQ.elem + cQ.queueSize - cQ.incrementSize + i) = *(cQ.elem + i);
  36. }
  37. // 多出的放队列数组首部
  38. else
  39. {
  40. *(cQ.elem + i - cQ.incrementSize) = *(cQ.elem + i);
  41. }
  42. }
  43. // 设置rear索引
  44. cQ.rear -= cQ.incrementSize;
  45. }
  46. }
  47. return OK;
  48. }
  1. - 注意**queueSize变量**在何时进行的操作,是**扩容前**的容量还是**扩容后**的容量。
  2. - 扩容**不仅仅是数组的扩容**,难点主要是要**维护循环队列的结构**,当进行入队和出队时,每个元素都是有效的。
  3. - rear之前的小地址空间中的元素个数 **小于等于扩容容量**,直接将这些元素放到队列的尾部。
  4. - rear之前的小地址空间元素个数 **大于扩容容量**
  5. - rear之前**等于扩容容量个数**的元素放到尾部
  6. - 将**剩余小地址空间的元素**按顺序放到头部。
  • 入队列

    1. /* 03_循环队列——入队列*/
    2. Status enqueue_Cq(CQueue& cQ, ElementType elem)
    3. {
    4. // 如果队列满了进行扩容
    5. if (queueFull_Cq(cQ))
    6. {
    7. // 如果扩容失败,返回
    8. if (!incrementQueue_Cq(cQ)) {
    9. return ERROR;
    10. }
    11. }
    12. // 入队列操作
    13. *(cQ.elem + cQ.rear) = elem;
    14. cQ.rear = (cQ.rear + 1) % cQ.queueSize;
    15. // 别忘记队列长度
    16. cQ.count++;
    17. return OK;
    18. }
    • 队满需要扩容,扩容失败要返回错误。
    • 入队是对队尾rear进行操作,队尾指针后移,但是需注意取模以保证循环。
    • 别忘记维护队列长度变量。
  • 出队列

    1. /* 04_循环队列——出队列*/
    2. Status dequeue_Cq(CQueue& cQ, ElementType& elem)
    3. {
    4. // 如果队列为空,返回失败
    5. if (queueEmpty_Cq(cQ))
    6. {
    7. elem = NULL;
    8. return ERROR;
    9. }
    10. // 进行出队列操作
    11. elem = *(cQ.elem + cQ.front);
    12. *(cQ.elem + cQ.front) = NULL;
    13. cQ.front = (cQ.front + 1) % cQ.queueSize;
    14. // 别忘记队列元素个数
    15. cQ.count--;
    16. return OK;
    17. }
    • 注意队空情况
    • 出队是对队头front操作,也是通过取模运算实现后移。
    • 别忘记队列元素个数

2.2 链队列

  • 存储结构定义 ```c // 引用单链表结点 typedef SL_Node QueueNode, *Queue;

// 定义维护链队列的结构 typedef struct LinkedQueueNode { // 队首结点指针 QueueNode front; // 队尾结点指针 QueueNode rear; // 队列元素个数计数 int count; }*LinkedQueue;

  1. - 队首结点指针可以指向队首元素(不带头结点),也可以指向头结点
  2. - count是对元素计数的变量,避免每次获取个数都需要遍历。
  3. - 常用操作
  4. - 初始化
  5. - 带头结点
  6. - 需要创建链队列结点
  7. - 需要创建头结点并且判断创建的结果
  8. - frontrear都指向头结点
  9. ```c
  10. /* 01_链队列——初始化_带头结点*/
  11. Status initQueue_Lqh(LinkedQueue& linkedQH)
  12. {
  13. // 分配队列结构维护结点的空间
  14. linkedQH = (LinkedQueue)malloc(sizeof(LinkedQueueNode));
  15. // 分配失败返回错误
  16. if (!linkedQH)
  17. {
  18. return ERROR;
  19. }
  20. // 创建头结点
  21. Queue head = (Queue)malloc(sizeof(QueueNode));
  22. // 创建失败返回错误
  23. if (!head)
  24. {
  25. return ERROR;
  26. }
  27. // 将头结点设置到队列维护结点中
  28. linkedQH->front = head;
  29. linkedQH->rear = head;
  30. linkedQH->count = 0;
  31. return OK;
  32. }
  1. - 不带头结点
  2. - 创建链队列结点,判断创建结果
  3. - frontrear均指向NULL
  1. /* 02_链队列——初始化_不带头结点*/
  2. Status initQueue_Lq(LinkedQueue& linkedQ)
  3. {
  4. // 分配队列维护结点空间
  5. linkedQ = (LinkedQueue)malloc(sizeof(QueueNode));
  6. if (!linkedQ)
  7. {
  8. return ERROR;
  9. }
  10. // 无头结点,则两个指向为NULL
  11. linkedQ->front = NULL;
  12. linkedQ->rear = NULL;
  13. linkedQ->count = 0;
  14. return OK;
  15. }
  • 求队列长度

    • 减少遍历直接用一个变量维护。
      1. /* 03_链队列——队列长度_头结点/不带头结点*/
      2. int queueLength_Lq(LinkedQueue linkedQ)
      3. {
      4. // 由于链队列的统计信息需要遍历,所以将长度等信息记录到队列的维护结点中
      5. return linkedQ->count;
      6. }
  • 判断队列为空

    • 带头结点

      1. /* 04_链队列——队空_带头结点*/
      2. Status queueEmpty_Lqh(LinkedQueue linkedQH)
      3. {
      4. // 同时指向头结点,则为空
      5. return (linkedQH->front == linkedQH->rear) ? OK : ERROR;
      6. }
    • 不带头结点

      1. /* 05_链队列——队空_不带头结点*/
      2. Status queueEmpty_Lq(LinkedQueue linkedQ)
      3. {
      4. // 都为空,则队列为空
      5. return (linkedQ->front == NULL && linkedQ->rear == NULL) ? OK : ERROR;
      6. }
  • 入队列

    • 带头结点 ```c / 06链队列——入队列带头结点/ Status enqueue_Lqh(LinkedQueue& linkedQH, ElementType elem) { // 对rear的修改需要取引用 Queue& rear = linkedQH->rear; // 建立结点 Queue newRear = (Queue)malloc(sizeof(QueueNode)); if (!newRear) { return ERROR; } newRear->data = elem; newRear->next = NULL;

    // 设置队尾结点 rear->next = newRear; rear = rear->next; // 别忘记队列元素个数 linkedQH->count++; return OK; }

    1. - 入队是对rear进行操作,操作的结果要维护到链队列结点, 所以rear取引用。
    2. - 同**尾插法**
    3. - 不带头结点
    4. ```c
    5. /* 07_链队列——入队列_不带头结点*/
    6. Status enqueue_Lq(LinkedQueue& linkedQ, ElementType elem)
    7. {
    8. // 先建立结点
    9. Queue enQueue = (Queue)malloc(sizeof(QueueNode));
    10. if (!enQueue)
    11. {
    12. return ERROR;
    13. }
    14. // 不带头结点的链队列,队空的时候队首和队尾都为NULL
    15. if (queueEmpty_Lq(linkedQ))
    16. {
    17. // 同时指向新入队结点
    18. linkedQ->front = enQueue;
    19. linkedQ->rear = enQueue;
    20. }
    21. else
    22. {
    23. // 先连接队尾,再修改维护结点的队尾指针
    24. linkedQ->rear->next = enQueue;
    25. linkedQ->rear = linkedQ->rear->next;
    26. }
    27. // 别忘记队列元素个数
    28. linkedQ->count++;
    29. return OK;
    30. }
    1. - 不带头结点需要判断队列是否为空。
    2. - 如果为空,则**frontrear指针**同时指向**新入队结点**
    3. - 否则插入元素只需要维护**rear指针**。
  • 出队列

    • 带头结点

      • 需要判断是否为空
      • front指针后移
      • 如果出队列前只有一个元素,出队列后队空,队尾指针也需要修改,指向头结点
        1. /* 08_链队列——出队列——带头结点*/
        2. Status dequeue_Lqh(LinkedQueue& linkedQH, ElementType& elem)
        3. {
        4. // 队列为空,报错
        5. if (queueEmpty_Lqh(linkedQH))
        6. {
        7. return ERROR;
        8. }
        9. // 保存待出队列的结点数据
        10. Queue& head = linkedQH->front;
        11. elem = head->next->data;
        12. head->next = head->next->next;
        13. // 如果只有一个元素,则涉及修改队尾指针
        14. if (linkedQH->count == 1)
        15. {
        16. linkedQH->rear = head;
        17. }
        18. // 别忘了队列元素个数
        19. linkedQH->count--;
        20. return OK;
        21. }
    • 不带头结点

      • 同理判断0个元素和1个元素的特殊情况
        1. /* 09_链队列——出队列_不带头结点*/
        2. Status dequeue_Lq(LinkedQueue& linkedQ, ElementType& elem)
        3. {
        4. // 队列为空,报错
        5. if (queueEmpty_Lqh(linkedQ))
        6. {
        7. return ERROR;
        8. }
        9. // 保存待出队列的结点数据
        10. elem = linkedQ->front->data;
        11. // 出队列,front结点后移
        12. linkedQ->front = linkedQ->front->next;
        13. // 如果只有一个元素,则涉及修改队尾指针
        14. if (linkedQ->count == 1)
        15. {
        16. linkedQ->rear = NULL;
        17. }
        18. // 别忘了队列元素个数
        19. linkedQ->count--;
        20. return OK;
        21. }