栈的定义:
· 书本定义:栈是一个后进先出的线性表,它只要求只在表尾 进行删除和插入操作。
· 通俗定义:栈就是一个特殊的线性表(顺序表,链表),操作上有一些特殊性:
-栈的元素必须“后进先出”。
-栈的操作只能在这个线性表的表尾进行。
-注:线性表的表尾对栈来说,是它的栈顶,响应的表头称为栈底。
栈的顺序存储结构:
· 因为栈的本质是一个线性表,线性表有两种存储形式,那么栈也有分为栈的
顺序存储结构和栈的链式存储结构。
· 最快开始没有数据称为空栈,此时栈顶就是栈底。然后数据从栈顶进入,栈顶栈底分
离,整个栈的当前容量变大。数据出栈时从栈顶弹出,栈顶下移,栈的当前容量变小。
注意:top指针始终指向栈顶元素的上方,也可根据自己的想法指在栈顶元素。
结构代码:
#define STACK_INIT_SIZE 100 //存储空间初始分配量
#define STACKINCREMENT 10 //存储空间分配增量
typedef int SElemType;
typedef struct {
SElemType *base; //在栈构造之前和销毁之后,base值为NULL
SElemType *top; //栈顶指针
int stacksize; //当前已分配的存储空间,以元素为单位
} SqStack;
· 这里定义了一个顺序存储的栈,它包含了三个元素:base,top,stacksize。
其中base是指向栈底的指针变量,top是指向栈顶的指针变量,stacksize
指示栈的当前可使用的最大容量。
创建一个栈://InitStack
int InitStack(SqStack &S) {
//构造一个空栈S
S.base = (SElemType *) malloc(STACK_INIT_SIZE * sizeof(SElemType));
//存储分配失败
if (!S.base)
exit(0);
S.top = S.base;
S.stacksize = STACK_INIT_SIZE;
return 1;
}
栈的插入和删除操作:
· 栈的插入操作(push),叫做进栈,也称为压栈,入栈。
· 栈的删除操作(pop),叫做出栈,也称为弹栈。
入栈操作
· 入栈操作又叫压栈操作,就是向栈中存放数据。
· 入栈操作要在栈顶进行,每次向栈中压入一个数据,top指针就要+1,直到栈满。
入栈算法://Push
int Push(SqStack &S, SElemType e) {
//插入元素e为新的栈顶元素
if (S.top - S.base >= S.stacksize) {
//栈满,追加存储空间
S.base = (SElemType *) realloc(S.base,
(S.stacksize + STACKINCREMENT) * sizeof(SElemType));
//出错退出
if (!S.base)
exit(0);
//使top指针重新回到栈顶
S.top = S.base + S.stacksize;
S.stacksize += STACKINCREMENT;
}
*S.top++ = e;//赋值后,指针上移
return 1;
}
出栈操作
· 出栈操作就是在栈顶取出数据,栈顶指针随之下移的操作。
· 每当从栈内弹出一个数据,栈的当前容量就-1。
出栈算法://Pop
int Pop(SqStack &S,SElemType &e){
//若栈不为空,则删除S的栈顶元素,用e返回其值
//并返回1,否则返回0
if(S.top==S.base)
return 0;
//top指针下移,并赋值给e
e=*--S.top;
return 1;
}
清空一个栈
· 所谓清空一个栈,就是将栈中的元素全部作废,但栈本身物理空间并不发生改变。
· 只需将s.top的内容赋值为s.base,这样s.base等于s.top,这就表明栈空。
清空栈算法://ClearStack
void ClearStack(SqStack &S){
S.top=S.base;
}
销毁一个栈
· 销毁一个栈与清空一个栈不同,销毁一个栈是要释放掉该栈所占的物理内存空间,
因此不要把销毁一个栈与清空一个栈混淆。
销毁栈算法://DestoryStack
void DestroyStack(SqStack &S){
int len;
len=S.stacksize;
for (int j = 0; j < len; ++j) {
free(S.base);
S.base++;
}
S.base=S.top=NULL;
S.stacksize=0;
}
计算栈的当前容量
· 只要返回S.top - S.base即可
· 注意:高地址减去低地址,因为返回int型,所以他会除以单个所分配的大小,
即元素个数(除以单个分配的sizeof)。
当前容量算法://StackLength
int StackLength(SqStack S){
return (S.top-S.base);
}
以下罗列出书上的一些算法:
int GetTop(SqStack S, SElemType &e) {
//若栈不空,则用e返回S的栈顶元素,并返回1,否则返回0
if (S.top == S.base)
return 0;
e = *(S.top - 1);
return 1;
}
int StackEmpty(SqStack S){
//判断栈是否为空,空则返回1,否则返回0
if(S.base==S.top)
return 1;
else
return 0;
}
栈的应用
题目:利用栈的特点,将用户输入十进制的数,转换为八进制。
算法实现://conversion
void conversion(){
//对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数
SElemType e;
int N;
SqStack S;
//构造空栈
InitStack(S);
scanf("%d",&N);
while (N){
//取余,压入
Push(S,N%8);
//取整
N=N/8;
}
while (!StackEmpty(S)){
Pop(S,e);
printf("%d",e);
}
}
题目:表达式求值。
这里不多解释,配合书本,理解下面代码就行。
char Preccede(SElemType t1, SElemType t2) {
//根据书本表3.1,判断t1,t2符号优先关系
char f;
switch (t2) {
case '+':
case '-':
if (t1 == '(' || t1 == '#')
f = '<';//t1<t2
else
f = '>';//t1>t2
break;
case '*':
case '/':
if (t1 == '*' || t1 == '/' || t1 == ')')
f = '>';//t1>t2
else
f = '<';//t1<t2
break;
case '(':
if (t1 == ')') {
printf("括号不匹配\n");
exit(0);
} else
f = '<';//t1<t2
break;
case ')':
switch (t1) {
case '(':
f = '=';//t1=t2
break;
case '#':
printf("缺少左括号\n");
exit(0);
default:
f = '>';//t1>t2
}
break;
case '#':
switch (t1) {
case '#':
f = '=';//t1=t2
break;
case '(':
printf("缺乏右括号\n");
exit(0);
default:
f = '>';//t1>t2
}
}
return f;
}
int In(SElemType c) {
//判断c是否为7种运算符之一
switch (c) {
case '+':
case '-':
case '*':
case '/':
case '(':
case ')':
case '#':
return 1;
default:
return 0;
}
}
SElemType Operate(SElemType a, SElemType theta, SElemType b) {
//做四则运算a theta b,返回运算结果
switch (theta) {
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
}
if (b == '0')
printf("除数不能为0");
return a / b;
}
SElemType EvaluateExpression() {
//算术表达式的算符优先算法。设OPTR和OPND分别为
//运算符栈和运算数栈,
SqStack OPTR, OPND;
SElemType a, b, c, x;
//初始化两个栈
InitStack(OPTR);
InitStack(OPND);
//将#压入运算符栈
Push(OPTR, '#');
//由键盘读入一个字符到c
c = getchar();
//当c不为#或者运算符栈顶不为#
while (c != '#' || GetTop(OPTR) != '#') {
if (In(c)) {//c是7种运算符之一
//判断栈顶运算符和c的优先级
switch (Preccede(GetTop(OPTR), c)) {
case '<'://栈顶优先权低
Push(OPTR, c);
c = getchar();
break;
case '='://优先权相等,脱去括号
Pop(OPTR, x);
c = getchar();
break;
case '>'://栈顶优先权高,进入运算
Pop(OPTR, x);
Pop(OPND, b);
Pop(OPND, a);
Push(OPND, Operate(a, x, b));
}
} else if (c >= '0' && c <= '9') {//c为运算数,减48压入栈
Push(OPND, c - 48);
c = getchar();
} else {//非法字符报错
printf("非法字符\n");
exit(0);
}
//将运算结果赋给x
GetTop(OPTR, x);
}
//弹出运算数栈顶给x,此时OPND栈应为空
Pop(OPND, x);
if (!StackEmpty(OPND)) {
printf("表达式不正确\n");
exit(0);
}
return x;
}
题目:汉诺塔问题。
也只要理解代码即可,这里不详述
int d;//记录步数
//d表示进行到的步数,将编号为n的盘子由from柱移动到to柱(目标柱)
void move(char from,int n,char to){
printf("第%d步:将%d号盘子%c---->%c\n",d++,n,from,to);
}
//汉诺塔递归 函数
//n表示要将多少个“圆盘”从起始柱子移动至目标柱子
//start_pos表示起始柱子,tran_pos表示过渡柱子,end_pos表示目标柱子
void Hanoi(int n,char start_pos,char tran_pos,char end_pos){
if(n==1)//当n==1的时候,只要直接将圆盘从起始柱子移至目标柱子即可
move(start_pos,n,end_pos);
else{
//递归处理,一开始的时候,先将n-1个盘子移至过渡柱上
Hanoi(n-1,start_pos,end_pos,tran_pos);
//然后再将底下大盘子直接移至目标柱子即可
move(start_pos,n,end_pos);
//然后重复以上步骤,递归处理放在过渡柱子上的n-1个盘子
//此时借助原来的起始柱作为过渡柱(因为起始柱已经空了)
Hanoi(n-1,tran_pos,start_pos,end_pos);
}
}
int main() {
int n;
while (scanf("%d",&n)==1&&n) {
d = 1;//全局变量赋初值
Hanoi(n, '1', '2', '3');
printf("最后总的步数为%d\n", d - 1);
}
return 0;
}
队列
· 队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
· 与栈相反,队列是先进先出的线性表。
· 与栈相同的是,队列同样需要顺序表或链表作为基础。
· 输入缓冲区接受键盘的输入就是按队列的形式输入和输出的。
· 队列既可以用链表实现,也可以用顺序表实现。跟栈相反的是,栈一般用顺序来表现,
而队列我们常用链表来实现,简称为链队列。
单链队列的链式存储结构:
typedef int QElemType;
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct {
QueuePtr front;//队头指针
QueuePtr rear;//队尾指针
}LinkQueue;
· 我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。
(注:头结点不是必要的,但为了方便操作,这里加上了)
· 空队列时,front和rear都指向头结点。
· 创建一个队列,第一步在内存中创建一个头结点,第二步把头尾指针都指向它。
void IniteQueue(LinkQueue &Q){
//构造一个空队列Q
Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));
if(!Q.front)
exit(0);
Q.front->next=NULL;
}
· 入队列操作
void EnQueue(LinkQueue &Q,QElemType e){
//插入元素e为Q的新的队尾元素
QueuePtr p;
//创建一个结点
p=(QueuePtr)malloc(sizeof(QNode));
if(!p)
exit(0);
//该结点赋值,next指向NULL
p->data=e;
p->next=NULL;
//尾指针的next指向p
Q.rear->next=p;
//p成为新的尾元素
Q.rear=p;
}
· 出队列操作
· 出队列操作是将队列中的第一个元素移出,队头指针不发生改变,改变头结点的next即可。
队列中有多个元素
队列中只有一个元素
int DeQueue(LinkQueue &Q,QElemType &e){
//若队列不空,则删除Q的队头元素,用e返回其值
//并返回1,否则返回0
QueuePtr p;
if(Q.front==Q.rear)
return 0;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
//如果只有一个元素,需处理一下尾指针
if(Q.rear==p)
Q.rear=Q.front;
free(p);
return 1;
}
销毁一个队列
· 由于链队列建立在内存的动态区,因此当一个队列不再有用时应当把它即时销毁,
避免过多占用内存。
void DestroyQueue(LinkQueue &Q){
//销毁队列Q
while (Q.front){
Q.rear=Q.front->next;
free(Q.front);
Q.front=Q.rear;
}
}
循环队列的顺序存储结构
· 循环队列它的容量是固定的,并且它的队头和队尾指针都可以随着元素入出
队列而发生改变,这样循环队列逻辑上就好像是一个环形存储空间。
· 注意的是,在实际内存中,不可能有真的环形存储区,只是模拟逻辑上的循环。
· 可以发现,循环队列的实现只需要灵活改变front和rear指针即可。
· 就是让front或rear+1,即超出了地址范围,也会自动从头开始。可以取模运算处理:
(rear+1)%QueueSize
(rear+1)%QueueSize
· 取模就是取余数的意思,他取到的值永远不会大于除数。
循环队列原理图:
我们可以发现,当循环队列属于上图的d1情况时,是无法判断当前状态是队空还是队满。
为了达到判断队列状态的目的,可以通过牺牲一个存储空间来实现。
如上图d2所示,
队头指针在队尾指针的下一位置时,队满。 Q.front == (Q.rear + 1) % MAXSIZE 因为
队头指针可能又重新从0位置开始,而此时队尾指针是MAXSIZE - 1,所以需要求余。
当队头和队尾指针在同一位置时,队空。 Q.front == Q.rear;
队列常用操作:
void InitQueue(SqQueue &Q) {
//构造一个空队列Q
Q.base = (QElemType *) malloc(MAXQSIZE * sizeof(QElemType));
if (!Q.base)
exit(0);
Q.front = Q.rear = 0;
}
//求队列长度操作
int QueueLength(SqQueue Q) {
//返回Q的元素个数,即队列的长度
return (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE;
}
//队尾插入元素操作
void EnQueue(SqQueue &Q, QElemType e) {
//插入元素e为Q的新的队尾元素
if ((Q.rear + 1) % MAXQSIZE == Q.front)
exit(0);
Q.base[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAXQSIZE;
}
//队头删除元素操作
int DeQueue(SqQueue &Q, QElemType &e) {
//若队列不空,则删除Q的队头元素,用e返回其值
//并返回1,否则返回0
if (Q.front == Q.rear)
return 0;
e = Q.base[Q.front];
Q.front = (Q.front + 1) % MAXQSIZE;
return 1;
}