一、栈
栈的特性就是先进后出,指针入口指向栈顶
例如:有一个袋子,每次都往里面放一个跟袋口差不多大的西瓜,第一个放进去的会在最下面,如果想拿出最底下的西瓜,只能先把上面的拿出去,才能拿到下面的
如上图所示,指针指向栈顶,只能读取栈顶元素,每次存放一个元素,栈内元素增长一,top指针向上增长一位
下面介绍常见的两种栈的设计方案:
顺序栈
与顺序表一样,顺序栈也是一个提前声明固定空间一个顺序表,通过top指针维持栈的特性
top指针默认索引为-1,即指向栈底;每次增加数据,只能通过push的方式压栈,top索引值+1, 到达设置最大值后不可在增加;每次删除数据,只能通过pop的方式来弹出顶层数据,索引值-1,索引值为-1后不能再减少
先定义一个顺序栈接结构
定义一个最大容量,存放一个MAXSize大小的数组,top为栈顶索引
#define MAXSIZE 1024 //定义最大容量
typedef struct OrderStack {
char data[MAXSIZE];
int top;
} LSOrderStack;
初始化
初始化的过程给栈顶索引赋值-1,标识默认指向栈底
LSOrderStack *stack = (LSOrderStack *)malloc(sizeof(LSOrderStack));
stack->top = -1;
push
push的时候传入栈结构和数据,如果栈满了则不push, 否则加入新元素,栈顶索引+1
_Bool push(LSOrderStack *stack, char c) {
if (stack->top + 1 > MAXSIZE) return 0; //栈满了
stack->top++;
stack->data[stack->top] = c;
return 1;
}
pop
pop的时候传入栈结构和数据,如果栈为空(索引为-1),则不pop, 否则置空栈顶元素,栈顶索引-1
void orderPop(LSOrderStack *stack) {
if (stack->top == -1) return; //栈为空无需操作
stack->data[stack->top] = 0;
stack->top--;
}
链栈
与链表相似,链栈的相邻元素通过next指针有序连接,且没有大小显示,当然可以手动限制
链栈的栈顶其实就是链表的head节点,通过控制head的位置,来控制栈的push和pop
因此当栈为空的时候,栈顶指针top为NULL;当存在元素的时候栈顶指针一直指向最新的元素,而最新的元素的next则指向加入前的栈顶元素
如上图所示,每增加一个新的元素,栈顶top都会向上移动,且指针指向先前的栈顶元素,
先定义一个顺序栈接结构
定义一个最大容量,存放一个MAXSize大小的数组,top指向栈顶
typedef struct ListStack {
char data;
struct ListStack *next;
} LSListStack;
LSListStack *top = NULL; //指向栈顶元素
初始化
由于不需要特别的结构来维护链栈,所以不需要初始化,如果硬要拿出那就是给top置空
push
push的时候传入栈结构和数据,如果栈满了则不push, 否则加入新元素,且更新数据next指向, 更改栈顶指针
void listPush(char c) {
LSListStack *next = (LSListStack *)malloc(sizeof(LSListStack));
next->data = c;
next->next = stack;
top = next;
}
pop
pop的时候传入栈结构和数据,如果栈为空(top指针为空),则不pop, 否则置空栈顶元素,栈顶指针后移
LSListStack *listPop(LSListStack *stack) {
if (!top) return stack; //栈为空
LSListStack *p = stack;
stack = stack->next;
free(p);
return stack;
}
二、队列
队列的特性就是先进先出,后进后出,分为队头和队尾, 队尾进,队头出
同栈结构一样,队列也有顺序队列和链队列
队列简易示意图如下所示:
顺序队列
与顺序表一样,顺序队列也是一个提前声明好的一个固定长度的顺序表,通过两个队列索引,队首(front)和队尾(rear)来维护
顺序表的队首和队尾默认都在初始位,每次队尾增加一项,则队尾索引值+1,减少则队首索引+1,因此可以看出顺序队列是从队首到队尾是按照索引从小到大的,如果队首和队尾指针指向同一个索引,那么此队列为空
假设原本入队和出队的索引值为0--2,那么经过入队或者出队后变化如下
因此如果入队和出队比较频繁的话,那么出队会造成数组前面的空闲空间很多,却无法使用的情况,实属浪费,因此引出环形结构队列。 本顺序栈也只讲解环形队列
环形队列
环形队列就是假设声明的顺序表数组是一个首尾相接的结构,每次操作通过检查索引位置是否超过两端,然后进行映射入数组即可实现,即取余法
环形结构的简易样式图如下所示:
那么接下一步一步实现队列的操作,从操作中一步一步解决环形队列问题
初始化队列
环形队列是逻辑上的一个环,需要我们在使用过程中来维护,即入队和出队过程来维护,因此初始化不需要,初始化只需要创建好结构,然后队首队尾指向初始节点即可
此处声明了顺序队列结构体,创建了队列结构,并把队列索引都指向了数组的最后一项,默认队列为空,
因此遍历队列的时候是从队首的下一个开始的,即front+1,而队尾元素则为队尾索引,即rear
#define MAXQUEUE 8
typedef struct OrderQueue {
char data[MAXQUEUE];
int near, front; //尾巴进,头出
} LSOrderQueue; //声明顺序队列结构体
LSOrderQueue * initOrderQueue() {
LSOrderQueue *queue = (LSOrderQueue *)malloc(sizeof(LSOrderQueue));
queue->front = queue->near = MAXQUEUE-1; //初始化索引
return queue;
}
入队
入队的过程,需要队尾元素自增,且队列满了不能继续入队
由于是环形队列,因此判断队列满需要在入队前判断队首队尾指针是否即将相遇(入队后索引相遇了),如果相遇则队满结束,否则队尾索引自增,通过取余法把索引映射到数组的对应位置入队(环形结构首尾相接,队首前面为空使用前面控件),以此来解决出队导致的空间浪费问题
_Bool orderQueueEnter(LSOrderQueue *queue, char c) {
if (queue->near + 1 % MAXQUEUE == queue->front) return 0;//队列满了不能入队
queue->near = ++queue->near % MAXQUEUE;// 取余法入队到相应的位置
queue->data[queue->near] = c; //赋值
return 1;
}
出队
出队的过程,需要队首元素自增,且队列为空不能出队
由于是环形结构,判断队列为空的时候判断两个指针是否已经相遇了,如果相遇,则队列为空,结束,否则队首元素自增,通过取余法把索引映射到数组的对应位置出队,其实就是剔除到队首->队尾这边区间之外
void orderQueueLeave(LSOrderQueue *queue) {
if (queue->front == queue->near) return; //队列为空
queue->front = ++queue->front % MAXQUEUE; 取余法出队到相应的位置
}
链队列
链队列和链表一样,为一个非连续的链式结构,不会存在空间浪费问题,因此不用直接以环式结构来维护,默认只需要额外增加两个指针,队首front和队尾rear,类似于顺序队列,只要维护好front和rear即可
然而此过程需要新增front和rear两个指针,多了一个结构,显得不够简洁,借鉴顺序队列的环式结构,因此又衍生除了第二种方案,环形队列结构
方案一:线性链队列
1.初始化
只需要初始化队列结构即可维护链式结构,默认两个指针指向
typedef struct endpointListQueue {
LSListNode *front, *near;
}LSPointListQueue; //队列基础指针
2.入队
入队前,先判断队伍是否存在元素,如果不存在,则需要将队首、队尾指针指向新元素,否则链表加入新元素,尾指针指向新节点
(2).非第一次入队,队列链表插入新元素,队尾指针指向新节点
void pointEnter(LSPointListQueue *queue, char c) {
LSListNode *node = (LSListNode *)malloc(sizeof(LSListNode));
node->data = c;
//不存在队首默认指向第一个
if (!queue->front) {
queue->near = queue->front = node;
return;
}
//队尾指向新增元素
queue->near->next = node;
queue->near = node;
}
3.出队
出队前先判断队伍是否为空,为空结束,否则队首指向的元素移除,且队首元素后移一位即可
void pointLeave(LSPointListQueue *queue) {
if (!queue->front) return; //队为空不需要出队
LSListNode *front = queue->front;
queue->front = queue->front->next; //出队,队伍指针后移
free(front);
}
方案二:环形链队列(推荐)
环形链队列由节点本身,依照头尾指针特点,参考环状结构衍生出来的结构,其默认只有一个初节点来维护,示意图如下:
由上图结合默认只有一个初节点来维护可以得到两个信息:
1.一个节点时队首队尾在一起;
2.队首节点在前,队尾节点灾后,最后队尾节点指向队首节点,形成环状,固定节点就是队尾节点rear, 固定节点next就是队首节点front,因此通过默认固定节点,加上其next可以同时获取队尾和队首节点;
由此条件可以推测入队出队操作是否可行: 入队时,链表可以随时入队,一个节点时next指向自己,多个节点时,新节点next指向队首节点,固定节点next指向新节点,固定节点指向新节点,实现了一次入队;出队时,队伍为空则不出队伍,不为空固定节点next指向队首节点的next,然后释放原队首节点即可
1.初始化
因为只需要一个固定节点来保证索引,所以不需要刻意初始化,因此只需要申明默认节点数据结构即可
typedef struct listNode {
char data;
struct listNode *next;
} LSListNode; //节点基础数据结构
2.入队
入队时,如果没有固定节点(其实就是尾结点),则节点指向自己
如果有固定节点,则取出队首节点,固定节点指向新节点,新节点的next指向取出的队首节点即可
LSListNode *listQueueEnter(LSListNode *queue, char c) {
LSListNode *node = (LSListNode *)malloc(sizeof(LSListNode));
node->data = c;
if (queue) {
//多个节点首尾相接
LSListNode *front = queue->next;
queue->next = node;
node->next = front;
}else {
queue = node->next = node; //一个节点尾指针指向自己
}
return queue;
}
3.出队
出队时,如果队伍不存在,直接结束,如果队首节点(固定节点next)是自己,那么直接释放,置空固定节点,否则固定节点的next指向队首节点next,然后释放队首节点即可
LSListNode *listQueueLeave(LSListNode *queue) {
if (!queue) return NULL;//没有节点
if (queue->next == queue) {
free(queue); //只有一个
queue = NULL;
}else {
LSListNode *front = queue->next->next; //指向头指针的下一个,两个和多个一样
free(queue->next);
queue->next = front;
}
return queue;
}
总结
1.栈分为顺序栈和链栈,先进后出
2.队列分为顺序队列和链式队列,顺序队列和链式队列有线状和环状,并且他们的实现方式有所不同,且方式为先进先出