Java 数据结构-链表

83 阅读4分钟

Java数据结构-链表

一、前言

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的 指针链接次序实现的。链表由一系列节点(链表中每一个元素称为节点)组成,节点可以在运行时动态生成。每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个节点地址的 指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

二、链表数据结构

链表是数据线性数据的集合,元素(节点)不是由它们在物理内存的位置决定的;它是由一组节点组成的数据结构,每个元素指向下一个元素,这些节点一起,表示线性序列。

image.png 在最简单的链表结构下,每个组节点由两部分组成,数据和指针(指针用来存放下一个节点的地址),这种数据结构允许在迭代时高效地从序列中的任何位置插入或删除元素。但在一些需要遍历、指定位置操作、或者访问任意元素下,是需要循环遍历的,这将导致时间复杂度的提升。

三、链表分类

链表的主要表现形式分为;单向链表、双向链表、循环链表

1.单向链表

单链表包含具有数据的节点以及指向节点中的下一个节点的节点。 image.png

2.双向链表

双向链表包含具有数据的节点以及指向节点中的下一个节点的节点之外还额外包含指向“前一个节点”的节点。可以用prev(previous 前一个)和 next(下一个)称呼。 image.png

3.循环链表

通常链表的最后一个节点都指向空(NULL),但是特殊情况时会存在最后一个节点指向该链的第一个节点,我们称之为循环链表,反之称之为线性的 循环链表包含具有数据的节点以及指向节点中的下一个节点的节点之外还额外包含指向“前一个节点”的节点。可以用 image.png

四、Java实现链表

在Java中LinkedList就是一个链表结构(双向链表);通过看源码来看下Java是怎么实现的。

1.链表节点

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

该类为Java实现链表的基础,在节点对象中关联当前节点的上一个和下一个节点。通过这样的方式构建出链表结构。每增加一个结点就需要创建一个对象,所以这也也是耗时的一个操作。

2.在头部插入节点

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

头部插入节点,先将当前的头部节点记录下来,并创建新的节点并将新创建的节点设置为头部节点,判断当前的头部节点是否为NULL如果当前头部节点为NULL则设置当前节点也为最后一个节点,否则就将新结点设置为当前头部结点的prev指向的新节点。增加链表数量,增加操作次数

3.向尾部添加节点

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

向尾部添加节点,先将当前的尾部节点记录下来,并创建新的节点并将新创建的节点设置为尾部节点,判断当前的尾部节点是否为NULL如果当前尾部节点为NULL则设置当前节点也为第一个结点,否则就将新结点设置为当前尾部结点的next指向的新节点。增加链表数量,增加操作次数

4.拆链

/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

它就可以把当前这个元素的上一个节点和一个节点进行相连,之后把自己拆除,首先将要拆除的节点的next 和 prev进行记录,首先判断当前节点的上一个节点是否为NUll如果为NULL将上一个节点设置为头部节点,否则就上个节点与要拆除的节点的下一个节点进行相连;判断当前的节点的下一个节点是否为NULL如果为NULL将当前节点的上一个节点设置为最后一个节点,否则将下一个节点与该节点的上一个节点进行连接,将当前节点设置为NULL,减少数据,怎加操作数。

5.删除节点

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

循环判断比较找到对应的节点,调用拆链方法将节点删除,因为需要循环比对节点,相对比较耗时

常见问题

什么场景下使用链表更合适

对线性表的长度或者规模难以估计;频繁做插入删除操作;