【面试高频题】热门数据结构面试题合集

1,526 阅读6分钟

本文正在参加「金石计划」

对于数据结构而言,栈的重要性不言而喻,往往是笔试/面试向中的重中之重。今天一起学习 44 道与栈相关的题目。

232. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

进阶:

  • 你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。

示例:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

提示:

  • 1 <= x <= 9
  • 最多调用 100 次 push、pop、peek 和 empty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)
基本思路

无论「用栈实现队列」还是「用队列实现栈」,思路都是类似的。

都可以通过使用两个栈/队列来解决。

我们创建两个栈,分别为 outin,用作处理「输出」和「输入」操作。

其实就是两个栈来回「倒腾」。

而对于「何时倒腾」决定了是 O(n) 解法 还是 均摊 O(1) 解法

O(n) 解法

我们创建两个栈,分别为 outin

  • in 用作处理输入操作 push(),使用 in 时需确保 out 为空
  • out 用作处理输出操作 pop()peek(),使用 out 时需确保 in 为空
class MyQueue {
    Deque<Integer> out, in;
    public MyQueue() {
        in = new ArrayDeque<>();
        out = new ArrayDeque<>();
    }
    
    public void push(int x) {
        while (!out.isEmpty()) in.addLast(out.pollLast());
        in.addLast(x);
    }
    
    public int pop() {
        while (!in.isEmpty()) out.addLast(in.pollLast());
        return out.pollLast();
    }
    
    public int peek() {
        while (!in.isEmpty()) out.addLast(in.pollLast());
        return out.peekLast();
    }
    
    public boolean empty() {
        return out.isEmpty() && in.isEmpty();
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)
均摊 O(1) 解法

事实上,我们不需要在每次的「入栈」和「出栈」操作中都进行「倒腾」。

我们只需要保证,输入的元素总是跟在前面的输入元素的后面,而输出元素总是最早输入的那个元素即可。

可以通过调整「倒腾」的时机来确保满足上述要求,但又不需要发生在每一次操作中:

  • 只有在「输出栈」为空的时候,才发生一次性的「倒腾」
class MyQueue {
    Deque<Integer> out, in;
    public MyQueue() {
        in = new ArrayDeque<>();
        out = new ArrayDeque<>();
    }
    
    public void push(int x) {
        in.addLast(x);
    }
    
    public int pop() {
        if (out.isEmpty()) {
            while (!in.isEmpty()) out.addLast(in.pollLast());
        }
        return out.pollLast();
    }
    
    public int peek() {
        if (out.isEmpty()) {
            while (!in.isEmpty()) out.addLast(in.pollLast());
        }
        return out.peekLast();
    }
    
    public boolean empty() {
        return out.isEmpty() && in.isEmpty();
    }
}
  • 时间复杂度:pop()peek() 操作都是均摊 O(1)O(1)
  • 空间复杂度:O(n)O(n)

155. 最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) —— 将元素 x 推入栈中。
  • pop() —— 删除栈顶的元素。
  • top() —— 获取栈顶元素。
  • getMin() —— 检索栈中的最小元素。

示例:

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:

[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

提示:

  • pop、top 和 getMin 操作总是在 非空栈 上调用。
双栈解法

为了快速找到栈中最小的元素,我们可以使用一个辅助栈 help。

通过控制 help 的压栈逻辑来实现:help 栈顶中始终存放着栈内元素的最小值。

代码:

class MinStack {
    Deque<Integer> data = new ArrayDeque<>();
    Deque<Integer> help = new ArrayDeque<>();

    public void push(int val) {
        data.addLast(val);
        if (help.isEmpty() || help.peekLast() >= val) {
            help.addLast(val);
        } else {
            help.addLast(help.peekLast());
        }
    }
    
    public void pop() {
        data.pollLast();
        help.pollLast();
    }
    
    public int top() {
        return data.peekLast();
    }
    
    public int getMin() {
        return help.peekLast();
    }
}
  • 时间复杂度:所有的操作均为 O(1)O(1)
  • 空间复杂度:O(1)O(1)

385. 迷你语法分析器

给定一个字符串 s 表示一个整数嵌套列表,实现一个解析它的语法分析器并返回解析的结果 NestedInteger

列表中的每个元素只可能是整数或整数嵌套列表

示例 1:

输入:s = "324",

输出:324

解释:你应该返回一个 NestedInteger 对象,其中只包含整数值 324。

提示:

  • 1<=s.length<=51041 <= s.length <= 5 * 10^4
  • s 由数字、方括号 "[]"、负号 '-' 、逗号 ','组成
  • 用例保证 s 是可解析的 NestedInteger
  • 输入中的所有值的范围是 [106,106][-10^6, 10^6]

每个 [ 的出现意味着存在一个嵌套类型的 NestedInteger 实例,同时每个 NestedInteger 实例(无论是嵌套类型还是数值类型)都归属于其最近一个左边的嵌套类型的 NestedInteger 实例(即其左边最近一个 [),因此我们可以使用栈来处理。

对出现的几类字符进行简单分情况讨论:

  • ,:跳过即可;
  • -数字:将连续段代表数值的字符串取出,创建数值型的 NestedInteger 实例并压入栈中;
  • [:创建一个嵌套类型的 NestedInteger 实例并压入栈中,同时为了方便,同时压入一个起「标识」作用的 NestedInteger 对象;
  • ]:从栈中取出元素,直到遇到起「标识」作用的 NestedInteger 对象(说明找到与当前 ] 成对的 [),将 [] 之间的所有元素添加到 [ 所代指的嵌套 NestedInteger 实例中,然后将 [ 所代指的嵌套 NestedInteger 实例重新放入栈中。

按照上述逻辑处理完整个 s,最终栈顶元素即是答案。

代码:

class Solution {
    static NestedInteger ph = new NestedInteger(0);
    public NestedInteger deserialize(String s) {
        Deque<NestedInteger> d = new ArrayDeque<>();
        char[] cs = s.toCharArray();
        int n = cs.length, i = 0;
        while (i < n) {
            if (cs[i] == ',' && ++i >= 0) continue;
            if (cs[i] == '-' || (cs[i] >= '0' && cs[i] <= '9')) {
                int j = cs[i] == '-' ? i + 1 : i, num = 0;
                while (j < n && (cs[j] >= '0' && cs[j] <= '9')) num = num * 10 + (cs[j++] - '0');
                d.addLast(new NestedInteger(cs[i] == '-' ? -num : num));
                i = j;
            } else if (cs[i] == '[') {
                d.addLast(new NestedInteger());
                d.addLast(ph);
                i++;
            } else {
                List<NestedInteger> list = new ArrayList<>();
                while (!d.isEmpty()) {
                    NestedInteger poll = d.pollLast();
                    if (poll == ph) break;
                    list.add(poll);
                }
                for (int j = list.size() - 1; j >= 0; j--) d.peekLast().add(list.get(j));
                i++;
            }
        }
        return d.peekLast();
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

71. 简化路径

给你一个字符串 path,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。

Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

  • 始终以斜杠 '/' 开头。
  • 两个目录名之间必须只有一个斜杠 '/'
  • 最后一个目录名(如果存在)不能 以 '/' 结尾。
  • 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.''..')。

返回简化后得到的 规范路径

示例 1:

输入:path = "/home/"

输出:"/home"

解释:注意,最后一个目录名后面没有斜杠。 

提示:

  • 1<=path.length<=30001 <= path.length <= 3000
  • path 由英文字母,数字,'.''/''_' 组成。
  • path 是一个有效的 Unix 风格绝对路径。
模拟

根据题意,使用栈进行模拟即可。

具体的,从前往后处理 path,每次以 item 为单位进行处理(有效的文件名),根据 item 为何值进行分情况讨论:

  • item 为有效值 :存入栈中;
  • item.. :弹出栈顶元素(若存在);
  • item. :不作处理。

代码:

class Solution {
    public String simplifyPath(String path) {
        Deque<String> d = new ArrayDeque<>();
        int n = path.length();
        for (int i = 1; i < n; ) {
            if (path.charAt(i) == '/' && ++i >= 0) continue;
            int j = i + 1;
            while (j < n && path.charAt(j) != '/') j++;
            String item = path.substring(i, j);
            if (item.equals("..")) {
                if (!d.isEmpty()) d.pollLast();
            } else if (!item.equals(".")) {
                d.addLast(item);
            }
            i = j;
        }
        StringBuilder sb = new StringBuilder();
        while (!d.isEmpty()) sb.append("/" + d.pollFirst());
        return sb.length() == 0 ? "/" : sb.toString();
    }
}
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

总结

与数组、链表等数据结构类型,栈同样是线性类型的数据结构。

其具有后进先出的特性,往往与先进先出的队列共同提及,栈作为基础数据结构往往出现在中小厂的面试/笔试题中,需要重点掌握。