Hexo

点滴积累 豁达处之

0%

leetcode算法-栈与队列

leetcode算法

如何理解栈

比如我们在放盘子的时候都是从下往上一个个放,拿的时候是从上往下一个个的那,不能从中间抽,这种其实就是一个典型的栈型数据结构。后进先出即Last In First Out (LIFO)。

栈如何实现

其实它是一个限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈其实就是一个特殊的链表或者数组。
既然栈也是一个线性表,那么我们肯定会想到数组和链表,而且栈还有这么多限制,那为什么我们还要使用这个数据结构呢?不如直接使用数组和链表来的更直接么?数组和链表暴露太多的接口,实现上更灵活了,有些技术理解不到位的人员就可能出错。所以在某些特定场景下最好是选择栈这个数据结构。

(LeetCode-232) 用栈实现队列

题目

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

实现 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(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
输入:
["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 操作)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MyQueue {

private static Stack<Integer> inStack;
private static Stack<Integer> outStack;

public MyQueue(){
inStack = new Stack<Integer>();
outStack = new Stack<Integer>();
}

public void push(int x){
inStack.push(x);
}

public int pop(){
if(outStack.isEmpty()){
in2Out();
}
return outStack.pop();
}

public int peek(){
if(outStack.isEmpty()){
in2Out();
}
return outStack.peek();
}

public void in2Out(){
while (!inStack.isEmpty()){
outStack.push(inStack.pop());
}
}

public boolean empty() {
if(inStack.isEmpty() && outStack.isEmpty()){
return true;
}
return false;
}
}

(LeetCode-394) 字符串解码

题目

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

示例 1:

1
2
输入:s = "3[a]2[bc]"
输出:"aaabcbc"

示例 2:

1
2
输入:s = "3[a2[c]]"
输出:"accaccacc"

示例 3:

1
2
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"

示例 4:

1
2
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"

提示:

  • 1 <= s.length <= 30
  • s 由小写英文字母、数字和方括号 ‘[]’ 组成
  • s 保证是一个 有效 的输入。
  • s 中所有整数的取值范围为 [1, 300]

分析

方法一:栈操作
思路和算法

本题中可能出现括号嵌套的情况,比如 2[a2[bc]],这种情况下我们可以先转化成 2[abcbc],在转化成 abcbcabcbc。我们可以把字母、数字和括号看成是独立的 TOKEN,并用栈来维护这些 TOKEN。具体的做法是,遍历这个栈:

  • 如果当前的字符为数位,解析出一个数字(连续的多个数位)并进栈
  • 如果当前的字符为字母或者左括号,直接进栈
  • 如果当前的字符为右括号,开始出栈,一直到左括号出栈,出栈序列反转后拼接成一个字符串,此时取出栈顶的数字(此时栈顶一定是数字,想想为什么?),就是这个字符串应该出现的次数,我们根据这个次数和字符串构造出新的字符串并进栈

重复如上操作,最终将栈中的元素按照从栈底到栈顶的顺序拼接起来,就得到了答案。注意:这里可以用不定长数组来模拟栈操作,方便从栈底向栈顶遍历

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class DecodeString {
public static void main(String[] args) {
String s = "3[a2[c]]";
System.out.println(decodeString( s));
}

static int ptr = 0;
public static String decodeString(String s) {
LinkedList<String> stk = new LinkedList<String>();
ptr = 0;

while (ptr < s.length()) {
char cur = s.charAt(ptr);
if (Character.isDigit(cur)) {
// 获取一个数字并进栈
String digits = getDigits(s);
stk.addLast(digits);
} else if (Character.isLetter(cur) || cur == '[') {
// 获取一个字母并进栈
stk.addLast(String.valueOf(s.charAt(ptr++)));
} else {
++ptr;
LinkedList<String> sub = new LinkedList<String>();
while (!"[".equals(stk.peekLast())) {
sub.addLast(stk.removeLast());
}
Collections.reverse(sub);
// 左括号出栈
stk.removeLast();
// 此时栈顶为当前 sub 对应的字符串应该出现的次数
int repTime = Integer.parseInt(stk.removeLast());
StringBuffer t = new StringBuffer();
String o = getString(sub);
// 构造字符串
while (repTime-- > 0) {
t.append(o);
}
// 将构造好的字符串入栈
stk.addLast(t.toString());
}
}

return getString(stk);
}

public static String getDigits(String s) {
StringBuffer ret = new StringBuffer();
while (Character.isDigit(s.charAt(ptr))) {
ret.append(s.charAt(ptr++));
}
return ret.toString();
}

public static String getString(LinkedList<String> v) {
StringBuffer ret = new StringBuffer();
for (String s : v) {
ret.append(s);
}
return ret.toString();
}
}

**(LeetCode-739) **每日温度

题目

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

1
2
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

示例 2:

1
2
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]

示例 3:

1
2
输入: temperatures = [30,60,90]
输出: [1,1,0]

分析

方法二:单调栈

可以维护一个存储下标的单调栈,从栈底到栈顶的下标对应的温度列表中的温度依次递减。如果一个下标在单调栈里,则表示尚未找到下一次温度更高的下标。

正向遍历温度列表。对于温度列表中的每个元素 temperatures[i],如果栈为空,则直接将 i 进栈,如果栈不为空,则比较栈顶元素 prevIndex 对应的温度 temperatures[prevIndex] 和当前温度 temperatures[i],如果 temperatures[i] > temperatures[prevIndex],则将 prevIndex 移除,并将 prevIndex 对应的等待天数赋为 i - prevIndex,重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度,然后将 i 进栈。

为什么可以在弹栈的时候更新 ans[prevIndex] 呢?因为在这种情况下,即将进栈的 i 对应的 temperatures[i] 一定是 temperatures[prevIndex] 右边第一个比它大的元素,试想如果 prevIndex 和 i 有比它大的元素,假设下标为 j,那么 prevIndex 一定会在下标 j 的那一轮被弹掉。

由于单调栈满足从栈底到栈顶元素对应的温度递减,因此每次有元素进栈时,会将温度更低的元素全部移除,并更新出栈元素对应的等待天数,这样可以确保等待天数一定是最小的。

以下用一个具体的例子帮助读者理解单调栈。对于温度列表 [73,74,75,71,69,72,76,73][73,74,75,71,69,72,76,73],单调栈 stack 的初始状态为空,答案ans 的初始状态是 [0,0,0,0,0,0,0,0][0,0,0,0,0,0,0,0],按照以下步骤更新单调栈和答案,其中单调栈内的元素都是下标,括号内的数字表示下标在温度列表中对应的温度。

  • 当i=0 时,单调栈为空,因此将 0 进栈。

    • stack=[0(73)]
    • ans=[0,0,0,0,0,0,0,0]
  • 当i=1 时,由于 74 大于 73,因此移除栈顶元素 0,赋值 ans[0]:=1−0,将 1 进栈。

    • stack=[1(74)]
    • ans=[1,0,0,0,0,0,0,0]
  • 当 i=2 时,由于 75 大于 74,因此移除栈顶元素 1,赋值 ans[1]:=2−1,将 2 进栈。

    • stack=[2(75)]
    • ans=[1,1,0,0,0,0,0,0]
  • 当 i=3 时,由于 71 小于 75,因此将 3 进栈。

    • stack=[2(75),3(71)]
    • ans=[1,1,0,0,0,0,0,0]
  • 当 i=4 时,由于 69 小于 71,因此将 4 进栈。

    • stack=[2(75),3(71),4(69)]
    • ans=[1,1,0,0,0,0,0,0]
  • 当 i=5 时,由于72 大于 69 和 71,因此依次移除栈顶元素 4 和 3,赋值 ans[4]:=5−4 和 ans[3]:=5−3,将 5 进栈。

    • stack=[2(75),5(72)]
    • ans=[1,1,0,2,1,0,0,0]
  • 当i=6 时,由于 76 大于 72 和 75,因此依次移除栈顶元素 5 和 2,赋值 ans[5]:=6−5 和 ans[2]:=6−2,将 66 进栈。

    • stack=[6(76)]
    • ans=[1,1,4,2,1,1,0,0]
  • 当i=7 时,由于 73 小于 76,因此将 7 进栈。

    • stack=[6(76),7(73)]
    • ans=[1,1,4,2,1,1,0,0]

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class DailyTemperatures {
public static void main(String[] args) {
int[] nums = {73,74,75,71,69,72,76,73};
System.out.println(Arrays.toString(new DailyTemperatures().dailyTemperatures(nums)));
}

/*用倒序遍历数组来求解*/
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
int[] ret = new int[length];

/*从右向左遍历,数组最后一个元素无需处理*/
for (int i = length - 2; i >= 0; i--) {
/*backIdx表示从当前元素开始往后寻找获得需要的结果,
backIdx=backIdx+ret[backIdx]是为了利用已经有的结果进行跳跃*/
for (int backIdx = i + 1; backIdx < length; backIdx=backIdx+ret[backIdx]) {
if (temperatures[backIdx] > temperatures[i]) {
ret[i] = backIdx - i;
break;
} else if (ret[backIdx] == 0) {/*遇到0表示后面不会有更大的值,那当然当前值就应该也为0*/
ret[i] = 0;
break;
}
}
}
return ret;
}

// 栈
public int[] dailyTemperatures1(int[] temperatures) {
int[] ret = new int[temperatures.length];
Stack<Integer> stack = new Stack<>();
for(int i = 0; i < temperatures.length; i++){
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]){
int index = stack.pop();
ret[index] = i - index;
}
stack.push(i);
}
return ret;
}
}

**(LeetCode-84) **柱状图中最大的矩形

题目

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

Leetcode_170

1
2
3
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

示例 2:

Leetcode_171

1
2
输入: heights = [2,4]
输出: 4

分析

方法一:单调栈

思路

我们归纳一下枚举「高」的方法:

  • 首先我们枚举某一根柱子 ii 作为高 h=heights[i];
  • 随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 h。换句话说,我们需要找到左右两侧最近的高度小于 h 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 i 能够扩展到的最远范围。

理解单调栈

我们用一个具体的例子 [6, 7, 5, 2, 4, 5, 9, 3][6,7,5,2,4,5,9,3] 来帮助读者理解单调栈。我们需要求出每一根柱子的左侧且最近的小于其高度的柱子。初始时的栈为空。

  • 我们枚举 6,因为栈为空,所以 6 左侧的柱子是「哨兵」,位置为 -1。随后我们将 6 入栈。

    • 栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置)
  • 我们枚举 7,由于 6<7,因此不会移除栈顶元素,所以 7 左侧的柱子是 6,位置为 0。随后我们将 7 入栈。

    • 栈:[6(0), 7(1)]
  • 我们枚举 5,由于 7≥5,因此移除栈顶元素 7。同样地,6≥5,再移除栈顶元素 6。此时栈为空,所以 5 左侧的柱子是「哨兵」,位置为−1。随后我们将 5 入栈。

    • 栈:[5(2)]
  • 接下来的枚举过程也大同小异。我们枚举 2,移除栈顶元素 5,得到 2 左侧的柱子是「哨兵」,位置为 −1。将 2 入栈。

    • 栈:[2(3)]
  • 我们枚举 4,5 和 9,都不会移除任何栈顶元素,得到它们左侧的柱子分别是 2,4 和 5,位置分别为 3,4 和 5。将它们入栈。

    • 栈:[2(3), 4(4), 5(5), 9(6)]
  • 我们枚举 3,依次移除栈顶元素 9,5 和 4,得到 3 左侧的柱子是 2,位置为 3。将 3 入栈。

    • 栈:[2(3), 3(7)]

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class LargestRectangleArea {

public static void main(String[] args) {
int[] nums = {2,1,5,6,2,3};
System.out.println(new LargestRectangleArea().largestRectangleArea(nums));
}

/*基于单调栈的实现*/
public int largestRectangleArea(int[] heights) {
int len = heights.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return heights[0];
}

int result = 0;
Deque<Integer> stack = new LinkedList<Integer>();
/*遍历数组*/
for (int i = 0; i < len; i++) {
while (!stack.isEmpty() && heights[i] < heights[stack.peekLast()]) {
/*当前需要计算面积的元素的下标*/
int curIndex = stack.pollLast();
/*获得当前元素的值,也就是矩形的高*/
int curHeight = heights[curIndex];
/*计算矩形的宽*/
int curWidth;
if (stack.isEmpty()) {
/*栈为空,表示目前遍历过所有元素都比当前的i要大,i是最小的一个,
宽度就可以直接取i的值*/
curWidth = i;
} else {
curWidth = i - stack.peekLast() - 1;
}
result = Math.max(result, curHeight * curWidth);
}
stack.addLast(i);
}

/*处理目前还在栈中的元素*/
while (!stack.isEmpty()) {
int curHeight = heights[stack.pollLast()];
int curWidth;
if (stack.isEmpty()) {
curWidth = len;
} else {
curWidth = len - stack.peekLast() - 1;
}
result = Math.max(result, curHeight * curWidth);
}
return result;
}
}

**(LeetCode-224) **基本计算器

题目

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval()

示例 1:

1
2
输入:s = "1 + 1"
输出:2

示例 2:

1
2
输入:s = " 2-1 + 2 "
输出:3

示例 3:

1
2
输入:s = "(1+(4+5+2)-3)+(6+8)"
输出:23

分析

方法一:括号展开 + 栈
由于字符串除了数字与括号外,只有加号和减号两种运算符。因此,如果展开表达式中所有的括号,则得到的新表达式中,数字本身不会发生变化,只是每个数字前面的符号会发生变化。

因此,我们考虑使用一个取值为{−1,+1} 的整数 sign 代表「当前」的符号。根据括号表达式的性质,它的取值:

  • 与字符串中当前位置的运算符有关;
  • 如果当前位置处于一系列括号之内,则也与这些括号前面的运算符有关:每当遇到一个以 -− 号开头的括号,则意味着此后的符号都要被「翻转」。

考虑到第二点,我们需要维护一个栈 ops,其中栈顶元素记录了当前位置所处的每个括号所「共同形成」的符号。例如,对于字符串 1+2+(3-(4+5)):

  • 扫描到 1+2 时,由于当前位置没有被任何括号所包含,则栈顶元素为初始值 +1;
  • 扫描到 1+2+(3 时,当前位置被一个括号所包含,该括号前面的符号为 + 号,因此栈顶元素依然 +1;
  • 扫描到 1+2+(3-(4 时,当前位置被两个括号所包含,分别对应着 + 号和 − 号,由于 + 号和 − 号合并的结果为 -− 号,因此栈顶元素变为 −1。

在得到栈 ops 之后, sign 的取值就能够确定了:如果当前遇到了 + 号,则更新 sign←ops.top();如果遇到了遇到了 -− 号,则更新 sign←−ops.top()。

然后,每当遇到 ( 时,都要将当前的 sign 取值压入栈中;每当遇到 ) 时,都从栈中弹出一个元素。这样,我们能够在扫描字符串的时候,即时地更新 ops 中的元素。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Calculate {

public static void main(String[] args) {
String str = "222 - 32 + ( 9 - ( 4 + 5))";
System.out.println(new Calculate().calculate(str));
}

public int calculate(String s) {
Deque<Integer> ops = new LinkedList<Integer>();
ops.push(1);
int sign = 1;

int ret = 0;
int n = s.length();
int i = 0;
while (i < n) {
if (s.charAt(i) == ' ') {
i++;
} else if (s.charAt(i) == '+') {
sign = ops.peek();
i++;
} else if (s.charAt(i) == '-') {
sign = -ops.peek();
i++;
} else if (s.charAt(i) == '(') {
ops.push(sign);
i++;
} else if (s.charAt(i) == ')') {
ops.pop();
i++;
} else {
long num = 0;
while (i < n && Character.isDigit(s.charAt(i))) {
num = num * 10 + s.charAt(i) - '0';
i++;
}
ret += sign * num;
}
}
return ret;
}
}

(LeetCode-98)验证二叉搜索树

题目

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

Leetcode_172

1
2
输入:root = [2,1,3]
输出:true

示例 2:

Leetcode_173

1
2
3
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

分析

方法二:中序遍历

思路和算法

基于方法一中提及的性质,我们可以进一步知道二叉搜索树「中序遍历」得到的值构成的序列一定是升序的,这启示我们在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。如果均大于说明这个序列是升序的,整棵树是二叉搜索树,否则不是,下面的代码我们使用栈来模拟中序遍历的过程。

可能有读者不知道中序遍历是什么,我们这里简单提及。中序遍历是二叉树的一种遍历方式,它先遍历左子树,再遍历根节点,最后遍历右子树。而我们二叉搜索树保证了左子树的节点的值均小于根节点的值,根节点的值均小于右子树的值,因此中序遍历以后得到的序列一定是升序序列。

Leetcode_174

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class IsValidBST {

public static void main(String[] args) {
TreeNode node6 = new TreeNode(6);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4, node3, node6);
TreeNode node1 = new TreeNode(1);
TreeNode node5 = new TreeNode(5, node1,node4);

System.out.println(new IsValidBST().isValidBST(node5));
}

public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

public boolean isValidBST(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<TreeNode>();
double inorder = -Double.MAX_VALUE;

while (!stack.isEmpty() || root != null) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root.val <= inorder) {
return false;
}
inorder = root.val;
root = root.right;
}
return true;
}


public boolean isValidBST(TreeNode root) {
Stack<Integer> stack = new Stack<>();
printMid( root, stack);
Integer next = Integer.MAX_VALUE;
while (!stack.isEmpty()){
Integer num = stack.pop();
if(next <= num){
return false;
}else {
next = num;
}
}
return true;
}

public void printMid(TreeNode root, Stack<Integer> stack) {
if (root.left != null){
printMid( root.left, stack);
}
stack.push(root.val);
if (root.right != null){
printMid( root.right, stack);
}

}
}

(LeetCode-102)二叉树的层序遍历

题目

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

Leetcode_175

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例 2:

1
2
输入:root = [1]
输出:[[1]]

示例 3:

1
2
输入:root = []
输出:[]

分析

方法一:广度优先搜索

思路和算法

我们可以用广度优先搜索解决这个问题。

我们可以想到最朴素的方法是用一个二元组 (node, level) 来表示状态,它表示某个节点和它所在的层数,每个新进队列的节点的 level 值都是父亲节点的 level 值加一。最后根据每个点的 level 对点进行分类,分类的时候我们可以利用哈希表,维护一个以 level 为键,对应节点值组成的数组为值,广度优先搜索结束以后按键 level 从小到大取出所有值,组成答案返回即可。

考虑如何优化空间开销:如何不用哈希映射,并且只用一个变量 node 表示状态,实现这个功能呢?

我们可以用一种巧妙的方法修改广度优先搜索:

  • 首先根元素入队
  • 当队列不为空的时候
    • 求当前队列的长度 si
    • 依次从队列中取 si 个元素进行拓展,然后进入下一次迭代

它和普通广度优先搜索的区别在于,普通广度优先搜索每次只取一个元素拓展,而这里每次取 si个元素。在上述过程中的第 i 次迭代就得到了二叉树的第 i 层的 si个元素。

为什么这么做是对的呢?我们观察这个算法,可以归纳出这样的循环不变式:第 i 次迭代前,队列中的所有元素就是第 i 层的所有元素,并且按照从左向右的顺序排列。证明它的三条性质(你也可以把它理解成数学归纳法):

  • 初始化:i=1 的时候,队列里面只有 root,是唯一的层数为 1 的元素,因为只有一个元素,所以也显然满足「从左向右排列」;
  • 保持:如果 i=k 时性质成立,即第 k 轮中出队 sk 的元素是第 k 层的所有元素,并且顺序从左到右。因为对树进行广度优先搜索的时候由低 k 层的点拓展出的点一定也只能是 k+1 层的点,并且 k+1 层的点只能由第 k 层的点拓展到,所以由这 sk 个点能拓展到下一层所有的 sk+1个点。又因为队列的先进先出(FIFO)特性,既然第 k 层的点的出队顺序是从左向右,那么第 k+1 层也一定是从左向右。至此,我们已经可以通过数学归纳法证明循环不变式的正确性。
  • 终止:因为该循环不变式是正确的,所以按照这个方法迭代之后每次迭代得到的也就是当前层的层次遍历结果。至此,我们证明了算法是正确的。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class LevelOrder {

public static void main(String[] args) {
TreeNode node7 = new TreeNode(7);
TreeNode node15 = new TreeNode(15);
TreeNode node20 = new TreeNode(20, node15,node7 );
TreeNode node9 = new TreeNode(9);
TreeNode node3 = new TreeNode(3,node9,node20 );
List<List<Integer>> lsit = new LevelOrder().levelOrder(node3);
System.out.println("");

}

public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
// 边界
if(root == null){
return result;
}
queue.offer(root);
while (!queue.isEmpty()){
// 获取每层的节点数
int levelNodeNum = queue.size();
List<Integer> tempList = new ArrayList<>();
for(int i = 0; i < levelNodeNum; i++){
TreeNode node = queue.poll();
tempList.add(node.getValue());
if(node.getLeft() != null){
queue.offer(node.getLeft());
}
if(node.getRight() != null){
queue.offer(node.getRight());
}
}
result.add(tempList);
}
return result;
}
}

(LeetCode-103)二叉树的锯齿形层序遍历

题目

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

示例 1:

Leetcode_175

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]

示例 2:

1
2
输入:root = [1]
输出:[[1]]

示例 3:

1
2
输入:root = []
输出:[]

分析

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
levelPreVisit( result, root, 0);
return result;
}

public void levelPreVisit(List<List<Integer>> result, TreeNode root, int height){
if(root == null) return;
if(height >= result.size()){
result.add(new LinkedList<>());
}
if(height % 2 == 0){
result.get(height).add(root.getValue());
}else{
result.get(height).add(0,root.getValue());
}

levelPreVisit(result, root.getLeft(), height+ 1);
levelPreVisit( result, root.getRight(), height + 1);
}

(LeetCode-105) 从前序与中序遍历序列构造二叉树

题目

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

1
2
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

示例 2:

1
2
输入: preorder = [-1], inorder = [-1]
输出: [-1]

分析

方法一:递归

思路

对于任意一颗树而言,前序遍历的形式总是

1
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]

即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是

1
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

Leetcode_176

只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。

这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。

细节

在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,我们就只需要 O(1) 的时间对根节点进行定位了。

方法二:迭代

我们以树

1
2
3
4
5
6
7
8
9
        3
/ \
9 20
/ / \
8 15 7
/ \
5 10
/
4

为例,它的前序遍历和中序遍历分别为

1
2
preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]

我们用一个栈 stack 来维护「当前节点的所有还没有考虑过右儿子的祖先节点」,栈顶就是当前节点。也就是说,只有在栈中的节点才可能连接一个新的右儿子。同时,我们用一个指针 index 指向中序遍历的某个位置,初始值为 0。index 对应的节点是「当前节点不断往左走达到的最终节点」,这也是符合中序遍历的,它的作用在下面的过程中会有所体现。

首先我们将根节点 3 入栈,再初始化 index 所指向的节点为 4,随后对于前序遍历中的每个节点,我们依次判断它是栈顶节点的左儿子,还是栈中某个节点的右儿子。

  • 我们遍历 9。9 一定是栈顶节点 3 的左儿子。我们使用反证法,假设 9 是 3 的右儿子,那么 3 没有左儿子,index 应该恰好指向 3,但实际上为 4,因此产生了矛盾。所以我们将 9 作为 3 的左儿子,并将 9 入栈。

    1
    2
    stack = [3, 9]
    index -> inorder[0] = 4
  • 我们遍历 854。同理可得它们都是上一个节点(栈顶节点)的左儿子,所以它们会依次入栈

    1
    2
    stack = [3, 9, 8, 5, 4]
    index -> inorder[0] = 4
  • 我们遍历 10,这时情况就不一样了。我们发现 index 恰好指向当前的栈顶节点 4,也就是说 4 没有左儿子,那么 10 必须为栈中某个节点的右儿子。那么如何找到这个节点呢?栈中的节点的顺序和它们在前序遍历中出现的顺序是一致的,而且每一个节点的右儿子都还没有被遍历过,那么这些节点的顺序和它们在中序遍历中出现的顺序一定是相反的。

    因此我们可以把 index 不断向右移动,并与栈顶节点进行比较。如果 index 对应的元素恰好等于栈顶节点,那么说明我们在中序遍历中找到了栈顶节点,所以将 index 增加 1 并弹出栈顶节点,直到 index 对应的元素不等于栈顶节点。按照这样的过程,我们弹出的最后一个节点 x 就是 10 的双亲节点,这是因为 10 出现在了 x 与 x 在栈中的下一个节点的中序遍历之间,因此 10 就是 x 的右儿子。

    回到我们的例子,我们会依次从栈顶弹出 458,并且将 index 向右移动了三次。我们将 10 作为最后弹出的节点 8 的右儿子,并将 10 入栈。

    1
    2
    stack = [3, 9, 10]
    index -> inorder[3] = 10
  • 我们遍历 20。同理,index 恰好指向当前栈顶节点 10,那么我们会依次从栈顶弹出 10,9 和 3,并且将 index 向右移动了三次。我们将 20 作为最后弹出的节点 3 的右儿子,并将 20 入栈。

    1
    2
    stack = [20]
    index -> inorder[6] = 15
  • 我们遍历 15,将 15 作为栈顶节点 20 的左儿子,并将 15 入栈。

    1
    2
    stack = [20, 15]
    index -> inorder[6] = 15
  • 我们遍历 7。index 恰好指向当前栈顶节点 15,那么我们会依次从栈顶弹出 15 和 20,并且将 index 向右移动了两次。我们将 7 作为最后弹出的节点 20 的右儿子,并将 7 入栈。

    1
    2
    stack = [7]
    index -> inorder[8] = 7

此时遍历结束,我们就构造出了正确的二叉树。

算法

我们归纳出上述例子中的算法流程:

  • 我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点;

  • 我们依次枚举前序遍历中除了第一个节点以外的每个节点。如果 index 恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向右移动 index,并将当前节点作为最后一个弹出的节点的右儿子;如果 index 和栈顶节点不同,我们将当前节点作为栈顶节点的左儿子;

  • 无论是哪一种情况,我们最后都将当前的节点入栈。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
  // 递归
private Map<Integer, Integer> indexMap;

public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return null;
}

// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = indexMap.get(preorder[preorder_root]);

// 先把根节点建立出来
TreeNode root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}

public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
// 构造哈希映射,帮助我们快速定位根节点
indexMap = new HashMap<Integer, Integer>();
for (int i = 0; i < n; i++) {
indexMap.put(inorder[i], i);
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}


// 迭代
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || preorder.length == 0) {
return null;
}
TreeNode root = new TreeNode(preorder[0]);
Deque<TreeNode> stack = new LinkedList<TreeNode>();
stack.push(root);
int inorderIndex = 0;
for (int i = 1; i < preorder.length; i++) {
int preorderVal = preorder[i];
TreeNode node = stack.peek();
if (node.val != inorder[inorderIndex]) {
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
node = stack.pop();
inorderIndex++;
}
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
}

**(LeetCode-114) **二叉树展开为链表

题目

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:

Leetcode_177

1
2
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]

示例 2:

1
2
输入:root = []
输出:[]

示例 3:

1
2
输入:root = [0]
输出:[0]

分析

方法三:寻找前驱节点

注意到前序遍历访问各节点的顺序是根节点、左子树、右子树。如果一个节点的左子节点为空,则该节点不需要进行展开操作。如果一个节点的左子节点不为空,则该节点的左子树中的最后一个节点被访问之后,该节点的右子节点被访问。该节点的左子树中最后一个被访问的节点是左子树中的最右边的节点,也是该节点的前驱节点。因此,问题转化成寻找当前节点的前驱节点。

具体做法是,对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为前驱节点,将当前节点的右子节点赋给前驱节点的右子节点,然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点设为空。对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束。

图示

Leetcode_178

Leetcode_179

Leetcode_180

Leetcode_181

Leetcode_182

Leetcode_183

Leetcode_184

Leetcode_185

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Flatten {
public static void main(String[] args) {
TreeNode node6 = new TreeNode(6);
TreeNode node4 = new TreeNode(4);
TreeNode node3 = new TreeNode(3);
TreeNode node5 = new TreeNode(5, null,node6);
TreeNode node2 = new TreeNode(2, node3,node4);
TreeNode node1 = new TreeNode(1, node2, node5);
new Flatten().flatten( node1);
System.out.println("");
}

public void flatten(TreeNode root) {
while(root != null){
if(root.left == null){
root = root.right;
}else {
// 寻找左子树最右边的节点
TreeNode rightmost = root.left;
while (rightmost.right != null){
rightmost = rightmost.right;
}
// 将原来的右子树挂到左子树的最右边节点的右指针上
rightmost.right = root.right;
// 将左子树挂到root节点的右指针上
root.right = root.left;
root.left = null;
// 此时root节点的左子树已经为null, 向下寻找,看看下面的节点上是否还存在着左子树,重复上述过程
root = root.right;
}
}
}
}

**(LeetCode-199) **二叉树的右视图

题目

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

Leetcode_186

1
2
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]

示例 2:

1
2
输入: [1,null,3]
输出: [1,3]

示例 3:

1
2
输入: []
输出: []

分析

方法1

这个题目仔细思考一下,按照从顶部到底部的顺序,返回从右侧所能看到的节点值,可以破题的点在哪里?

首先,可以确定的是,这个题目和树的层次有关,有几层就可以看到几个节点,也就是说,看得的节点数和树的层次数是相等,不管每层有几个节点,只能看到一个。

第二,看到的一定是右子树上的节点吗?虽然示例1里,看到的都是右子树上的1,3,4这三个节点,但是我们稍稍想一下就能知道,如果节点4不存在,我们看到的将是1,3,5这三个节点。

Leetcode_187

但是5什么时候可以看到呢?只有4不存在的时候,如果4是3的右子树,那么不管5是3的左子树,还是2的右子树,2的左子树,都是看不到5的。

所以我们可以先递归的搜索树的右子树,再递归搜索左子树,在搜索的过程中,对于右子树来说,遇到一个右子树的节点,就加入结果集,对左子树来说,只有结果集不存在右子树的节点,才会加入结果集。怎么做到呢?只需要看看当前层高是否和结果集的size大小相等就可以了。

以示例1来举例,比如处理整个树的根结点1,层高为0,结果集的size=0,把根结点加入结果集。往下递归,到了下一层,层高为1,结果集的size=1,又把右子树的根结点3加入结果集,很明显4也要加入结果集,这个时候结果集的size=3。

当递归处理整个树的左子树时,节点2的层高为1,结果集的size=3,节点2不能加入结果集,节点5的层高为2,结果集的size=3,节点5不能加入结果集。

如果节点4不存在,会发生什么情况,当整棵树的右子树遍历完成,结果集的size=2,当处理到节点5时,层高为2,结果集的size=2,应该把节点5加入结果集。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RightSideView {
public static void main(String[] args) {
TreeNode node6 = new TreeNode(6);
TreeNode node4 = new TreeNode(4);
TreeNode node3 = new TreeNode(3);
TreeNode node5 = new TreeNode(5, null,null);
TreeNode node2 = new TreeNode(2, node3,node4);
TreeNode node1 = new TreeNode(1, node2, node5);
new RightSideView().rightSideView( node1);
System.out.println("");
}

public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
toRightSideView( result, root, 0);
return result;
}
public void toRightSideView(List<Integer> result, TreeNode root, int height) {
if(root == null){
return ;
}
if(result.size() == height){
result.add(root.val);
}
toRightSideView( result, root.right, height + 1);
toRightSideView( result, root.left, height + 1);
}
}

(LeetCode-208) 实现 Trie (前缀树)

题目

Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。