【数据结构与算法】栈和队列

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_38371360/article/details/86299588

栈的定义

栈是一种重要的线性结构,可以这样说,栈是前面将国的线性表的一种具体形式。

栈是后进先出的数据结构,它要求只在表尾进行删除和插入操作。

所谓的栈其实就是一个特殊的线性表(顺序表、链表),但是它在操作上有一些特殊的要求和限制:

  • 栈的元素必须(后进先出)
  • 栈的操作只能够在这个线性表的表尾进行
  • 注意:对于栈来说,这个表尾称为栈的栈顶(top),相应的表头称为栈底(buttom)。

栈的插入和删除操作

栈的插入操作(Push),叫做进栈,也称为压栈,入栈。类似于子弹放入弹夹的动作。

栈的删除操作(Pop),叫做出战,也称为弹栈。如同弹夹中的子弹出夹。

栈的顺序存储

 结构

因为栈的本质是一个线性表,线性表有两种存储形式,那么栈也分为栈的顺序存储结构和栈的链式存储结构。

 

typedef struct
{
    ElemType *base;
    ElemType *top;
    int stackSize;
}sqStack;

这里定义了一个顺序春初的栈,它包含了三个元素:base,top和stackSize。其中base是指向栈底的指针变量,top是指向站定的指针变量,stackSize是指栈的当前可使用的最大容量。

创建一个栈

#define STACK_INIT_SIZE 100
initStack(sqStack *s)
{
	s->base = (ElemType *)malloc(
	STACK_INIT_SIZE * sizeof(ElemType));
	if (!s->base)
		exit(0);
	s->top = s->base;	//最开始,栈顶就是栈底
	s->stackSize = STACK_INIT_SIZE;
}

入栈操作

入栈操作又叫压栈操作,就是向栈中存放数据。

入栈操作要在栈顶进行,每次向栈中亚茹一个数据,top指针就要+1,直到栈满为止。

#define SATCKINCREMENT 10

Push(sqStack *s, ElemType e)
{
    // 如果栈满,追加空间
    if( s->top – s->base >= s->stackSize )
    {
        s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
        if( !s->base )
            exit(0);

        s->top = s->base + s->stackSize;              // 设置栈顶
        s->stackSize = s->stackSize + STACKINCREMENT; // 设置栈的最大容量
    }

    *(s->top) = e;
    s->top++;
}

出栈操作

出栈操作就是在栈顶取出数据,栈顶指针随之下移的操作。

每当从栈内弹出一个数据,栈的当前容量就-1。

Pop(sqStack *s,ElemType *e)
{
    if( s->top == s->base )    //栈已空空如也
        return;
    *e = *--(s->top);
}

清空一个栈

所谓清空一个栈就是讲栈中的元素全部作废,但栈本身物理空间并不发生改变(不是销毁)

因此我们只要将s->top的内容赋值给s->base即可,这样s->base = s->top,也就表明这个栈是空的了。这个原理跟高级格式化只是单纯的晴空文件列表而没有覆盖硬盘的原理是一样的。

ClearStack(sqStack *s){
    s->top = s->base;
}

 销毁一个栈

销毁一个栈和清空一个栈不同,销毁一个栈是要释放掉该栈所占据的物理内存空间,因此不要把销毁一个栈和清空一个栈混淆。

DestroyStack(sqStack *s) {
	int i, len;
	len = s->stackSize;
	for (i = 0; i < len; i++) {
		free(s->base);
		s->base++;
	}
	s->base = s->top = NULL;
	s->stackSize = 0;
}

 计算栈的当前容量

计算栈的当前容量也就是计算栈中元素的个数,因此只要返回s.top - s.base即可。

注意,栈的最大容量是指该栈占据内存空间大小,其值是s.stackSize,它与栈的当前容量不是一个概念。

int StackLen(sqStack s)
{
    return (s.top - s.base);    //初学者需要重点理解,实际山除以了sizeof()
}

实例分析

利用栈的特点,把用户输入的二进制数转换为十进制数。

由于栈具有后进先出的特性,例如我们输入11001001这样的二进制数,如图:

 二进制转换成十进制代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT  10

typedef char ElemType;
typedef struct
{
	ElemType *base;
	ElemType *top;
	int stackSize;
}sqStack;

void InitStack(sqStack *s)
{
	s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
	if (!s->base)
	{
		exit(0);
	}

	s->top = s->base;
	s->stackSize = STACK_INIT_SIZE;
}

void Push(sqStack *s, ElemType e)
{
	if (s->top - s->base >= s->stackSize)
	{
		s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
		if (!s->base)
		{
			exit(0);
		}
	}

	*(s->top) = e;
	s->top++;
}

void Pop(sqStack *s, ElemType *e)
{
	if (s->top == s->base)
	{
		return;
	}
	*e = *--(s->top);
}

int StackLen(sqStack s)
{
	return (s.top - s.base);
}

int main()
{
	ElemType c;
	sqStack s;
	int len, i, sum = 0;

	InitStack(&s);

	printf("请输入二进制数,输入#符号表示结束!\n");
	scanf("%c", &c);
	while (c != '#')
	{
		Push(&s, c);
		scanf("%c", &c);
	}

	getchar();  // 把'\n'从缓冲区去掉

	len = StackLen(s);
	printf("栈的当前容量是: %d\n", len);

	for (i = 0; i < len; i++)
	{
		Pop(&s, &c);
		sum = sum + (c - 48) * pow(2, i);
	}

	printf("转化为十进制数是: %d\n", sum);

	return 0;
}

二进制转八进制:

为什么会有八进制呢,因为一个字节(8bit)刚好用两个十六进制可以表示完整,也大大的节省了显示空间。

/*****************************/
/**  二进制/八进制转换器   **/
/** By www.fishc.com 小甲鱼 **/
/*****************************/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT  10

typedef char ElemType;
typedef struct
{
    ElemType *base;
    ElemType *top;
    int stackSize;
}sqStack;

// 函数功能:初始化栈
// 参数*s:栈的地址
void InitStack(sqStack *s)
{
    s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
    if( !s->base )
    {
        exit(0);
    }

    s->top = s->base;
    s->stackSize = STACK_INIT_SIZE;
}

// 函数功能:入栈操作
// 参数*s:栈的地址
// 参数e:待压入栈的元素
void Push(sqStack *s, ElemType e)
{
    if( s->top - s->base >= s->stackSize )
    {
        s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
        if( !s->base )
        {
            exit(0);
        }

        s->top = s->base + s->stackSize;
        s->stackSize = s->stackSize + STACKINCREMENT;
    }

    *(s->top) = e;
    s->top++;
}

// 函数功能:弹栈操作
// 参数*s:栈的地址
// 参数e:存放从栈里弹出的数据
void Pop(sqStack *s, ElemType *e)
{
    if( s->top == s->base )
    {
        return;
    }
    *e = *--(s->top);
}

// 函数功能:计算栈s的当前长度
// 参数s:栈
int StackLen(sqStack s)
{
    return (s.top - s.base);
}

int main()
{
    ElemType c;
    sqStack s1;
    sqStack s2;
    int len, i, j, sum = 0;

    InitStack(&s1); // 初始化栈s1,用来存放二进制输入

    printf("请输入二进制数,输入‘#’号表示结束!\n\n");
    scanf("%c", &c);
    while( c != '#' )
    {
        if( c=='0' || c=='1' )  // 检查输入是否二进制
            Push(&s1, c);
        scanf("%c", &c);
    }
    getchar();      // 把'\n'从缓冲区去掉
    len = StackLen(s1);

    InitStack(&s2); // 初始化栈s2,用来存放转换的八进制

    for( i=0; i < len; i+=3 )
    {
        for( j=0; j < 3; j++ )
        {
            Pop( &s1, &c ); // 取出栈顶元素
            sum = sum + (c-48) * pow(2, j);

            if( s1.base == s1.top )
            {
                break;
            }
        }

        Push( &s2, sum+48 );
        sum = 0;
    }

    printf("\n转化为八进制数是: ");
    while( s2.base != s2.top )
    {
        Pop( &s2, &c );
        printf("%c", c);
    }
    printf("(O)\n");

    return 0;
}

栈的链式存储结构

栈的链式存储结构简称为栈链,栈因为只是栈顶来做插入和删除操作,所以比较好的方法就是将栈顶放在单链表的头部,栈顶指针和单链表的头指针合二为一。

 

typedef struct StackNode
{
    ElemType data;            //存放栈的数据
    struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LinkStack
{
    LinkStackPtr top;        //top指针
    int count;               //栈元素计数器
}

进栈操作

对于栈链的Push操作,假设元素值为e的新结点是s,top为栈顶指针,我们得到以下代码:

Status Push(LinkStack *s,ElemType e)
{
    LinkStackPtr p = (LinkStackPtr) malloc (sizeof(StackNode));
    p->data = e;
    p->next = s->top;
    s->top = p;
    s->count++;
    return OK;
}

出栈操作

至于栈链的出栈Pop操作,假设变量p用来存储要删除的栈顶结点,将栈顶指针下移以为,最后释放p即可。

Status Pop(LinkStack *s, ElemType *e)
{
    LinkStackPtr p;
    if( StackEmpty(*s))    //判断是否为空栈
        retrun ERROR;
    *e = s->top->data;
    p = s->top;
    s->top = s->top->next;
    free(p);
    s->count--;
    return OK;
}

逆波兰表达式

正常表达式------->逆波兰表达式

a+b -----> a b +

a+(b-c) -----> a b c - +

a+(b-c)*d -----> a b c - d * +

a+d*(b-c) -----> a d b c - * +

对于(1-2)*(4+5)如果用逆波兰表达法,应该是这样:1 2 - 4 5 + *

  • 数字1和数字2进栈,遇到减号运算符则弹出两个元素进行运算并把结果入栈。

  • 4和5入栈,遇到加号运算符,4和5弹出栈,相加后将结果9入栈。

  •  现在又遇到了乘法运算符,将9和-1弹出栈并进行乘法运算,此时栈空并无数据压栈,-9为最终运算结果。
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT  10
#define MAXBUFFER       10

typedef double ElemType;
typedef struct
{
    ElemType *base;
    ElemType *top;
    int stackSize;
}sqStack;

InitStack(sqStack *s)
{
    s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
    if( !s->base )
        exit(0);

    s->top = s->base;
    s->stackSize = STACK_INIT_SIZE;
}

Push(sqStack *s, ElemType e)
{
    // 栈满,追加空间,鱼油必须懂!
    if( s->top - s->base >= s->stackSize )
    {
        s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
        if( !s->base )
            exit(0);

        s->top = s->base + s->stackSize;
        s->stackSize = s->stackSize + STACKINCREMENT;
    }

    *(s->top) = e;      // 存放数据
    s->top++;
}

Pop(sqStack *s, ElemType *e)
{
    if( s->top == s->base )
        return;

    *e = *--(s->top);   // 将栈顶元素弹出并修改栈顶指针
}

int StackLen(sqStack s)
{
    return (s.top - s.base);
}

int main()
{
    sqStack s;
    char c;
    double d, e;
    char str[MAXBUFFER];
    int i = 0;

    InitStack( &s );

    printf("请按逆波兰表达式输入待计算数据,数据与运算符之间用空格隔开,以#作为结束标志: \n");
    scanf("%c", &c);

    while( c != '#' )
    {
        while( isdigit(c) || c=='.' )  // 用于过滤数字
        {
            str[i++] = c;
            str[i] = '\0';
            if( i >= 10 )
            {
                printf("出错:输入的单个数据过大!\n");
                return -1;
            }
            scanf("%c", &c);
            if( c == ' ' )
            {
                d = atof(str);
                Push(&s, d);
                i = 0;
                break;
            }
        }

        switch( c )
        {
            case '+':
                Pop(&s, &e);
                Pop(&s, &d);
                Push(&s, d+e);
                break;
            case '-':
                Pop(&s, &e);
                Pop(&s, &d);
                Push(&s, d-e);
                break;
            case '*':
                Pop(&s, &e);
                Pop(&s, &d);
                Push(&s, d*e);
                break;
            case '/':
                Pop(&s, &e);
                Pop(&s, &d);
                if( e != 0 )
                {
                    Push(&s, d/e);
                }
                else
                {
                    printf("\n出错:除数为零!\n");
                    return -1;
                }
                break;
        }

        scanf("%c", &c);
    }

    Pop(&s, &d);
    printf("\n最终的计算结果为:%f\n", d);

    return 0;
}

// 5 - (6 + 7) * 8 + 9 / 4
// 5 - 13 * 8 + 9 / 4
// 5 - 104 + 2.25
// -99 + 2.25
// 5 6 7 + 8 * - 9 4 / +

中缀表达式转换为后缀表达式

如何转换呢?比如1+(2-3)*4+10/5

首先遇到的第一个输入是数字1,数字在后缀表示中都是直接输出,接着是符号“+”,入栈:

 第三个字符是“(”,依然是符号,入栈,接着是数字2,输出,然后是符号“-”,入栈:

 接下来是数字3,输出,紧接着的是“)”,此时,我们需要去匹配栈里的“(”,然后在匹配前将栈顶数据依次出栈(这就好比括号里优先执行的道理)

 紧接着是符号“+”,直接入栈:

 遇到数字4,输出,之后是符号“+”,此时栈顶元素是符号“*”,按照先乘除后加减原理,此时栈顶的*优先级比即将入展的+要大,所以出栈。

紧接着是数字10,输出,最后是符号“/”,进栈:

 最后一个数字5,输出,所以的输入处理完毕,但是栈中仍然有数据,所以讲栈中符号依次出栈。

总结规则:从左到右遍历中缀表达式的每个数字和符号,若是数组就直接输出,若是符号,则判断其与栈顶符号的优先级,是右括号或者优先级低于栈顶符号,则栈顶符号依次出栈并输出,直到遇到左括号或者栈空才将那个吃屎符号入栈。

 队列

什么是队列呢???

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

与栈相反,队列是一种先进先出(First In Fist Out ,FIFO)的线性表

 队列既可以用链表实现,也可以用顺序表实现。与栈相反的是,栈一般我们用顺序表来实现,而队列我们常用链表来实现,简称链队列。

typedef struct QNode{
    ElemType data;
    struct QNOde *next;
}Qnode,*QueuePtr;
typedef struct{
    QueuePtr front,rear;    //对头,尾指针
}LinkQueue;

我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。(注:头结点不是必要的,但为了方便操作,我们加上了)

 空队列时,front和rear都指向头结点。

创建一个队列

创建一个队列要完成连个任务:一是在内存中创建一个头结点,二是将队列的头指针和尾指针都指向这个生成的头结点,因为此时是空队列。

initQueue(LinkQueue *q)
{
    q->front = q->rear = (QueuePtr)malloc(sizeof(QNode));
    if( !q->front )
        exit(0);
    q->front->next = NULL;
}

入队列的操作过程

InsertQueue(LinkQueue *q, ElemType e)
{
    QueuePtr p;
    p = (QueuePtr)malloc(sizeof(QNode));
    if( p == NULL )
        exit(0);
    p->data = e;
    p->next = NULL;
    q->rear->next = p;
    q->rear = p;
}

出队列操作

出队列操作是将队列中的第一个元素移出,对头指针不发生改变,改变头结点的指针即可。

 如果原队列中有一个元素,那么我们就应该处理一下队尾指针。

DeleteQueue(LinkQueue *q,ElemType *e)
{
    QueuePtr p;
    if( q->front == q->rear )
        return;
    p = q->front->next;
    *e = p->data;
    q->front->next = p->next;
    if( q->rear = p )
        q->rear = q->front;
    free(p);
}

销毁队列

由于链队列建立在内存的动态区,因此,当一个队列不再有用时,应当把它及时销毁掉,意面过多地占用内存空间。

DestroyQueue(LinkQueue *q)
{
    while( q->front ){
        q->rear = q->front->next;
        free( q->front );
        q->front = q->rear;
    }
}

队列的顺序存储结构

 入队列操作其实就是在队尾追加一个元素,不需要任何移动,时间复杂度为O(1)。出队列则不同,因为我们已经假设下标为0的位置是队列的对头,因此每次出队列操作所有元素都要向前移动,时间复杂度为O(n)

循环队列

定义一个循环队列

#define MAXSIZE 100
typedef struct 
{
    ElemType *base;    //用于存放内存分配基地址
                       //这里你也可以用数组存放
    int front;
    int rear;
}

初始化一个循环队列

initQueue(cycleQueue *q)
{
    q->base = (ElemType *)malloc(MAXSIZE *sizeof(ElemType));
    if( !q->base )
        exit(0)
    q->front = q->next = 0;
}

入队列操作

InsertQueue(cycleQueue *q,ElemType e)
{
    if((q->rear+1)%MAXSIZE == q->front )
        return;    //队列已满
    q->base[q->rear] = e;
    q->rear = (q->rear+1)%MAXSIZE;
}

出队列操作

DeleteQueue(cycleQueue *q,ElemType e)
{
    if( q->front == q->rear )
        return;    //队列为空
    *e = q->base[q->rear];
    q->rear = (q->rear+1)%MAXSIZE;
}

以上内容为FishC《数据结构与算法》栈和队列的全部内容整理。

猜你喜欢

转载自blog.csdn.net/weixin_38371360/article/details/86299588