线性表基础:栈(二)经典面试题(栈的基本操作)

经典⾯试题 - 栈的基本操作

推荐刷题顺序:
LeetCode #⾯试题03.04 化栈为队
LeetCode #682 棒球⽐赛
LeetCode #844 ⽐较含退格的字符串
LeetCode #946 验证栈序列

1. LeetCode #⾯试题03.04 化栈为队

题目描述:
实现一个MyQueue类,该类用两个栈来实现一个队列。
在这里插入图片描述

解题思路:
利⽤两个栈来实现,⼀个输⼊栈、⼀个输出栈。

  • 输⼊栈⽤于读⼊数据。当需要输出元素时,若输出栈为空,则将输⼊栈的所有元素推送到输出栈,然后取栈顶元素;若输出栈⾮空,则直接输出栈顶即可。
  • 输出栈的作⽤是对已经被反转的序列进⾏⼆次反转。

创建两个栈,Stack1为输入栈、Stack2为输出栈
在这里插入图片描述

入队列直接就往Stack1入栈即可,假设依次入队1,2,3:
在这里插入图片描述

当碰到出队列动作时,需要先判断Stack2是否为空,若Stack2为空,则将Stack1此时所有元素依次出栈,再依次入栈到Stack2,由于栈的特性,先进后出,后进先出,所以Stack1最先出栈的元素就是最后入队列的元素:
在这里插入图片描述

当Stack1全部元素都移到Stack2后,出队列元素就是出栈Stack2栈顶元素,如下出队列一个元素为1,满足队列先进先出性质:
在这里插入图片描述

假设再入队元素4,5,直接入栈Stack1即可:
在这里插入图片描述

当再出队时,再次判断Stack2是否为空,若不为空,直接出栈Stack2的元素,出队列元素2:
在这里插入图片描述

注意每次出队的时候,只有当Stack2为空的时候,再将Stack1里的元素移到Stack2中,否则会造成乱序

// 化栈为队
class MyQueue {
    private LinkedList<Integer> inStack = new LinkedList<>();
    private LinkedList<Integer> outStack = new LinkedList<>();

    /** Initialize your data structure here. */
    public MyQueue() {}

    /** Push element x to the back of queue. */
    public void push(int x) {
        inStack.push(x);
    }

    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        transfer();
        return outStack.pop();
    }

    private void transfer() {
        if (outStack.isEmpty()) {
            // 只有当输出栈为空的时候,再将输入栈的元素全部转移到输出栈
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
    }

    /** Get the front element. */
    public int peek() {
        transfer();
        return outStack.peek();
    }

    /** Returns whether the queue is empty. */
    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

2. LeetCode #682 棒球⽐赛

题目描述:
你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。

比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:

  • 整数 x - 表示本回合新获得分数 x
  • “+” - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
  • “D” - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
  • “C” - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。

请你返回记录中所有得分的总和。
在这里插入图片描述

解题思路:
比较简单,用栈实现即可。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

遍历完成后,最终求栈中所有元素和即可,15+10+5=30

class Solution {
    public int calPoints(String[] ops) {
        LinkedList<Integer> stack = new LinkedList<>();
        for (String op : ops) {
            switch (op) {
                case "+":
                    // 表示本回合新获得的得分是前两次得分的总和
                    Integer pre1 = stack.pop();
                    Integer pre2 = stack.peek();
                    stack.push(pre1);
                    stack.push(pre1 + pre2);
                    break;
                case "D":
                    // 表示本回合新获得的得分是前一次得分的两倍
                    stack.push(stack.peek() * 2);
                    break;
                case "C":
                    // 表示前一次得分无效,将其从记录中移除
                    stack.pop();
                    break;
                default:
                    stack.push(Integer.valueOf(op));
                    break;
            }
        }
        int result = 0;
        while (!stack.isEmpty()) {
            result += stack.pop();
        }
        return result;
    }
}

3. LeetCode #844 ⽐较含退格的字符串

题目描述:
给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。
在这里插入图片描述

进阶:

  • 你可以用 O(N) 的时间复杂度和 O(1) 的空间复杂度解决该问题吗?

解题思路:
思路一:直接将给定的字符串中的退格符和应当被删除的字符都去除,还原给定字符串的一般形式。然后直接比较两字符串是否相等即可。

具体地,我们用栈处理遍历过程,每次我们遍历到一个字符:

  • 如果它是退格符,那么我们将栈顶弹出;
  • 如果它是普通字符,那么我们将其压入栈中。
class Solution {
    public boolean backspaceCompare(String S, String T) {
        LinkedList<Character> s = new LinkedList<>();
        LinkedList<Character> t = new LinkedList<>();
        handle(S, s);
        handle(T, t);
        if (s.size() != t.size()) return false;

        while (!s.isEmpty()) {
            if (s.pop() != t.pop()) {
                return false;
            }
        }

        return true;
    }

    private void handle(String str, LinkedList<Character> stack) {
        for (char c : str.toCharArray()) {
            if ('#' == c && !stack.isEmpty()) {
                stack.pop();
            } else if ('#' != c) {
                stack.push(c);
            }
        }
    }
}

可以不用栈,直接利用StringBuilder实现:

class Solution {
    public boolean backspaceCompare(String S, String T) {
        return handle(S).equals(handle(T));
    }

    private String handle(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (c == '#' && sb.length() > 0) {
                sb.deleteCharAt(sb.length() - 1);
            } else if (c != '#') {
                sb.append(c);
            }
        }
        return sb.toString();
    }
}

思路二:双指针法,
一个字符是否会被删掉,只取决于该字符后面的退格符,而与该字符前面的退格符无关。因此当我们逆序地遍历字符串,就可以立即确定当前字符是否会被删掉。

具体地,我们定义 skip 表示当前待删除的字符的数量。每次我们遍历到一个字符:

  • 若该字符为退格符,则我们需要多删除一个普通字符,我们让 skip 加 1;
  • 若该字符为普通字符:
    • 若 skip 为 0,则说明当前字符不需要删去(需要进行比较);
    • 若 skip 不为 0,则说明当前字符需要删去(不需要比较),我们让 skip 减 1。

这样,我们定义两个指针,分别指向两字符串的末尾。每次我们让两指针逆序地遍历两字符串,直到两字符串能够各自确定一个字符,然后将这两个字符进行比较。重复这一过程直到找到的两个字符不相等,或遍历完字符串为止。

假设两个字符串S和T,分别逆序寻找“确定字符”进行比较:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以此类推,S下一个确定字符是’a’,T下一个确定字符是’a’,还是相等:
在这里插入图片描述

继续遍历,最终两个指针都指向-1,说明退格字符串相等。
在这里插入图片描述

class Solution {
    public boolean backspaceCompare(String S, String T) {
        // skip为允许跳过的字符数量
        int skipS = 0;
        int skipT = 0;
        // i 是S的指针,j 是T的指针,只要有任何一个字符串遍历完就一直循环
        for (int i = S.length() - 1, j = T.length() - 1; i >= 0 || j >= 0;) {
            // 逆序找S中的确定字符
            for (; i >= 0; i--) {
                if (S.charAt(i) == '#') {
                    skipS++;
                } else {
                    if (skipS > 0) {
                        skipS--;
                    } else {
                        // skipS = 0,说明此时i指向的是确定字符
                        break;
                    }
                }
            }
            // 逆序找T中的确定字符
            for (; j >= 0; j--) {
                if (T.charAt(j) == '#') {
                    skipT++;
                } else {
                    if (skipT > 0) {
                        skipT--;
                    } else {
                        // skipT = 0,说明此时j指向的是确定字符
                        break;
                    }
                }
            }
            if (i >= 0 && j >= 0) {// 说明两个字符串都找到了确定字符,进行比较
                if (S.charAt(i) != T.charAt(j)) {
                    return false;
                }
            } else {
                // 如果有任何一个找到,一个没找到,肯定不相等
                if (i >= 0 || j >= 0) {
                    return false;
                }
            }
            i--;
            j--;
        }
        return true;
    }
}

4. LeetCode #946 验证栈序列

题目描述:
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
在这里插入图片描述

解题思路:
由题意可知,pushed代表的是入栈的序列,popped代表的是出栈的序列,观察出栈序列popped思考:

  • 第一个出栈元素,该元素在入栈序列中所在位置前面的元素肯定已经入栈了(出栈序列popped[0]=4,说明入栈序列pushed[0]=1,pushed[1]=2,pushed[2]=3已经入栈了)
  • 第二个出栈元素,要么是已经入栈的栈顶元素,要么是未来入栈的元素(出栈4以后,栈顶元素为3,而出栈序列第二个元素popped[1]=5,其只可能是未来入栈的元素)

因此,我们可以创建一个栈,重点关注出栈的序列,模拟入栈和出栈:

  • 如果当前栈顶元素不是出栈序列指定要出栈的元素(或者栈为空),则根据入栈序列入栈元素,直到和要出栈的元素相等或者入栈序列全部都入栈了
  • 如果当前栈顶元素是出栈序列指定要出栈的元素,弹栈即可,出栈序列指针向后走一步,继续遍历处理下一个要弹栈的元素

当我们创建的栈能够按照popped序列指定元素顺序依次弹栈走完popped,说明栈序列是合法的,否则如果当遍历过程中,入栈序列pushed已经全部入栈了也不能弹栈,说明是非法的。

核心思想就是根据出栈顺序将入栈的栈里的值清空。

图解:
首先,看到出栈序列第一个出栈的元素是4,而我们创建的栈中是空的,所以我们先根据入栈序列入栈一个元素:
在这里插入图片描述

入栈之后入栈序列的指针向后移动一位,然后开始比较栈顶元素是否和出栈序列指针指向的元素相等,发现不等,重复入栈操作,直到相等:
在这里插入图片描述

当入栈序列第四个元素入栈的时候,栈顶元素和出栈序列指针指向的元素相等,此时允许弹栈:
在这里插入图片描述

弹栈以后,出栈序列指针向后移动一位,继续比较弹栈序列下一个元素:
在这里插入图片描述

此时发现栈顶元素3,与出栈序列第二个元素不相等,继续入栈操作:
在这里插入图片描述

将5入栈之后,栈顶和出栈序列指针指向的值相等,进行弹栈,之后从栈中依次弹出的值和出栈序列中的值顺序完全一致,图就不细画了,当poped出栈序列所有元素都处理完了,说明栈序列是合法的。
在这里插入图片描述

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        LinkedList<Integer> stack = new LinkedList<>();
        // 创建一个栈 模拟入栈出栈过程,按照出栈序列进行出栈,如果最终能清空该栈,说明合法
        // i代表出栈序列索引、j为入栈序列索引
        for (int i = 0, j = 0; i < popped.length; i++) {
            // 当前出栈元素是poped[i]
            // 如果我们创建的栈此时为空、或者栈顶元素不是出栈序列指定的出栈元素,则按照入栈序列一直入栈
            // 因为要出栈的元素,只能是当前栈顶元素,或者是未来要入栈的元素
            while ((stack.isEmpty() || stack.peek() != popped[i]) && j < pushed.length) {
                stack.push(pushed[j++]);
            }
            //走到这要么是当前栈顶元素 等于 要出栈的元素,要么入栈序列已经遍历完了
            if (stack.peek() != popped[i]) return false;//不等于说明入栈序列已经遍历完了,是非法的序列
            stack.pop();//相等说明此时栈顶元素就是要出栈的元素,弹栈
        }
        return true;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

犬豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值