N日一篇——Java实现栈
Posted 从零开始的智障生活
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了N日一篇——Java实现栈相关的知识,希望对你有一定的参考价值。
栈详解:数据结构第五篇——栈和队列_从零开始的智障生活的博客-CSDN博客
目录
一、顺序栈
1.1 用接口表示顺序栈抽象数据类型
public interface SequentialStack
public void setup(int size);// 创建指定大小顺序栈
public void push(Object ob); //压栈
public Object pop(); // 出栈并返回出栈值
public Object topValue();// 返回栈顶值
public boolean isEmpty(); // 判断栈是否为空
1.2 带头结点的顺序栈实现
// 把索引为1的位置作为栈尾,顺序栈中索引为0的位置的数据无效,即初始化的时候,第一个结点作为无效结点
public class SStackwithHead implements SequentialStack
private static final int DEFAULTSIZE = 10;
private int size;
private int topIndex;
private Object[] stack;
public SStackwithHead()
setup(DEFAULTSIZE+1);
public SStackwithHead(int sz)
setup(sz+1);// 有效数据应当是stack[1...sz],还有无效结点stack[0],故sz+1
@Override
public void setup(int sz) // 初始化链式表
size = sz; // 容量
topIndex = 0;// 第一个结点,即索引为0的结点作为无效结点,作为头结点,第一个有效节点索引是1
stack = new Object[sz];
@Override
public void push(Object ob)
if(topIndex<size)// topIndex代表第topIndex个有效数据
stack[++topIndex] = ob; // 先执行++topIndex
@Override
public Object pop()
if(!isEmpty())
return stack[topIndex--];// 先执行stack[topIndex]
return null;
@Override
public Object topValue()
if(!isEmpty())
return stack[topIndex];// 返回栈顶值
return null;
@Override
public boolean isEmpty()
if(topIndex==0)
return true;
return false;
1.3 不带头结点的顺序表实现
// 把索引为0的位置作为栈尾,即栈中所有数据都是有效数据,属于不带头结点,即没有一个无效数据点
// 对于顺序栈而言,不会有线性表的插入删除操作需要花费O(n)的时间复杂度的问题,因为只会在栈顶插入
public class SStackwithoutHead implements SequentialStack
private final static int DEFAULTSIZE = 10;
private int size; // 栈容量
private Object[] stack;
private int topIndex; // 栈顶位置,从0索引,同时topIndex+1表示栈大小
public SStackwithoutHead()
setup(DEFAULTSIZE);
public SStackwithoutHead(int size)
setup(size);
@Override
public void setup(int sz) // 初始化链式表
size = sz;
topIndex = -1; // 不带头结点,所以初始化为topIndex为-1
stack = new Object[sz];// 用sz个对象大小来存储有效数据
@Override
public void push(Object ob)
if(topIndex<size)
stack[++topIndex] = ob;// 先执行++topIndex
@Override
public Object pop()
if(!isEmpty())
return stack[topIndex--];// 先执行stack[topIndex]
else
return null;
@Override
public Object topValue()
if(!isEmpty())
return stack[topIndex];
else
return null;
@Override
public boolean isEmpty()
if(topIndex<0)
return true;
else
return false;
1.4 测试带头结点与不带头结点的顺序栈
public class TestSS
public static void main(String[] args)
char[] cArray = 'a','b','c','d';
// 创建两个顺序表对象
SStackwithoutHead sswithoutHead = new SStackwithoutHead();
SStackwithHead sswithHead = new SStackwithHead();
// 对两个顺序栈压栈
for(char c:cArray)
sswithoutHead.push(c);
sswithHead.push(c);
System.out.println("压栈"+c);
// 对不带头结点的顺序栈压栈
while(!sswithoutHead.isEmpty())
System.out.println("不带头结点出栈"+sswithoutHead.pop());
while(!sswithHead.isEmpty())
System.out.println("带头结点出栈"+sswithHead.pop());
运行结果:
压栈a
压栈b
压栈c
压栈d
不带头结点出栈d
不带头结点出栈c
不带头结点出栈b
不带头结点出栈a
带头结点出栈d
带头结点出栈c
带头结点出栈b
带头结点出栈a
二、链栈
2.1 用接口表示链栈抽象数据类型
public interface LinkedStack
public void setup(); // 初始化链式表
public void push(Object ob); // 压栈
public Object pop(); // 出栈
public Object topValue(); // 栈顶值
public void clear(); // 清除栈
public boolean isEmpty(); // 判断是否为空
2.2 用简单链表类型(无功能)表示链栈的每个结点类型
// 用单链表来实现链栈存储数据的基本功能,相当于顺序栈中的数组
public class Link
private Object element; // 数据元素
private Link next; // 下一节点指针
public Object getElement()
return element;
public void setElement(Object element)
this.element = element;
public Link getNext()
return next;
public void setNext(Link next)
this.next = next;
// 设置下一节点
public Link(Object it,Link nextNode)
element = it; // 定义数据元素
next = nextNode; // 设置下一节点
2.3 不带头结点的链栈(带头结点的链栈毫无意义)
// 链栈可以选择在链式表的表头或表尾进行插入删除,如果在表尾时间复杂度为O(n),故选择在表头,即头插法实现链栈
// 不带头结点的链式表实现。即头结点为无效数据。
public class LStackwithoutHead implements LinkedStack
private Link top; // Link有两个数据项,element和指向下一节点的指针
public LStackwithoutHead() // 创建链栈
setup();
public LStackwithoutHead(int sz) // 创建链栈,忽略sz
setup();
@Override
public void setup() // 初始化不带头结点的链栈
top = null; // 由于不带头结点,所以链栈一个开始不占任何空间
@Override
public void push(Object ob)
// 将旧top作为新top的下一节点,并为新top的数据域赋值
top = new Link(ob,top);// 这个方式可以好好参考
@Override
public Object pop()
if(!isEmpty())
Object it = top.getElement();// 获取出栈值
top = top.getNext();//出栈
return it;
return null;
@Override
public Object topValue()
return top.getElement();
@Override
// 这与C语言C++不同,java有独特的垃圾收集器GC,所以不要程序员特定去释放内存
public void clear()
top = null;
@Override
public boolean isEmpty()
return top == null;
2.4 测试不带头结点的链栈
public class TestLS
public static void main(String[] args)
LStackwithoutHead stack = new LStackwithoutHead();
char[] cAarry = 'a','b','c','d';
for(char c:cAarry)
stack.push(c);
System.out.println("压栈:\\t"+c);
while(!stack.isEmpty())
System.out.println("栈顶值:\\t"+stack.topValue());
System.out.println("出栈:\\t"+stack.pop());
运行结果:
压栈: a
压栈: b
压栈: c
压栈: d
栈顶值: d
出栈: d
栈顶值: c
出栈: c
栈顶值: b
出栈: b
栈顶值: a
出栈: a
三、递归的实现
大多数的程序设计语言都有子程序调用。子程序调用通过把关于子程序的必要信息(包括返回地址、参数、局部变量)存储到一个栈中来实现。详细内容可以参考上面给出的链接。
即递归会有两个问题:
1. 递归太多很容易导致栈溢出;
2. 递归容易造成重复调用,如Fibonacci数列f(n)=f(n-1)+f(n-2)。
3.1 阶乘的非递归实现
这里用栈来代替递归实现阶乘,实际上,阶乘函数的一般递归迭代实现比栈迭代实现简单快捷。
import com.zyx.stack.linkedstack.LStackwithoutHead;
public class Example
// 用栈的迭代代替递归的迭代实现阶乘
public static long factorial(int n) // 要求 0<=n<21
if(n<21 && n>=0)
LStackwithoutHead stack = new LStackwithoutHead();
while(n>1)
stack.push(new Integer(n--));// 栈顶到栈尾:2、3、...、n-1、n
long result = 1;
while(!stack.isEmpty())
result = result * ((Integer)stack.pop()).longValue();
return result;
return -1;
public static void main(String[] args)
System.out.println("用栈的迭代实现借阶乘:");
for(int i=1;i<21;i++)
System.out.println("n="+i+"\\t结果:\\t"+factorial(i));
运行结果:
用栈的迭代实现借阶乘:
n=1 结果: 1
n=2 结果: 2
n=3 结果: 6
n=4 结果: 24
n=5 结果: 120
n=6 结果: 720
n=7 结果: 5040
n=8 结果: 40320
n=9 结果: 362880
n=10 结果: 3628800
n=11 结果: 39916800
n=12 结果: 479001600
n=13 结果: 6227020800
n=14 结果: 87178291200
n=15 结果: 1307674368000
n=16 结果: 20922789888000
n=17 结果: 355687428096000
n=18 结果: 6402373705728000
n=19 结果: 121645100408832000
n=20 结果: 2432902008176640000
3.2 汉诺塔的递归实现
有三根相邻的柱子,标号为A、B、C,A柱子从上到下按金字塔状放着n个大小不同的圆盘,要把所有盘子全部移动到柱子C上,并且每次移动同一个柱子都不能出现大圆盘在小圆盘上的情形,请问怎么移,以及至少需要多少次移动,设移动次数为M(n)?
这其实有点数据建模的意思。开始先给个起始状态,然后蓝色表示从哪来,红色是到哪去,其实是执行了一次move(蓝,红)操作。
当只有1个圆环:进行了一次Move操作
当只有2个圆环:进行了3次move操作
当只有3个圆环:进行了7次move操作
可以发现中间有许多感觉重复的步骤,将其进行总结,发现规律。虽然最终的目的是将A上的数字按照规则全部移动到C上。但是我们可以明显发现。
(1)如果要将某个圆环移动到目的地,那么必须将该圆环上面的圆环全部移动到【非目的地】的另一个柱子上。
- 即如果要实现最终目标,我们必须将A的最底层圆环之上的所有圆环,先放到B上,然后将A上最后一个圆环,放到C上。
- 这时A上已经空。而剩余圆环全在B上,C上有了一个圆环,那么就要将B的最底层上的所有圆环,放到A上,然后将B上最后一个圆环,放到C上。
- 重复1、2步骤直到结束。
(2)如果要将某个圆环移动到目的地,首先必须解决如何将该圆环上面的所有圆环移动到【非目的地】的另一个柱子上。
- 如果A上有3个圆环,那么我们必须首先将B作为目标,在A上按有2个圆环的情形执行以B为目的地的步骤,这样就实现了(1)的条件,将A的第三个圆环Move到C上。
- 然后B上有2个圆环,那么我们必须首先将A作为目标,在B上按有1个圆环的情形执行以A为目的地的步骤,这样就实现了(1)的条件,将B的第二个圆环Move到C上。
- 最后A上有1圆环,那么我们必须首先将B作为目标,在A上按有0个圆环的情形执行以B为目的地的步骤,然而,0个圆环表示无效执行,但也同样实现了(1)的条件,将A上仅有的一个圆环移动到C上。
- 如果A上有n个圆环,那么我们必须首先将B作为目标,在A上按有n-1个圆环的情形执行以B为目的地的步骤。由1可以推得:
- 源头给定n个圆环。如果n=0,退出。
- 将源头上的n-1个圆环,以【非目标的另一柱子】为目标移动。
- 然后对源头与【原目标】进行一次Move操作。
- 剩余的n-1个圆环回到步骤1。
递归实现汉诺塔模型。
// 汉诺塔
public class Example_Hanoi
// 递归调用实现汉诺塔过程
// 把start最上面的圆盘移动到goal上
public static void move(char start,char goal)
System.out.println("from\\t"+start+"\\tto\\t"+goal);
public static void hanoi_recursive(int n,char start,char temp,char goal)
if(n==0)// 初始情况,可以直接解决,而不需要再次递归的情形。
return;
hanoi_recursive(n-1, start, goal, temp);// 递归调用n-1圆环
move(start,goal);
hanoi_recursive(n-1, temp, goal, start);// 递归调用n-1圆环
// 用栈实现汉诺塔过程
public static long hanoi_stack()
return -1;
// 测试过程
public static void main(String[] args)
hanoi_recursive(3,'A','B','C');
运行结果:
from A to C
from A to B
from C to A
from A to C
from B to C
from B to A
from C to B
可以对照上面图片发现一致。
递归的实际过程,往往比逻辑过程复杂很多,所以,很多时候,将递归的模型给抽象出来是很困难的。
其实可以将上面的递归过程总结理解为:
- 针对小的情形进行归纳。
- 这个过程中一般可以找出递归终止条件。
- 这个过程一般可以确定每次递归时,要进行的操作。比如这里的move函数,仅仅只是打印start与goal,这样的静态的操作,并没有进行插入、删除、加减乘除动态操作。
- 这个过程也可以确认,最终要实现的目标。比如这里就是要打印其过程。
- 假设n的情形进行考虑。只要将规模为n的情形的逻辑规律描述出来即可。比如让源头带有n个项,那么我要不要依次考虑n-1,n-2...的情形?不用!!!
那么正好顺手来考虑一下二阶Fibonacci:F(n) = F(n-1)+F(n-2)。
选择当n=3时来考虑:即F(3),要进入F(2),F(1),由F(2),又要进入F(1),F(0),由F(1)又要进入F(0),F(-1),……这样子无止尽下去。
所以要设定一个递归终止条件,如当n=0时F(0)返回0,那么就要求输入的n必须>=0,或当n=1时F(1)返回1,那么就要求输入的n必须>=1,更甚者或当n=100是F(100)返回1,这就要求n>=100。
最重要实现的目标:直接返回计算结果,或者加上计算过程。
要进行的操作:直接返回计算结果,即返回值设为计算结果返回即可,即 return result;或者再加上打印计算过程,用打印函数打印F(n-1)+F(n-2)。
然后将规模为n的情形的逻辑规律描述出来:
- 输入规模为n;终止条件:设当n=1时,F(1)=1,当n=0时,F(0)=0;
- 计算F(n-1)+F(n-2);中间可能有需要执行的操作。
- 将n-1和n-2重新代入到步骤1执行;
public class Example_Fibonacci
// 最终目标:获取
public static long Fibonacci(int n)
if(n<0) return -1;
if(n==0) return 0;
if(n==1) return 1;
// return Fibonacci(n-1)+Fibonacci(n-2);
// 如果需要打印过程可以这样
// 如果设置临时变量的话,递归过程会开出大量临时空间
long result = Fibonacci(n-1)+Fibonacci(n-2);
// 打印执行运算过程
// System.out.println(Fibonacci(n-1)+"\\t+\\t"+Fibonacci(n-2)+"\\t=\\t"+result);
return result; // Java是这样规定的吗?没有这条注释,它要求return后不能加;。因为;会作为独立的语句,而且会报错
// 如果有这条注释,则return后必须加;。好诡异的感觉关键是我也没再Java其他代码中见过有这种变态要求啊?
public static void main(String[] args)
for(int i=1;i<10;i++)
System.out.println(Fibonacci(i));
运行结果:
1
1
2
3
5
8
13
21
34
3.3 汉诺塔的非递归实现
上面递归实现的汉诺塔有两个递归调用,第一个把n-1个圆盘放到非目标柱的一个柱子上,第二个接着对这n-1个圆盘放到目标柱上。为了消除递归,我们用栈来存储代表Hanoi的必须执行的三个操作两个递归调用和一个移动操作。用一个类来表示这三个不同的操作:
public class HanoI
public int operation; // 操作
public int num; // 要移动的盘子数
public char start,temp,goal; // 三根柱子
// 保存当前汉诺塔的状态:【一个HANOI操作,当前剩余未放到目标柱圆盘的数量,三个柱子】
public HanoI(int o,int n,char s,char t,char g)
operation = o;num = n;start=s;temp=t;goal=g;
// 保存移动操作的状态:【一个MOVE操作,源柱子,目标柱子】
public HanoI(int o,char s,char g)
operation = o;start=s;goal=g;
这个类可以Hanoi的两个状态,一个是汉诺塔状态,和移动操作的状态,而汉诺塔状态根据上面递归的方法中,可以看出一个形式可以表示两个意义,在加上移动操作的状态可以表示为三个状态:
- 将当前n个圆盘的前n-1个圆盘,移动到非目标的柱子后的状态;
- 要进行Move时所需的状态;
- 继续处理剩余n-1个圆盘的状态。
这里同时将递归方法实现的汉诺塔方法放在一起,方便对比。
import com.zyx.stack.squentialstack.SStackwithoutHead;
// 汉诺塔
public class Example_Hanoi
// 递归调用实现汉诺塔过程
// 把start最上面的圆盘移动到goal上
public static void move(char start,char goal)
System.out.println("from\\t"+start+"\\tto\\t"+goal);
public static void hanoi_recursive(int n,char start,char temp,char goal)
if(n==0)// 初始情况,可以直接解决,而不需要再次递归的情形。
return;
hanoi_recursive(n-1, start, goal, temp);// 将最底下的圆盘的上面圆盘,移动到非目标柱上
move(start,goal);
hanoi_recursive(n-1, temp, goal, start);// 放好一个后,再将剩余的n-1个放好
private static final int MOVE = 1; // 移动操作
private static final int HANOI = 2; // 汉诺塔操作
// 用栈代替递归实现汉诺塔过程
public static void hanoi_stack(int n,char start,char temp,char goal)
SStackwithoutHead stack = new SStackwithoutHead(2*n+1); // 给栈分配刚好大的大小
stack.push(new HanoI(HANOI,n, start,temp,goal)); // 压入初始格式
while(!stack.isEmpty()) // 当栈中非空时
HanoI it = (HanoI) stack.pop(); // 获取下一任务
if(it.operation == MOVE)
move(it.start, it.goal);
else if(it.num>0)// 当剩余要处理的圆盘数>0
// 将递归的解决办法反过来压入,这样执行的时候,才是正序
stack.push(new HanoI(HANOI, it.num-1, it.temp,it.goal,it.start)); // 继续对这n-1个圆盘进行处理
stack.push(new HanoI(MOVE, it.start, it.goal));// 执行MOVE
stack.push(new HanoI(HANOI, it.num-1, it.start, it.goal, it.temp));// 将n-1个圆盘放到非目标盘的柱子
// 测试过程
public static void main(String[] args)
System.out.println("用递归实现汉诺塔");
hanoi_recursive(3,'A','B','C');
System.out.println("用栈实现汉诺塔");
hanoi_stack(3, 'A', 'B', 'C');
运行结果:
用递归实现汉诺塔
from A to C
from A to B
from C to A
from A to C
from B to C
from B to A
from C to B
用栈实现汉诺塔
from A to C
from A to B
from C to A
from A to C
from B to C
from B to A
from C to B
以上是关于N日一篇——Java实现栈的主要内容,如果未能解决你的问题,请参考以下文章