数据结构与算法(五)——栈

本文详细解析了内存中的堆栈概念,区分了与数据结构中堆栈的不同,介绍了代码区、静态数据区和动态数据区的功能,重点阐述了栈区和堆区的特性,以及栈在函数调用、表达式求值、括号匹配和浏览器前进后退功能中的应用。

堆栈

首先要注意的是,内存中的堆栈和数据结构中的堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。

内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。

  1. 后进者先出,先进者后出,这就是典型的“栈”结构。
  2. 从栈的操作特性来看,是一种“操作受限”的线性表,只允许在端插入和删除数据。

举个例子,我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。这就是一种栈结构的形式。

为什么需要栈?

  1. 栈是一种操作受限的数据结构,其操作特性用数组和链表均可实现。
  2. 任何数据结构都是对特定应用场景的抽象,数组和链表虽然使用起来更加灵活,但却暴露了太多的操作接口,难免会引发错误操作的风险。
  3. 当某个数据集合只涉及在某端插入和删除数据,且满足后进者先出,先进者后出的操作特性时,应该首选栈这种数据结构。
  4. 从调用函数进入被调用函数,对于数据来说,变化的是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。

如何实现栈?

栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。

栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,叫作顺序栈,用链表实现的栈,叫作链式栈。

数组实现(自动扩容)

时间复杂度分析:根据均摊复杂度的定义,可以得数组实现(自动扩容)符合大多数情况是O(1)级别复杂度,个别情况是O(n)级别复杂度,比如自动扩容时,会进行完整数据的拷贝。
空间复杂度分析:在入栈和出栈的过程中,只需要一两个临时变量存储空间,所以O(1)级别。分析空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。

链表实现

时间复杂度分析:在入栈的时候,使用头插法把数据存到链表的头结点,出栈时返回头结点并删除即可。压栈和弹栈的时间复杂度均为O(1)级别,因为只需更改单个节点的索引即可。
空间复杂度分析:在入栈和出栈的过程中,只需要一两个临时变量存储空间,所以O(1)级别。

栈的应用

1.栈在函数调用中的应用

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将其中的临时变量作为栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

2.栈在表达式求值中的应用(比如:34+13*9+44-12/3)

利用两个栈,其中一个用来保存操作数,另一个用来保存运算符。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较,若比运算符栈顶元素优先级高,就将当前运算符压入栈,若比运算符栈顶元素的优先级低或者相同,从运算符栈中取出栈顶运算符,从操作数栈顶取出2个操作数,然后进行计算,把计算完的结果压入操作数栈,继续比较。

3.栈在括号匹配中的应用(比如:{}{()} )

用栈保存为匹配的左括号,从左到右一次扫描字符串,当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号,如果能匹配上,则继续扫描剩下的字符串。如果扫描过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明未匹配的左括号为非法格式。

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

使用两个栈X和Y,我们把首次浏览的页面依次压如栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据一次放入Y栈。当点击前进按钮时,我们依次从栈Y中取出数据,放入栈X中。当栈X中没有数据时,说明没有页面可以继续后退浏览了。当Y栈没有数据,那就说明没有页面可以点击前进浏览了。

堆栈相关算法题

有效的括号

这是leetcode上第20题:https://leetcode.com/problems/valid-parentheses/

题目描述:

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。

示例 1:

输入: “()”

输出: true

示例 2:

输入: “()[]{}”

输出: true

示例 3:

输入: “(]”

输出: false

方法一:直接替换

直接使用字符串的替换方法,将字符串里面的合法括号用空字符""代替,如果字符串是合法的,那么所有的括号都会被代替,字符串最后就会成为一个空字符串,否则就不为空。代码如下:

public boolean isValid1(String s) {
	if(s.length()==0)
            return true;
	int length;
	do {
	    length = s.length();
	    s=s.replace("()", "").replace("[]", "").replace("{}", "");
	}while(length != s.length());
	return s.length() == 0;	
}

但是这种方法效率不能得到保障,由于这三种括号出现的位置不确定,所以替换的操作执行多少次也不确定,最坏的情况是括号出现的位置与上面代码完全相反,比如([{}]),那么执行第一次的时候只能替换掉{},最多要执行n次才能把所有的括号都替换,而replace操作本身就是一个时间复杂度为O(n)的操作,所以最坏的时间复杂度为O(n2),效率有点低。

方法二:使用堆栈

先把字符串转成字符数组。由于栈具有先进后出的特点,所以可以把左括号先存入栈中,如果下一个是右括号则从栈弹出并与之比较,不匹配则返回false,匹配则判断下一个。当然,如果一开始就是右括号或者栈为空时进来的括号是右括号,就直接返回false。当整个字符数组已经选择完成时,如果是合法的,那么在前面判断时栈内的每个括号会弹栈,最终栈会为空。代码如下:

public boolean isValid(String s) {
	Stack<Character> stack = new Stack<>();
        char[] chars = s.toCharArray();
        if(s.length()==0)
            return true;
        if(chars[0] == ')'||chars[0]==']'||chars[0]=='}')
            return false;
        for(int i=1; i<chars.length;i++) {
            if (chars[i] == '(')
				stack.push(')');
            else if (chars[i] == '{')
				stack.push('}');
            else if (chars[i] == '[')
				stack.push(']');
            else if(stack.isEmpty()||stack.pop() != chars[i]){
        		return false;
            }
        }
	return stack.isEmpty();
 }

下一个更大元素 I

这是leetcode第496题:https://leetcode.com/problems/next-greater-element-i/

题目描述:

给定两个没有重复元素的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出-1。

示例 1:

输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:

  • 对于num1中的数字4,你无法在第二个数组中找到下一个更大的数字,因此输出 -1。
  • 对于num1中的数字1,第二个数组中数字1右边的下一个较大数字是 3。
  • 对于num1中的数字2,第二个数组中没有下一个更大的数字,因此输出 -1。

方法一

根据题目描述,最简单粗暴的就是循环比较两个数组,在对应位置开始比较,如果目标数组的下一个位置有比它大的数,则这个数就是下一个更大的数,没有则继续比较下一个位置,如果目标数组都到最后了还是没找到,则置为-1。代码如下:

public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        //定义一个结果数组用于输出
        int[] result = new int[nums1.length];
        Map<Integer, Integer> m = new HashMap<>();
    
        //存储目标数组元素和其位置的对应关系
        for(int i = 0;i < nums2.length;i++) {
            m.put(nums2[i], i);
        }

        for(int i = 0;i < nums1.length;i++) {
            //将结果数组初始化
            result[i] = -1;
            //在目标数组对应的位置开始招找,一直到最后,找不到不做处理
            for(int j = m.get(nums1[i]) + 1;j < nums2.length;j++) {
                if(nums2[j] > nums1[i]) {
                    result[i] = nums2[j];
                    break;
                }
            }
        }

        return result;
}

方法二:使用堆栈

使用堆栈和map建立目标数组中所有存在下一个更大元素的元素及其目标元素的关系,比如在上面的例子中建立的map关系就是(1,3)。先看代码:

public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        Map<Integer,Integer> map = new HashMap<Integer, Integer>();
        Stack<Integer> stack = new Stack<>();
        //遍历目标数组
        for(int num : nums2){
            while(!stack.isEmpty() && stack.peek() < num)
                //建立栈内较小的数与num的关系
                map.put(stack.pop(),num);
            //num进栈
            stack.push(num);
        }
        for(int i = 0; i<nums1.length;i++)
            nums1[i] = map.getOrDefault(nums1[i],-1);
        return nums1;
}

首先,栈是空的,所以把第一个数放进去,然后进行下一次循环即比较第二个数,如果栈顶的数小于num,说明比它大的下一个数就是num,建立它们的映射关系,然后把num放入栈中,如果不小于,就不建立并放入栈中,再进行下一次循环。

循环结束后,所有存在比它大的下一个数的元素都已经建立了映射关系,没有建立关系的说明不存在下一个数比它大,所以只需要将nums1中的元素作为map中的key,就可以得到映射关系中比它大的那个数,而如果value的值为空即不存在这个数,就返回默认的映射值-1。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值