目录
- 一、栈
- 1.1、栈的基本概念
- 1.2、栈的基本操作
- 1.3、栈的顺序存储实现
- 1.3.1、顺序栈的定义
- 1.3.2、顺序栈的初始化
- 1.3.3、顺序栈的入栈和出栈
- 1.3.4、读取栈顶元素
- 1.3.5、共享栈(即两个栈共享同一片空间)
- 1.4、栈的链式存储实现
- 1.4.1、链栈的定义
- 1.4.2、链栈的入栈和出栈
- 二、队列
- 2.1、队列的基本概念
- 2.2、队列的基本操作
- 2.3、队列的顺序存储实现
- 2.3.1、顺序队列的定义
- 2.3.2、顺序队列的初始化
- 2.3.3、入队出队(循环队列)
- 2.3.4、获取队首元素
- 2.4、队列的链式存储实现
- 2.4.1、链队列的定义
- 2.4.2、链式队列的初始化
- 2.4.3、入队出队(带头结点)
- 2.4.4、不带投结点的链式队列
- 2.5、双端队列
- 三、栈与队列的应用
- 3.1、栈在括号匹配中的应用
- 3.2、栈在表达求值中的应用
- 3.3、栈在递归中的应用
- 3.4、队列的应用
一、栈
1.1、栈的基本概念
- 只允许在一端(栈顶top)进行插入和删除操作的受限的线性表;
- 遵循后进先出(last in first out)LIFO原则。
1.2、栈的基本操作
1. InitStack(&S): 初始化栈,构造一个空栈,分配内存空间;
**2. DestroyStack(&S):**销毁栈,销毁并释放栈S所占用的空间
3. Push(&S, x): 进栈,若栈S未满,则将元素x加入其中使其成为新的栈顶元素;
4. Pop(&S, x): 出栈,若栈S非空,则弹出(删除)栈顶元素,并用x返回;
5. GetTop(S, &x): 读取栈顶元素,若栈S非空,则用x返回栈顶元素;
6. StackEmpty(S): 判空,判断栈S是否为空,若S为空,则返回true,否则返回false。
1.3、栈的顺序存储实现
1.3.1、顺序栈的定义
#define MaxSize 10;//定义栈内元素的最大个数typedef struct{ElemType data[MaxSize];//静态数组存放栈的元素int top;//栈顶元素
}SqStack;void testStack(){SqStack S;//声明一个顺序栈(分配空间)
}
1.3.2、顺序栈的初始化
#define MaxSize 10;typedef struct{ElemType data[MaxSize];int top;
}SqStack;void initStack(SqStack &S){S.top = -1;//初始化栈顶指针
}bool EmptyStack(SqStack S){if(S.top == -1)return true;else return false;
}
1.3.3、顺序栈的入栈和出栈
上溢:满栈时入栈
下溢:空栈时出栈
//进栈
bool Push(SqStack &S, ElemType x){if(S.top == MaxSize - 1) //判断栈是否为满return false;S.data[++S.top] = x; //栈顶指针先加1,再送值到栈顶元素return true;
}
//出栈
bool Pop(SqStack &S, ElemType x){if(S.top == -1)return false;x = data[S.top--]; //先取栈顶的值,再将栈顶指针减1return true;
}
1.3.4、读取栈顶元素
bool GetElem(SqStack S, ElemType x){if(S.top == -1)return false;x = data[S.top];return true;
}
1.3.5、共享栈(即两个栈共享同一片空间)
共享栈是特殊的顺序栈,将栈底设置在共享空间的两端,栈顶向中间靠拢。
#define MaxSize 10;typedef struct{ElemType data[MaxSize];int top0; //0号栈的栈顶指针int top1; //1号栈的栈顶指针
}ShStack;void initSqStack(ShStack &S){S.top0 = -1;S.top1 = MaxSzie;
}
1.4、栈的链式存储实现
1.4.1、链栈的定义
采用链式存储的栈被称为链栈。
优点:便于多个栈共享存储空间和提高效率,且不存在满栈上溢的情况。
链表的头部作为栈顶,意味着:
- 在实现数据入栈时,需要将数据从链表的头部插入(头插);
- 在实现数据的出栈时,需要删除链表头部的收元结点。
因此,链栈就是一个只能采用头插法插入或删除数据的链表。
typedef struct{ElemType data;Linknode *next;
}Linknode, *LinkStack;//初始化链栈
void initStack(LinkStack &L){L = (Linknode*)malloc(sizeof(Linknode));if(L == NULL)return false;L->next = NULL;return true;
}
//判断链栈是否为空
bool EmptyStack(LinkStack L){if(L->next == NULL)return true;else return false;
}
1.4.2、链栈的入栈和出栈
typedef struct{ElemType data;Linknode *next;
}Linknode, *LinkStack;//入栈(头插)
bool pushStack(LinkStack &L, ElemType x){*s = (Linknode*)malloc(sizeof(Linknode));if(s == NULL)return false;s->next = x;s->next = L->next;L->next = s;return true;
}
//出栈
bool popStack(LinkStack &L, Elemtype x){if(L == NULL)return false;Linknode *s = L->next;x = L->next;L->next = s->next;free(s);return true;
}
二、队列
2.1、队列的基本概念
- 只允许在一端(队尾)进行插入操作,在另一端(队首)进行删除操作的受限的线性表。
- 遵循先进先出(first in first out)FIFO原则。
2.2、队列的基本操作
- initQueue(&Q):初始化队列,构造一个空队列Q;
- EmptyQueue(Q):判空,判断队列Q是否为空,若为空返回true,否则返回false;
- EnQueue(&Q, x):入队,若队列未满,则把元素x加入Q,使之成为新的队尾;
- DeQueue(&Q, x):出队,若队列非空,则删除队首元素,并用x返回元素值;
- GetQueue(Q,&x):读取队首元素,若队列非空,则用x返回队首元素;
- ClearQueue(&Q):销毁队列并释放队列Q所占用的空间。
2.3、队列的顺序存储实现
队首指针front:指向队首元素
队尾指针rear:指向队尾元素的下一个位置
2.3.1、顺序队列的定义
#define MaxSize 10;typedef struct{ElemType data[MaxSize];//用静态数组存储队列元素int front; //队首指针int rear; //对尾指针
}SqQueue;void test(){SqQueue Q;//声明一个队列
}
2.3.2、顺序队列的初始化
#define MaxSize 10;typedef struct(){ElemType data[MaxSize];int front;int rear;
}SqQueue;void initQueue(SqQueue &Q){//初始化时,队尾和队首指针都指向0Q.front = Q.rear = 0;
}
//判空
bool EmptyQueue(SqQueue){if(Q.front == Q.rear)return true;else return false;
}
2.3.3、入队出队(循环队列)
#define MaxSize 10;typedef struct{ElemType data[MaxSize];int front;int rear;
}SqQueue;
//入队
bool EnQueue(SqQueue &Q, ElemType x){if((Q.rear+1)%MaxSize == Q.front)return false;Q.data[Q.rear] = x;Q.rear = (Q.rear + 1)%MaxSize;//通过取余的方式实现循环队列return true;
}
//出队
bool DeQueue(SqQueue &Q, ElemType x){if(Q.rear == Q.front)return false;x = Q.data[Q.front];Q.front = (Q.front + 1)%MaxSize;return true;
}
注意判满和判空的条件!
2.3.4、获取队首元素
#define MaxSize 10;typedef struct{ElemType data[MaxSize];int front;int rear;
}SqQueue;//获取队首元素
void GetQueue(SqQueue Q, ElemType x){if(Q.rear == Q.front)return false;x = Q.data[Q.front]return true;
}
2.4、队列的链式存储实现
2.4.1、链队列的定义
//链式队列的结点
typedef struct{ElemType data;Linknode *next;
}Linknode;//链式队列
typedef struct{//头尾指针Linknode *front, *rear;
}LinkQueue;
2.4.2、链式队列的初始化
带头结点的链式队列初始化:
typedef struct{ElemType data;Linknode *next;
}Linknode;typedef struct{LinkQueue *front, *rear;
}LinkQueue;void initLinkQueue(LinkQueue &O){//初始化时,头尾指针均指向头结点Q.front = Q.rear =(Linknode*)malloc(sizeof(Linknode));Q.front->next = NULL;
}
//判空
bool EmptyQueue(LinkQueue Q){if(Q.front == Q.rear)return true;else return false;
}
2.4.3、入队出队(带头结点)
typedef struct{ElemType data;Linknode *next;
}Linknode;
typedef struct{LinkQueue *front, *rear;
}LinkQueue;
//入队
void EnQueue(LinkQueue &Q, ElemType x){Linknode *s = (Linlnode*)malloc(sizeof(Linknode));if(s == NULL)return false;s->data = x;s->next = NULL;Q.rear->next = s;Q.rear = s;
}
//出队
bool DeQueue(LinkQueue &Q, ElemType x){if(Q.front == Q.rear)return false;Linknode *p = Q.front->next;x = p->data;Q.front->next = p->next;if(Q.rear == p)Q.rear = Q.front;free(p);return true;
}
2.4.4、不带投结点的链式队列
typedef struct{ElemType data;Linknode *next;
}Linknode;
typedef struct{LinkQueue *front, *rear;
}LinkQueue;
//初始化
void initQueue(LinkQueue &Q){Q.front == NULL;Q.rear = NULL;
}
//判空
bool EmptyQueue(LinkQueue Q){if(Q.front == Q.rear)return true;else return false;
}
//入队
void EnQueue(LinkQueue &Q, ElemType x){Linknode *s =(Linknode*)malloc(sizeof(Linknode));s->data = x;s->next = NULL;//第一个元素入队时需要特殊处理if(Q.front == NULL){Q.front = s;Q.rear = s;}else{Q.rear->next = s;Q.rear = s;}
}
//出队
bool DeQueue(LinkQueue &Q, ElemType x){if(Q.front == NULL)return false;Linknode *s = Q.front;x = s->data;//队列只有一个结点if(Q.front == Q.rear){Q.front = Q.rear = NULL;}else{Q.front = Q.front->next;}free(s);return true;
}
2.5、双端队列
- 双端队列是允许从两端插入、删除的队列;
- 如果只使用其中一端的插入、删除操作,则等同于栈;
- 输入受限的双端队列:允许一段插入,两端删除的线性表;
- 输出受限的双端队列:允许两端插入,一端删除的线性表。
考点:判断输出序列的合法化
例题分析:数据元素输入序列为1,2,3,4,判断4!=24输出序列的合法化
对于输入受限的双端队列:只有 4213 和 4231 不合法
对于输出受限的双端队列:只有 4132 和 4231 不合法
三、栈与队列的应用
3.1、栈在括号匹配中的应用
用栈实现括号匹配:
- 最后遇到的左括号最先被匹配(栈的特性–LIFO)
- 遇到左括号就存一个左括号(入栈);
- 遇到右括号就消耗一个左括号(出栈)。
匹配失败的情况:
- 扫描到右括号且栈空,则该右括号单身;
- 扫描完所有括号后,栈非空,则该左括号单身;
- 左右括号不匹配。
#define MaxSize 10;typedef struct{char data[MaxSize];int top;
}SqStack;void initStack(SqStack &S);
bool EmptyStack(SqStack S);
bool PushStack(SqStack &S, char x);
bool PopStack(SqStack &S, char x);//判断长度为length的字符串str中的括号是否匹配
bool bracketCheck(char str[], int length){SqStack S;initStack S;//遍历字符串strfor(int i = 0;i<=length;i++){//扫描到左括号就入栈if(str[i] == '('||str[i] == '{'||str[i] == '['){pushStack(S, str[i]);}else{//扫描到右括号且栈空就直接返回if(EmptyStack(S))return false;char topElem;//用于接收栈顶元素PopStack(S,topElem);//括号不匹配if(str[i] == ')'&&topElem != '(')return false;if(str[i] == '}'&&topElem != '{')return false;if(str[i] == ']'&&topElem != '[')return false;}}//扫描完成若栈空,则说明括号匹配return EmptyStack(S);
}
3.2、栈在表达求值中的应用
1. 中缀表达式: 中缀表达式是一种通用的算术或逻辑公式表示方法,运算符以中缀形式处于操作数的中间。对于计算机来说,中缀表达式是很复杂的,因此计算表达式的值时,通常需要将中缀表达式转换为前缀或者后缀表达式,然后再进行求值。
2. 前缀表达式(波兰表达式): 前缀表达式的运算符位于两个操作数之前;
3. 后缀表达式(逆波兰表达式): 后缀表达式的运算符位于两个操作数之后。
中缀表达式转后缀表达式-手算
步骤一: 确定中缀表达式中各个运算符的运算顺序;
步骤二: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数;
步骤三: 如果还有运算符未被处理,继续步骤二。
遵循“左优先”原则;只要左边的运算符能先计算,就优先计算左边的。(保证运算顺序唯一)
示例:
中缀:A + B -C * D / E + F
后缀:A B + C D * E / - F +
中缀表达式转后缀表达式-机算
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符,从左到右处理各个元素,直至末尾,可能会遇到三种情况:
- 遇到操作数:直接加入后缀表达式;
- 遇到界限符:如遇到左括号"(“则直接入栈,遇到右括号”)“则依次弹出栈内运算符并加入后缀表达式,直到弹出”(“为止,注意左括号”("不加入后缀表达式;
- 遇到运算符:依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到")"或栈空则停止,之后再把当前运算符入栈。
#define MaxSize 40
typedef struct{ char data[MaxSize]; int top;
}SqStack;typedef struct{ char data[MaxSize]; int front,rear;
}SqQueue;void InitStack(SqStack &S);
bool StackEmpty(SqStack S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
void InitQueue(SqQueue &Q);
bool EnQueue(LQueue &Q, char x);
bool DeQueue(LQueue &Q, char &x);
bool QueueEmpty(SqQueue Q);// 判断元素ch是否入栈
int JudgeEnStack(SqStack &S, char ch){char tp = S.data[S->top]; // 如果ch是a~z则返回-1 if(ch >= 'a' && ch <= 'z') return -1; // 如果ch是+、-、*、/且栈顶元素优先级大于等于ch则返回0 else if(ch == '+' && (tp == '+' || tp == '-' || tp == '*' || tp == '/')) return 0; else if(ch == '-' && (tp == '+' || tp == '-' || tp == '*' || tp == '/')) return 0; else if(ch == '*' && (tp == '*' || tp == '/')) return 0; else if(ch == '/' && (tp == '*' || tp == '/')) return 0; // 如果ch是右括号则返回2 else if(ch == ')') return 2; // 其他情况ch入栈,返回1 else return 1;
}// 中缀表达式转后缀表达式
int main(int argc, char const *argv[]) { SqStack S; SqQueue Q; InitStack(S); InitQueue(Q); char ch; printf("请输入表达式,以“#”结束:"); scanf("%c", &ch); while (ch != '#'){ // 当栈为空时 if(StackEmpty(&S)){ // 如果输入的是数即a~z,直接入队 if(ch >= 'a' && ch <= 'z') EnQueue(Q, ch); // 如果输入的是运算符,直接入栈 else Puch(S, ch); }else{ // 当栈非空时,判断ch是否需要入栈 int n = JudgeEnStack(S, ch); // 当输入是数字时直接入队 if(n == -1){ EnQueue(Q, ch); }else if(n == 0){ // 当输入是运算符且运算符优先级不高于栈顶元素时 while (1){ // 取栈顶元素入队 char tp; Pop(S, tp); EnQueue(Q, tp); // 再次判断是否需要入栈 n = JudgeEnStack(S, ch);// 当栈头优先级低于输入运算符或者栈头为‘)’时,入栈并跳出循环 if(n != 0){ EnStack(S, ch); break; } } }else if(n == 2){ // 当出现‘)’时 将()中间的运算符全部出栈入队 while(1){ char tp; Pop(S, tp); if(tp == '(') break; else EnQueue(Q, tp); } }else{ // 当运算符优先级高于栈顶元素或出现‘(’时直接入栈 Push(S, ch); } } scanf("%c", &ch); } // 将最后栈中剩余的运算符出栈入队 while (!StackEmpty(S)){ char tp; Pop(S, tp); EnQueue(Q, tp); } // 输出队中元素 while (!QueueEmpety(Q)){ printf("%c ", DeQueue(Q)); } return 0;
}
后缀表达式的计算–手算:
从左到右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行相应运算,合体为一个新的操作数,再往后继续扫描。
后缀表达式的计算–机算:
用栈来实现后缀表达式的计算,栈内存放当前暂时不能确定运算次序的操作数
- 从左往右扫描元素,直到处理完所有元素;
- 若扫描到操作数,则压入栈,并回到步骤1;否则执行步骤3;
- 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1。
实现中缀表达式的计算:
- 初始化两个栈,操作数栈和运算符栈;
- 若扫描到操作数,则压入操作数栈;
- 扫描到运算符,则按照“中缀转后缀”中相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈) 。
3.3、栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:
- 调用返回地址
- 实参
- 局部变量
递归调用时,函数调用栈称为“递归工作栈”:
- 每进入一层递归,就将递归调用所需信息压入栈顶;
- 每退出一层递归,就从栈顶弹出相应信息。
缺点:太多层递归可能会导致栈溢出;适合用递归算法解决:可以把原始问题转换为属性相同,但规模较小的问题。
3.4、队列的应用
- 队列应用:树的层次遍历
- 队列应用:图的广度优先遍历
- 队列应用:操作系统中多个进程争抢使用有限资源时,先来先服务算法(First Come First Service)是一种常用的策略。