趣说数据结构(二)栈

145 阅读11分钟

关于本系列

​ 如果你能够在阅读本文之后对该知识有一个初步的了解,知道基本的概念,那就足够了。

​ 这就是本系列的创作初衷。

​ 希望能帮助到正在学习这个知识点的你。

​ 同时,为了不让编程语言成为阅读的障碍,本系列的文章都会给出C,Java,Python的代码(Python的代码在有空之后会补上)。但是本人是菜鸡,基本上是一边查语法一边进行写作,肯定会有诸多不足,请大家多多担待。

​ 欢迎大家的任何意见,由衷感激。

前言

​ 这天,小明带领他的好朋友小A,小B,小C来到了森林之中探险。在探险的过程中,他们发现了一个仅能通过一个人的山洞,大家都无一例外的 想到了《桃花源记》 想进去探险,小明自告奋勇决定第一个进去,小A,小B,小C则紧随其后。

01.png

​ 他们进去之后发现,这个山洞和他们想象的完全不一样,根本没有 小桥流水人家 通道,进去之后发现这个山洞是走不通的,他们感到很气馁,但也没办法,走不下去了,只能出去。

​ 小明作为领头大哥,肯定要第一个出去,所以小明就让小A让一下,让他先出去。

02.png

​ 小A想侧身让大哥先出去,但是他也没办法动,因为位置太小了,他也没有办法做侧身的动作,他只能让小B赶紧先出去。

03.png

​ 小B也很无奈呀,他也动不了,于是只能喊小C快点出去。

04.png

​ 小C听到催促,发现确实是不太好侧身,于是他直接自己就走出去了,然后喊他们快点出来。

05.png

​ 小B看到小C出去了,寻思着他也没办法转身,所以也直接出去了,然后喊小A快出来,不要挡到大哥小明。

06.png

​ 小A看现在这个情况,好像也只能先出去,不然小明也没办法动,所以他也出去了。

07.png

​ 小明一看他们都出去,山洞里面终于没有人阻挡他,于是他终于能出去了。

08.png

​ 小明成功从山洞出来之后说道,这破山洞,以后都不会再来了。

​ 可是真的会这样吗?小明之后可能会经常遇到类似这个山洞的事情,也许会想起这一次的探险吧。

​ 像前言故事中所述,那一个阻挡小明探索步伐的山洞,其实就是一个栈。

​ 在前文中提到了线性表。而栈,其实是一种受到一定操作约束的线性表,因为,它只允许在一端进行插入删除操作,如同故事中的山洞,小明一伙人只能从山洞口进去,从山洞口出来。

​ 像这种只能从山洞口进去,从山洞口出来的操作,也就是我们所说的栈的压入弹出。也就是压入(插入)数据,弹出(删除)数据

​ 经过上面的故事来看,小明是第一个进去的,但是却是最后一个出来的;同样,小C是最后一个进去的,但是是第一个出来的。所以我们发现,栈的特点就是LIFO(Last in, First out)——先入后出,后入先出。

​ 书接上文,我们知道 (不知道也无所谓,后续可能会出一个线性表的专题,大概?) ,线性表是可以有两种存储结构的:

  • ​ 顺序存储结构,是将数据依次存储在连续的整块内存空间之中,比较直观浅显的说就是用数组来存储
  • ​ 链式存储结构,是将数据分散存储在内存空间之中,通过''指针''来建立链接的联系,浅显的说就如同上一篇文章中的链表,通过节点来将数据互相连接起来。

​ 在实现栈之前,补充一个调用栈的概念,方便大家理解为什么本文选用顺序存储结构来实现栈,当然,用链式存储结构来做也是没有问题的,各有利弊,只是我的选择带有比较强的主观因素


调用栈

​ 我们先简单了解一下什么是调用栈。

​ 假设我们的main()方法(也可以叫函数,之后都用方法代替)中调用了a()方法,a()方法中调用了b()方法,然后b()方法中调用了a()方法,结果会发生什么呢?

​ 首先执行main()方法,该方法入栈。

09.png ​ main()方法先执行,调用a()方法,a()方法入栈。

10.png ​ a()方法又调用b()方法,b()方法接着入栈。

11.png

​ b()方法又继续调用a()方法..形成循环调用,如果一直循环下去,那么会怎么样呢?

12.png

​ 当然是boom! Stack Overflow! (栈溢出)

​ 因为我们的每个方法的调用都会需要内存,如果我们不对调用栈进行限制,像我们上面的操作,不断循环调用a(),b(),a(),b(),那么会导致内存不断被侵占,这种情况是很恐怖的。因此我们必须要对涉及栈的操作进行限制,设置一定的上限,超出设定的上限就爆栈,保护内存安全。

​ 在此考虑下,我选择用顺序存储的结构来简单实现一个栈。


实现一个栈

​ 本着从简,易理解的目的,所以关于一些泛型和扩容之类的操作会进行省略,只实现最基础的功能 压入(push)弹出(pop)

C

//设置栈上限 
#define MAXSIZE 64

//顺序存储结构实现 
typedef struct
{
	int data[MAXSIZE];
	int top;	//用于指向栈顶的指针 
}Stack;

//建立一个空栈
Stack* createStack(){
	//分配地址 
	Stack* stack = (Stack*)malloc(sizeof(Stack));
	//初始化栈顶指针 
	stack->top = 0;	//top=0表示空栈
	return stack; 
}

Java

//顺序存储结构实现
public class Stack {
    
    //设置栈上限
    private final int MAXSIZE = 64;
    
    private int[] data;
    
    //栈顶指针
    private int top;
    
    //无参构造,初始化
    public Stack(){
        data = new int[MAXSIZE];
        top = 0;
    }
    
}

​ 以Java的代码来分析吧,

  • MAXSIZE: 设置了栈的上限。

  • data: 具体存放数据的数组,用作模拟栈。

  • top: 用来指向栈顶,同时作为下标来使用,进行存放数据。

​ 初始化之后,我们就得到了一个空栈,该栈的一共有64层,也可以说栈的深度是64。

13.png

​ 此时,我们的top指向下标0,代表下标0是没有元素的,也就是当前为空栈。


压入(psuh)

​ 压入操作也可以叫做插入元素。

C

//压入 :添加元素到栈顶
void push(Stack* stack,int parameter){
	//先检查是否超过栈的上限
    if( stack->top==MAXSIZE?1:0 ){
        //在控制台打印爆栈消息并且结束方法
        printf("Stack Overflow!\n");
        return ;
    }
    //先设置当前数组下标位置的值为parameter,然后指向下一个位置
	stack->data[stack->top++] = parameter;
}

Java

    //检查是否超过栈的上限
    private boolean isOverFlow(){
        //如果top==MAXSIZE,则代表栈满了
        return top==MAXSIZE?true:false;
    }

    //压入 :添加元素到栈顶
    public void push(int parameter){
        //先检查是否超过栈的上限 下限:Underflow
        if( isOverFlow() ){
            //在控制台打印爆栈消息并且结束方法
            System.out.println("Stack Overflow!");
            return ;
        }
        //先设置当前数组下标位置的值为parameter,然后指向下一个位置
        data[top++] = parameter;
    }

​ 执行插入操作的时候必须检查索引是否合法,也就是先检查当前的栈也没有满,在没有满的情况下才能执行插入操作。

data[top++] = parameter 的意思是,先将当前位置放入元素,然后top指针指向上一层,这里我们假设放入一个5。

14.pngtop的作用在兼具下标的同时指向当前栈的栈顶,上面我们加入了一个元素,那么我们当前的栈顶就是下标为1的位置,但是我们栈里面还是只有一个1元素,也就是top指针永远指向为空的上一层,保证栈的压入和弹出操作。

​ 当top指向最后一个下标63的上面,就代表当前的栈满了,不能再进行操作了。

15.png


弹出(pop)

​ 弹出也叫做删除操作,对栈来说,就是删除栈顶元素。

C

//弹出 :删除栈顶元素
void pop(Stack* stack){
	//检查是否超过栈的下限 
	if( stack->top==0?1:0 ){
        //在控制台打印爆栈消息并且结束方法
        printf("Stack Overflow!\n");
        return ;
    }
    //因为添加元素之后指针会指向下一个元素,那么我们实际存储的元素是指针前一个
    //也就是我们需要先将指针指回上一个元素,令该元素为0,代表当前位置是没有元素了的
    stack->data[--stack->top] = 0;
}

Java

    //检查是否越界
    private boolean isUnderflow(){
        //如果top为0,则代表当前栈没有元素
        return top==0?true:false;
    }
    //弹出 :删除栈顶元素
    public void pop(){
        if( isUnderflow() ){
            //在控制台打印爆栈消息并且结束方法
            System.out.println("Stack Overflow!");
            return ;
        }
        //因为添加元素之后指针会指向下一个元素,那么我们实际存储的元素是指针前一个
        //也就是我们需要先将指针指回上一个元素,令该元素为0,代表当前位置是没有元素了的
        data[--top] = 0;
    }

​ 执行删除的操作的时候同样要注意检查索引,也就是需要判断top>0,因为当top=0的时候,此时的栈是空栈,里面是没有元素的。

​ 然后就是区别与压入的操作,压入是先将元素放到top的位置,然后top++;

​ 弹出的话需要先top--,再将对应位置的元素删除,令当前位置为空,这样就完成了弹出,同时top保持指向栈顶。

​ 先让top自减,指向当前的栈顶元素。

16.png ​ 然后删除了该元素,就成功弹出了一个元素,并且top指针还是保持指向栈顶。

17.png

​ 然后再模拟弹出至空栈的情况。

​ 1. 当前栈内的情况:

18.png

​ 2. top自减,此时top为0:

19.png

​ 3. 删除掉当前下标为0处的元素,这样栈就为空了:

20.png


完整代码

​ 如果有写错的地方,欢迎大家指出,由衷感激。

C

#include <stdio.h> 

//设置栈上限 
#define MAXSIZE 64

//顺序存储结构实现 
typedef struct
{
	int data[MAXSIZE];
	int top;	//用于栈顶指针 
}Stack;

//建立一个空栈
Stack* createStack(){
	//分配地址 
	Stack* stack = (Stack*)malloc(sizeof(Stack));
	//初始化栈顶指针 
	stack->top = 0;	//表示空栈
	return stack; 
}

//压入 :添加元素到栈顶
void push(Stack* stack,int parameter){
	//先检查是否超过栈的上限
    if( stack->top==MAXSIZE?1:0 ){
        //在控制台打印爆栈消息并且结束方法
        printf("Stack Overflow!\n");
        return ;
    }
    //先设置当前数组下标位置的值为parameter,然后指向下一个位置
	stack->data[stack->top++] = parameter;
}

//弹出 :删除栈顶元素
void pop(Stack* stack){
	//检查是否超过栈的下限 
	if( stack->top==0?1:0 ){
        //在控制台打印爆栈消息并且结束方法
        printf("Stack Overflow!\n");
        return ;
    }
    //因为添加元素之后指针会指向下一个元素,那么我们实际存储的元素是指针前一个
    //也就是我们需要先将指针指回上一个元素,令该元素为0,代表当前位置是没有元素了的
    stack->data[--stack->top] = 0;
}

//检查当前栈顶元素
int peek(Stack* stack){
	int top = stack->top;
	//先判断索引是否合法
    if( top <= MAXSIZE && top > 0){
        return stack->data[top-1];
    }
    return -1;
}

//写了比较简陋的测试方法 
int main(){
	Stack* stack = createStack();
	int i;
	for(i = 0;i<70;i++){
		push(stack,i);
	}
	printf("当前栈顶元素为:%d\n",peek(stack));
	for(i = 0;i<62;i++){
		pop(stack);
	}
	printf("删除之后栈顶元素为:%d\n",peek(stack));
	pop(stack);
	printf("删除之后栈顶元素为:%d\n",peek(stack));
	pop(stack);
	printf("删除之后栈顶元素为:%d\n",peek(stack));
	return 0;
}

Java

//顺序存储结构实现
public class Stack {
    //设置栈上限
    private final int MAXSIZE = 64;
    private int[] data;
    //栈顶指针
    private int top;
    //无参构造,初始化
    public Stack(){
        data = new int[MAXSIZE];
        top = 0;
    }

    //检查是否超过栈的上限
    private boolean isOverFlow(){
        //如果top==MAXSIZE,则代表栈满了
        return top==MAXSIZE?true:false;
    }

    //压入 :添加元素到栈顶
    public void push(int parameter){
        //先检查是否超过栈的上限 下限:Underflow
        if( isOverFlow() ){
            //在控制台打印爆栈消息并且结束方法
            System.out.println("Stack Overflow!");
            return ;
        }
        //先设置当前数组下标位置的值为parameter,然后指向下一个位置
        data[top++] = parameter;
    }

    //检查是否越界
    private boolean isUnderflow(){
        //如果top为0,则代表当前栈没有元素
        return top==0?true:false;
    }
    //弹出 :删除栈顶元素
    public void pop(){
        if( isUnderflow() ){
            //在控制台打印爆栈消息并且结束方法
            System.out.println("Stack Overflow!");
            return ;
        }
        //因为添加元素之后指针会指向下一个元素,那么我们实际存储的元素是指针前一个
        //也就是我们需要先将指针指回上一个元素,令该元素为0,代表当前位置是没有元素了的
        data[--top] = 0;
    }

    //检查当前栈顶元素
    public int peek(){
        //先判断索引是否合法
        if( top <= MAXSIZE && top > 0){
            return data[top-1];
        }
        return -1;
    }
}