三、链表(LinkedList)

下面将有一种新的数据存储结构,它可以解决上面的一些问题。这种数据存储结构就是链表。链表可能是继数组之后第二种使用最广泛的通用存储结构。

  • 单链表
  • 双端链表
  • 有序链表
  • 双向列表
  • 有迭代器的列表

链表与数组一样,都作为数据的基本存储结构,但是在存储原理上二者是不同的。在数组中,数据是存储在一段连续的内存空间中,我们可以通过下标来访问数组中的元素;而在链表中,元素是存储在不同的内存空间中,前一个元素的位置维护了后一个元素在内存中的地址,在Java中,就是前一个元素维护了后一个元素的引用。在本教程我们,我们将链表中的每个元素称之为一个节点(Node)。对比数组, 链表的数据结构可以用下图表示:

java 栈实现 链表 java链表数据结构_java 栈实现 链表

这张图显示了一个链表的数据结构,链表中的每个Node都维护2个信息:一个是这个Node自身存储的数据Data,另一个是下一个Node的引用,图中用Next表示。对于最后一个Node,因为没有下一个元素了,所以其并没有引用其他元素,在图中用紫色框来表示。

这张图主要显示的是链表中Node的内部结构和Node之间的关系。一般情况下,我们在链表中还要维护第一个Node的引用,原因是在链表中访问数据必须通过前一个元素才能访问下一个元素,如果不知道第一个Node的话,后面的Node都不可以访问。事实上,对链表中元素的访问,都是从第一个Node中开始的,第一个Node是整个链表的入口;而在数组中,我们可以通过下标进行访问元素。

public class Node {
    //Node中维护的数据
    private Object data;
    //下一个元素的引用
    private Node next;
    // setters and getters
}

1、单链表Java实现

1)、SingleLinkList中要维护的信息:维护第一个节点(firstNode)的引用,作为整个链表的入口;

2)、插入操作分析:基于链表的特性,插入到链表的第一个位置是非常快的,因为只要改变fisrtNode的引用即可。因此对于单链表,我们会提供addFirst方法。

3)、查找操作分析:从链表的fisrtNode开始进行查找,如果确定Node中维护的data就是我们要查找的数据,即返回,如果不是,根据next获取下一个节点,重复这些步骤,直到找到最后一个元素,如果最后一个都没找到,返回null。

4)、删除操作分析 : 首先查找到要删除的元素节点,同时将这个节点的上一个节点和下一个节点也要记录下来,只要将上一个节点的next引用直接指向下一个节点即可,这就相当于 删除了这个节点。如果要删除的是第一个节点,直接将LinkList的firstNode指向第二个节点即可。如果删除的是最后一个节点,只要将上一个节 点的next引用置为null即可。上述分析,可以删除任意节点,具有通用性但是效率较低。通常情况下,我们还会提供一个removeFirst方法,因为这个方法效率较高,同样只要改变fisrtNode的引用即可。

此外,根据情况而定,可以选择是否要维护链表中元素的数量size,不过这不是实现一个链表必须的核心特性。

下面是代码实现:

public class SingleLinkList<V> {
    protected Node firstNode = null;// 链表的第一个节点
    protected int size;// 链表中维护的节点总数

    /**
     * 添加到链表最前面
     * @return 
     */
    public Node addFirst(V v) {
        Node node = new Node();
        node.setData(v);
        Node currentFirst = firstNode;
        node.setNext(currentFirst);
        firstNode = node;
        size++;
        return node;
    }

    /**
     * 如果链表中包含要删除的元素,删除第一个匹配上的要删除的元素
     */
    public void remove(V v) {
        if (size == 0) {
            return;
        }
        if (size == 1) {
            firstNode = null;
            size--;
            return;
        }
        if (Objects.equals(firstNode.getData(), v)) {
            firstNode = firstNode.getNext();
            size--;
        }
        Node pre = firstNode;
        Node next = pre.getNext();
        while (next != null) {
            if (Objects.equals(next.getData(), v)) {
                pre.setNext(next.getNext());
                size--;
                next = pre.getNext();
            }else {
                pre = pre.getNext();
                next = pre.getNext();

            }
        }
    }
    /**
     * 是否包含,包含返回true,不包含返回false
     */
    public boolean contains(V v){
        if (size == 0) {
            return false;
        }
        Node current = firstNode;
        while (current != null) {
            if (Objects.equals(v, current.getData())) {
                return true;
            }
            current = current.getNext();
        }
        return false;
    }
    /**
     * 获取第一个元素
     */
    public V getFirst(){
        if (size == 0) {
            return null;
        }
        return (V)firstNode.getData();
    }
    /**
     * 删除第一个元素
     */
    public V removeFirst(){
        if (size == 0) {
            return null;
        }
        Node temp = firstNode.getNext();
        firstNode = temp;
        if (temp == null) {
            return null;
        }
        return (V)temp.getData();
    }
    /**
     * 打印链表的所有元素
     */
    public void showAll(){
        if (size != 0) {
            Node current = firstNode;
            while (current != null){
                System.out.print(current.getData() + "/");
                current = current.getNext();
            }
        }
    }
    /**
     * 获取元素个数
     */
    public int getSize(){
        return size;
    }
}

2、双端链表Java实现

双端链表与传统的链表非常类似,但是它有一个新增的特性:即对链表中最后一个节点的引用lastNode。我们可以像在单链表中在表头插入一个元素一样,在链表的尾端插入元素。如果不维护对最后一个节点的引用,我们必须要迭代整个链表才能得到最后一个节点,然后再插入,效率很低。因此我们在双链表中添加一个addLast方法,用于添加节点到末尾。

addLast方法分析:直接将链表中维护的lastNode的next引用指向新的节点,再将lastNode的引用指向新的节点即可。

因为单链表中,大部分的代码在双端链表中都可以重用,所以此处我们编写的DoubleLinkList只要继承SingleLinkList,添加必要的属性和方法支持从尾部操作即可。

下面是代码实现:

public class DoubleLinkList<V> extends SingleLinkList<V> {
    protected Node lastNode = null;

    /**
     * 添加到链表最后
     */
    public void addLast(V v) {
        Node node = new Node();
        node.setData(v);
        if (size == 0) {// 说明没有任何元素,说明第一个元素
            firstNode = node;
        } else {// 如果有元素,将最后一个节点的next指向新的节点即可
            /*
             * 这里有一个要注意的地方: 当size=1的时候,firstNode和lastNode指向同一个引用
             * 因此lastNode.setNext时,fisrtNode的next引用也会改变;
             * 当size!=1的时候,lastNode的next的改变与firstNode无关
             */
            lastNode.setNext(node);
        }

        // 将lastNode引用指向新node
        lastNode = node;
        size++;
    }

    /**
     * 当链表中没有元素时,清空lastNode引用
     */
    @Override
    public void remove(V v) {
        super.remove(v);
        if (size == 0) {
            lastNode = null;
        }
    }
    /**
     * 因为在SingleLinkList中并没有维护lastNode的信息,我们要自己维护
     */
    @Override
    public Node addFirst(V v) {
        Node node = super.addFirst(v);
        if (size == 1) {// 如果链表为size为1,将lastNode指向当前节点
            lastNode = node;
        }
        return node;
    }

}

3、有序链表

所谓有序链表,就是链表中Node节点之间的引用关系是根据Node中维护的数据data的某个字段为key值进行排序的。为了在一个有序链表中插入,算法必须首先搜索链表,直到找到合适的位置:它恰好在第一个比它大的数据项前面。

当算法找到了要插入的数据项的位置,用通常的方式插入数据项:把新的节点Node指向下一个节点,然后把前一个节点Node的next字段改为指向新的节点。然而,需要考虑一些特殊情况,连接点有可能插入在表头或者表尾。