栈:栈是限定仅在表尾进行插入和删除操作的线性表。
队列:队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。
栈
允许插入和删除的一端为栈顶(Top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表。
Stack和Vector
Stack继承Vector,Stack也是用数组来实现的。 Stack整个类的方法就5个,push(E item)入栈,pop()出栈,peek()获取栈顶元素,enpty()判断是否为空栈,search(Object o)查询栈内元素位置。每个方法声明上都有synchronized修饰。因此Stack是线程安全的。而大部分的业务逻辑是调用父类Vector的方法实现的。
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//数据存储
protected Object[] elementData;
//元素数量
protected int elementCount;
//扩容增长因子
protected int capacityIncrement;
public Vector(int initialCapacity, int capacityIncrement) {
}
通过源码我们可以看到Vector是由Object[]实现的,扩容因子变量capacityIncrement和数组长度initialCapacity是可以通过构造方法设置的,如果不设置initialCapacity默认是10,capacityIncrement默认是0。
public synchronized E peek() {
...
return elementAt(len - 1);
}
public synchronized E elementAt(int index) {
...
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
peek()方法只是获取栈顶的元素,并不会删除元素,数组的下标0为栈底,数组的下表length-1为栈顶。源码很简单很清晰。
public synchronized E pop() {
...
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized void removeElementAt(int index) {
...
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
pop()出栈是先调用peek()获取栈顶元素,然后elementCount元素数量减1,elementData栈顶元素设置为空。
出栈后,elementData数组的大小没有改变,也就是说没有缩小容量。只是把元素的数量减1了。
public E push(E item) {
addElement(item);
return item;
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
private void ensureCapacityHelper(int minCapacity) {
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
push(E item)入栈直接调用父类Vector的addElement(E obj)方法,然后调用ensureCapacityHelper(int minCapacity)方法判断是否要扩容。注意:elementData.length是数组的长度,elementCount是元素的个数,如果elementCount+1>elementData.length我们需要调用grow(int minCapacity)方法扩容了。如果扩容因子capacityIncrement大于0,就扩容capacityIncrement大小。如果小于0就扩容一倍。如果扩容之后大小还是不够,就用元素数量的大小来扩容。而hugeCapacity(int minCapacity)方法一般是调用不到的,我们不会用到0x7fffffff-8这么大的容量。然后我们回到addElement(E obj)方法,扩容结束后,把obj放入栈顶,整个入栈就结束了。search(Object o)的业务逻辑也很简单,从栈顶往栈底遍历元素,一个一个比较。返回第一个相等元素的坐标。我就不贴源码了。
逆波兰表达式
栈最经典的应用就是逆波兰表达式,逆波兰表达式也叫后缀表达式。例如:783+3*+102/+;而8+(2-1)*10,这是中缀表达式。中缀表达式是我们计算用的,而后缀表达式是计算机采用的。具体运算方式:
中缀表达式运算:7+(8+1)*3+10/2=39;
后缀表达式运算:7 8 1+3*+10 2/+
7 9 3*+10 2/+ 8 1+运算结果为9
7 27+10 2/+ 9 3*运算结果27
34 10 2 /+ 7 27+运算结果34
34 5+ 10 2 /运算结果5
39 34 5+ 运算结果39
后缀运算是碰到符号,把符号前两个数运算。中缀表达式转后缀表达式就是利用的栈结构。让我们来看看是怎么转的。
上图就是中缀表达式转后缀表达式,看着是不是有点迷糊,很难找到规律。其实是有规律的,记住这个口诀:数字输出,运算符进栈,括号匹配出栈,栈顶优先级低出栈。大概意思就是遇到数字就打印,遇到运算符运算符进栈,当匹配到闭合括号时括号内的运算符就出栈,如果栈顶的优先级比下面的低,优先级高的出栈,运算符低的入栈。同级的运算符栈底比栈顶高。最后数字都输出完了栈内符号按优先级全部输出。下面看看计算机是怎么运算的:
队列
队列(queue):只允许在一端进行插入操作,而在另一端进行删除操作的线性表。插入的一段称为队尾,删除的一段称为队头。
队列的顺序结构
缺点:出队复杂度高o(n),容易假溢出。front队头,rear队尾。队列出栈时是把队头置为空,其他数据元素不动。假溢出就是队头还有空间,队尾没有空间。
2. 循环队列
把队列的这种头尾相接的顺序存储结构称为循环队列。
顺序存储效率低,容易假溢出。队列最好的存储方式还是链式存储。
3. 队列的链式存储及结构模式
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已。
如果要出栈只要把头结点指针指向a2,a1就出栈了。如果新元素要入栈的话,只要尾节点指针指向新节点元素就可以了。
查看java源码会发现,Queue是个接口,有趣的是它的实现类有LinkedList,也就是说LinkedList可以当队列使用。你会发现LinkedList里面有offer()入队方法,在队尾添加元素。poll()出队方法,删除队头元素。