栈、队列笔记
一、栈
1、定义:
栈,存储货物或供旅客住宿的地方,可引申为仓库、中转站,所以引入到计算机领域里,就是指数据暂时存储的地方,所以才有进栈、出栈的说法。
栈是一种表类型的数据结构,是一种只能在一段进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进后出的数据被压入栈底(push),最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(pop)。
2、实现:
由于栈是一个表,因此任何实现表的方法都能实现栈。故ArrayList与LinkedList都可以实现栈操作。而笔者通过这两种方式来实现栈这一数据结构:
ArrayList的方式实现:
成员变量:
private MyArrayList theArray;
private int topOfStack;
构造方法:
public Stack_A() {
theArray = new MyArrayList();
topOfStack = -1;
}
由于是由ArrayList实现的栈,因此引用了之前自己写的ArrayList。而topOfStack是栈中指向栈顶元素的指针,对于空栈而言它是-1。
其他方法:
这里只将Stack中的isEmpty()、peek()、push()和pop()方法进行实现,因此以下将详细的描述,而ArrayList类部分的代码请见 ([数据结构] 二、数组笔记_芋yu头的博客-CSDN博客)。
public boolean isEmpty(){
return theArray.isEmply();
}
public T peek(){
return (T) theArray.get(topOfStack); //类型擦除
}
public boolean push(T ele){
topOfStack++;
return theArray.add(ele);
}
public T pop(){
T ele = peek();
theArray.remove();
topOfStack--;
return ele;
}
peek()方法是返回栈顶部的元素,而该方法由于用到了泛型,以此JVM在执行时会进行类型擦除为Object类型,故此时需要向下转型成T类型。
push()方法是将元素压入栈中。为将某个元素ele推入栈中,我们使topOfStack增1然后置theArray[topOfStack] = ele。(这里是调用MyArrayList中的add()方法,在数组末尾添加元素)
pop()方法是将元素弹出栈中。为了弹出栈元素,我们置返回值为theArray[topOfStack] = ele然后使topOfStack减1。(这里是调用MyArrayList中的remove()方法,在数组末尾删除元素)
注意:这些操作都是以常数时间运行,而且是非常快的常数时间运行。因此栈是计算机科学在数组之后的最基本的数据结构。
LinkedList的方式实现:
成员变量:
SingleLinkedList s;
int topOfStack;
这里笔者使用单链表的方式来实现栈结构。
构造方法:
public Stack_L() {
s = new SingleLinkedList();
topOfStack = -1;
}
其他方法:
public boolean isEmply(){
return s.isEmpty();
}
public T peek(){
return (T) s.get(topOfStack); //泛型类型擦除
}
// 后进先出
public boolean push(T ele){
topOfStack++;
return s.add(ele);
}
public T pop(){
T ele = peek();
s.remove(s.size() - 1);
topOfStack--;
return ele;
}
具体的方法与数组实现的方式类似,这里不重复赘述。
3、应用:
- 平衡符号:编译器检查符号是否对称
- 后缀表达式:计算机处理具有优先级的数学符号运算
- 方法调用:当前方法执行完成跳转到另一方法中(递归的调用)
笔者用栈来完成“后缀表达式”的应用,故详细介绍该部分内容
后缀表达式:
逆波兰式(Reverse Polish notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后)。
处理后缀表达式分为两步:将中缀表达式转化为后缀表达式、后缀表达式的计算。
后缀表达式的转化
中缀转化成后缀表达式主要处理符号,即通过栈的方式存储并按照相应规则弹出,数字则按顺序输出。代码如下:
public static ArrayList transfer(String input){
char[] i = input.toCharArray();
Stack<Character> s = new Stack<>();
ArrayList output = new ArrayList();
int idx = 0;
while(i[idx] != ' '){
// 符号处理
if (i[idx] > 39 && i[idx] < 48 || i[idx] == 94){
//防止空栈异常
if (s.empty()){
s.push('O');
}
//小括号的处理.
if (i[idx] == ')'){
while (s.peek() != '('){
output.add(s.pop());
}
s.pop();
idx++;
popStack(i, s, output, idx);
continue;
}
//优先级出栈
while(!priority(i[idx], s.peek())){
output.add(s.pop());
}
//Input中的符号元素若优先级大于栈中的符号,压栈
if (priority(i[idx], s.peek())){
s.push(i[idx]);
}
}
//数字处理
else
output.add(i[idx]);
idx++;
popStack(i, s, output, idx);
}
return output;
}
首先,输入为用户输入的字符串String,输出则将转化为后缀表达式的结果存储在ArrayList。由于该程序主要对于简单的带有整数的中缀表达式的转换,因此这里用到字符char的处理,故将输入的字符串转化成char[]数组;同时使用栈来处理符号。
由于这里涉及到用户输入,因此想到使用ASCII码来区分符号与数字。故查询ASCII码发现数字为48-57。数字计算符号主要的ASCII码范围为40-47。(不包含幂运算情况)
观察代码可发现while循环内分别处理符号与数字,当读到一个操作数时,立即把它放到输出中。操作符不立即输出,而是将其推入推入栈中,由于计算从一个空栈开始,为了防止后面在比较符号时出现EmptyStackException,故先在栈中压入一个“O”。
可观察到这里需要调用了priority()私有方法来判断符号的优先级。该优先级的判断是如果输入的运算符号(不包括括号)不比栈顶元素的优先级低,则将其压入栈中。例如中缀表达式子为 a + b * c,则后缀表达式为a b c * +, 乘法的运算优先级比加法的运算优先级高,故将乘号压入栈中。
如果输入的运算符号优先级比栈顶元素的优先级低,则从栈顶开始逐一将比输入符号的优先级低的弹出栈,并输出。例如a / b * c + d,先将除号压入栈中,当乘号输入时,比较乘号与除号具有相同的优先级,故乘号压入栈中;而当加号输入时,由于加号的优先级比乘除低,故从栈顶开始将元素弹出,结果为a b c * /,此时栈为空,故将加号压入栈中。
现在来看括号,若是左括号( ,则其具有最高优先级,执行压栈操作,且括号内的操作符不按照以上的优先级,而是只有输入为右括号时,才执行弹栈操作,并且将栈元素直到(所有元素弹出(括号内的操作符按照先后顺序压入栈中)。例如中缀表达式为a * ( b *c + d ),首先,将乘号压入栈中,当输入为左括号时,由于左括号优先级最高,故也将其压入栈中。而括号内的符号按照顺序压入栈中,故先将乘号压入栈中,输入为加号时不需要将乘号弹出,而是继续将加号压入栈中,直到出现右括号时把直到左括号内所有的操作符依次弹出栈(这里不输出括号)。后缀表达式为:a b c * d + *。
若输入为空时,将栈中的符号全部弹出并输出,直到栈变成空栈。
实现过程中调用了私有方法priority()方法,笔者这里通过量化来比较操作符的优先级。
// 当栈内无元素时,添加O防止空栈异常。
private static boolean priority(char a, char b){
int ap = 0;
int bp = 0;
if (a == '*' || a == '/'){
ap += 1;
}
if (b == '*' || b == '/'){
bp += 1;
}
if (a == '^'){
ap += 3;
}
if (b == '^'){
bp += 2;
}
if (a == '('){
ap += 4 ;
}
if (b == '('){
bp -= 4;
}
if (b == 'O'){
bp -= 1;
}
return ap > bp;
}
后缀表达式的计算:
后缀表达式的计算规则为当见到一个数就把它推入栈中;在遇到一个运算符时该算符就作用在该栈弹出的两个数(符号)上,再将所得结果推入栈中。笔者通过对各个符号的ASCII码来判断执行相应的运算操作。
public static int calculate(ArrayList<Character> output){
Stack<Integer> s = new Stack();
for (int idx = 0; idx < output.size(); idx++){
if (output.get(idx) > 47 && output.get(idx) != 94){
s.push(output.get(idx) - 48);
}
else if (output.get(idx) == 43){
int a = s.pop();
int b = s.pop();
s.push(a + b);
}
else if (output.get(idx) == 45){
int a = s.pop();
int b = s.pop();
s.push(b - a);
}
else if (output.get(idx) == 42){
int a = s.pop();
int b = s.pop();
s.push(a * b);
}
else if (output.get(idx) == 47){
int a = s.pop();
int b = s.pop();
s.push(b / a);
}
else if (output.get(idx) == 94){
int a = s.pop();
int b = s.pop();
s.push((int) Math.pow(b, a));
}
}
return s.pop();
}
补充:
幂运算也在该程序之中,其不同点是:有些操作符是从左到右结合的,如:a - b - c 转化为 ab - c - ,而幂运算是从右到左结合的:
2
(
2
2
)
=
2
8
=
256
2^(2^2) = 2^8=256
2(22)=28=256
(代码中的体现为输入的的优先级比栈中的的低。)
二、队列
1、定义:
队列也是表,其使用队列时插入在一端进行而删除则在另一端进行。其核心为“先进先出”。
队列的基本操作是enqueue,它是在表的末端插入一个元素,和dequeue,它是删除在表的开头的元素,如图所示:
2、队列的数组实现:
由于在java中有专门实现队列的接口Queue,且其实现的原理于List不太相同,因此这里只是简单的模拟普通的队列:
对于每一个队列数据结构,我们保留一个数组theArray以及位置front和back,它们代表队列的两端。同时记录实际存在于队列的元素的个数currentSize。
为了使一个元素ele入队(enqueue),则让currentSize和back增1,然后置theArray[back] = ele。
若使元素出队(dequeue),我们置返回值为theArray[front],且currentSize减1,然后使front增1。
为了避免不停的入队使底层的数组容量溢出,故这里设置了一个循环数组,即只要front或back到达数组的尾端,它就又回到开头。
具体实现代码如下:
public class Queue<T> {
private T[] theArray;
private static final int CAPACITY = 10;
private int currentSize;
private int front;
private int back;
public Queue() {
theArray = (T[]) new Object[CAPACITY];
currentSize = 0;
front = 0;
back = CAPACITY - 1;
}
public boolean isEmpty(){
return currentSize == 0;
}
public int size(){
return CAPACITY;
}
// 先进先出
public void enqueue(T ele){
back++;
if (back == CAPACITY){
back = 0;
}
theArray[back] = ele;
currentSize++;
}
public T dequeue(){
if (currentSize == 0){
new Queue();
}
if (front == CAPACITY){
front = 0;
}
currentSize--;
return theArray[front++];
}
}
3、队列的应用:
-
文件服务器:使用计算机的用户按照现到先使用的原则访问文件
-
排队论。
} theArray[back] = ele; currentSize++;
}
public T dequeue(){
if (currentSize == 0){
new Queue();
}
if (front == CAPACITY){
front = 0;
}
currentSize–;
return theArray[front++];
}
}
### 3、队列的应用:
- 文件服务器:使用计算机的用户按照现到先使用的原则访问文件
- 排队论。