1.栈和队列的定义
栈和队列都是操作受限的线性表。其中栈(stack) 是限定在 表尾(称为栈顶top)进行插入和删除操作的线性表,即后进先出(LIFO)。队列(queue) 是限定在表头(称为队头front)进行删除,在表尾(称为队尾巴rear)进行插入的线性表,即先进先出(FIFO)。
2.表示和实现
和线性表类似,栈和队列的实现有两种存储表示方式:顺序存储和链式存储。
基本操纵有初始化、入栈(队)、出栈(队)、判空、取栈顶(队首)。
2.1 栈的实现
顺序栈即使用一组地址连续的存储单元依次存放从栈底到栈顶的数据元素,同时使用指针top表示栈顶,使用指针base表示栈底,即栈的起始位置。
为了让栈里能存放任意类型的数据,额外设置一个elemsize来表示栈中元素的大小。
同时注意栈空的时候,top==base;栈非空的时候,top始终在栈顶元素的下一位置上。
// 顺序栈的实现
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define STACK_INIT_SIZE 10; // 栈大小初始值
#define STACK_INCREMENT 10; // 栈的增量
// 为了栈的通用性,使用内存复制的方式
typedef struct stack_t {
void* base; // 栈底
void* top; // 栈顶
int len; // 栈中当前元素个数,等于top-base / elemsize
int elemsize; // 栈中元素大小,以字节为单位
int stacksize; // 当前已分配空间,以元素为单位
}Stack;
// --- 栈的基本操作 ---
// 初始化
int InitStack(Stack* S,int elemsize) {
S->stacksize = STACK_INIT_SIZE;
S->elemsize = elemsize;
S->len = 0;
S->base = malloc(S->stacksize * S->elemsize);
S->top = S->base;
if (!S->base) return 0;
else return 1;
}
// 增加栈的最大容量
int IncreatStack(Stack* S) {
int newsize = S->stacksize + STACK_INCREMENT;
S->base = realloc(S->base, newsize * S->elemsize);
S->top = (char*)S->base + S->stacksize * S->elemsize; // 防止新分配的内存首地址变化
S->stacksize += STACK_INCREMENT;
if (!S->base) exit(-1);
else return 1;
}
// 判空
int StackEmpty(Stack S) {
if (S.len == 0) return 1;
else return 0;
}
int StackLen(Stack S) {
// 栈中元素的多少
return S.len;
}
// 入栈和出栈
int Push(Stack* s, void* e) {
if (s->top == (char*)s->base + s->stacksize * s->elemsize) IncreatStack(s);
memcpy(s->top, e, s->elemsize);
s->top = (char*)s->top + s->elemsize;
s->len++;
return 1;
}
int Pop(Stack* s, void* e) {
if (StackEmpty(*s)) return 0; // 栈空
s->top = (char*)s->top - s->elemsize;
memcpy(e, s->top, s->elemsize);
s->len--;
return 1;
}
// 取栈顶元素
void* Top(Stack* s) {
if (StackEmpty(*s)) return NULL; // 栈空
void* source = (char*)s->top - s->elemsize;
return source;
}
int main() {
// 顺序栈的应用
Stack S;
char elem; // 栈中元素类型
InitStack(&S, sizeof(elem));
// 压栈字符
char str[] = "hello world";
for (int i = 0; i < strlen(str); i++) {
elem = str[i];
Push(&S, (void*)&elem);
}
while (!StackEmpty(S)) {
Pop(&S, (void*)&elem);
printf("%c ", elem);
}
system("pause");
return 0;
}
链式栈的实现与前面链表实现类似,其示意图如下:
2.2 队列的实现
和单链表类似,链队列需要两个指针:头指针(front,指向队头)和尾指针(rear,指向队尾)。为了操作方便,我们也设置一个头结点(Head),并令头指针指向头结点。因此,空的链队列的判断条件是头指针和尾指针均指向头结点,如下图所示:
上图中阴影表示,头结点数据域中不存放数据。
常见的队列操作,只需要修改头指针和尾指针即可,如下图所示:
头文件
#pragma once
// === 队列Queue的抽象表示
// --- 单链队列(队列的链式存储结构) ---
typedef struct node {
void* data;
struct node* next;
}QNode,*QueuePtr;
// 链队列声明
typedef struct {
QueuePtr front; // 队头指针
QueuePtr rear; // 队尾指针
int elemsize; // 数据域的字节大小
int len; // 队列长度
}LinkQueue;
// --- 基本队列操作声明 ---
int InitQueue(LinkQueue* Q,int elemsize); // 初始化
int DestroyQueue(LinkQueue* Q); // 销毁队列
void ClearQueue(LinkQueue* Q); // 清空队列元素
int QueueEmpty(LinkQueue* Q); // 判断队列是否为空
int QueueLength(LinkQueue* Q); // 返回队列长度
int GetHead(LinkQueue* Q, void* e); // 返回队头元素e
int EnQueue(LinkQueue* Q, void* e); // 将元素e压入队列
int DeQueue(LinkQueue* Q, void* e); // 将元素e弹出队列
具体实现
// 链式队列的实现
#include "Queue.h"
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int InitQueue(LinkQueue* Q,int elemsize) {
// 先声明一个头结点
QNode* head = (QNode*)malloc(sizeof(QNode));
head->data = malloc(sizeof(elemsize));
Q->front = Q->rear = head;
Q->front->next = NULL;
if (!head) exit(-1);
Q->len = 0;
Q->elemsize = elemsize;
return 1;
}
int DestroyQueue(LinkQueue* Q) {
while (Q->front) {
Q->rear = Q->front->next;
free(Q->front->data);
free(Q->front);
Q->front = Q->rear;
}
return 1;
}
void ClearQueue(LinkQueue* Q) {
Q->rear = Q->front;
Q->front->next = NULL;
Q->len = 0;
}
int QueueEmpty(LinkQueue* Q) {
if (Q->rear == Q->front) return 1;
else return 0;
}
int QueueLength(LinkQueue* Q) {
return Q->len;
}
int GetHead(LinkQueue* Q, void* e) {
// 返回队头部元素e
if (QueueEmpty(Q)) {
e = NULL;
return 0;
}
QNode* first = Q->front->next;
memcpy(e, first->data, Q->elemsize);
return 1;
}
int EnQueue(LinkQueue* Q, void* e) {
// 插入元素e为新的队尾
QNode* last = (QNode*)malloc(sizeof(QNode));
last->data = malloc(sizeof(Q->elemsize));
memcpy(last->data, e, Q->elemsize);
last->next = Q->rear->next;
Q->rear->next = last;
Q->rear = last;
Q->len++;
return 1;
}
int DeQueue(LinkQueue* Q, void* e) {
// 删除队列头部的元素e,若为空,则返回0;否则,返回1
if (QueueEmpty(Q)) {
e = NULL;
return 0;
}
QNode* head = Q->front; // 头结点
QNode* first = head->next;
head->next = first->next;
memcpy(e, first->data, Q->elemsize);
// 注意,如果删除的是尾结点,则需要重置尾结点
if (first == Q->rear) Q->rear = Q->front;
free(first->data);
free(first);
Q->len--;
return 1;
}
3.应用
3.1 栈的应用
1.表达式的求值
算术表达式的计算是栈应用的常见例子,广为使用的算法是 “算符优先法”。
任何一个表示式由操作数(operand)、运算符(operator)组成,按照算术四则运算的规则:
- 先乘除,后加减;
- 从左到右依次计算;
- 先括号内,再括号外。
我们能定义任意两个相继出现的算符 θ 1 \theta_{1} θ1和 θ 2 \theta_{2} θ2之间的优先关系:
- θ 1 < θ 2 \theta_{1} < \theta_{2} θ1<θ2 ,即 θ 1 \theta_{1} θ1的优先级低于 θ 2 \theta_{2} θ2
- θ 1 = θ 2 \theta_{1} = \theta_{2} θ1=θ2 ,即 θ 1 \theta_{1} θ1的优先级等于 θ 2 \theta_{2} θ2
- θ 1 > θ 2 \theta_{1} > \theta_{2} θ1>θ2 ,即 θ 1 \theta_{1} θ1的优先级大于 θ 2 \theta_{2} θ2
结合四则运算规则,我们有下列的(相邻)算符间的优先关系:
因此我们可以建立两个工作栈:操作数栈(OPND),存放操作数或中间结果;算符栈(OPTR),存放运算符。则有算法:
(1) 操作数栈置空,表达式起始符"#"为算符栈的栈的元素;
(2) 依次读入表达式的每个字符,若是操作数则压入OPND;若是算符
θ
2
\theta_{2}
θ2,则和OPTR栈顶算符
θ
1
\theta_{1}
θ1比较:
- 若栈顶算符 θ 1 \theta_{1} θ1优先级高,则栈顶算符出栈,并弹出OPND的相应操作数计算后将结果压入OPND;
- 若栈顶算符 θ 1 \theta_{1} θ1优先级低,则当前算符压入OPTR栈;
- 若栈顶算符
θ
1
\theta_{1}
θ1优先级相同,则弹出栈顶算符。
直至整个表达式求值完毕(即OPTR栈顶元素和当前读入字符均为结束符"#")。
// -- 简单表达式的计算 --
// 仅包含+ - * / 和括号,例如 (4+3)*10/2=
// 使用算符优先法,包含操作数栈OPTR(operator)和操作符栈OPND(operand)
int DEBUG = 0;
int isOperator(char op) {
if (op >= '0' && op <= '9') return 0;
else return 1;
}
int splitValue(char expr[100], int* loc) {
// 分词 数字
char tmp[100];
int i = 0;
while (!isOperator(expr[*loc])) {
tmp[i++] = expr[*loc];
(*loc)++;
//if (DEBUG) printf("当前索引%d", *loc);
}
tmp[i] = '\0';
return atoi(tmp);
}
int Calculate(char op, int left, int right) {
// 根据操作符op和操作数left、right计算表达式的值
switch (op) {
case '+': return left + right; break;
case '-': return left - right; break;
case '*': return left * right; break;
case '/': return left / right; break;
}
}
char Precede(char op1, char op2) {
// 判断op1和op2的优先级,op1为栈内,op2为栈外
switch (op1) {
case '+':
case '-':
if (op2 == '+' || op2 == '-' || op2 == ')' || op2 == '=') return '>';
else return '<';
break;
case '*':
case '/':
if (op2 == '(') return '<';
else return '>';
break;
case '(':
if (op2 == ')') return '=';
else return '<';
case ')':
return '>';
case '=':
if (op2 == '=') return '=';
else return '<';
}
}
int EvaluateExpr(char expr[100]) {
// 计算expr的值
Stack OPTR, OPND;
int value; char op; // 数值和操作符
InitStack(&OPTR, sizeof(char));
InitStack(&OPND, sizeof(int));
op = '='; Push(&OPTR, &op); // = 表示结算符,书中常用#
int len = strlen(expr); // 表达式的长度
int index = 0; // 当前的分词索引
op = expr[index];
while (op != '=' || *(char*)Top(&OPTR) != '=') {
if (DEBUG) {
int va;
char ch;
if (!StackEmpty(OPND)) {
va = *(int*)Top(&OPND);
printf("当前操作数栈%d\n", va);
}
if (!StackEmpty(OPTR)) {
ch = *(char*)Top(&OPTR);
printf("当前操作符栈%c\n", ch);
}
printf("当前符号:%c\n\n", op);
}
if (!isOperator(op)) {
// 不是操作符
value = splitValue(expr, &index);
if (DEBUG) printf("切分出的值:%d\n", value);
Push(&OPND, &value);
op = expr[index];
}
else { // 操作符号
char top = *(char*)Top(&OPTR);
switch(Precede(*(char*)Top(&OPTR), op)) {
case '<':
Push(&OPTR, &op);
op = expr[++index];
break;
case '>': {
char tmp;
Pop(&OPTR, &tmp);
int left, right;
Pop(&OPND, &right);
Pop(&OPND, &left);
int result = Calculate(tmp, left, right);
Push(&OPND, &result);
break;
}
case '=':
Pop(&OPTR, &op);
op = expr[++index];
break;
}
}
}
Pop(&OPND, &value);
return value;
}
int main() {
char expr[100] = "(4+3)*10/2=";
int result = EvaluateExpr(expr);
printf("结果是%d\n", result);
system("pause");
return 0;
}
2.栈的模拟
3.2 队列的应用
参考资料
《数据结构 C语言描述》 严蔚敏著