N日一篇——Java实现栈

Posted 从零开始的智障生活

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了N日一篇——Java实现栈相关的知识,希望对你有一定的参考价值。

栈详解:数据结构第五篇——栈和队列_从零开始的智障生活的博客-CSDN博客

目录

一、顺序栈

1.1 用接口表示顺序栈抽象数据类型

1.2 带头结点的顺序栈实现

1.3 不带头结点的顺序表实现

1.4 测试带头结点与不带头结点的顺序栈

 二、链栈

2.1 用接口表示链栈抽象数据类型

2.2 用简单链表类型(无功能)表示链栈的每个结点类型

2.3 不带头结点的链栈(带头结点的链栈毫无意义)

2.4  测试不带头结点的链栈

 三、递归的实现

3.1 阶乘的非递归实现

3.2 汉诺塔的递归实现

3.3 汉诺塔的非递归实现


一、顺序栈

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)如果要将某个圆环移动到目的地,那么必须将该圆环上面的圆环全部移动到【非目的地】的另一个柱子上。

  1. 即如果要实现最终目标,我们必须将A的最底层圆环之上的所有圆环,先放到B上,然后将A上最后一个圆环,放到C上。
  2. 这时A上已经空。而剩余圆环全在B上,C上有了一个圆环,那么就要将B的最底层上的所有圆环,放到A上,然后将B上最后一个圆环,放到C上。
  3. 重复1、2步骤直到结束。

(2)如果要将某个圆环移动到目的地,首先必须解决如何将该圆环上面的所有圆环移动到【非目的地】的另一个柱子上。

  1. 如果A上有3个圆环,那么我们必须首先将B作为目标,在A上按有2个圆环的情形执行以B为目的地的步骤,这样就实现了(1)的条件,将A的第三个圆环Move到C上。
    1. 然后B上有2个圆环,那么我们必须首先将A作为目标,在B上按有1个圆环的情形执行以A为目的地的步骤,这样就实现了(1)的条件,将B的第二个圆环Move到C上。
    2. 最后A上有1圆环,那么我们必须首先将B作为目标,在A上按有0个圆环的情形执行以B为目的地的步骤,然而,0个圆环表示无效执行,但也同样实现了(1)的条件,将A上仅有的一个圆环移动到C上。
  2. 如果A上有n个圆环,那么我们必须首先将B作为目标,在A上按有n-1个圆环的情形执行以B为目的地的步骤。由1可以推得:
    1. 源头给定n个圆环。如果n=0,退出
    2. 将源头上的n-1个圆环,以【非目标的另一柱子】为目标移动。
    3. 然后对源头与【原目标】进行一次Move操作。
    4. 剩余的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

可以对照上面图片发现一致。

递归的实际过程,往往比逻辑过程复杂很多,所以,很多时候,将递归的模型给抽象出来是很困难的

其实可以将上面的递归过程总结理解为:

  1. 针对小的情形进行归纳。
    1. 这个过程中一般可以找出递归终止条件。
    2. 这个过程一般可以确定每次递归时,要进行的操作。比如这里的move函数,仅仅只是打印start与goal,这样的静态的操作,并没有进行插入、删除、加减乘除动态操作。
    3. 这个过程也可以确认,最终要实现的目标。比如这里就是要打印其过程。
  2. 假设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的情形的逻辑规律描述出来:

  1. 输入规模为n;终止条件:设当n=1时,F(1)=1,当n=0时,F(0)=0;
  2. 计算F(n-1)+F(n-2);中间可能有需要执行的操作。
  3. 将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的两个状态,一个是汉诺塔状态,和移动操作的状态,而汉诺塔状态根据上面递归的方法中,可以看出一个形式可以表示两个意义,在加上移动操作的状态可以表示为三个状态:

  1. 将当前n个圆盘的前n-1个圆盘,移动到非目标的柱子后的状态;
  2. 要进行Move时所需的状态;
  3. 继续处理剩余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实现栈的主要内容,如果未能解决你的问题,请参考以下文章

N日一篇——二叉树

N日一篇——二叉树

N日一篇——二叉树

汉诺塔问题java实现

N日一篇——Java实现队列

N日一篇——Java实现队列