数据结构(C语言版 严蔚敏著)——栈和队列

栈的定义

· 书本定义:栈是一个后进先出的线性表,它只要求只在表尾 进行删除和插入操作。

· 通俗定义:栈就是一个特殊的线性表(顺序表,链表),操作上有一些特殊性:

    -栈的元素必须“后进先出”。

    -栈的操作只能在这个线性表的表尾进行。

    -注:线性表的表尾对栈来说,是它的栈顶,响应的表头称为栈底。

栈的顺序存储结构:

· 因为栈的本质是一个线性表,线性表有两种存储形式,那么栈也有分为栈的

  顺序存储结构和栈的链式存储结构。

· 最快开始没有数据称为空栈,此时栈顶就是栈底。然后数据从栈顶进入,栈顶栈底分

  离,整个栈的当前容量变大。数据出栈时从栈顶弹出,栈顶下移,栈的当前容量变小。


注意: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;
}


猜你喜欢

转载自blog.csdn.net/super_sloppy/article/details/79683802