数据结构——栈

数据结构—栈

栈的特点

  • 先进后出 first in last out (FILO) 结构
  • 只能在栈定进行插入和删除的操作

栈的一些概念

  • 栈顶与栈底:允许元素插入与删除的一端称为栈顶,另一端称为栈底
  • 压栈:栈的插入操作,叫做进栈,也称压栈、入栈
  • 弹栈:栈的删除操作,也叫做出栈

栈的操作

  • 弹栈,通常命名为pop

  • 压栈,通常命名为push

  • 求栈的大小

  • 判断栈是否为空

  • 获取栈顶元素的值

    c++ STL中定义的一些操作 #include<stack>
    s.empty();         //如果栈为空则返回true, 否则返回false;
    s.size();          //返回栈中元素的个数
    s.top();           //返回栈顶元素, 但不删除该元素
    s.pop();           //弹出栈顶元素, 但不返回其值
    s.push();          //将元素压入栈顶
    

栈的分类

  • 基于数组的栈:以数组为底层数据结构时,通常以数组头为栈底,数组头到数组尾为栈顶的生长方向;

在这里插入图片描述

  • 支持动态扩容的顺序栈

    刚才那个基于数组实现的栈,是一个固定大小的栈,也就是说,在初始化栈时需要事先指定栈的大小。当栈满之后,就无法再往栈里添加数据了。尽管链式栈的大小不受限,但要存储 next 指针,内存消耗相对较多。那我们如何基于数组实现一个可以支持动态扩容的栈呢?

    如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。如:

    img

  • 基于单链表的栈:以链表为底层的数据结构时,以链表头为栈顶,便于节点的插入与删除,压栈产生的新节点将一直出现在链表的头部。

在这里插入图片描述

栈的实现

  • 基于数组的栈

    #include <stack>
    #include <iostream>
    using namespace std;
     
    int main()
    {
    	stack<int> mystack;
    	int sum = 0;
    	for (int i = 0; i <= 10; i++){
    		mystack.push(i);
    	}
    	cout << "size is " << mystack.size() << endl;
    	while (!mystack.empty()){
    		cout << " " << mystack.top();
    		mystack.pop();
    	}
    	cout << endl;
    	system("pause");
    	return 0;
    }
    
  • 基于链表的栈

    #include <iostream>
    using namespace std;
    template<class T>class Stack
    {
    private:
    	struct Node
    	{
    		T data;
    		Node *next;
    	};
    	Node *head;
    	Node *p;
    	int length;
     
    public:
    	Stack()
    	{
    		head = NULL;
    		length = 0;
    	}
    	void push(T n)//入栈
    	{
    		Node *q = new Node;
    		q->data = n;
    		if (head == NULL)
    		{
    			q->next = head;
    			head = q;
    			p = q;
    		}
    		else
    		{
    			q->next = p;
    			p = q;
    		}
    		length++;
    	}
     
    	T pop()//出栈并且将出栈的元素返回
    	{
    		if (length <= 0)
    		{
    			abort();
    		}
    		Node *q;
    		int data;
    		q = p;
    		data = p->data;
    		p = p->next;
    		delete(q);
    		length--;
    		return data;
    	}
    	int size()//返回元素个数
    	{
    		return length;
    	}
    	T top()//返回栈顶元素
    	{
    		return p->data;
    	}
    	bool isEmpty()//判断栈是不是空的
    	{
    		if (length == 0)
    		{
    			return true;
    		}
    		else
    		{
    			return false;
    		}
    	}
    	void clear()//清空栈中的所有元素
    	{
    		while (length > 0)
    		{
    			pop();
    		}
    	}
    };
    int main()
    {
    	Stack<char> s;
    	s.push('a');
    	s.push('b');
    	s.push('c');
    	while (!s.isEmpty())
    	{
    		cout << s.pop() << endl;
    	}
    	system("pause");
    	return 0;
    }
    

应用

  • 函数调用栈

    我们知道,操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。

    int main() {
       int a = 1; 
       int ret = 0;
       int res = 0;
       ret = add(3, 5);
       res = a + ret;
       printf("%d", res);
       reuturn 0;
    }
    
    int add(int x, int y) {
       int sum = 0;
       sum = x + y;
       return sum;
    }
    

    从代码中我们可以看出,main() 函数调用了 add() 函数,获取计算结果,并且与临时变量 a 相加,最后打印 res 的值。为了让你清晰地看到这个过程对应的函数栈里出栈、入栈的操作,我画了一张图。图中显示的是,在执行到 add() 函数时,函数调用栈的情况。

    img

  • 栈在表达式中的应用

    将算术表达式简化为只包含加减乘除四则运算,比如:34+13*9+44-12/3。

    实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈

    我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。

    如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。这里将 3+5*8-6 这个表达式的计算过程画成了一张图,你可以结合图来理解我刚讲的计算过程。

    img

  • 栈在括号匹配中的应用

    我们假设表达式中只包含三种括号,圆括号 ()、方括号[]和花括号{},并且它们可以任意嵌套。比如,{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?

    这里也可以用栈来解决。我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

如何实现浏览器中的前进后退功能?

用两个栈即可解决此问题。

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。

当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。

当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据就是这个样子:

img

当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:

img

这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:

img

这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:

img

为什么函数调用需要用“栈”来存储临时变量?

其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。

从调用函数进入被调用函数,对于数据来说,变化的是什么呢?是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。

JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?

JVM里面的栈和我们这里说的是一回事,被称为方法栈。和前面函数调用的作用是一致的,用来存储方法中的局部变量。

https://blog.csdn.net/qq_32258777/article/details/81353638这里有更加详细的解释。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值