链表
上一篇,我们用数组实现了一个动态扩容栈,这次我们来看一下链表怎么实现栈结构。
说到链表,来看这篇文章的人应该都不会陌生,在计算机数据结构中,同离不开数组一样,我们也离不开链表结构,它属于线性表中的一种,在物理存储结构上来说,并不是连续空间存储,可以通过内部的引用指针寻找其它节点。
假如有老师或者妹子问你,为了显得不明觉厉,要这样说:链表是一种递归的数据结构,它或者为null,或者指向一个节点(node)的引用,该节点含有一个泛型的元素和一个指向另一条指针
好吧,咱们说人话,打个比方,链表就像火车箱一样一节节相连,你跟朋友(数据)身处不同的车厢,那么你在内部只有通过火车的连接处才能找到下一节车厢,一节节找才能找到朋友(数据)的位置,这在链表中火车连接处保存的是下个结点(车厢)的地址,可以通过地址找到数据。
链表中的结点是一个可能含有任意程序数据的抽象实体,和递归程序一样,一开始令人费解,但其简洁性赋予了巨大的价值。
简单实现
在面向对象编程中,实现链表并不困难,首先用一个嵌套类来定义结点抽象数据类型:
private class Node{
Item item;//数据
Node next;//下个相连节点引用
}
现在我们只需要创建一个Node类型的变量就嫩好表示一条链表,只要保证他的元素值是null或者指向另一个Node对象,且该对象的next域指向来另一条链表即可。例如要构造 d、e、f,只需要:
Node d = new Node();
Node e = new Node();
Node f = new Node();
并且每个结点的item对象设为所需值,我们先假设item存储的是String。
d.item = "d";
e.item = "e";
f.item = "f"
//然后通过设置next引用来串联在一起
d.next = e;
e.next = f;
//此时c.next = null
接下来学习如何从链表中插入或移除数据,并且在追踪链表与其它链式结构时,我们采用可视化表示方法(画图):
1.用长方形表示对象。
2.把实例变量值写在长方形中。
3.用指向被引用对象的箭头表示引用关系。
这种表示方法可以明显表现链表的特性。方便起见,我们采用链接表示对结点的引用。
在表头插入节点
最容易做到这一点的就是在链表的开头,我们要在上面的“d,e,f”链表的开头插入“C”结点,首先要创建一个新结点命名为“C”,在 C结点的item内写入字符串“C”,并且将C的next域指向d. 如图:
从表头删除结点
假设要删除头部结点C,只需要将C.next指向C即可,意思是将C的下一个节点的引用覆盖了C结点的引用,此时C结点变成了游离元素等待被垃圾回收器回收。首结点first又变为了d。上图:
在表尾部插入节点
如何能在链表的尾部插入一个节点,我们需要一个指向链表最后一个节点的链接,因为只要将该节点的链接指向一个新的节点即可。上图:
其他位置的插入删除操作
我们现在可以通过first(首结点)链接访问头部结点,也可以通过last访问链表尾节点。表头的插入删除、表尾部的添加实现了,还剩:删除指定结点、在指定结点前插入一个新的节点。
比如我们想删除尾节点,那么last(g)结点就帮不上忙,因为我们需要的是它的前一个节点,让该节点的链接(next)指向null。唯一的解决办法就是遍历所有的节点,并找出指向last(g)的节点,这个方案并不理想,但确实能实现我们的效果,目前业内解决方案是采用双向链表,其中的每个节点都含有两个链接,分别指向不同的方向。
遍历
访问链表中的每一个元素也有对应的方式,将循环的索引变量初始化为链表的首结点,不断寻找next指向的下个节点,直到首结点为null时说明已经到了链表的结尾。
for(Node x = first; x!=null;x.next){
//处理x.item......
}
栈的实现方式
有了上面写的这些预备知识,我们利用链表实现一个动态扩容栈的API应该不难了。
当push一个元素时,我们在该元素添加在链表的表头即首结点。
当我们pop一个元素,就从表头删除,另外实现size方法,来查看栈内元素数量,用size变量来标识实际元素数量,在压入元素时size+1,反之弹出元素size-1,再实现一个isEmpty()方法,来查看first是否为null,或者可以检查size是否为0.
该实现使用了泛型Item.
目前的设计达到了我们初期的目标:
1.它可以处理任意类型的数据
2.所需的空间总是和集合的大小成正比
3.操作所需的时间总是和集合的大小无关
上代码:
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 用链表实现下压栈
* @author wangxinfu
*/
public class Stack<Item> implements Iterable<Item> {
/**
* 实际元素数量
*/
private int size;
/**
* 首结点
*/
private Node first;
/**
* 私有内部类构成栈结构
*
*/
private class Node {
Item item;
Node next;
}
/**
* 压栈
*
* @param item element
* @return true
*/
public boolean push(Item item) {
//在头结点前添加,无需判断first是否null
Node oldFirst = first;
first = new Node();
first.item = item;
first.next = oldFirst;
size++;
return true;
}
/**
* 弹栈
*
* @return element
*/
public Item pop() {
if (isEmpty()) {
//空栈则抛出异常
throw new NoSuchElementException("Stack is Empty!");
}
//暂存
Item item = first.item;
Node next = first.next;
//help GC
first.item = null;
first.next = null;
first = next;
size--;
return item;
}
public int size() {
return size;
}
public boolean isEmpty() {
return first == null;
}
@Override
public StackIterator iterator() {
return new StackIterator();
}
//iterator用来实现遍历
private class StackIterator implements Iterator<Item> {
//当前结点
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
if (!hasNext()) {
throw new NoSuchElementException("Stack is Empty !");
}
Item item = current.item;
current = current.next;
return item;
}
}
//测试 也可以存取其它数据类型
public static void main(String[]args){
Stack<Integer> stack = new Stack<>();
for (int i = 0;i < 100; i++){
stack.push(i);
}
System.out.println("Stack size : "+ stack.size());
System.out.println("================================");
//iterator遍历
Iterator<Integer> iterator = stack.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
最好的学习方法,往往是实践加上教给他人,希望看到这篇博客的读者自己能够将代码自行敲一遍,否则没有两天就忘记了。
下一篇我们将学习下利用链表实现队列,不积跬步无以至千里,不积小流无以成江海,加油。