栈和队列是基于线性表存储结构而实现的存取结构。
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;
- 常用操作- **初始化** -> 对一个栈类型的结构体变量,分配数据部分存储空间,给定结构维护部分变量的初始值。```c/* 01_顺序栈——初始化*/Status initStack_Sq(SqStack& sS){sS.elem = (ElementType*)malloc(STACK_INIT_SIZE * sizeof(ElementType));if (!sS.elem){return ERROR;}sS.top = -1;sS.stackSize = STACK_INIT_SIZE;sS.incrementSize = STACK_INCREMENT;return OK;}
- 栈顶指针**直接指向**当前栈顶的元素。
- 需要注意的是,栈为空时,栈顶指针top == -1。
- 获取栈顶元素 -> 获取栈顶元素的值,不出栈。
/* 02_顺序栈——取栈顶元素*/Status getTop_Sq(SqStack sS, ElementType& e){// 如果栈空,返回错误if (stackEmpty_Sq(sS)){return ERROR;}// 赋值e = *(sS.elem + sS.top);return OK;}
- 获取栈顶元素 -> 获取栈顶元素的值,不出栈。
- 注意栈为空的情况。
扩容
/* 08_顺序栈——扩容*/Status incrementStack_Sq(SqStack& sS){ElementType* newElem;// 进行扩容操作,当前容量加上增量newElem = (ElementType*)realloc(sS.elem, (sS.stackSize + (size_t)sS.incrementSize) * sizeof(ElementType));// 扩容失败,返回错误if (!sS.elem){return ERROR;}sS.elem = newElem;// 扩容不要忘记修改容量sS.stackSize += sS.incrementSize;return OK;}
- realloc函数第一个参数是待扩容的空间首地址。
- 扩容完成别忘记栈容量。
入栈
/* 03_顺序栈——入栈*/Status push_Sq(SqStack& sS, ElementType e){// 达到栈大小,进行扩容if (sS.top + 1 == sS.stackSize){// 扩容失败,返回失败if (!incrementStack_Sq(sS)){return ERROR;}}// 压入栈顶*(sS.elem + ++sS.top) = e;// 成功return OK;}
出栈
/* 04_顺序栈——出栈*/Status pop_Sq(SqStack& sS, ElementType& e){// 如果栈为空,返回结果if (stackEmpty_Sq(sS)){return ERROR;}// 赋值并移动栈顶索引e = *(sS.elem + sS.top--);return OK;}
清空栈
/* 07_顺序栈——清空栈*/Status clearStack_Sq(SqStack& sS){// 只需要改变栈顶索引,因为对元素的存取都只对栈顶操作,其他位置的值无法读到sS.top = -1;return OK;}
- 只是清空数据,结构还在。
- 销毁栈
/* 05_顺序栈——销毁*/Status destroyStack_Sq(SqStack& sS){// 不存在,则标识已经销毁过了if (!sS.elem){return ERROR;}// 删除指针delete sS.elem;// 结构定义字段置默认值sS.top = -1;sS.stackSize = 0;sS.incrementSize = 0;return OK;}
1.2 链栈
- 存储结构定义
```c
// 定义返回状态及返回码
define Status int
define OK 1
define ERROR 0
// 定义存储的元素类型define ElementType int
// 采用单链表的结点定义,由于操作简单,所以不带头结点即可 // 定义指向该单链栈结点的指针类型 typedef SL_Node S_Node, *LS;
- 和单链表的结点结构一样,数据域 + 指针域。```c/* 定义单链表结点*/typedef struct SL_Node{// 存储的元素ElementType data;// 指向下一个结点的指针SL_Node* next;}HNode, *SL;
- 由于栈只能在一端操作数据,结合单链表的特性,链表头指针一段即可以轻松对数据进行增加和删除。
- 常用操作
- 初始化 -> 不带头结点,只需将头指针赋值为NULL
取栈顶元素 -> 同顺序栈,不出栈。
/* 02_单链栈——取栈顶*/Status getTop_LS(LS& Ls, ElementType& elem){if (stackEmpty_LS(Ls)){return ERROR;}elem = Ls->data;return OK;}
入栈
/* 03_单链栈——入栈*/Status push_LS(LS& Ls, ElementType elem){// 类似于单链表的头插法// 指针p用于新创建结点LS p = (LS)malloc(sizeof(S_Node));// 指针q用来保存LS q = Ls;// 创建结点失败,返回if (!p){return ERROR;}p->data = elem;// 进行头插p->next = q;Ls = p;return OK;}
- 和链表头插法一样,需要用一个指针来保存上一个结点,才能将新结点作为它的前驱。
出栈
/* 04_单链栈——出栈*/Status pop_LS(LS& Ls, ElementType& elem){// 栈为空,返回错误if (stackEmpty_LS(Ls)){return ERROR;}LS p = Ls;// 赋值elem = p->data;// 指针后移Ls = Ls->next;// 销毁出栈结点delete p;return OK;}
- 出栈,可以顺着出栈元素的next域,找到新的栈顶元素,所以不需要额外指针。
栈长度
/* 05_单链栈——栈长度*/int stackLength_LS(LS Ls){int length = 0;// 直接用s做迭代,所以不能取引用类型while (Ls){length++;Ls = Ls->next;}return length;}
- 由于没有设置维护结点,这里直接用遍历方式来求长度。
判空
/* 06_单链栈——判空*/Status stackEmpty_LS(LS Ls){// 为空返回非0,否则返回0return Ls == NULL ? OK : ERROR;}
- 不带头结点,直接判断头指针是否为空
清空
/* 07_单链栈——清空*/Status clearStack_LS(LS& Ls){// 指针p,保存栈顶元素LS p;while (Ls){// 保存栈顶元素p = Ls;// 栈顶指针后移Ls = Ls->next;// 销毁栈顶元素delete p;}delete Ls;return OK;}
- 与单链表清空一致。
2 队列
队列是一种FIFO结构,先进先出,从线性表的两端对数据进行操作。 队头:允许删除的一端; 队尾:允许插入的一端。
- 队列操作从两端操作数据,如果以顺序表的方式实现,那么不可能每次入队和出队都移动元素,这将极大影响性能。
- 通过队头索引front和队尾索引rear之间即是队列的所有数据,入队和出队时,除了操作数据,只需要修改这两个值,即可维护顺序队列的结构。
- 顺序队列存在的最大问题是入队过程需要扩容,而出队过程中空出来的小地址空间却无法得到利用。于是,优化后的顺序队列——循环队列产生。
2.1 循环队列
存储结构定义
// 循环队列的存储结构typedef struct CircularQueue{// 存储数据的数组ElementType* elem;// 队列的当前元素个数int count;// 队头的索引int front;// 队尾索引,需要注意rear始终指向队尾元素的下一个位置int rear;// 队列容量,需要注意【实际容量】是比这个值小1的,为了区别队满和队空的情况int queueSize;// 循环队列扩容的增量int incrementSize;}CQueue;
- 与顺序队列的定义没有不同, 只是实现的时候对队首及队尾的处理不同。
- 顺序存储的方式,队列的数据在这个结构的数组elem中。
- 用一个变量count记录当前队列的元素个数,避免遍历方式获取元素个数。
- 队列容量queueSize用于记录当前数组的大小,但实际容量比这个值小1,为了区分队满和队空的情况。
- 队头索引front直接指向队头元素,而队尾索引rear指向的是队尾元素的下一个位置。
常用操作
初始化
/* 01_循环队列——初始化*/Status initQueue_Cq(CQueue& cQ){// 数据部分的存储空间申请cQ.elem = (ElementType*)malloc((Queue_INIT_SIZE) * sizeof(ElementType));// 空间分配失败,返回错误if (!cQ.elem){return ERROR;}// 初始状态,队首和队尾重合// 有元素时候,需要注意rear始终指向队尾元素的下一个位置cQ.front = 0;cQ.rear = 0;cQ.count = 0;// 容量和增量初始化cQ.queueSize = Queue_INIT_SIZE;cQ.incrementSize = Queue_INCREMENT;return OK;}
- 初始为空,队首和队尾指针重合。(区分于队满的情况,队尾下一个元素是队首)
获取队首元素
/* 08_循环队列——获取队首元素*/Status getFront_Cq(CQueue cQ, ElementType& elem){// 如果为空,返回NULLif (queueEmpty_Cq(cQ)){elem = NULL;return ERROR;}elem = *(cQ.elem + cQ.front);return OK;}
- 注意队列为空的情况
队列长度
/* 02_循环队列——队列长度*/int queueLength_Cq(CQueue cQ){int length;// 1. 方法1,通过front和rearlength = cQ.front > cQ.rear ? cQ.rear - cQ.front + cQ.queueSize : cQ.rear - cQ.front;// 2. 方法2,直接通过的元素个数计数变量length = cQ.count;return length;}
- 可通过count变量直接获取;
- 若通过队首front和队尾rear指针方式来获取的话注意两种情况。
- 若 front > rear,则队列经规了循环,采用了出队列后的小地址空间。
- 若 front < rear,则可以直接求差得出长度。
队空
/* 07_循环队列——队空*/Status queueEmpty_Cq(CQueue cQ){// 队首指针与队尾指针重合即表示队列为空return cQ.front == cQ.rear;}
队满
/* 06_循环队列——队满*/Status queueFull_Cq(CQueue cQ){// 判断队满条件是循环【队首元素】和【队尾】中间隔一个,由于rear指向队尾的下一个,所以rear相邻的下一个是frontif ((cQ.rear + 1) % cQ.queueSize == cQ.front){return OK;}else{return ERROR;}}
扩容
- 如果队列满时候 rear > front,即满队状态没有进行循环。则realloc可以直接使用。
- 如果队列满时候 rear < front,即满队状态有元素越过 size - 1的索引,从0再存,
- 那么此时直接使用realloc会导致获取队列元素,队列中会有很多空的位置。
- 这种情况是需要处理的。
- a. 先进行realloc重新分配,再进行元素移动。本函数采用该方法实现。
- b. 创建新数组,依次出队列,入队到新数组,毁原数组。
Status incrementQueue_Cq(CQueue& cQ){ElementType* newElem;newElem = (ElementType*)realloc(cQ.elem, (cQ.queueSize + (size_t)cQ.incrementSize) * sizeof(ElementType));// 扩容失败返回错误if (!newElem){return ERROR;}cQ.elem = newElem;cQ.queueSize += cQ.incrementSize;printf("\n扩容后,移动前\n");testTraverse(cQ);// 当有用到循环,从数组0处再入队时候,需要对元素的位置进行操作,设置rear索引if (cQ.rear < cQ.front){// 当数组索引 rear 之前元素个数小于等于扩容的大小if (cQ.rear <= cQ.incrementSize){// 队头的元素移动部分或全部到队尾for (int i = 0; i < cQ.rear; i++){*(cQ.elem + cQ.queueSize - cQ.incrementSize + i) = *(cQ.elem + i);}// 设置rear索引cQ.rear += cQ.queueSize - cQ.incrementSize;}else{for (int i = 0; i < cQ.rear; i++){// 前increment个元素全部移到后面if (i < cQ.incrementSize){*(cQ.elem + cQ.queueSize - cQ.incrementSize + i) = *(cQ.elem + i);}// 多出的放队列数组首部else{*(cQ.elem + i - cQ.incrementSize) = *(cQ.elem + i);}}// 设置rear索引cQ.rear -= cQ.incrementSize;}}return OK;}
- 注意**queueSize变量**在何时进行的操作,是**扩容前**的容量还是**扩容后**的容量。- 扩容**不仅仅是数组的扩容**,难点主要是要**维护循环队列的结构**,当进行入队和出队时,每个元素都是有效的。- 当 rear之前的小地址空间中的元素个数 **小于等于扩容容量**,直接将这些元素放到队列的尾部。- 当 rear之前的小地址空间元素个数 **大于扩容容量**- 将rear之前**等于扩容容量个数**的元素放到尾部- 将**剩余小地址空间的元素**按顺序放到头部。
入队列
/* 03_循环队列——入队列*/Status enqueue_Cq(CQueue& cQ, ElementType elem){// 如果队列满了进行扩容if (queueFull_Cq(cQ)){// 如果扩容失败,返回if (!incrementQueue_Cq(cQ)) {return ERROR;}}// 入队列操作*(cQ.elem + cQ.rear) = elem;cQ.rear = (cQ.rear + 1) % cQ.queueSize;// 别忘记队列长度cQ.count++;return OK;}
- 队满需要扩容,扩容失败要返回错误。
- 入队是对队尾rear进行操作,队尾指针后移,但是需注意取模以保证循环。
- 别忘记维护队列长度变量。
出队列
/* 04_循环队列——出队列*/Status dequeue_Cq(CQueue& cQ, ElementType& elem){// 如果队列为空,返回失败if (queueEmpty_Cq(cQ)){elem = NULL;return ERROR;}// 进行出队列操作elem = *(cQ.elem + cQ.front);*(cQ.elem + cQ.front) = NULL;cQ.front = (cQ.front + 1) % cQ.queueSize;// 别忘记队列元素个数cQ.count--;return OK;}
- 注意队空情况。
- 出队是对队头front操作,也是通过取模运算实现后移。
- 别忘记队列元素个数
2.2 链队列
- 存储结构定义 ```c // 引用单链表结点 typedef SL_Node QueueNode, *Queue;
// 定义维护链队列的结构 typedef struct LinkedQueueNode { // 队首结点指针 QueueNode front; // 队尾结点指针 QueueNode rear; // 队列元素个数计数 int count; }*LinkedQueue;
- 队首结点指针可以指向队首元素(不带头结点),也可以指向头结点- count是对元素计数的变量,避免每次获取个数都需要遍历。- 常用操作- 初始化- 带头结点- 需要创建链队列结点- 需要创建头结点并且判断创建的结果- front和rear都指向头结点```c/* 01_链队列——初始化_带头结点*/Status initQueue_Lqh(LinkedQueue& linkedQH){// 分配队列结构维护结点的空间linkedQH = (LinkedQueue)malloc(sizeof(LinkedQueueNode));// 分配失败返回错误if (!linkedQH){return ERROR;}// 创建头结点Queue head = (Queue)malloc(sizeof(QueueNode));// 创建失败返回错误if (!head){return ERROR;}// 将头结点设置到队列维护结点中linkedQH->front = head;linkedQH->rear = head;linkedQH->count = 0;return OK;}
- 不带头结点- 创建链队列结点,判断创建结果- front和rear均指向NULL。
/* 02_链队列——初始化_不带头结点*/Status initQueue_Lq(LinkedQueue& linkedQ){// 分配队列维护结点空间linkedQ = (LinkedQueue)malloc(sizeof(QueueNode));if (!linkedQ){return ERROR;}// 无头结点,则两个指向为NULLlinkedQ->front = NULL;linkedQ->rear = NULL;linkedQ->count = 0;return OK;}
求队列长度
- 减少遍历直接用一个变量维护。
/* 03_链队列——队列长度_头结点/不带头结点*/int queueLength_Lq(LinkedQueue linkedQ){// 由于链队列的统计信息需要遍历,所以将长度等信息记录到队列的维护结点中return linkedQ->count;}
- 减少遍历直接用一个变量维护。
判断队列为空
带头结点
/* 04_链队列——队空_带头结点*/Status queueEmpty_Lqh(LinkedQueue linkedQH){// 同时指向头结点,则为空return (linkedQH->front == linkedQH->rear) ? OK : ERROR;}
不带头结点
/* 05_链队列——队空_不带头结点*/Status queueEmpty_Lq(LinkedQueue linkedQ){// 都为空,则队列为空return (linkedQ->front == NULL && linkedQ->rear == NULL) ? OK : ERROR;}
入队列
- 带头结点 ```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; }
- 入队是对rear进行操作,操作的结果要维护到链队列结点, 所以rear取引用。- 同**尾插法**- 不带头结点```c/* 07_链队列——入队列_不带头结点*/Status enqueue_Lq(LinkedQueue& linkedQ, ElementType elem){// 先建立结点Queue enQueue = (Queue)malloc(sizeof(QueueNode));if (!enQueue){return ERROR;}// 不带头结点的链队列,队空的时候队首和队尾都为NULLif (queueEmpty_Lq(linkedQ)){// 同时指向新入队结点linkedQ->front = enQueue;linkedQ->rear = enQueue;}else{// 先连接队尾,再修改维护结点的队尾指针linkedQ->rear->next = enQueue;linkedQ->rear = linkedQ->rear->next;}// 别忘记队列元素个数linkedQ->count++;return OK;}
- 不带头结点需要判断队列是否为空。- 如果为空,则**front和rear指针**同时指向**新入队结点**- 否则插入元素只需要维护**rear指针**。
出队列
带头结点
- 需要判断是否为空
- front指针后移
- 如果出队列前只有一个元素,出队列后队空,队尾指针也需要修改,指向头结点
/* 08_链队列——出队列——带头结点*/Status dequeue_Lqh(LinkedQueue& linkedQH, ElementType& elem){// 队列为空,报错if (queueEmpty_Lqh(linkedQH)){return ERROR;}// 保存待出队列的结点数据Queue& head = linkedQH->front;elem = head->next->data;head->next = head->next->next;// 如果只有一个元素,则涉及修改队尾指针if (linkedQH->count == 1){linkedQH->rear = head;}// 别忘了队列元素个数linkedQH->count--;return OK;}
不带头结点
- 同理判断0个元素和1个元素的特殊情况
/* 09_链队列——出队列_不带头结点*/Status dequeue_Lq(LinkedQueue& linkedQ, ElementType& elem){// 队列为空,报错if (queueEmpty_Lqh(linkedQ)){return ERROR;}// 保存待出队列的结点数据elem = linkedQ->front->data;// 出队列,front结点后移linkedQ->front = linkedQ->front->next;// 如果只有一个元素,则涉及修改队尾指针if (linkedQ->count == 1){linkedQ->rear = NULL;}// 别忘了队列元素个数linkedQ->count--;return OK;}
- 同理判断0个元素和1个元素的特殊情况
