一、堆栈的基本概念:
堆栈(也简称作栈)是一种特殊的线性表,堆栈的数据元素以及数据元素间的逻辑关系和线性表完全相同,其差别是线性表允许在任意位置进行插入和删除操作,而堆栈只允许在固定一端进行插入和删除操作。
先进后出:堆栈中允许进行插入和删除操作的一端称为栈顶,另一端称为栈底。堆栈的插入和删除操作通常称为进栈或入栈,堆栈的删除操作通常称为出栈或退栈。
备注:栈本身就是一个线性表,所以我们之前讨论过线性表的顺序存储和链式存储,对于栈来说,同样适用。
二、堆栈的抽象数据类型:
数据集合:
堆栈的数据集合可以表示为a0,a1,…,an-1,每个数据元素的数据类型可以是任意的类类型。
操作集合:
(1)入栈push(obj):把数据元素obj插入堆栈。
(2)出栈pop():出栈, 删除的数据元素由函数返回。
(3)取栈顶数据元素getTop():取堆栈当前栈顶的数据元素并由函数返回。
(4)非空否notEmpty():若堆栈非空则函数返回true,否则函数返回false。
三、顺序栈:
顺序存储结构的堆栈称作顺序堆栈。其存储结构示意图如下图所示:
1、顺序栈的实现:
(1)设计Stack接口
(2)实现SequenceStack类
注:栈是线性表的特例,线性表本身就是用数组来实现的。于是,顺序栈也是用数组实现的。
代码实现:
(1)Stack.java:(Stack接口)
1 package com.myutil.stack; 2 3 public interface Stack { 4 5 //入栈 6 public void push(Object obj) throws Exception; 7 8 //出栈 9 public Object pop() throws Exception; 10 11 //获取栈顶元素 12 public Object getTop() throws Exception; 13 14 //判断栈是否为空 15 public boolean isEmpty(); 16 }
(2)SequenceStack.java:
空栈是Top=-1
1 package com.myutil.stack; 2 3 //顺序栈 4 public class SequentailStack implements Stack { 5 6 Object[] stack; //对象数组(栈用数组来实现) 7 final int defaultSize = 10; //默认最大长度 8 int top; //栈顶位置(的一个下标):其实可以理解成栈的实际长度 9 int maxSize; //最大长度 10 11 //如果用无参构造的话,就设置默认长度 12 public SequentailStack() { 13 init(defaultSize); 14 } 15 16 //如果使用带参构造的话,就调用指定的最大长度 17 public SequentailStack(int size) { 18 init(size); 19 } 20 21 public void init(int size) { 22 this.maxSize = size; 23 top = -1; 24 stack = new Object[size]; 25 } 26 27 //获取栈顶元素 28 @Override 29 public Object getTop() throws Exception { 30 if (isEmpty()) { 31 throw new Exception("堆栈为空!"); 32 } 33 34 return stack[top]; 35 } 36 37 //判断栈是否为空 38 @Override 39 public boolean isEmpty() { 40 return top == -1; 41 } 42 43 //出栈操作 44 @Override 45 public Object pop() throws Exception { 46 if (isEmpty()) { 47 throw new Exception("堆栈为空!"); 48 } 49 top--; 50 51 return stack[top+1]; 52 } 53 54 //进栈操作 55 @Override 56 public void push(Object obj) throws Exception { 57 //首先判断栈是否已满 58 if (top == maxSize-1) { 59 throw new Exception("堆栈已满!"); 60 } 61 62 top++; 63 stack[top] = obj; 64 } 65 }
2、测试类:
设计一个顺序栈,从键盘输入十个整数压进栈,然后再弹出栈,并打印出栈序列。
代码实现:
(3)Test.java:
1 package com.myutil.stack.use; 2 3 import java.util.Scanner; 4 5 import com.myutil.stack.SequentailStack; 6 7 public class Test { 8 public static void main(String[] args) throws Exception { 9 SequentailStack stack = new SequentailStack(10); 10 11 Scanner in = new Scanner(System.in); 12 int temp; 13 for (int i = 0; i < 10; i++) { 14 System.out.println("请输入第" + (i + 1) + "个整数:"); 15 temp = in.nextInt(); 16 stack.push(temp); 17 } 18 19 //遍历输出 20 while (!stack.isEmpty()) { 21 System.out.println(stack.pop()); 22 } 23 } 24 }
四、Java中栈与堆的区别:
栈(stack):(线程私有)
是一个先进后出的数据结构,通常用于保存方法(函数)中的参数,局部变量。在java中,方法中所有基本类型和引用类型的引用都在栈中存储。栈中数据的生存空间一般在当前scopes内(就是由{...}括起来的区域)。
Java虚拟机栈(JVM Stack):也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame:栈帧是方法运行时的基础数据结构)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
(经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),虽然比较粗糙)
局部变量表存放了编译器可知的各种基本数据类型,对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)。
64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译器完成分配,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展(当前大部分的Java虚拟机都可以动态扩展,只不过Java虚拟机规范中允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
堆(heap):(线程共享)
是一个可动态申请的内存空间(其记录空闲内存空间的链表由操作系统维护),C中的malloc语句所产生的内存空间就在堆中。在java中,所有使用new xxx()构造出来的对象都在堆中存储,当垃圾回收器检测到某对象未被引用,则自动销毁该对象。所以,理论上说java中对象的生存空间是没有限制的,只要有引用类型指向它,则它就可以在任意地方被使用。
对大多数应用来书,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java发虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称作“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分带收集算法,所以Java堆中还可以细分为:新生代和老年带;再细致一点的有Eden空间,From Survivor空间,To Surviror空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
五、hashCode与对象之间的关系:
如果两个对象的hashCode不相同,那么这两个对象肯定也不同。
如果两个对象的hashCode相同,那么这两个对象有可能相同,也有可能不同。
总结一句:不同的对象可能会有相同的hashCode;但是如果hashCode不同,那肯定不是同一个对象。
代码举例:
1 public class StringTest { 2 3 public static void main(String[] args) { 4 5 //s1 和 s2 其实是同一个对象。对象的引用存放在栈中,对象存放在方法区的字符串常量池 6 String s1 = "china"; 7 String s2 = "china"; 8 9 //凡是用new关键创建的对象,都是在堆内存中分配空间。 10 String s3 = new String("china"); 11 12 //凡是new出来的对象,绝对是不同的两个对象。 13 String s4 = new String("china"); 14 15 System.out.println(s1 == s2); //true 16 System.out.println(s1 == s3); 17 System.out.println(s3 == s4); 18 System.out.println(s3.equals(s4)); 19 20 System.out.println(" ----------------- "); 21 /*String很特殊,重写从父类继承过来的hashCode方法,使得两个 22 *如果字符串里面的内容相等,那么hashCode也相等。 23 **/ 24 25 //hashCode相同 26 System.out.println(s3.hashCode()); //hashCode为94631255 27 System.out.println(s4.hashCode()); //hashCode为94631255 28 29 //identityHashCode方法用于获取原始的hashCode 30 //如果原始的hashCode不同,表明确实是不同的对象 31 32 //原始hashCode不同 33 System.out.println(System.identityHashCode(s3)); //2104928456 34 System.out.println(System.identityHashCode(s4)); //2034442961 35 36 System.out.println(" ----------------- "); 37 38 //hashCode相同 39 System.out.println(s1.hashCode()); //94631255 40 System.out.println(s2.hashCode()); //94631255 41 42 //原始hashCode相同:表明确实是同一个对象 43 System.out.println(System.identityHashCode(s1)); //648217993 44 System.out.println(System.identityHashCode(s2)); //648217993 45 } 46 }
上面的代码中,注释已经标明了运行的结果。通过运行结果我们可以看到,s3和s4的字符串内容相同,但他们是两个不同的对象,由于String类重写了hashCode方法,他们的hashCode相同,但原始的hashCode是不同的。
六、链式堆栈:
链式存储结构的堆栈称作链式堆栈。
与单链表相同,链式堆栈也是由一个个结点组成的,每个结点由两个域组成,一个是存放数据元素的数据元素域data,另一个是存放指向下一个结点的对象引用(即指针)域next。
堆栈有两端,插入数据元素和删除数据元素的一端为栈顶,另一端为栈底。链式堆栈都设计成把靠近堆栈头head的一端定义为栈顶。
依次向链式堆栈入栈数据元素a0, a1, a2, ..., an-1后,链式堆栈的示意图如下图所示:
1、设计链式堆栈:
(1)Node.java:结点类
1 package com.myutil.stack; 2 3 //结点类 4 public class Node { 5 6 Object element; //数据域 7 Node next; //指针域 8 9 //头结点的构造方法 10 public Node(Node nextval) { 11 this.next = nextval; 12 } 13 14 //非头结点的构造方法 15 public Node(Object obj, Node nextval) { 16 this.element = obj; 17 this.next = nextval; 18 } 19 20 //获得当前结点的后继结点 21 public Node getNext() { 22 return this.next; 23 } 24 25 //获得当前的数据域的值 26 public Object getElement() { 27 return this.element; 28 } 29 30 //设置当前结点的指针域 31 public void setNext(Node nextval) { 32 this.next = nextval; 33 } 34 35 //设置当前结点的数据域 36 public void setElement(Object obj) { 37 this.element = obj; 38 } 39 40 public String toString() { 41 return this.element.toString(); 42 } 43 }
(2)Stack.java:
1 //栈接口 2 public interface Stack { 3 4 //入栈 5 public void push(Object obj) throws Exception; 6 7 //出栈 8 public Object pop() throws Exception; 9 10 //获得栈顶元素 11 public Object getTop() throws Exception; 12 13 //判断栈是否为空 14 public boolean isEmpty(); 15 }
(3)LinkStack.java:
1 package com.myutil.stack; 2 3 public class LinkStack implements Stack { 4 5 Node head; //栈顶指针 6 int size; //结点的个数 7 8 public LinkStack() { 9 head = null; 10 size = 0; 11 } 12 13 @Override 14 public Object getTop() throws Exception { 15 return head.getElement(); 16 } 17 18 @Override 19 public boolean isEmpty() { 20 return head == null; 21 } 22 23 @Override 24 public Object pop() throws Exception { 25 if (isEmpty()) { 26 throw new Exception("栈为空!"); 27 } 28 Object obj = head.getElement(); 29 head = head.getNext(); 30 size--; 31 return obj; 32 } 33 34 @Override 35 public void push(Object obj) throws Exception { 36 head = new Node(obj, head); 37 size++; 38 } 39 }
(4)Test.java:测试类
1 package com.myutil.stack; 2 3 import java.util.Scanner; 4 5 public class Test { 6 7 public static void main(String[] args) throws Exception { 8 //SequenceStack stack = new SequenceStack(10); 9 LinkStack stack = new LinkStack(); 10 Scanner in = new Scanner(System.in); 11 int temp; 12 for (int i = 0; i < 10; i++) { 13 System.out.println("请输入第" + (i + 1) + "个整数:"); 14 temp = in.nextInt(); 15 stack.push(temp); 16 } 17 //遍历输出 18 while (!stack.isEmpty()) { 19 System.out.println(stack.pop()); 20 } 21 } 22 }
七、堆栈的应用:
堆栈是各种软件系统中应用最广泛的数据结构之一。括号匹配和表达式计算是编译软件中的基本问题,其软件设计中都需要使用堆栈。
- 括号匹配问题
- 表达式计算
1、括号匹配问题:
假设算术表达式中包含圆括号,方括号,和花括号三种类型。使用栈数据结构编写一个算法判断表达式中括号是否正确匹配,并设计一个主函数测试。
比如:
{a+[b+(c*a)/(d-e)]} 正确
([a+b)-(c*e)]+{a+b} 错误,中括号的次序不对
括号匹配有四种情况:
1.左右括号匹配次序不正确
2.右括号多于左括号
3.左括号多于右括号
4.匹配正确
下面我们就通过代码把这四种情况列举出来。
代码实现:
1 public class Test { 2 3 //方法:将字符串转化为字符串数组 4 public static String[] expToStringArray(String exp) { 5 int n = exp.length(); 6 String[] arr = new String[n]; 7 for (int i = 0; i < arr.length; i++) { 8 arr[i] = exp.substring(i, i + 1); 9 } 10 return arr; 11 } 12 13 //方法:括号匹配问题的检测 14 public static void signCheck(String exp) throws Exception { 15 SequenceStack stack = new SequenceStack(); 16 String[] arr = Test.expToStringArray(exp); 17 for (int i = 0; i < arr.length; i++) { 18 if (arr[i].equals("(") || arr[i].equals("[") || arr[i].equals("{")) { //当碰到都是左边的括号的时候,统统压进栈 19 stack.push(arr[i]); 20 } else if (arr[i].equals(")") && !stack.isEmpty() && stack.getTop().equals("(")) { //当碰到了右小括号时,如果匹配正确,就将左小括号出栈 21 stack.pop(); 22 } else if (arr[i].equals(")") && !stack.isEmpty() && !stack.getTop().equals("(")) { 23 System.out.println("左右括号匹配次序不正确!"); 24 return; 25 } else if (arr[i].equals("]") && !stack.isEmpty() && stack.getTop().equals("[")) { 26 stack.pop(); 27 } else if (arr[i].equals("]") && !stack.isEmpty() && !stack.getTop().equals("[")) { 28 System.out.println("左右括号匹配次序不正确!"); 29 return; 30 } else if (arr[i].equals("}") && !stack.isEmpty() && stack.getTop().equals("{")) { 31 stack.pop(); 32 } else if (arr[i].equals("}") && !stack.isEmpty() && !stack.getTop().equals("{")) { 33 System.out.println("左右括号匹配次序不正确!"); 34 return; 35 } else if (arr[i].equals(")") || arr[i].equals("]") || arr[i].equals("}") && stack.isEmpty()) { 36 System.out.println("右括号多于左括号!"); 37 return; 38 } 39 } 40 if (!stack.isEmpty()) { 41 System.out.println("左括号多于右括号!"); 42 } else { 43 System.out.println("括号匹配正确!"); 44 } 45 } 46 47 48 public static void main(String[] args) throws Exception { 49 50 String str = "([(a+b)-(c*e)]+{a+b}"; 51 //括号匹配的检测 52 Test.signCheck(str); 53 } 54 }
2、表达式计算:
比如:
3+(6-4/2)*5=23
其后缀表达式为:3642/-5*+# (#符号为结束符)
现在要做的是:
使用链式堆栈,设计一个算法计算表达式,当我们输入后缀表达式后,能输出运行结果。
代码实现:
1 public class Test { 2 3 //方法:使用链式堆栈,设计一个算法计算表达式 4 public static void expCaculate(LinkStack stack) throws Exception { 5 char ch; //扫描每次输入的字符。 6 int x1, x2, b = 0; //x1,x2:两个操作数 ,b字符的ASCII码 7 System.out.println("输入后缀表达式并以#符号结束:"); 8 while ((ch = (char) (b = System.in.read())) != '#') { 9 //如果是数字,说明是操作数则压入堆栈 10 if (Character.isDigit(ch)) { 11 stack.push(new Integer(Character.toString(ch))); 12 } 13 //如果不是数字,说明是运算符 14 else { 15 x2 = ((Integer) stack.pop()).intValue(); 16 x1 = ((Integer) stack.pop()).intValue(); 17 switch (ch) { 18 case '+': 19 x1 += x2; 20 break; 21 case '-': 22 x1 -= x2; 23 break; 24 case '*': 25 x1 *= x2; 26 break; 27 case '/': 28 if (x2 == 0) { 29 throw new Exception("分母不能为零!"); 30 } else { 31 x1 /= x2; 32 } 33 break; 34 } 35 stack.push(new Integer(x1)); 36 } 37 } 38 System.out.println("后缀表达式计算结果是:" + stack.getTop()); 39 } 40 41 public static void main(String[] args) throws Exception { 42 LinkStack stack = new LinkStack(); 43 //(2+3)*(3-1)/2=5的后缀表达式为:23+31-*2/ 44 //方法:键盘输入后缀表达式,输出的得到计算结果 45 Test.expCaculate(stack); 46 47 } 48 }