一、定义及特点
栈(stack) 是只在表的一端进行插入和删除操作的线性表(线性表指的是元素之间一对一的关系而不是顺序存储与链式存储)。被操作端称为栈顶,另一端称为栈底。
从上面可以看出先入栈的元素被压在栈底后出来,后入栈的元素在栈顶先出来,因此栈有后进先出的特点。
二、栈的两种实现
(一)顺序栈
public class MyStack {
private int[] arr; // 顺序栈底层使用数组
private int maxSize; // 最大容量
private int top; // 栈顶指针
// 构造函数,也是栈的初始化
public MyStack(int maxSize) {
arr = new int[maxSize];
this.maxSize = maxSize;
top = -1;
}
// 判断栈满
private boolean isFull() {
return top == maxSize - 1;
}
// 判断栈空
private boolean isEmpty() {
return top == -1;
}
// 入栈,需要先判断栈是否已满
public void push(int num) {
if (isFull()) {
System.out.println("栈满!");
return;
}
arr[++top] = num;
}
// 出栈,需要先判断栈是否为空
public int pop() {
if (isEmpty()) {
System.out.println("栈空!");
return -1; // 此处 return -1 并不代表返回栈中的真实数值,只作为栈空时返回的标记
}
return arr[top--];
}
// 取栈顶元素,栈顶指针不变
public int getPeak() {
if (isEmpty()) {
System.out.println("栈空!");
return -1;
}
return arr[top];
}
// 输出栈中所有元素,栈底-->栈顶
public void printAll() {
int bottom = 0;
for (; bottom <= top; bottom++) {
System.out.println(arr[bottom]);
}
}
// 获取栈中元素个数
public int getSize() {
return top + 1;
}
}
(二)链栈
public class MyStack {
// 内部类,用于创建新的结点
private class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
// 链栈的底层使用单链表,因为单链表的单向性以及栈的操作只在栈顶端的特点,所以单链表表头为链栈的栈顶
private Node top;
// 统计栈中元素个数
private int size;
// 构造函数,也是初始化,使栈顶指针置空。因为成员变量有默认值,所以构造函数也可以不写
public MyStack() {
top = null;
size = 0;
}
// 入栈,与顺序栈相比不需要判断栈满
public void push(int input) {
Node node = new Node(input);
if (top == null) {
top = node;
} else {
node.next = top;
top = node;
}
size++;
}
// 出栈,需要判断栈是否为空
public int pop() {
if (top == null) {
System.out.println("栈空!");
return -1; // return -1 同样是栈空的标志
} else {
/*
* 下面这段代码还应该释放栈顶元素的空间,但因为java中是引用关系,top置为null只是断开了被引用对象的地址
* 且java自带垃圾回收机制,所以暂不释放
*/
int num = top.data;
top = top.next;
size--;
return num;
}
}
// 取栈顶元素
public int getPeak() {
if (top == null) {
System.out.println("栈空!");
return -1;
} else {
return top.data;
}
}
// 输出栈中所有元素,因为底层使用的单链表,所以只能栈顶-->栈底
public void printAll() {
Node node = top;
while (node != null) {
System.out.println(node.data);
node = node.next;
}
}
// 获取栈中元素总数
public int getSize() {
return size;
}
}
三、栈与递归
(一)递归
简单来说,就是一个函数内部又调用了这个函数本身,调用函数需要被调函数的运行结果才能执行。如下图描述
构成递归的条件:
1.一个问题的解可以分解为几个子问题的解,并且这个问题与分解之后的子问题除了数据规模或是处理对象不同,求解思路完全一样。子问题的数据规模或是处理对象更小且有规律变化。
2.分解必须有限次,即存在递归终止条件也就是递归出口。
(二)递归与递归工作栈
高级语言中,调用函数与被调用函数之间的信息交换通过栈来进行,例如函数A调用函数B,那么首先需要将A的一些变量信息保存起来,再调用函数B,当B被调用完获得返回值后,程序仍要回到函数A处执行,这时必须将B的变量信息pop出来,否则就无法拿到A的变量信息。
通常,当一个函数在运行期间调用另一个函数时,在运行被调用函数之前,系统要先完成3件事:
(1)将所有实参,返回地址等信息传递给被调用函数保存;
(2)为被调用函数的局部变量分配存储区;
(3)将控制转移到被调函数的入口。
而从被调用函数返回调用函数之前,系统也需完成3件事:
(1)保存被调函数的运行结果;
(2)释放被调函数的数据区;
(3)根据被调函数保存的返回地址将控制转移到调用函数。
当有多个函数构成嵌套调用时,按照“后调用先返回”的原则,系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区;每当一个函数退出时,就释放它的存储区;因此当前正在运行的函数的数据区一定在栈顶。
递归函数的运行过程与函数的嵌套调用类似,只不过被调函数是它本身,因此,递归函数一个非常重要的参数是它的递归层次,因为递归层次直接影响系统工作栈的大小。
(三)利用栈将递归转换为非递归
递归函数在执行时是系统提供了隐式栈保存每个函数的信息,而将递归函数改写为非递归其实就是是自己主动开辟一个栈并用这个栈来模拟递归过程。
在递归过程中每个函数被调用时都只存在下面两种情况:
(1)满足执行条件当前必须执行;
(2)不满足执行条件,比如函数内部又调用了其它函数,本函数需要被调函数的返回结果才能执行完成。这种情况需要暂缓执行,需要先将它的局部变量,返回地址等函数信息压入栈中。
因此利用栈消除递归的一般步骤可以总结为:
(1)设置一个工作栈存放递归工作记录(包括实参、返回地址及局部变量等)。
(2)进入非递归调用入口(即被调用程序开始处),将调用程序传来的实参和返回地址入栈(递归程序不可以作为主程序,因而可认为初始是被某个调用程序调用)。
(3)进入递归调用入口:当不满足递归结束条件时,逐层递归,将实参、返回地址及局部变量入栈,这一过程可用循环语句来实现———模拟递归分解过程。
(4)递归结束条件满足,将到达递归出口的给定常数作为当前的函数值。
(5)返回处理:在栈不空的情况下,反复退出栈顶记录,根据记录中的返回地址进行题意规定的操作,即逐层计算当前函数值,直到栈空为止————模拟递归求值过程。
以上手工复制《数据结构C语言第2版》,不太明白的话再参考下这篇博客:博客地址。(很棒的一篇讲解,推荐一看~)
他的描述是:
(1)首先需要自己建个栈。栈保存的东西是一个记录,包括所有局部变量的值,执行到的代码位置。
(2)再将局部变量初始化为一开始的状态,然后进入主循环。
(3)执行代码时,遇到递归,就制作状态压栈保存,然后更新局部变量进入下一层。如果一个调用结束了,就要返回上层状态。直接将栈里的记录弹出,拿来更新当前状态即可。
(4)某个调用结束时如果栈为空则所有调用都结束,退出主循环。
总之,根据上面的步骤可以将任何递归类型改写成非递归。
具体实现详见二叉树的非递归遍历。