算法学习之栈与队列

84 阅读12分钟

关于栈和队列,首先我们不但要理解栈和队列的使用,更为重要的是我们应该要理解栈和队列的源码和其底层实现。由于网站中没有具体讲java内部的栈与队列的底层实现,因此我们这里直接抄人家的结论吧,指不定用得上,现在我们先拾人牙慧,先记下来。首先栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。其他的知识,不是很敢确定是否适用于java,这里就不写了。

用栈实现队列

考察对栈和队列的功能的理解能力的最基础的也是经典的题目就是,用栈去实现队列或者用队列去实现栈,我们这里先来看看其前者

这里要求用两个栈来实现我们的队列,我们可以在草稿纸上用两个栈来模拟一个队列的行为。我们可以将栈分为进栈和出栈,对于我们的push方法,我们只是单纯的令其在进栈中执行入栈的方法,但是每次弹栈时,我们检查进栈中有没有元素,若有,则将其全部移动到出栈中,然后弹出出栈的元素,对于peek方法,我们也进行同样的处理,然后执行出栈的peek方法。最后的empty我们只要看我们的进栈和出栈是否为空就可以了。

请看代码:

class MyQueue {
    Stack<Integer> inStack;
    Stack<Integer> ouStack;
​
    public MyQueue() {
        inStack=new Stack<>();
        ouStack=new Stack<>();
    }
​
    public void push(int x) {
        inStack.add(x);
    }
​
    public int pop() {
        if(!ouStack.isEmpty()){
            return ouStack.pop();
        }
        while (!inStack.isEmpty()){
            ouStack.add(inStack.pop());
        }
        return ouStack.pop();
    }
​
    public int peek() {
        if(!ouStack.isEmpty()){
            return ouStack.peek();
        }
        while (!inStack.isEmpty()){
            ouStack.add(inStack.pop());
        }
        return ouStack.peek();
    }
​
    public boolean empty() {
        return inStack.isEmpty() && ouStack.isEmpty();
    }
}

这样的代码的确可以完成我们的需求,但是其有美中不足之处,那就是其没有实现代码复用,显得臃肿。实际上,如果我们总是在有相同功能的代码上随便复制下就改改就完了,这样不但效率低,也不便于我们的维护,因此我们要学会代码复用,将同样的代码抽象到一个方法中。一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)

我们这里容易观察到,我们的peek和pop方法所用的代码逻辑是十分相似的,因此我们可以将其抽象为一个方法,我们改造后的代码如下

class MyQueue {
​
    Stack<Integer> stackIn;
    Stack<Integer> stackOut;
​
    /** Initialize your data structure here. */
    public MyQueue() {
        stackIn = new Stack<>(); // 负责进栈
        stackOut = new Stack<>(); // 负责出栈
    }
​
    /** Push element x to the back of queue. */
    public void push(int x) {
        stackIn.push(x);
    }
​
    /** Removes the element from in front of queue and returns that element. */
    public int pop() {
        dumpstackIn();
        return stackOut.pop();
    }
​
    /** Get the front element. */
    public int peek() {
        dumpstackIn();
        return stackOut.peek();
    }
​
    /** Returns whether the queue is empty. */
    public boolean empty() {
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
​
    // 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
    private void dumpstackIn(){
        if (!stackOut.isEmpty()) return;
        while (!stackIn.isEmpty()){
            stackOut.push(stackIn.pop());
        }
    }
}

可能有的同学会觉得这样的思路并不容易想到,虽然说的确第一次做这样将栈范围出栈和进栈然后实现队列的思路可以不容易想到,但是起码我们可以学习到把两个栈分为进栈和出栈并令其交换元素的解题思路,以后可以便于我们解开类似的题目。接下来我们来做一个进阶题目

对于本题,我们自然会用到我们上一题学过的知识,将其分为两个队列并通过交换其中的元素来实现栈的功能,然而很可惜的是,由于队列的结构特性,我们的交换没有什么用,所以这种方法是没什么作用的。那我们应该怎么办呢?难道我们上一节学习的知识就这样毫无作用了?当然不是,我们仍然用到上一节的分析,我们可以同样将队列分为两份,但是这一次,我们的队列不再分进和出,我们只将一个队列用于备份,另一个作为我们的主队列。然后我们每次添加都往备份队列中添加元素,然后判断主队列是否为空,若为空则将主队列元素移动到备份栈中,然后让备份栈成为主栈,这样就可以用队列实现栈的功能了,对应的pop,top,empty方法直接调用主栈的对应方法就可以了。那么我们可以构造其代码如下

class MyStack {
​
    Deque<Integer> deque;
    Deque<Integer> backups_deque;
​
    public MyStack() {
        deque = new LinkedList<>();
        backups_deque=new LinkedList<>();
    }
​
    public void push(int x) {
        backups_deque.add(x);
        while (!deque.isEmpty()){
            backups_deque.add(deque.pop());
        }
        Deque<Integer> tempDeque;
        tempDeque = deque;
        deque = backups_deque;
        backups_deque = tempDeque;
    }
​
    public int pop() {
        return deque.pop();
    }
​
    public int top() {
        return deque.peek();
    }
​
    public boolean empty() {
        return deque.isEmpty();
    }
}

不过我们对本题的解法其实不止一种,我们还有一种方式是利用Deque是双端队列的特性,我们可以调用其中的对应的First和Last方法来完成我们的需求。同样我们将队列分为备份队列和主队列,但是这一次我们让我们的主队列存放我们的所有元素,而在pop方法中,我们总是让我们的主队列的除了最后一位元素之外的其他元素移动到我们的备份队列中,然后我们弹出最后一个元素,再让备份队列成为主队列,这样也可以实现栈的功能,而对于top方法,我们可以简单调用Deque中的peekLast方法来完成栈中所需要的功能。请看代码

class MyStack {
    // Deque 接口继承了 Queue 接口
    // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
    Deque<Integer> que1; // 和栈中保持一样元素的队列
    Deque<Integer> que2; // 辅助队列
    /** Initialize your data structure here. */
    public MyStack() {
        que1 = new ArrayDeque<>();
        que2 = new ArrayDeque<>();
    }
​
    /** Push element x onto stack. */
    public void push(int x) {
        que1.addLast(x);
    }
​
    /** Removes the element on top of the stack and returns that element. */
    public int pop() {
        int size = que1.size();
        size--;
        // 将 que1 导入 que2 ,但留下最后一个值
        while (size-- > 0) {
            que2.addLast(que1.peekFirst());
            que1.pollFirst();
        }
​
        int res = que1.pollFirst();
        // 将 que2 对象的引用赋给了 que1 ,此时 que1,que2 指向同一个队列
        que1 = que2;
        // 如果直接操作 que2,que1 也会受到影响,所以为 que2 分配一个新的空间
        que2 = new ArrayDeque<>();
        return res;
    }
​
    /** Get the top element. */
    public int top() {
        return que1.peekLast();
    }
​
    /** Returns whether the stack is empty. */
    public boolean empty() {
        return que1.isEmpty();
    }
}

这里我们有一点要提一下,就是在Deque中,peek()方法默认调用peekFirst(),add()方法默认调用addLast(),poll()方法默认调用pollFirst,pop()方法默认调用removeFirst()。最后我们其实还有一种实现方式,就是我们其实可以只用一个队列就可以模拟栈的功能,我们注意到我们第二个方法里的pop方法其实是可以让我们的队列的所有元素重新进入一遍的,然后在我们队列开头的就是我们所需要的最后一个元素的,这时我们只要弹出该元素就可以了,这里也是利用了Deque双端队列的特性实现的,请看代码

class MyStack {
    // Deque 接口继承了 Queue 接口
    // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
    Deque<Integer> que1;
    /** Initialize your data structure here. */
    public MyStack() {
        que1 = new ArrayDeque<>();
    }
​
    /** Push element x onto stack. */
    public void push(int x) {
        que1.addLast(x);
    }
​
    /** Removes the element on top of the stack and returns that element. */
    public int pop() {
        int size = que1.size();
        size--;
        // 将 que1 导入 que2 ,但留下最后一个值
        while (size-- > 0) {
            que1.addLast(que1.peekFirst());
            que1.pollFirst();
        }
​
        int res = que1.pollFirst();
        return res;
    }
​
    /** Get the top element. */
    public int top() {
        return que1.peekLast();
    }
​
    /** Returns whether the stack is empty. */
    public boolean empty() {
        return que1.isEmpty();
    }
}

但其实我们还有一个最偷懒的方式,就是我们的所谓的栈,其实可以理解成为是双端队列的一端不能移动,被堵住了,那我们只要调用双端队列的对应方法令其去模拟栈就可以了其实。其代码如下

class MyStack {
    // Deque 接口继承了 Queue 接口
    // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
    Deque<Integer> que1;
    /** Initialize your data structure here. */
    public MyStack() {
        que1 = new ArrayDeque<>();
    }
​
    /** Push element x onto stack. */
    public void push(int x) {
        que1.addLast(x);
    }
​
    /** Removes the element on top of the stack and returns that element. */
    public int pop() {
        return que1.pollLast();
    }
​
    /** Get the top element. */
    public int top() {
        return que1.peekLast();
    }
​
    /** Returns whether the stack is empty. */
    public boolean empty() {
        return que1.isEmpty();
    }
}

当然了,这只是玩着用用而已,实际解题肯定不能这样啊。所以说这题也为什么要规定只能用单向队列去实现,要不然这题属实是太简单了,没一点含金量。而我们这里之所以还介绍双端队列的方法,是因为我们这里也正好顺便让各位理解双端队列Deque中的方法

我们重点要记住两点1、相同功能的代码尽量去利用代码复用。2、要有用其他栈或者队列去模拟其他数据结构的思想,包括但不限于交换元素以及交换内存地址。

栈在算法题中的应用

那么学习了这么久理论知识了,现在我们正式来利用栈来去解题,先来看一道无比经典的题目

显然,这需要使用栈。这个解题思路就是遇见左括号就入栈,遇见右括号就弹栈,然后看左右括号是否对应,不对就返回false,如果中间栈为空,或者是遍历完了栈不为空,都返回false,其他情况返回true。比较简单,不过我们还是来看看思路分析

请看代码

class Solution {
    public boolean isValid(String s) {
        if(s.length()%2!=0){
            return false;
        }
        Stack<Character> stack = new Stack<>();
        Map<Character,Character> map = new HashMap<>();
        map.put('(',')');
        map.put('[',']');
        map.put('{','}');
        for (int i = 0; i < s.length(); i++) {
            if(map.containsKey(s.charAt(i))){
                stack.add(s.charAt(i));
            }else {
                if(stack.isEmpty()){
                    return false;
                }
                char c = stack.pop();
                if(map.get(c)!=s.charAt(i)){
                    return false;
                }
            }
        }
        if(!stack.isEmpty()){
            return false;
        }
        return true;
    }
}

这里虽然利用了Map集合来帮助我们的判断,但是由于用上了Map集合,因此效率各方面来说都不算高,我们还可以对其进行改造

class Solution {
    public boolean isValid(String s) {
        Deque<Character> deque = new LinkedList<>();
        char ch;
        for (int i = 0; i < s.length(); i++) {
            ch = s.charAt(i);
            //碰到左括号,就把相应的右括号入栈
            if (ch == '(') {
                deque.push(')');
            }else if (ch == '{') {
                deque.push('}');
            }else if (ch == '[') {
                deque.push(']');
            } else if (deque.isEmpty() || deque.peek() != ch) {
                return false;
            }else {//如果是右括号判断是否和栈顶元素匹配
                deque.pop();
            }
        }
        //最后判断栈中元素是否匹配
        return deque.isEmpty();
    }
}

我们原来的代码碰上遇上右括号就要通过弹出栈中对应的左括号然后寻找map中的右括号进行比较,但是在我们改造的代码里不需要,我们遇上左括号就让对应的右括号入栈,遇上右括号直接弹出进行比较,这样我们的效率就高的多了,而且代码上也比前一个代码简洁不少

有的可能会问,我们不是要用栈吗?为什么这里是一个队列?这其实是因为,双端队列的一边只要不调用,那么其就能实现栈的功能,这一点我们上一节其实也说过了

有的同学可能还会问,为什么我们要学习这些栈或者是队列等一系列的数据结构呢呢?难道是我们以后工程上经常用得到吗?其实不是的,我们之所以学习这些,是因为数据结构与算法的应用往往隐藏在我们看不到的地方!比如我们的编码纠正、Linux的目录管理等等,这些底层代码依赖的都是数据结构,因此我们要学习这些。

栈数据结构的进阶应用

对于这一题,一个简单的想法就是用两个栈,不断模拟两个字符串翻转的过程,直到两个字符串无法通过翻转删减任何字符为止。根据这个思路,我们可以构造代码如下

class Solution {
    public String removeDuplicates(String s) {
        Stack<Character> stack1 = new Stack<>();
        Stack<Character> stack2 = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            stack1.add(s.charAt(i));
        }
        while (true){
            boolean judge = true;
            judge = isJudge(stack2, stack1, judge);
            judge = isJudge(stack1, stack2, judge);
            if(judge){
                break;
            }
        }
        StringBuffer stringBuffer = new StringBuffer();
        while (!stack1.isEmpty()){
            stringBuffer.append(stack1.pop());
        }
        while (!stack2.isEmpty()){
            stringBuffer.append(stack2.pop());
        }
        return stringBuffer.reverse().toString();
    }

    private boolean isJudge(Stack<Character> stack1, Stack<Character> stack2, boolean judge) {
        while (!stack2.isEmpty()){
            char c2 = stack2.pop();
            if(stack1.isEmpty()){
                stack1.add(c2);
            }else {
                if(c2==stack1.peek()){
                    judge=false;
                    stack1.pop();
                }else {
                    stack1.add(c2);
                }
            }
        }
        return judge;
    }
}

虽然这份代码是可行的,也符合我们的思路,但是这份代码稍显臃肿了,在效率上也不是很理想,因此需要改进。实际上,我们这里采用的是两个队列不断遍历字符的方式,但实际上,我们可以只遍历一遍字符就完成我们的需求,我们只需要在每次遍历的时候判断栈是否为空以及栈顶的字符是否与字符串对应的字符是否相等就可以了。请看代码

class Solution {
    public String removeDuplicates(String S) {
        ArrayDeque<Character> deque = new ArrayDeque<>();
        char ch;
        for (int i = 0; i < S.length(); i++) {
            ch = S.charAt(i);
            if (deque.isEmpty() || deque.peek() != ch) {
                deque.push(ch);
            } else {
                deque.pop();
            }
        }
        String str = "";
        //剩余的元素即为不重复的元素
        while (!deque.isEmpty()) {
            str = deque.pop() + str;
        }
        return str;
    }
}

这份代码就很让人满意,不但效率不错,而且代码也不臃肿,看起来很清爽。但其实对这份代码我们还可以进一步优化,我们可以将字符串作为栈进行我们的拼接,这样就省去了栈中字符转换为字符串的操作,请看代码

class Solution {
    public String removeDuplicates(String s) {
        // 将 res 当做栈
        StringBuffer res = new StringBuffer();
        // top为 res 的长度
        int top = -1;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            // 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
            if (top >= 0 && res.charAt(top) == c) {
                res.deleteCharAt(top);
                top--;
            // 否则,将该字符 入栈,同时top++
            } else {
                res.append(c);
                top++;
            }
        }
        return res.toString();
    }
}

这里我们定义top为我们目标字符串的长度,如果我们的长度压根没有,那我们就往字符串里添加字符,如果超过了我们就判断字符串的最后一位字符是否和我们指向的相同,若相同我们就删除对应的字符并不加入,这样遍历一次我们就完成我们的目标了。这是效率最高的方式

逆波兰表达式求值

逆波兰表达式真的是经典到不能再经典了,我们实际上也看得多了,其思路就是用栈解决,遇见数字压入栈,遇上非数字就弹出两个栈顶的值,进行了相应的运算之后再将该值压入到栈中。请看代码

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<String> stack = new Stack<>();
        for (int i = 0; i < tokens.length; i++) {
            if(tokens[i].length()>1){
                stack.add(tokens[i]);
            }else {
                char chars = tokens[i].charAt(0);
                if(chars=='+'){
                    int c = Integer.parseInt(stack.pop());
                    int d = Integer.parseInt(stack.pop());
                    stack.add((d+c)+"");
                }else if(chars=='-'){
                    int c = Integer.parseInt(stack.pop());
                    int d = Integer.parseInt(stack.pop());
                    stack.add((d-c)+"");
                }else if(chars=='*'){
                    int c = Integer.parseInt(stack.pop());
                    int d = Integer.parseInt(stack.pop());
                    stack.add((d*c)+"");
                }else if(chars=='/'){
                    int c = Integer.parseInt(stack.pop());
                    int d = Integer.parseInt(stack.pop());
                    stack.add((d/c)+"");
                }else {
                    stack.add(tokens[i]);
                }
            }
        }
        return Integer.parseInt(stack.pop());
    }
}

这个代码就是非常符合我们的思路的一个代码了,其效率也还行。但是其还有改进的空间,首先我们可以看到其判断是否为字符的方式实在不算好,先判断长度后判断具体值,但实际上大可以直接用四个字符的equals方法来进行判断,其次是由于栈中存放的是字符,因此总是要不断地去调用字符串转换为数字的方法,导致效率不算太好,我们可以一开始就存放数字,只是每次存放都将字符串转换为数字就可以了

class Solution {
    public int evalRPN(String[] tokens) {
        Deque<Integer> stack = new LinkedList();
        for (int i = 0; i < tokens.length; ++i) {
            if ("+".equals(tokens[i])) {        // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
                stack.push(stack.pop() + stack.pop());      // 注意 - 和/ 需要特殊处理
            } else if ("-".equals(tokens[i])) {
                stack.push(-stack.pop() + stack.pop());
            } else if ("*".equals(tokens[i])) {
                stack.push(stack.pop() * stack.pop());
            } else if ("/".equals(tokens[i])) {
                int temp1 = stack.pop();
                int temp2 = stack.pop();
                stack.push(temp2 / temp1);
            } else {
                stack.push(Integer.valueOf(tokens[i]));
            }
        }
        return stack.pop();
    }
}

这里还有一点值得一提的,就是我们的-和/是要进行特殊处理的,因为这两个是要分先后的,但是其他的运算就不需要。

优先队列的使用

关于我们的队列,我们除了简单的栈和队列之外,其实我们还有一种数据结构是需要我们学习的,那就是以堆排序为基础的优先队列。关于优先队列的结构的原理和其实现在数据结构一书中就已经学习过了,之前其实也看得多了,这里就不在提了。我们直接来看题目吧

对于这一道题,我们的一个简单思路当然是我们用Map集合统计所有数字出现的频率,然后根据出现的频率对其进行排序,由大到小获得我们所需要的数组集合,然后返回给题目要求。按照这个思路,我们可以构造代码如下

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>();
        for (int num : nums) map.put(num, map.getOrDefault(num, 0) + 1);
        Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
        PriorityQueue<Map.Entry<Integer,Integer>> priorityQueue = new PriorityQueue<>((o1, o2) -> o2.getValue()-o1.getValue());
        priorityQueue.addAll(entrySet);
        int[] arr = new int[k];
        for (int i = 0; i < k; i++) arr[i]= priorityQueue.poll().getKey();
        return arr;
    }
}

这里我们最需要解释的就是第5行及其之后的代码,首先我们要明白Map集合里的entrySet()方法其作用是返回一个Set集合,该集合内存放的所有元素都是map的键值对,没错其实Map.Entry就是键值对的意思,这里为什么其类型是Map.Entry而不直接写Entry以前在动力节点的java教程里是有过解释的,不过现在我忘了,但是总之我们是可以理解其为什么类型是Map.Entry的,那就可以了。

然后我们创建一个优先队列,该优先队列就存放我们的键值对,但是我们都知道,优先队列是存在比较关系的,因此我们要给其提供迭代方法,否则怎么比啊?我们可以在创建优先队列的时候就给其提供一个指定的比较方法,这个比较方法就是(o1, o2) -> o2.getValue()-o1.getValue(),事实上,这种类型的比较是允许的,我猜测实其内部就有写这样的方法,是因为键值对配合优先队列的使用总是比较频繁的,因此提供了这种方式,用户只需要输入一个比较方式其就可以在内部自己生成一个迭代器。这里我们比较的方式是拿两个键值对的value的值来进行比较,采用这样倒序相减的方式可以让我们的得到的结果是降序排序的(也就是最大堆),如果是正序相减那么我们得到的优先队列会是升序排序的(也就是最小堆)。

最后我们只要获得我们所需要的值就完了。

利用原有数据结构构建所需的数据结构

接下来让我们来看一道真正重量级的题目,也是我们第一次去挑战困难题,先看题目

对于这题,很多人可能会想要利用优先队列来做,但是我们要知道优先队列每次只能删除最大值或者是最小值,而我们这里的滑动窗口每次移动删除的值却不一定是最大值或者是最小值,因此优先队列是行不通的。我们这里我们会设想,我们最好就有一种数据结构,这个数据结构可以让其记录我们所需要的滑动窗口的最大值并在滑动窗口每次移动时都维护这个最大值,但是很可惜,我们没有这种数据结构,因此我们要自己去实现这个数据结构。

我们首先要明确我们的核心方法,就是如何去维护我们的滑动窗口里的最大值,我们的核心思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。因此我们设计我们的数据结构的两个核心方法的原则如下

那么明确了这些之后,我们还有一点要明确,那就是我们选择哪个数据结构比较好,我们这里提供一个原则默认我们都选择Deque双向队列作为我们的初始结构比较好。这是因为双端队列不但是队列,同时还可以作为栈,方法还多,还就那个方便,一个顶俩,不用他用谁啊。

那么最后我们可以构造代码如下

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length==1){
            return nums;
        }
        MyStack stack = new MyStack(new LinkedList<>());
        int[] arr = new int[nums.length-k+1];
        int index = 0;
        for (int i = 0; i < k; i++) {
            stack.add(nums[i]);
        }
        arr[index++]=stack.top();
        for (int i = k; i < nums.length; i++) {
            stack.pop(nums[i-k]);
            stack.add(nums[i]);
            arr[index++]=stack.top();
        }
        return arr;
    }
}

class MyStack{
    Deque<Integer> deque;

    public MyStack(Deque<Integer> deque) {
        this.deque = deque;
    }

    public void add(int x){
        while (!deque.isEmpty() && deque.peekLast() < x){
            deque.pollLast();
        }
        deque.addLast(x);
    }

    public void pop(int x){
        if(!deque.isEmpty() && deque.peekFirst() == x){
            deque.pollFirst();
        }
    }

    public int top(){
        return deque.peekFirst();
    }
}

有的同学可能会问,凭啥你就知道怎么做就可以呢?我冥思苦想都想不出来,你一说就想到了,这我想不到我咋整啊?我只能所凉拌吧,因为我也是看答案的。对于我们而言,想不到可以理解,但是我们可以多去理解这种思想,以后遇到类似的题目就可以用差不多的思想去解决,比如我们这题,最起码经过这题的之后我们的确已经记住了一种能够维护窗口最大值的数据结构了是吧,以后真的需要用就自己原地构造一个也并非不可啊。以后如果需要最小值的数据结构,我们也可以依葫芦画瓢写出来。再以后如果还有类似题目,我们就算解不开,也可以试着按照这种只维护所有可能成为最大值的较大值的思路去构造一个数据结构,起码就不会毫无思路是吧。

本章节的DLC题目

最后我们来做一下我们的之前讲过的利用栈来实现Linux中路径的显示的类似题目,请看题目

对于本题,我们的分析思路其实也是要利用分析法,最开始的例子很难分析,那么我们可以将例子简化,我们注意到其总是用/作为目录的符号,那么我们就可以用/作为关键字对我们的字符串进行分割,然后我们将分割后的字符串按照一定规律放入我们的栈中,最后弹出我们的栈中的字符串并进行适当的拼接最后就是我们所需要的答案了,请看代码

class Solution {
    public String simplifyPath(String path) {
        String[] strings = path.split("/");
        Deque<String> deque = new ArrayDeque<>();
        StringBuffer sb = new StringBuffer();
        for (String s:strings) {
            if("..".equals(s)){
                if(!deque.isEmpty()){
                    deque.pollLast();
                }
            }else if(s.length()>0 && !".".equals(s)){
                deque.addLast(s);
            }
        }
        if(deque.isEmpty()){
            sb.append("/");
        }else {
            while (!deque.isEmpty()){
                sb.append("/");
                sb.append(deque.pollFirst());
            }
        }
        return sb.toString();
    }
}

这里我们要解释下为什么我们要对加入的字符串里判断其长度是否大于0,这里主要解释第11行的条件,这是因为我们分割字符串的函数,是会将其左右两边一起分开的,如果其左边没有任何的数据,那么其就会给其分配一个空字符串,而空字串显然不是我们所需要的,因此我们这里要对其进行长度的判断。最后我们是存在我们的栈中什么都没有存放的情况的,比如我们的例子为///////的时候,此时我们是分割的字符串为空,这时我们的栈不会有任何数据,此时我们就需要给我们的地址手动赋予一个/,代表其指向根目录,其他情况我们只要让我们的队列拼接/并对应弹出字符串就可以了

栈与队列的总结

最后我们可以对我们本章的知识做一个总结,首先我们对栈和队列,我们要理解的双端队列Deque和单向队列Queue的异同,如果没有特殊指定,我们默认使用Deque来作为我们基本的数据结构。其次是我们要学会如何利用已知的数据结构,比如栈或者是队列来去实现另外一个数据结构或者是我们所需要的特殊结构。最后我们要学会的关于优先队列的使用,其使用之前要指定比较的规则,否则是无法使用的,其本身也常常和键值对联系起来,这里会用到Map集合的entryset方法。