手写一个Stack(栈)的集合类(链表实现+数组实现)
一、Stack栈集合的原理
栈是程序中一种数据结构,它的出入口是同一个,所以,出栈操作,和入栈操作,都是从同一个口子。看一下示意图。
图中,类似杯子的容器其实就是栈这种容器集合结构。如图所示,将元素A,B,C,D按照顺序加入到栈中,先加入的A元素反而在容器栈的最底端。这时候如果我们需要取元素,那么只能从栈的最上面元素开始取,而D元素,作为栈最上面的元素,却是最后一个加入的。所以栈这种数据结构有一个重要的特性:先进后出(First In Last Out),简称FILO。
二、栈的常见操作:
1.压栈
将元素放进栈中,这个操作叫压栈。
1.弹栈
将元素从栈中取出来,这个操作叫弹栈,也可以叫出栈。
三、实现Stack数据结构的思路
1.链表实现思路
这里的链表我们采用单向链表,双向的链表也可以实现,只是相对来说会比单向链表复杂一点。我们要理解栈的特性:先进后出。所以我们可以让每次新加入的元素,放在后加入元素的前面,例如:0,加入一个元素1,变成0->1;如果再加入一个元素2,此时变成了0->2->1;再加入3,变成0->3->2->1。由于我们的链表只能从头节点开始遍历,这样我们在遍历链表的时候,就可以完成栈这个先进后出的特性了。
a.链表实现代码
/**
* @author:
* @date: 2022/3/8 09:45
* @description: 实现栈的思路:链表实现
*/
public class MeOneTypeStack<T> implements Iterable<T> {
/**
* 首先需要一个头节点
*/
private Node<T> head;
/**
* 记录栈中元素的个数
*/
private int size;
/**
* 无参构造函数
*/
public MeOneTypeStack() {
// 初始化头节点
this.head = new Node<>(null, null);
// 初始化元素个数
this.size = 0;
}
/**
* 栈的元素个数获取
*
* @return
*/
public int size() {
return size;
}
/**
* 栈是否为空
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 压栈操作
* 例子:0->1
* 0->2->1
* 0->3->2->1
*
* @param t
*/
public void push(T t) {
// 如果头节点下一个为空,那么直接加入头节点后面
if (head.next == null) {
head.next = new Node<>(t, null);
// 元素个数自增一
size = size + 1;
return;
}
// 如果头节下一个节点有值了,那么这时候再加入,需要插入在头节点和下一个节点之间:0->1 再插入一个变成 0->2->1
Node<T> oldNode;
Node<T> newNode = new Node<>(t, null);
// 将头节点后面所有的节点剥离出来
oldNode = head.next;
// 将新节点接入到头结点后面
head.next = newNode;
// 将老的节点再接到新的节点后面
newNode.next = oldNode;
// 元素个数自增一
size = size + 1;
}
/**
* 弹栈操作
*/
public T pop() {
// 检查头节点后面有没有数据
if (head.next == null) {
return null;
}
Node<T> currentNode = head.next;
// 头节点后面有节点,0->2->1 变成 0->1
Node<T> oldNode = head.next.next;
// 将头节点接回到去掉第一个元素后的链表上
head.next = oldNode;
// 元素个数减一
size = size - 1;
return currentNode.item;
}
private class Node<T> {
/**
* 当前节点里面的对象
*/
private T item;
/**
* 下一个元素
*/
private Node<T> next;
public Node(T item, Node<T> next) {
this.item = item;
this.next = next;
}
}
@Override
public Iterator<T> iterator() {
return new SIterator();
}
/**
* 实现Iterator接口,从而实现栈的增强for循环
*/
class SIterator implements Iterator {
// 指针节点
private Node cursor;
// 初始化指针节点为头节点
public SIterator() {
cursor = head;
}
@Override
public boolean hasNext() {
// 只要指针节点的next不等于null就说明还有元素
return cursor.next != null;
}
@Override
public T next() {
// 从当前指针拿到下一个节点
Node<T> currNode = cursor.next;
// 将指针变为下一节点
cursor = cursor.next;
// 从当前节点中拿到元素
return currNode.item;
}
}
}
以上我们就用链表这种数据结构实现了一个MeOneTypeStack栈集合,接下来我们可以开测试一下。
b.测试代码
/**
* @author:
* @date: 2022/3/8 10:35
* @description:
*/
public class MeOneTypeStackTest {
public static void main(String[] args) {
MeOneTypeStack<String> stack = new MeOneTypeStack<>();
stack.push("A");
stack.push("B");
stack.push("C");
stack.push("D");
// ABCD顺序压栈后,数据在栈中的顺序应该是DCBA
for(String s:stack){
System.out.println(s);
}
System.out.println("=======================");
//弹栈测试
System.out.println("第一次弹出元素:"+stack.pop());
for(String s:stack){
System.out.println(s);
}
}
}
c.测试结果
D
C
B
A
=======================
第一次弹出元素:D
C
B
A
2.数组实现思路
其实用数组实现会相对链表实现会复杂一下,因为数组你需要初始化容量,如果容量不够了需要进行扩容操作,如果容器中元素过少了,为了节约内存空间,需要进行缩容操作,有点类似于ArrayList集合里面的动态扩容,缩容。现在有一个数组[1,2],我们按照栈的要求,放进数组,我们可以直接顺序放入,比如这个数组再放入3,变成[1,2,3],但是弹栈怎么让3,这个最后进去的先出来呢?这里我们可以利用数组的特性,从后往前遍历,这样我们就能让最后放进数组的元素先拿出来,同时再用一个变量去记录元素的个数即可。
a.数组实现代码
/**
* @author:
* @date: 2022/3/8 10:47
* @description: 数组实现栈
*/
public class MeTwoTypeStack<T> implements Iterable<T> {
/**
* 数组
*/
private T[] elementData;
/**
* 栈中的元素个数
*/
private int elementCount;
/**
* 默认数组长度
*/
private int default_capacity = 10;
/**
* 无参构造函数
*/
public MeTwoTypeStack() {
this.elementData = (T[]) new Object[default_capacity];
this.elementCount = 0;
}
/**
* 获取当前栈的元素个数
*
* @return
*/
public int size() {
return elementCount;
}
/**
* 判断当前栈是否为空
*
* @return
*/
public boolean isEmpty() {
return elementCount == 0;
}
/**
* 压栈 我们就正常往数组中放入元素即可
* 1,2,3,4
* 这样放入数组
*
* @param t
*/
public void push(T t) {
// 如果当前元素个数和数组容器大小一样时,需要对容器进行两倍的扩容
if (elementCount == elementData.length) {
reSize(2 * elementData.length);
}
elementData[elementCount++] = t;
}
/**
* 弹栈,由于弹栈需要先进后出,所以我们从数组的后面开始遍历弹出
* 这里我们弹栈操作后,数组里面的数据并没有减少,我们仅仅是将最后一个元素的值返回,
* 我们要用数组元素个数这个变量来控制,下次返回的元素的下标应该是elementCount-1
*
* @return
*/
public T pop() {
// 如果当前栈中元素为空,则直接返回null
if (elementCount == 0) {
return null;
}
// 拿到数组最后一位的元素
T current = elementData[elementCount - 1];
// 数组的长度减一
elementCount = elementCount - 1;
// 元素个数小于等于数组容器长度的四分之一时,需要缩容两倍
if (elementCount <= elementData.length / 4) {
reSize(elementData.length / 2);
}
return current;
}
/**
* 数组的扩容缩容
*
* @param capacity
*/
private void reSize(int capacity) {
// 创建一个临时数组
T[] temp = elementData;
// 原来的数组指向新的扩容或者缩容后的数组
elementData = (T[]) new Object[capacity];
// 将原来的数组中的元素赋值到新的数组中
for (int i = 0; i < elementCount; i++) {
elementData[i] = temp[i];
}
}
@Override
public Iterator<T> iterator() {
return new SIterator();
}
class SIterator implements Iterator {
// 设置指针
private int n;
public SIterator() {
// 初始化指针的位置,从数组末尾开始遍历,所以是数组的最后以为开始
this.n = elementCount - 1;
}
@Override
public boolean hasNext() {
return n >= 0;
}
@Override
public T next() {
T current = elementData[n];
n = n - 1;
return current;
}
}
}
以上就是我们数组的实现栈的思路了,可能有人会疑惑,我出栈的操作,并没有让数组中的元素减少,只是让最后一个元素的值返回了,这样不等于没有删除吗,这里我用了元素个数elementCount来标识数组中出栈后应该还剩的元素个数,所以每次出栈操作我都会让元素个数减一,因为元素是按照顺序排列的,下标识固定的。而且在缩容的时候,会重新创建一个新的数组去取代原来的数组,此时就会把里面实际应该清除,但是没有清除的数据让JVM在垃圾回收时将这个没有被引用的对象给清除掉。
b.测试代码
加入十一个元素,目的是检验扩容功能有没有问题,我们初始化数组长度是10,如果有问题,在加入第十一个元素的时候就会出现数组越界。
/**
* @author:
* @date: 2022/3/8 10:35
* @description:
*/
public class MeTwoTypeStackTest {
public static void main(String[] args) {
MeTwoTypeStack<String> stack = new MeTwoTypeStack<>();
stack.push("A");
stack.push("B");
stack.push("C");
stack.push("D");
stack.push("E");
stack.push("F");
stack.push("G");
stack.push("H");
stack.push("I");
stack.push("J");
stack.push("K");
// ABCD顺序压栈后,数据在栈中的顺序应该是DCBA
for(String s:stack){
System.out.println(s);
}
System.out.println("=======================");
//弹栈测试
System.out.println("第一次弹出元素:"+stack.pop());
for(String s:stack){
System.out.println(s);
}
}
}
c.测试结果
K
J
I
H
G
F
E
D
C
B
A
=======================
第一次弹出元素:K
J
I
H
G
F
E
D
C
B
A
注意:jdk里面的Stack类其实就是用了数组的这种形式实现了栈,它是通过继承Vector集合类,然后使用Vector类的api组合从而实现的栈的这种先进后出的特性。
总结
或许你会再次疑惑我们为什么要实现已经有了的数据结构,主要是我们通过自己实现更好的理解设计者的思路,并且这种思路往往可以解决现实中的很多业务场景问题。