一、栈的介绍
栈是限定仅在表尾进行插入和删除操作的线性表
允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表
和单链表,LinkedList并没有太多的区别,唯一的不同,就是在使用上有更多的限制,只允许在一头进行删除和插入。
数组和链表都能实现栈。(所以栈不是数组)
二、栈的实现之顺序方式 — Stack源码分析
只能进行尾插和尾删
栈满,以2倍的方式扩容
public class Stack<E> extends Vector<E> {
protected int capacityIncrement;
// 入栈,压栈:把数据放进去的过程
public E push(E item) {
addElement(item); // 调用了Vector的addElement
return item;
}
// 出栈:线程安全
public synchronized E pop() {
E obj;
int len = size(); // 栈的大小
obj = peek();
removeElementAt(len - 1); // 删除最后一个元素:调用了Vector的removeElementAt
return obj;
}
// 只是获取出栈数据,并不操作
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
}
// Vector 和 ArrayList 的关系:JDK1.0 Vector是线程安全的动态数组;JDK2.0后ArrayList替换掉了Vector
public class Vector<E> {
public synchronized void addElement(E obj) { ... }
public synchronized boolean removeElement(Object obj) { ... }
// 添加元素到尾部
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj; // elementCount:图解中的top,就是头指针
}
// 数组扩容的方式
private void ensureCapacityHelper(int minCapacity) {
if (minCapacity - elementData.length > 0) // 如果不够放了,那么就扩容,扩容方式和ArrayList一样
grow(minCapacity);
}
/**
* 1、数组的扩容:和ArrayList 一样,ArrayList是1.5倍扩容,Stack是2倍扩容
* 2、数组的拷贝
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
// capacityIncrement默认值是0 newCapacity = oldCapacity + oldCapacity
// newCapacity是一倍一倍增长的
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity); // 数组的拷贝
}
// 删除指定位置的元素
public synchronized void removeElementAt(int index) {
// --- 容量检测开始 -----
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
// --- 容量检测结束 -----
elementCount--; // 栈顶指针往下移一个
elementData[elementCount] = null; // 找到这个元素,置为空
}
}
三、栈的实现之链式方式
1、链式方式内存空间肯定要比顺序方式节省很多,不需要扩展空间,执行效率高。
2、Java虚拟机代码压栈的时候,一段一段的代码都是通过链式方式放进去的,只需要从一段代码的起点到终点压栈。
3、链式方式内存空间不用连续。
(一)、入栈
1、入栈,压栈:添加元素到栈内
public void add(E e) {
Node<E> newNode = new Node(e, stack.top);
stack.top = newNode;
size++;
}
2、【实操】单链表倒序
链表逆序的过程,就是链表不断进栈的过程。
public void invertedOrder() {
Node s = first;
stack.top = s; // 栈顶等于获取到的first节点
size++;
s = null;
first = fist.next; // fist向后移动
}
(二)、出栈
public void pop() {
Node s = stack.top;
stack.top = s.next; // top指针向下移动
s = null; // 释放s节点
size--;
}
四、逆波兰表达式
- a / 2; a * 2 这个是需要进栈,我们使用c、c++、Java这些高级语言执行这段代码,都需要6个以上的指令集才能完成操作。
- a >> 1; a << 1; 只需要1个指令集(直接调汇编的指令)即可完成操作。
高级语言计算乘除等表达式,用的是逆波兰表达式。
总结:现在使用的计算机,1秒钟几百万次以上的操作,高级语言在使用的时候,并不能体现出来,但是比如要硬件开发,激光头精确定位,就要采用位移,汇编指令去写这个运算表达式了。
【例1】9 + ( 3 - 1 ) * 3 + 10 / 2 计算机中的体现
标准四则运算表达式—中缀表达式,计算机会先将这个表达式,转换成后缀表达式来进行计算。
注:高级语言不是直接操作硬件,在后台,从源码中,取出一个个字符,然后转化数字,转二进制编码,然后采用逆波兰表达式计算。
- 1)后缀表达式转换过程:数字输出,运算符进栈,括号匹配出栈,比栈顶优先级低就出栈
结果:9 3 1 - 3 * + 10 2 / +
- 2)计算过程:1、数字入栈 2、符号就取2个进行计算(先取出来的放在操作符的右边,后取的放在操作符的左边),再入栈
【例2】a+b 需要6个指令集
- 1)转换为后缀表达式:a b + ==> 3个指令集
- 2)计算结果:a + b ==> 3个指令集
五、递归
(一)递归的解析
程序调用自身的编程技巧称为递归(recursion)。 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法, 它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的能力在于用有限的语句来定义对象的无限集合。 一般来说,递归需要有边界条件、递归前进段和递归返回段。 当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
- 代码栈(操作系统的栈)也采用链式结构存储的。
- 将代码放到代码栈里面会消耗性能,这个是递归的弊端。
- 递归没有出口,就是死循环,会造成栈内存溢出。
(二)递归的应用
1、求1x2x3x4x5… n!
/**
* n! = n * (n-1)! = n * (n-1) * (n-2)!
* @param n
* @return
*/
public int multiply(int n) {
if (n <= 1) {
return 1;
} else {
return n * multiply(n - 1);
}
}
2、斐波那契数列
有一堆兔子,每过三个月会再生一堆兔子:前两项都为1,后面是前面两项之和
1 1 2 3 5 8 13 21 34 55 89 144 …
(这个用动态规划的效率是最快的,但是此处先用递归来解决。)
/**
* 斐波那契数列
*
* @param n
* @return
*/
public int fibonacciSequence(int n) {
if (n == 1 || n == 2) {
return 1;
} else {
return fibonacciSequence(n - 1) + fibonacciSequence(n - 2);
}
}
- 注:类似生成了一颗树,树会在后续文章中讲解(递归和树之间有着千丝万缕的关系)。
3、汉诺塔算法
有三根柱子,需要将将第一根柱子上的盘子,全部移动到第三根柱子上,要求小盘子只能放在大盘子上面,并且一次只能移动一个。
- 【思路图解】:
/**
* 汉诺塔算法
*
* @param n 总共n个盘子
* @param start 开始柱子
* @param middle 中介柱子
* @param end 结束柱子
*/
public void hanoi(int n, int start, int middle, int end) {
if (n == 1) {
System.out.println(start + " --> " + end); // 将第1个盘子,从start位置,移动到end位置
return;
} else {
// 目标:start --> middle 因此【start】= start, 【middle】(中介) = end,【end】= middle
hanoi(n - 1, start, end, middle); // 将n-1个盘子移动到第二个柱子,从start位置,通过end位置,移动到middle位置
System.out.println(start + " --> " + end); // 将第n个盘子移动到最后一个柱子,从start位置,移动到end位置
// 目标:middle --> end 因此【start】= middle, 【middle】(中介) = start,【end】= end
hanoi(n - 1, middle, start, end); // 将n-1个盘子移动到最后一个柱子,从middle位置,通过start位置,移动到end位置
}
}
- 运行结果: