链表(LinkedList)

239 阅读5分钟

一、链表(LinkedList)

一种链式存储的线性表,所有元素的内存地址不一定是连续的

在 java.utils.LiskedList 的是使用双向链表


二、单向链表(SingleLinkedList)

单向链表只能通过头节点中的 next 节点进行遍历

1、构造方法

单向链表元素内存是临时创建的,内存地址是不连续的,使用默认构造方法即可

2、节点设计

private static class Node<E> {
    E element;
    Node<E> next;

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

设置虚拟头结点,设置虚拟头结点有助于元素的操作,leetcode删除链表倒数第N个节点

Node<E> first;

3、插入元素

  • 指定元素的插入位置,获得元素的前一个节点 prev,创建新的节点 node ,使新插入 node 节点的next指针指向要插入位置的节点,之后修改 prev 节点指向将要插入的节点。
  • 特殊情况处理,如果要插入的节点是链表的第一个元素的节点此时的 prve 便是虚拟头结点
public void add(int index, E element) {
    if (index < 0 || index > size){
        throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
    }
    if (index == 0) {
        first = new Node<>(element,first);
    } else {
        Node<E> prev = node(index - 1);
        prev.next = new Node<>(element,prev.next);
    }
    size++ ; // 成员变量,存储链表中元素的个数
}
3.1、根据链表下标获取节点
private Node<E> node(int index) {
    if (index < 0 || index > size){
        throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
    }
    Node<E> node = first;
    for (int i = 0; i < index; i++) {
        node = node.next;
    }
    return node;
}

4、增加节点

调用插入节点在末尾插入即可

public void add(E element) {
    add(size,element);
}

5、查找元素

  • 即遍历元素
private Node<E> node(int index) {
    rangeCheck(index);
    Node<E> node = first;
    for (int i = 0; i < index; i++) {
        node = node.next;
    }
    return node;
}
public E get(int index) {
    return node(index).element;
}

6、删除元素

  • 删除节点的关键在于获得将要删除节点的前一个 prev 节点和 后一个 nextNode 节点,prev 节点的next指针指向nextNode即可

  • 另一种删除节点的思路 leetcode删除节点:将该元素的值换成后一个元素的值,将后一个元素删除

public E remove(int index) {
    if (index < 0 || index > size){
        throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
    }
    Node<E> node = node(index);
    E oldElement = node.element;
    if (index == 0){
        first = first.next;
    }else {
        Node<E> prev = node(index - 1);
        prev.next = prev.next.next;
    }
    size --; // 成员变量,存储链表中元素的个数
    return oldElement;
}

7、修改元素

  • 先遍历根据下标获得该节点,然后在进行修改
private Node<E> node(int index) {
    rangeCheck(index);
    Node<E> node = first;
    for (int i = 0; i < index; i++) {
        node = node.next;
    }
    return node;
}
public E set(int index, E element) {
    Node<E> node = node(index);
    E oldElement = node.element;
    node.element = element;
    return oldElement;
}

三、双向链表(LinkedList)

相比单向链表,在节点设计中增设指向前一个节点的 prev 之中,进而又添加 last 虚拟尾节点指向链表的尾结点。

1、节点设计

//指向头节点的指针
Node<E> first;
//指向尾节点的指针
Node<E> last;

// 构造节点
private static class Node<E> {
    E element;
    Node<E> prev;
    Node<E> next;

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

2、查找元素

双向链表的查找与单向链表有所不同,单项链表只能通过开头开始查找,双向链表也可以从尾部进行查找。所以可以通过先判断索引所在的位置,选择从头 first 开始或从尾 last 开始查找,进而优化查找。

public E get(int index) {
    return node(index).element;
}

/**
 * 根据节点的index获得该节点
 */
private Node<E> node(int index) {
    rangeCheck(index);  // 自定义对index进行是否非法判断
    if (index < (size >> 2)) {
        Node<E> node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node;
    } else {
        Node<E> node = last;
        for (int i = size - 1; i > index; i--) {
            node = node.prev;
        }
        return node;
    }
}

3、增加元素

双向链表的增加元素相比单向链表更加复杂,插入位置节点是nextNode,插入节点为node,插入位置前一个节点是 prevNode,核心是维护个个节点的 prev 和 next 属性

  • prevNode 的 next 是 node

    • 特殊情况:prevNode 是 first ,即在首部插入元素,此时需要修改 first 指针的指向
  • node 的 prev 是 prevNode

  • nextNode 的 prev 是 node

    • 特殊情况:nextNode 是 last,即在尾部插入元素,此时需要修改 last 指针的指向
  • node 的 next 是 nextNode

  • 如果链表是空,即链表插入第一个元素,需要同时修改 first 和 last 指针

public void add(int index, E element) {
    rangeCheckForAdd(index);
    if (index == size) {
        Node<E> oldLast = last;
        last = new Node<>(oldLast, element, null);
        if (oldLast == null) { // 这是链表添加的第一个元素
            first = last;
        } else {
            oldLast.next = last;
        }
    }else {
        Node<E> next = node(index);
        Node<E> prev = next.prev;
        Node<E> node = new Node<>(prev, element, next);
        next.prev = node;

        if (prev == null) { // index == 0
            first = node;
        } else {
            prev.next = node;
        }
    }
    size++;
}

4、删除元素

删除节点和添加节点类似,主要是处理维护 prev 和 next 指针,特殊情况主要是删除头结点和尾结点

public E remove(int index) {
    rangeCheck(index);
    Node<E> node = node(index);
    Node<E> prev = node.prev;
    Node<E> next = node.next;

    if (prev == null) { // index == 0
        first = next;
    } else {
        prev.next = next;
    }

    if (next == null) { // index == size - 1
        last = prev;
    } else {
        next.prev = prev;
    }
    
    size--;
    return node.element;
}

5、清空链表

单向链表与双向链表类似

public void clear() {
    size = 0;
    first = null;
    last = null;
}

四、链表的复杂度

单看链表的操作是 O(1)级别的时间复杂度,但是还有遍历查找的过程,所以最好情况下时间复杂度是 o(1), 最坏是 O(n),平均复杂度是 O(n)

五、双向链表和动态数组的比较

  • 双向链表相比动态数组浪费的内存更少,但是开辟、销毁空间的操作比较多
  • 使用情形
    • 如果频繁在任意位置进行增加、删除操作,建议使用双向链表
    • 如果频繁使用查询功能,建议使用数组