栈的定义:
· 书本定义:栈是一个后进先出的线性表,它只要求只在表尾 进行删除和插入操作。
· 通俗定义:栈就是一个特殊的线性表(顺序表,链表),操作上有一些特殊性:
-栈的元素必须“后进先出”。
-栈的操作只能在这个线性表的表尾进行。
-注:线性表的表尾对栈来说,是它的栈顶,响应的表头称为栈底。
栈的顺序存储结构:
· 因为栈的本质是一个线性表,线性表有两种存储形式,那么栈也有分为栈的
顺序存储结构和栈的链式存储结构。
· 最快开始没有数据称为空栈,此时栈顶就是栈底。然后数据从栈顶进入,栈顶栈底分
离,整个栈的当前容量变大。数据出栈时从栈顶弹出,栈顶下移,栈的当前容量变小。
注意: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; }