说到栈,或许不陌生,先进后出,后进先出等等的特点,在日常的编程中也经常用到,但是Java中如何去使用栈,通过这篇文章来详细了解一下
文章目录
- Java中如何使用栈
- 自己实现一个栈
- 数组栈
- 链式栈
- 总结
Java中如何使用栈
栈是一种数据结构,可以分为链式栈和数组栈,顾名思义,链式栈的底层是用链表实现的,数组栈的底层是用数组实现的,下面就来谈一下JDK中的栈。
很多人都说Java中官方实现了专门的栈类Stack,该类位于java.util包下,一起来看一下继承结构:
该类继承了Vector类,Vector类是Java中已经淘汰的一个类,该类底层是数组实现,并且是线程安全的,Stack类继承了该类后就具有了这两个特性,这两个特性也是Stack最大的缺点:
- 底层使用数组,具体的来说就是动态数组,扩容方式需要重新申请大的内存空间 ,而且要把原数组中的值拷贝,效率是很低的
- 该类线程安全,从另一方面来说,确保了线程安全就需要一定的开销,那么在很多场景下是不需要线程安全的,所以这个功能显得有些赘余,降低效率。
既然JDK提供的Stack类有这两个缺点,那么在实际的使用中应该如何去使用栈?
在Java中提供了一个类LinkedList,该类相信有些人不会陌生,链表啊,但是该类其实不光是链表,其实还是链式栈,链式队列,该类提供了一些方法来满足栈和队列的功能,而且该栈的底层是链表实现的,所以扩容就不用担心了,不存在数组动态扩容的低效问题,并且非线程安全,所以完美的解决了上面的两个缺点。一起来看一下常用方法:
push : 入栈
pop : 出栈
peek : 返回栈顶元素
empty : 是否为空
size : 返回栈中元素的个数
用法非常简单,要想更加深入的了解栈,一起来实现一下:
自己实现一个栈
数组栈
class MyStack<T> {
private T[] elemData;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public MyStack() {
elemData = (T[])new Object[10];
size = 0;
}
//判断栈满
public boolean isFull() {
return size >= elemData.length;
}
//判断栈空
public boolean isEmpty() {
return size <= 0;
}
//扩容
private void grow() {
elemData = Arrays.copyOf(elemData,elemData.length*2+2);
}
//入栈
public boolean push(T data) {
if(isFull()) {
grow();
}
elemData[size++] = data;
return true;
}
//出栈
public T pop() {
if(isEmpty()) {
try {
throw new IllegalAccessException("栈空,不可出栈");
} catch (IllegalAccessException e) {
e.printStackTrace();;
}
}
T temp = elemData[--size];
//防止内存泄漏
elemData[size] = null;
return temp;
}
//获得栈顶元素
public T peek() {
if(isEmpty()) {
try {
throw new IllegalAccessException("栈空,无栈顶元素");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return elemData[size-1];
}
}
下面来解释一下这段代码,这是自己实现的一个栈,,使用泛型进行编写,内部是使用数组进行实现的,需要注意的有几点:
- 因为底层是使用数组进行编写,所以为了防止开辟的大小不够,改进为动态数组,也就是每次入栈元素前先进行判断是否需要扩容,如果需要扩容则进行2倍扩容,开辟新的数组把原数组的值拷贝过来
- 在出栈的时候要注意栈空
- 为了防止内存泄漏,在出栈的时候记得把不用的对象置为空
链式栈
class MiNiStack<T> {
//定义top指针
private Entry<T> top;
public MiNiStack() {
this.top = new Entry<>();
}
/**
* 入栈,相当于链表的头插过程
* @param data
*/
public void push(T data) {
Entry<T> entry = new Entry<>(data, top.next);
top.next = entry;
}
/**
* 出栈,删除第一个节点
* @return
*/
public T pop() {
if(isEmpty()) {
throw new IllegalArgumentException("栈空,出栈失败");
}
T var = top.next.data;
top.next = top.next.next;
return var;
}
/**
* 返回栈中元素的个数
* @return
*/
public int size() {
int size = 0;
Entry<T> cur = top.next;
while (cur != null) {
size++;
cur = cur.next;
}
return size;
}
/**
* 判断栈是否为空
* @return
*/
private boolean isEmpty() {
return top.next == null;
}
//定义节点Entry
class Entry<T>{
private T data;
private Entry<T> next;
public Entry() {
this(null,null);
}
public Entry(T data, Entry<T> next) {
this.data = data;
this.next = next;
}
}
}
- 链式栈的实现底层使用了链表,这样就不用担心扩容的问题,一定程度上提高了效率
- 在实现上是把链表的头部当做栈顶,入栈和出栈都是从链表的头部进行操作
- 入栈其实就是链表的头插操作
- 出栈直接删除链表的第一个数据节点
总结
综上,我们是不推荐使用jdk提供的Stack,除非是并发情况下。可以使用LinkedList或者自己去实现一个泛型的链式栈,把功能实现的完备一点,方便自己的经常使用。