关联文章:
Java数据结构与算法解析(一)——表
栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈顶。对栈的基本操作有push(进栈)和pop(出栈),对空栈进行push和pop,一般被认为栈ADT的一个错误。当push时空间用尽是一个实现限制,而不是ADT错误。栈有时又叫做LIFO(后进先出)表。
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表
栈的顺序存储结构
栈的数序结构可以使用数组来实现,栈底是:下标为0的一端
栈的实现
栈的实现,一般分为两种形式,链式结构和数组。两者均简化了ArrayList和LinkedList中的逻辑。
抽象出栈的必备接口
public interface Stack {
boolean isEmpty();
void push(T data);
T pop();
int size();
}
栈的数组实现形式
public class ArrayStack implements Stack, Iterable {
private T[] mArray;
private int mStackSize;
private static final int DEFAULT_CAPACITY = 10;
public ArrayStack(int capacity) {
if (capacity < DEFAULT_CAPACITY) {
ensureCapacity(DEFAULT_CAPACITY);
} else {
ensureCapacity(capacity);
}
}
public boolean isEmpty() {
return mStackSize == 0;
}
public int size() {
return mStackSize;
}
public void push(T t) {
if (mStackSize == mArray.length) {
ensureCapacity(mStackSize * 2 + 1);
}
mArray[mStackSize++] = t;
}
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
T t = mArray[--mStackSize];
mArray[mStackSize] = null;
//调整数组的大小,防止不必要的内存开销
if (mStackSize > 0 && mStackSize < mArray.length / 4) {
ensureCapacity(mArray.length / 2);
}
return t;
}
private void ensureCapacity(int newCapacity) {
T[] newArray = (T[]) new Object[newCapacity];
for (int i = 0; i < mArray.length; i++) {
newArray[i] = mArray[i];
}
mArray = newArray;
}
@Override
public Iterator iterator() {
return null;
}
private class ArrayStackIterator implements Iterator {
@Override
public boolean hasNext() {
return mStackSize > 0;
}
@Override
public T next() {
return
mArray[--mStackSize];
}
}
}
对象游离
Java的垃圾收集策略是回收所有无法被访问对象的内存,如果我们pop()弹出对象后,不调用如下代码,就会造成游离,因为数组中仍然持有这个对象的引用,保存一个不需要的对象的引用,叫做游离。
mArray[mStackSize] = null;
动态调整数组大小
pop()中,删除栈顶元素后,如果栈的大小小于数组的1/4,就将数组的大小减半,这样栈永远不会溢出,使用率也不会小于1/4。
采用链式存储结构的栈,由于我们操作的是栈顶一端,因此这里采用单链表(不带头结点)作为基础,直接实现栈的添加,获取,删除等主要操作即可。
链栈的出入栈操作
链栈的入栈操作:
“`
public class LinkedStack implements Stack, Iterable {
private int mSize;
private Node endNote;
private int modCount;
public LinkedStack() {
init();
}
private void init() {
endNote = new Node(null, null);
modCount++;
}
@Override
public boolean isEmpty() {
return mSize == 0;
}
@Override
public void push(T data) {
Node newNote = new Node(data, null);
endNote.mNext = newNote;
mSize++;
modCount++;
}
@Override
public T pop() {
if (endNote.mNext == null) {
throw new NoSuchElementException();
}
T t = endNote.mNext.mData;
endNote.mNext = endNote.mNext.mNext;
mSize--;
modCount++;
return t;
}
@Override
public int size() {
return mSize;
}
@Override
public Iterator iterator() {
return new LinkedStackIterator();
}
private static class Node {
private Node mNext;
private T mData;
public Node(T data, Node next) {
mData = data;
mNext = next;
}
}
private class LinkedStackIterator implements Iterator {
private Node currentNode = endNote.mNext;
private int expectedModCount = modCount;
@Override
public boolean hasNext() {
return currentNode != null;
}
@Override
public T next() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (!hasNext()) {
throw new NoSuchElementException();
}
T t = currentNode.mData;
currentNode = currentNode.mNext;
return t;
}
}
}
顺序栈复杂度
操作 | 时间复杂度 |
---|---|
空间复杂度(用于N次push) | O(n) |
push() | O(1) |
pop() | O(1) |
isEmpty() | O(1) |
链式栈复杂度
操作 | 时间复杂度 |
---|---|
空间复杂度(用于N次push) | O(n) |
push() | O(1) |
pop() | O(1) |
isEmpty() | O(1) |
可知栈的主要操作都可以在常数时间内完成,这主要是因为栈只对一端进行操作,而且操作的只是栈顶元素。
我们在小学学习的四则运算表达式就是中缀表达式 ,但是计算机是不认识中缀表达式的,它采用的是后缀表达式
计算规则:
它的规则是,从头开始遍历,遇到数字进行压栈,遇到运算符号,将栈顶开始的两个元素进行运算符操作后,弹栈,结果进栈,931遇到“—”时,进行3-1=2,将2进栈,然后3进栈,遇到“*”,3*2=6进栈,遇到“+”,进行9+6=15进栈,然后10和2进栈,遇到“/”,进行10/2后结果进栈,最后是15+5=20,就完成了后缀表达式的计算操作。
中缀表达式转后缀表达式
数字输出,运算符进栈,括号匹配出栈,是当栈顶是运算符时,又压进来一个运算符,如果压进来的运算符优先级比栈顶的高,则这个压进来的运算符出栈。
如果我们见到任何其他的符号(+,*,(),那么我们从栈中弹出栈元素直到发现优先级更低的元素为止。有一个例外:除非是在处理一个)的时候,否则我们决不从栈中移走(。对于这种操作,+的优先级最低,而(的优先级最高。当从栈弹出元素的工作完成后,我们再将操作符压入栈中。