栈与队列

  1. 1. 3.6 栈ADT
    1. 1.1. 3.6.1 栈模型
    2. 1.2. 3.6.2 栈的实现
    3. 1.3. 3.6.3 应用
  2. 2. 3.7 队列ADT
    1. 2.1. 3.7.1 队列模型
    2. 2.2. 3.7.2 队列的数组实现

《数据结构与算法分析Java语言描述》第二章3.6与3.7节节读书笔记

3.6 栈ADT

3.6.1 栈模型

栈是插入和删除只能在末端的表,该位置叫栈顶(类似一个桶)。对栈的操作只有进栈(push)与出栈(pop)。

  栈是一个后进先出(LIFO)表。我们对栈能进行的操作基本只有push和pop操作。位于栈顶的元素,是唯一直接可见(可访问)的元素。

3.6.2 栈的实现

  栈是一个表,任何能实现表的的方法都能实现栈,给出两种流行的实现方法:一种使用链式结构,一种使用数组,二者均简化了在ArrayList和LinkedList的实现。

  栈的链表实现
  使用单链表,在表的顶端插入实现push,删除表顶端的元素实现pop,top操作获取表顶端的元素并返回值,有时pop与top合二为一。

  栈的数组实现
  数组的实现避免了链而且可能是最流行的解决方案。push模仿ArrayList的add操作,实现简单。与栈相关的操作是theArray和topOfStack,对空栈,topOfStack为-1(初始化操作),当由元素x入栈时,topOfStack + 1,theArray[topOfStack] = x。 同理弹栈的时候返回theArray[topOfStack] ,之后topOfStack - 1。
  上述操作皆以常数时间运行,再某些机器上push与pop都可以写作一条机器指令,现代化计算机将栈操作作为指令系统的一部分。

3.6.3 应用

  平衡符号
  编译器检查具有成对的符号(如大括号、括号)缺失引起的编译错误。例如[()]是合法的,但是[(])是错误的。对该算法的简单叙述如下:
  将所有字符读入,如果字符是是一个开放符号(成对符号左边的),就将符号入栈(注意是只入栈符号)。如果是封闭符号,当空栈时报错,否则将栈顶的元素弹出,如果弹出的不是对应的开放符号,则报错,如果读到文件尾,栈非空,则报错。该算法时线性的,且他是联机算法。

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
/**
* 平衡符号简化版,leetCode 20 有效括号
* 栈Stack是线程安全的,可以考虑使用非线程安全的Deque,或者使用数组、链表模拟栈
* Deque<Character> stack = new LinkedList<>();
* @param str 需要检测的字符串
* @return
*/
public static boolean isBalanceChar(String str) {
if (str.length() < 2) {
return false;
}

char[] chars = str.toCharArray();
Stack<Character> stack = new Stack<>();
for (int i = 0; i < chars.length; i++) {
if (chars[i] == '(' || chars[i] == '[' || chars[i] == '{') {
stack.push(chars[i]);
} else {
if (stack.isEmpty()) {
return false;
}
// 返回并弹出栈顶的元素,(如果不弹出使用peek)
char c = stack.pop();
// 如果弹出的符号与所有的符号都不匹配,返回false
if ((chars[i] == ')' && c != '(') || (chars[i] == ']' && c != '[') || (chars[i] == '}' && c != '{')) {
return false;
}
}
}
// 最后当所有元素被弹出,则代表true
return stack.isEmpty();
}

  后缀表达式
  在我们使用的四则运算中,我们都知道乘除优先,需要指定优先级时,可以使用括号,但这(中序表达式)对于计算机则非常不友好,于是有了后缀表达式或称逆波兰式。操作符号放在需要参与运算的数的后面,并且与他前面的数参与运算。他的一个显著的优点是不需要知道运算的优先规则。
  例如表达式 6 5 2 3 + 8  + 3 + 6 \ 5 \ 2 \ 3\ + \ 8 \ * \ + \ 3 \ + \ * 的含义:将6、5、2、3入栈,遇到+号,表示+号前面的3+前面的2弹栈,并将得到5入栈。再遇到8,8入栈,接着遇到*号,则*号前面的8与5的弹栈,并将8*5的结果40入栈。。。(即遇到数字就继续入栈,遇到四则符号就将前面的两个数弹栈,并将计算的结果入栈)。上式的计算结果如下。

((((3+2)8)+5)+3)6=288((((3+2) * 8) + 5)+3)*6 = 288

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
/**
* 逆波兰表达式计算 LeetCode剑指Offer 第二版 036 后缀表达式 (除法只取整数)
* 简化版,前提:该逆波兰表达式是正确的,且已经转为数组
*/
public class EvalRPNTest {

public static void main(String[] args) {
String[] tokens = {"2","1","+","3","*"};
int i = evalRPN(tokens);
System.out.println(i);
}

public static int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String token : tokens) {
switch (token) {
case "+" :
case "-" :
case "*" :
case "/" :
// 如果遇到了运算符,获取并弹出栈顶的两个元素
int num2 = stack.pop();
int num1 = stack.pop();
// 计算两个元素并将结果入栈
stack.push(calc(num1, num2, token));
break;
default:
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}

/**
* 计算结果
*/
private static Integer calc(int num1, int num2, String token) {
switch (token) {
case "+":
return num1 + num2;
case "-":
return num1 - num2;
case "*":
return num1 * num2;
default:
return num1 / num2;
}
}
}

  中缀转后缀表达式
  日常使用的中缀表达式可以转为后缀表达式,之后方便计算机进行计算。
范例:a + b * c + (d * e + f) * g ====== a b c *+ d e * f + g *+
逻辑:

  1. 初始化一个栈,用于存放运算符;初始化一个List用于存放数字。List为最终结果
  2. 如果遇到数字,则直接存入List
  3. 如果遇到运算符,则与栈顶的运算符比较,如果优先级比栈顶的高,则入栈,否则(同优先级或者低)将栈顶的运算符弹栈再存入队列,之后再将运算符与栈顶的元素继续比较,直到入栈。(优先级 ‘*’ = ‘/’ > ‘+‘ = ‘-’)。注意运算符不与括号比较,直接入栈。
  4. 如果遇到左括号,则直接入栈,如果遇到右括号,则将栈顶的运算符依次弹出存入队列,直到遇到左括号(不判断栈空的情况:表达式错误),弹出左括号。(注意括号只弹出,不输出)
  5. 如果传入数组遍历完毕,则依次弹出栈顶元素存入List。
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
public class SuffixExpression {

public static void main(String[] args) {
String expression = "1+((23+3)*4)-5";
List<String> expressionList = expressionToList(expression);
System.out.println("中缀表达式字符串转List:" + expressionList);
//将中缀表达式转换为后缀表达式
List<String> suffixList = parseToSuffixExpression(expressionList);
System.out.println("中缀表达式:" + suffixList);
// 使用前面的计算中缀表达式的方法,
// 将List转数组包括泛型:suffixList.toArray(new String[suffixList.size()])
int i = EvalRPNTest.evalRPN(suffixList.toArray(new String[suffixList.size()]));
System.out.println("后缀表达式计算结果:" + i);
}

/**
* 中缀表达式转后缀表达式
* @param expressList 需要转换的List,假设List的中缀表达式是正确的
* @return
*/
public static List<String> parseToSuffixExpression(List<String> expressList) {
// 保存运算符的栈
Stack<String> opStack = new Stack<>();
// 初始化保存数字的List,最终用来保存结果的List
ArrayList<String> suffixList = new ArrayList<>();
for (String item : expressList) {
if (item.matches("\\d+")) {
// 如果是数字,则直接存入List
suffixList.add(item);
} else if ("+".equals(item) || "-".equals(item) || "*".equals(item) || "/".equals(item)) {
// 如果是运算符
if (opStack.isEmpty() || "(".equals(opStack.peek()) || MyPriority.getPriority(item) > MyPriority.getPriority(opStack.peek())) {
// 如果是空栈(先判断避免NPE),或者如果栈顶是左括号,或者如果优先级比栈顶的运算符高
opStack.push(item);
} else {
// 是运算符,且优先级小于或者等于
// 将运算符弹栈,直到遇到左括号或者优先级更低的或者空栈(见下面的while解释)
while (!opStack.isEmpty() && !"(".equals(opStack.peek())) {
// 如果item的优先级小于等于栈顶元素
if (MyPriority.getPriority(item) <= MyPriority.getPriority(opStack.peek())) {
suffixList.add(opStack.pop());
}
}
// 当前操作符入栈
opStack.push(item);
}

} else if ("(".equals(item)) {
opStack.push(item);
} else if (")".equals(item)) {
// 如果是右括号,则弹出栈中元素,直到遇到左括号,
// 假设中缀表达式正确,故不再判断栈是否为空(为空之前一定会遇到左括号)
while (!"(".equals(opStack.peek())) {
suffixList.add(opStack.pop());
}
// 弹出左括号
opStack.pop();
}
}

// 遍历完毕,将栈中的所有运算符弹出
while (!opStack.isEmpty()) {
suffixList.add(opStack.pop());
}
return suffixList;
}

/**
* 字符串转List,不用toCharArray是因为有多位
* @param expression
* @return
*/
public static List<String> expressionToList(String expression) {
int index = 0;
List<String> list = new ArrayList<>();
do{
char ch = expression.charAt(index);
if(ch < 47 || ch > 58){
//是操作符,直接添加至list中
index ++ ;
list.add(ch+"");
}else if(ch >= 47 && ch <= 58){
//是数字,判断多位数的情况
String str = "";
while (index < expression.length() && expression.charAt(index) >=47 && expression.charAt(index) <= 58){
str += expression.charAt(index);
index ++;
}
list.add(str);
}
}while (index < expression.length());
return list;
}

/**
* 优先级,也可以写成方法
*/
private static class MyPriority {

private static final Integer ADD = 1;
private static final Integer SUB = 1;
private static final Integer MUL = 2;
private static final Integer DIV = 2;

// 不需要break,因为已经return
public static Integer getPriority(String opr) {
switch (opr) {
case "+":
return ADD;
case "-":
return SUB;
case "*":
return MUL;
case "/":
return DIV;
default:
return 0;
}
}
}
}

3.7 队列ADT

3.7.1 队列模型

队列的入队发生在队列尾,出队出现在队首。类似排队,只能在队尾增加,队首删除。

3.7.2 队列的数组实现

  队列的数组与链表都可以实现,都是O(1)的时间运行。如下是数组的实现。
顺序队列:初始时,head与tail指针同时指向0,每当新增元素,tail++,删除元素,head–。直到数组尾部。

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
// 顺序队列简单实现,无扩容
public class SeqArrayQueue<T> {

// 队列容量
private int capacity;
// 队列存储元素个数
private int currentSize;

private int head;
private int tail;
private T[] arr;

public SeqArrayQueue(int cap) {
capacity = cap;
currentSize = 0;
head = 0;
tail = 0;
arr = (T[]) new Object[cap];
}
// 入队
public void enqueue(T element) {
if (isFull()) {
throw new ArrayIndexOutOfBoundsException("队列越界");
} else {
arr[tail++] = element;
currentSize++;
}
}

// 出队
public T dequeue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法删除");
}
currentSize--;
return arr[head++];
}

public T getHeadElement() {
if (isEmpty()) {
return null;
} else {
return arr[head];
}
}

public T getTailElement() {
if (isEmpty()) {
return null;
} else {
return arr[tail];
}
}

// 这里判断是否满,之判断是否数组的末尾是否被使用
public boolean isFull() {
return tail == capacity-1;
}

public boolean isEmpty() {
return head == tail;
}
}

循环队列:初始时,head与tail指针同时指向0,每当新增元素,tail++,删除元素,head–。如数组越界,则插入倒队首对应的位置。

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
public class LoopArrayQueue<T> {

// 队列容量
private int capacity;
// 队列存储元素个数
private int currentSize;

private int head;
private int tail;
private T[] arr;

public LoopArrayQueue(int cap) {
capacity = cap;
currentSize = 0;
head = 0;
tail = 0;
arr = (T[]) new Object[cap];
}

// 循环队列改变点
public void enqueue(T element) {
if (isFull()) {
throw new ArrayIndexOutOfBoundsException("队列越界");
} else {
arr[tail] = element;
// 尾指针还是+1,对容量取余:如果越界,就移动到队首对应的位置
tail = (tail+1) % capacity;
currentSize++;
}
}

// 循环队列改变点
public T dequeue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法删除");
}
currentSize--;
T deQueueElement = arr[head];
// 头指针还是+1,对容量取余:如果越界,就移动到队首对应的位置
head = (head + 1) % capacity;
return deQueueElement;
}

public T getHeadElement() {
if (isEmpty()) {
return null;
} else {
return arr[head];
}
}

public T getTailElement() {
if (isEmpty()) {
return null;
} else {
return arr[tail];
}
}

// 循环队列改变点
public boolean isFull() {
// 头指针+1等于尾指针,取余是为了处理越界,放道队首对应的位置
return tail == (head + 1) % capacity;
}

public boolean isEmpty() {
return head == tail;
}
}