《Java版数据结构 & 集合类剖析》链表与LinkedList:节点手拉手,增删不用愁

《Java版数据结构 & 集合类剖析》链表与LinkedList:节点手拉手,增删不用愁

本章节主要讲的是数据结构中经典的链表,对应Java集合类中的LinkedList封装和源码剖析,还会额外用Java实现一个简单的单链表。如果你对此有兴趣或正在复习相关知识,那么欢迎光临!

1、LinkedList的封装与设计

LinkedList 继承实现结构图如下:

  • LinkedList 并没有直接继承 AbstractList,而是继承自 AbstractSequentialList 。这个抽象类专门为顺序访问的列表(如链表)提供骨架实现,它基于迭代器 实现 get、set、add、remove 等方法(每次操作都从头或尾遍历,效率 O(n)),与 ArrayList 的随机访问父类形成对比。
  • LinkedList 额外实现了 Deque 和 Queue 接口 ,因此它天然就是一个双端队列支持 offer、poll、push、pop 等操作。这是 ArrayList 完全没有的能力
  • LinkedList因为其底层的数据结构没有实现RandomAccess 这个随机访问接口,又因为Java中LinkedList的API是以索引为中心 ,所以它的插入的时间复杂度并没有O(1),除了头尾插和头尾删以外的插入和删除都是O(N) ,所以LinkedList的插入效率真正对比ArrayList并没有明显优势。
    • 作为对比,C++中的list,它的API是以迭代器为中心,所以它只要拿到对应迭代器位置就能实现任意位置的插入和删除时间复杂度为O(1)。

2、链表

2.1、链表的概念及结构

为了更直观地理解链表的结构,我们可以用Mermaid图表来展示:
#mermaid-svg-VrSY5ErA0UGxOgqm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VrSY5ErA0UGxOgqm .error-icon{fill:#552222;}#mermaid-svg-VrSY5ErA0UGxOgqm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VrSY5ErA0UGxOgqm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VrSY5ErA0UGxOgqm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VrSY5ErA0UGxOgqm .marker.cross{stroke:#333333;}#mermaid-svg-VrSY5ErA0UGxOgqm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VrSY5ErA0UGxOgqm p{margin:0;}#mermaid-svg-VrSY5ErA0UGxOgqm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm .cluster-label text{fill:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm .cluster-label span{color:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm .cluster-label span p{background-color:transparent;}#mermaid-svg-VrSY5ErA0UGxOgqm .label text,#mermaid-svg-VrSY5ErA0UGxOgqm span{fill:#333;color:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm .node rect,#mermaid-svg-VrSY5ErA0UGxOgqm .node circle,#mermaid-svg-VrSY5ErA0UGxOgqm .node ellipse,#mermaid-svg-VrSY5ErA0UGxOgqm .node polygon,#mermaid-svg-VrSY5ErA0UGxOgqm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VrSY5ErA0UGxOgqm .rough-node .label text,#mermaid-svg-VrSY5ErA0UGxOgqm .node .label text,#mermaid-svg-VrSY5ErA0UGxOgqm .image-shape .label,#mermaid-svg-VrSY5ErA0UGxOgqm .icon-shape .label{text-anchor:middle;}#mermaid-svg-VrSY5ErA0UGxOgqm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VrSY5ErA0UGxOgqm .rough-node .label,#mermaid-svg-VrSY5ErA0UGxOgqm .node .label,#mermaid-svg-VrSY5ErA0UGxOgqm .image-shape .label,#mermaid-svg-VrSY5ErA0UGxOgqm .icon-shape .label{text-align:center;}#mermaid-svg-VrSY5ErA0UGxOgqm .node.clickable{cursor:pointer;}#mermaid-svg-VrSY5ErA0UGxOgqm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VrSY5ErA0UGxOgqm .arrowheadPath{fill:#333333;}#mermaid-svg-VrSY5ErA0UGxOgqm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VrSY5ErA0UGxOgqm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VrSY5ErA0UGxOgqm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VrSY5ErA0UGxOgqm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VrSY5ErA0UGxOgqm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VrSY5ErA0UGxOgqm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VrSY5ErA0UGxOgqm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VrSY5ErA0UGxOgqm .cluster text{fill:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm .cluster span{color:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VrSY5ErA0UGxOgqm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VrSY5ErA0UGxOgqm rect.text{fill:none;stroke-width:0;}#mermaid-svg-VrSY5ErA0UGxOgqm .icon-shape,#mermaid-svg-VrSY5ErA0UGxOgqm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VrSY5ErA0UGxOgqm .icon-shape p,#mermaid-svg-VrSY5ErA0UGxOgqm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VrSY5ErA0UGxOgqm .icon-shape .label rect,#mermaid-svg-VrSY5ErA0UGxOgqm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VrSY5ErA0UGxOgqm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VrSY5ErA0UGxOgqm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VrSY5ErA0UGxOgqm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过引用链接实现
逻辑顺序结构(连续)
A
B
C
物理存储结构(非连续)
节点1

data: A

next: →
节点2

data: B

next: →
节点3

data: C

next: null

  • 链表是一种物理存储结构上非连续 ,但逻辑顺序上是连续 的存储结构。数据元素的逻辑顺序是通过链表中的引用链接实现的
  • 链表是一种物理存储结构上非连续 ,但逻辑顺序上是连续 的存储结构。数据元素的逻辑顺序是通过链表中的引用链接实现的
  1. 从上图可以看出,链表的结构在逻辑上是连续的,但是在物理空间上不一定连续
  2. 实际上的节点一般都是从堆上申请出来的

链表的结构有非常多种,以下几个特点组合起来就有八种链表结构:

  • 单向或双向

  • 带头或不带头

  • .循环或非循环

    虽然有这么多种链表,但是我们只需掌握两种

  1. 无头单向非循环链表 :结构简单,一般不会单独存储数据,实际更多是作为其它数据结构的子结构,如哈希桶,图的邻接表等等。
  2. 无头双向非循环链表 :Java集合框架中的LinkedList的底层就是无头双向链表
    • C++中的list底层是带头双向循环链表,这其中的取舍我们不进行讨论,只需知道java以这个链表为特性很好的适配了双端队列的特性。

虽然有这么多种链表,但是我们只需掌握两种

2.2、单链表的模拟实现

在许多算法题中,单链表的题占大部分,所以我们有必要来对其进行模拟实现

下面是单链表的基本结构示意图:
#mermaid-svg-Bqs7wddSgYqyZYGf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bqs7wddSgYqyZYGf .error-icon{fill:#552222;}#mermaid-svg-Bqs7wddSgYqyZYGf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bqs7wddSgYqyZYGf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bqs7wddSgYqyZYGf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bqs7wddSgYqyZYGf .marker.cross{stroke:#333333;}#mermaid-svg-Bqs7wddSgYqyZYGf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bqs7wddSgYqyZYGf p{margin:0;}#mermaid-svg-Bqs7wddSgYqyZYGf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf .cluster-label text{fill:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf .cluster-label span{color:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf .cluster-label span p{background-color:transparent;}#mermaid-svg-Bqs7wddSgYqyZYGf .label text,#mermaid-svg-Bqs7wddSgYqyZYGf span{fill:#333;color:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf .node rect,#mermaid-svg-Bqs7wddSgYqyZYGf .node circle,#mermaid-svg-Bqs7wddSgYqyZYGf .node ellipse,#mermaid-svg-Bqs7wddSgYqyZYGf .node polygon,#mermaid-svg-Bqs7wddSgYqyZYGf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Bqs7wddSgYqyZYGf .rough-node .label text,#mermaid-svg-Bqs7wddSgYqyZYGf .node .label text,#mermaid-svg-Bqs7wddSgYqyZYGf .image-shape .label,#mermaid-svg-Bqs7wddSgYqyZYGf .icon-shape .label{text-anchor:middle;}#mermaid-svg-Bqs7wddSgYqyZYGf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Bqs7wddSgYqyZYGf .rough-node .label,#mermaid-svg-Bqs7wddSgYqyZYGf .node .label,#mermaid-svg-Bqs7wddSgYqyZYGf .image-shape .label,#mermaid-svg-Bqs7wddSgYqyZYGf .icon-shape .label{text-align:center;}#mermaid-svg-Bqs7wddSgYqyZYGf .node.clickable{cursor:pointer;}#mermaid-svg-Bqs7wddSgYqyZYGf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Bqs7wddSgYqyZYGf .arrowheadPath{fill:#333333;}#mermaid-svg-Bqs7wddSgYqyZYGf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Bqs7wddSgYqyZYGf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Bqs7wddSgYqyZYGf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bqs7wddSgYqyZYGf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Bqs7wddSgYqyZYGf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bqs7wddSgYqyZYGf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Bqs7wddSgYqyZYGf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Bqs7wddSgYqyZYGf .cluster text{fill:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf .cluster span{color:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Bqs7wddSgYqyZYGf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Bqs7wddSgYqyZYGf rect.text{fill:none;stroke-width:0;}#mermaid-svg-Bqs7wddSgYqyZYGf .icon-shape,#mermaid-svg-Bqs7wddSgYqyZYGf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Bqs7wddSgYqyZYGf .icon-shape p,#mermaid-svg-Bqs7wddSgYqyZYGf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Bqs7wddSgYqyZYGf .icon-shape .label rect,#mermaid-svg-Bqs7wddSgYqyZYGf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Bqs7wddSgYqyZYGf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Bqs7wddSgYqyZYGf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Bqs7wddSgYqyZYGf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} head指针
节点1

val: A

next: →
节点2

val: B

next: →
节点3

val: C

next: null

我们先写其要实现的接口部分

在许多算法题中,单链表的题占大部分,所以我们有必要来对其进行模拟实现

我们先写其要实现的接口部分

IList接口

java 复制代码
public interface IList<T> {
    //头插
    public void addFirst(T data);
    //尾插
    public void addLast(T data);
    //指定插入
    public void addIndex(int index,T data);
    //头删
    public void removeFirst();
    //尾删
    public void removeLast();
    //是否包含关键字
    public boolean contains(T data);
    //按顺序删除第一个值为key的节点
    public void remove(T key);
    // 删除所有值为key的节点
    public void removeAll(T key);
    //打印链表
    public void display();
    //获取单链表的长度
    //清理链表
    public void clear();
	//修改指定位置的值
	public void set(int index,T data);
}

MySingleList类

java 复制代码
import java.util.Objects;

public class MySingleList<T> implements IList<T> {

    ListNode head;
    int size;
    private static class ListNode<T>{
        T val;
        ListNode next;

        public ListNode(T val) {
            this.val = val;
        }
    }

    @Override
    public void addFirst(T data) {
        //头插:思考头为空的情况,发现头为空和头不为空的情况可以合并,结果是对的
        ListNode<T> newHead = new ListNode<>(data);
        newHead.next = head;
        head = newHead;
        size++;
    }

    @Override
    public void addLast(T data) {
        //尾插:思考头为空的情况:避免空指针异常
        if(head == null){
            head = new ListNode<>(data);
            return;
        }
        //头不为空的情况:
        ListNode<T> newNode = new ListNode<>(data);
        ListNode<T> cur = head;
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = newNode;
        size++;
    }

    @Override
    public void addIndex(int index, T data) {
        //先判断index是否合理
        if (index < 0 || index > size){
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        //考虑头插,顺便处理头节点为空的情况:
        if (index == 0){
            ListNode<T> newHead = new ListNode<>(data);
            newHead.next = head;
            head = newHead;
            size++;
            return;
        }
        ListNode<T> cur = head;
        while (--index != 0){
            cur = cur.next;
        }
        ListNode<T> newNode = new ListNode<>(data);
        newNode.next = cur.next;
        cur.next = newNode;
        size++;
    }

    @Override
    public void removeFirst() {
        //头删,要保证有节点可删除
        if(size == 0){
            throw new IndexOutOfBoundsException( "size: " + size);
        }
        head = head.next;
    }

    @Override
    public void removeLast() {
        //尾删:也要保证尾部有节点可删除
        if(size == 0){
            throw new IndexOutOfBoundsException("size :" + size);
        }
        if(size == 1){
            head = null;
        }else{
            ListNode<T> cur = head;
            while (cur.next.next != null){
                cur = cur.next;
            }
            cur.next = null;
        }
        size--;
    }

    @Override
    public boolean contains(T data) {
        if(head == null){
            return false;
        }
        ListNode<T> cur = head;
        while (cur != null){
            if(Objects.equals(cur.val, data)){
                return true;
            }
            cur = cur.next;
        }
        return false;

    }

    @Override
    public void remove(T key) {
        if(head == null){
            return;
        }
        ListNode<T> cur = head.next;
        ListNode<T> prev = head;
        //处理头结点
        if(Objects.equals(head.val, key)){
            head = head.next;
            size--;
            return;
        }
        while (cur != null){

            if (Objects.equals(cur.val,key)){
                prev.next = cur.next;
                size--;
                break;
            }
            prev = cur;
            cur = cur.next;
        }
    }

    @Override
    public void removeAll(T key) {
        if(head == null){
            return;
        }
        //处理头结点:
        while (head != null && Objects.equals(head.val, key)) {
            head = head.next;
            size--;
        }
        if (head == null) return; // 链表已空

        ListNode<T> cur = head.next;
        ListNode<T> prev = head;
        while (cur != null){
            if(Objects.equals(cur.val,key)){
                prev.next = cur.next;
                size--;
            }else{
                //没有删除节点时,prev才会更新,不然prev会移到要删除的节点上
                prev = cur;
            }
            cur = cur.next;
        }
    }

    @Override
    public void display() {
        ListNode<T> cur = head;
        while (cur != null){
            System.out.println(cur.val);
            cur = cur.next;
        }
    }



    @Override
    public void clear() {
        head = null;
        size = 0;
    }
    
    @Override
    public void set(int index,T data){
        if (index < 0 || index >= this.size){
            return;
        }
        ListNode<T> cur = head;
        while (index-- != 0){
            cur = cur.next;
        }
        cur.val = data;
    }
}

接下来对以上代码分段解析:

java 复制代码
 public class MySingleList<T> implements IList<T> {

    ListNode head;
    int size;
    private static class ListNode<T>{
        T val;
        ListNode next;

        public ListNode(T val) {
            this.val = val;
        }
    }
}
  • 以上是MySingleList与其内部类ListNode的组成设计:
  • 由于单链表内都是由一个个节点组成的,所以我们将ListNode类设为内部类。又因为这里内部类不依赖外部类的成员变量 ,所以从轻便防止内存泄漏方便创建节点不依赖于外部实例来创建 的角度考虑,我们设为静态内部类
  • 这里的设置了size属性来记录节点个数,避免了用size方法每次使用都要进行遍历造成的时间成本

插入节点:

java 复制代码
  @Override
    public void addFirst(T data) {
        //头插:思考头为空的情况,发现头为空和头不为空的情况可以合并,结果是对的
        ListNode<T> newHead = new ListNode<>(data);
        newHead.next = head;
        head = newHead;
        size++;
    }

    @Override
    public void addLast(T data) {
        //尾插:思考头为空的情况:避免空指针异常
        if(head == null){
            head = new ListNode<>(data);
            return;
        }
        //头不为空的情况:
        ListNode<T> newNode = new ListNode<>(data);
        ListNode<T> cur = head;
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = newNode;
        size++;
    }

    @Override
    public void addIndex(int index, T data) {
        //先判断index是否合理
        if (index < 0 || index > size){
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        //考虑头插,顺便处理头节点为空的情况:
        if (index == 0){
            ListNode<T> newHead = new ListNode<>(data);
            newHead.next = head;
            head = newHead;
            size++;
            return;
        }
        ListNode<T> cur = head;
        while (--index != 0){
            cur = cur.next;
        }
        ListNode<T> newNode = new ListNode<>(data);
        newNode.next = cur.next;
        cur.next = newNode;
        size++;
    } 
  • 单链表的插入,我们发现都要考虑头节点为空的情况
  • 头插中,头节点为空的情况可以直接合并
  • 尾插中,头节点为空的情况要特殊处理,将head引用指向新节点
  • 指定插入中,除了考虑头节点为空的情况还要考虑** index下标的合法性** 。
    • 在这里,因为指定插入 还要考虑与后面节点的连接 ,所以这里的index如果是0,头插的逻辑是不能包括在内的 (不能访问到头节点前面的节点进行插入),所以针对头插和头节点为空的情况进行特殊处理。

删除节点:

java 复制代码
@Override
    public void removeFirst() {
        //头删,要保证有节点可删除
        if(size == 0){
            throw new IndexOutOfBoundsException( "size: " + size);
        }
        head = head.next;
    }

    @Override
    public void removeLast() {
        //尾删:也要保证尾部有节点可删除
        if(size == 0){
            throw new IndexOutOfBoundsException("size :" + size);
        }
        if(size == 1){
            head = null;
        }else{
            ListNode<T> cur = head;
            while (cur.next.next != null){
                cur = cur.next;
            }
            cur.next = null;
        }
        size--;
    }
@Override
    public void remove(T key) {
        if(head == null){
            return;
        }
        ListNode<T> cur = head.next;
        ListNode<T> prev = head;
        //处理头结点
        if(Objects.equals(head.val, key)){
            head = head.next;
            size--;
            return;
        }
        while (cur != null){

            if (Objects.equals(cur.val,key)){
                prev.next = cur.next;
                size--;
                break;
            }
            prev = cur;
            cur = cur.next;
        }
    }

    @Override
    public void removeAll(T key) {
        if(head == null){
            return;
        }
        //处理头结点:
        while (head != null && Objects.equals(head.val, key)) {
            head = head.next;
            size--;
        }
        if (head == null) return; // 链表已空

        ListNode<T> cur = head.next;
        ListNode<T> prev = head;
        while (cur != null){
            if(Objects.equals(cur.val,key)){
                prev.next = cur.next;
                size--;
            }else{
                //没有删除节点时,prev才会更新,不然prev会移到要删除的节点上
                prev = cur;
            }
            cur = cur.next;
        }
    }
  • 删除这里有四种:头删,尾删,首个关键字删除,全部关键字删除
  • 无论哪种删除,首先都要保证节点个数不为0,就是有节点可删
  • 在java中,并不能做到针对某些部分进行内存的释放,我们是通过配合gc(垃圾回收机制)来进行处理,我们只需要将要删除的对象的引用给重新指向别处即可。在链表中我们就是将next引用跳过下个节点连接到下下个节点即可完成删除。
  • 但这也导致一个问题,在上面写的后两个删除方法中,如果我们要删除的是头节点,那么它的逻辑就和删除其他节点不同,所以我们对头节点的删除进行特殊处理:将head引用给到下一个节点
  • 上面的删除逻辑包含了泛型的处理,在比较两个值是否相等,**使用的是Object.equals()**方法,而不是某一方对象.equals(),避免了某一边为空导致的空指针异常的情况
  • 最后两个删除方法还用到了双指针中的前后指针,要注意removeAll方法中删除完一个节点后,prev不需要进行更新,不然会走到被删除的节点的位置。

可以看到单链表对比顺序表只有在头部的插入和删除不浪费额外空间上有明显优势。

可以看到单链表对比顺序表只有在头部的插入和删除不浪费额外空间上有明显优势。

3、LinkedList的使用:

3.1、LinkedList的构造

方法 解释
LinkedList() 无参构造
public LinkedList(Collection<?extends E> c 使用其他集合容器中的元素构造list

3.2、LinkedList的其他常用方法介绍:

方法 解释
boolean add(E e) 尾插 e
void add(int index, E element) 将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) 尾插 c 中的元素
E remove(int index) 删除 index 位置元素
boolean remove(Object o) 删除遇到的第一个 o
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
void clear() 清空
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
int lastIndexOf(Object o) 返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) 截取部分 list

使用演示:

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
            // 创建 ArrayList
            List<String> list = new ArrayList<>();

            // 1. 尾插 add(E e)
            list.add("Apple");
            list.add("Banana");
            list.add("Cherry");
            System.out.println("after add: " + list);

            // 2. 指定位置插入 add(int index, E element)
            list.add(1, "Blueberry");
            System.out.println("after add(1, Blueberry): " + list);

            // 3. 获取元素 get(int index)
            String fruit = list.get(2);
            System.out.println("get(2): " + fruit);

            // 4. 修改元素 set(int index, E element)
            list.set(2, "Coconut");
            System.out.println("after set(2, Coconut): " + list);

            // 5. 删除指定位置元素 remove(int index)
            String removed = list.remove(3);
            System.out.println("removed at index 3: " + removed + ", now list: " + list);

            // 6. 删除指定对象 remove(Object o)
            boolean removedFlag = list.remove("Apple");
            System.out.println("remove(Apple): " + removedFlag + ", list: " + list);

            // 7. 判断包含 contains
            System.out.println("contains(Coconut): " + list.contains("Coconut"));
            System.out.println("contains(Apple): " + list.contains("Apple"));

            // 8. 查找索引 indexOf / lastIndexOf
            list.add("Banana");
            System.out.println("list: " + list);
            System.out.println("indexOf(Banana): " + list.indexOf("Banana"));
            System.out.println("lastIndexOf(Banana): " + list.lastIndexOf("Banana"));

            // 9. 截取子列表(视图) subList
            List<String> sub = list.subList(1, 3);
            System.out.println("subList(1,3): " + sub);
                //对视图修改会影响原链表
                sub.add("orange");
                System.out.println(list);
                //如果想创建一个新的对象赋上截取的数据应该这样写:
                List<String> list2 = new LinkedList<>(list.subList(1,3));
            // 10. 清空 clear
            list.clear();
            System.out.println("after clear, size: " + list.size());
    }
}

3.3、LinkedList的遍历

有三种访问方式:foreach 遍历,正向迭代器遍历,反向迭代器遍历

java 复制代码
public static void main(String[] args) {
	LinkedList<Integer> list = new LinkedList<>();list.add(1); // add(elem): 表示尾插list.add(2);list.add(3);list.add(4);list.add(5);list.add(6);list.add(7);System.out.println(list.size());// foreach遍历
	for (int e:list) {
		System.out.print(e + " ");
	}
	System.out.println();// 使用迭代器遍历---正向遍历
		ListIterator<Integer> it = list.listIterator();
	while(it.hasNext()){
		System.out.print(it.next()+ " ");
	}
	System.out.println();// 使用反向迭代器---反向遍历
	//注意这里传入的是list.size()而不是list.size() - 1
	
	ListIterator<Integer> rit = list.listIterator(list.size());
	while (rit.hasPrevious()){
		System.out.print(rit.previous() +" ");
	}
	System.out.println();
}

4、 LinkedList的模拟实现

LinkedList的底层实现是无头双向不循环链表,结构如下:

  • 可以看到,这个结构仅仅比我们单链表多了双向这一个特性
  • 有了这个特性可以让我们查找尾部节点 ,在尾部插入删除 的时间复杂度大大降低,且允许反向进行遍历 ,所以我们需要多添加一个引用last指向最后一位节点,方便进行操作。

4.1、代码展示:

依旧先写接口:

IList接口

java 复制代码
public interface IList<T> {
    //头插
    public void addFirst(T data);
    //尾插
    public void addLast(T data);
    //指定插入
    public void addIndex(int index,T data);
    //头删
    public void removeFirst();
    //尾删
    public void removeLast();
    //是否包含关键字
    public boolean contains(T data);
    //按顺序删除第一个值为key的节点
    public void remove(T key);
    // 删除所有值为key的节点
    public void removeAll(T key);
    //打印链表
    public void display();
    //获取单链表的长度
    //清理链表
    public void clear();
	//修改指定位置的值
	public void set(int index,T data);

MyLinkedList类:

java 复制代码
import java.util.Objects;

public class MyLinkedList<T> implements IList<T> {

    private static class ListNode<T>{
        T val;
        ListNode<T> next;
        ListNode<T> prev;

        public ListNode(T val) {
            this.val = val;
        }
    }
    ListNode<T> head;
    ListNode<T> last;
    int size = 0;
    @Override
    public void addFirst(T data) {
        ListNode<T> newHead = new ListNode<>(data);
        //插入节点考虑头为空的情况
        if(head == null){
           head = newHead;
           last = newHead;
        }else{
            newHead.next = head;
            head.prev = newHead;
            head = newHead;
        }
        size++;
    }

    @Override
    public void addLast(T data) {
        ListNode<T> newNode = new ListNode<>(data);
        if (head == null){
            head = newNode;
            last = newNode;
        }else {
            last.next = newNode;
            newNode.prev = last;
            last = newNode;
        }
        size++;
    }

    @Override
    public void addIndex(int index, T data) {
        if(index < 0 || index > size){
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        if (index == 0){
            this.addFirst(data);
        }else if(index == size){
            this.addLast(data);
        }
        else{
            ListNode<T> newNode = new ListNode<>(data);
            //此处可优化遍历,看index离head近离还是last近
            ListNode<T> preNode = node(index - 1);
            //先连接后面的
            preNode.next.prev = newNode;
            newNode.next = preNode.next;
            //再连接前面的
            preNode.next = newNode;
            newNode.prev = preNode;
            size++;
        }

    }

    private ListNode<T> node(int index) {
        if(index < (size >> 1)){
            ListNode<T> x = head;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
            return x;
        }else{
            ListNode<T> x = last;
            for (int i = size - 1; i > index; i++) {
                x = x.prev;
            }
            return x;
        }
    }

    @Override
    public void removeFirst() {
        //处理无节点的情况
        if(head == null){
            throw new IndexOutOfBoundsException("remove fail");
        }
        //只有一个节点的情况:
        if (size == 1){
            head = null;
            last = null;
        }else{
            head = head.next;             // 头节点后移
            head.prev = null;
        }
        size--;
    }

    @Override
    public void removeLast() {
        //处理无节点的情况
        if(head == null){
            throw new IndexOutOfBoundsException("remove fail");
        }
        //处理只有一个节点的情况:
        if(size == 1){
            head = null;
            last = null;
        }else{
            ListNode<T> preNode = last.prev;
            preNode.next = null;
            last = preNode;
        }
        size--;

    }

    @Override
    public boolean contains(T data) {
        ListNode<T> cur = head;
        while (cur != null){
            if(Objects.equals(cur.val,data)){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    @Override
    public void remove(T key) {
        if (head == null) return;
        if (Objects.equals(head.val, key)) {
            removeFirst();
            return;
        }
        if (Objects.equals(last.val, key)) {
            removeLast();
            return;
        }
        ListNode<T> cur = head.next; // 跳过已检查的头
        while (cur != null && cur != last) { // 也可不加 last 判断,因为 last 已排除
            if (Objects.equals(cur.val, key)) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                size--;
                return;
            }
            cur = cur.next;
        }
    }

    @Override
    public void removeAll(T key) {
        // 1. 循环删除头节点
        while (head != null && Objects.equals(head.val, key)) {
            head = head.next;
            if (head != null) head.prev = null;
            else last = null;
            size--;
        }
        if (head == null) return; // 全部删完

        // 2. 循环删除尾节点(此时头已确定非匹配)
        while (last != null && Objects.equals(last.val, key)) {
            last = last.prev;
            if (last != null) last.next = null;
            // 注意:头已经在步骤1中处理,不会因为删除尾而变空(因为 head != null)
            size--;
        }

        // 3. 遍历中间节点(不包括头尾)
        ListNode<T> cur = head.next;
        while (cur != null && cur != last) {
            if (Objects.equals(cur.val, key)) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                size--;
            }
            cur = cur.next;
        }
    }

    @Override
    public void display() {
        ListNode<T> cur = head;
        while (cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    @Override
    public void clear() {
        head = null;
        last = null;
        size = 0;
    }

    @Override
    public void set(int index, T data) {
        if (index < 0 || index >= size){
            throw new IndexOutOfBoundsException("set fail");
        }
        ListNode<T> cur = node(index);
        cur.val = data;
    }
}

可以看到,在实现无头双向非循环链表时的细节与单链表还是有很多不同的,接下来依旧分段进行解析:

4.2、分段剖析:

泛型容器与核心成员:

java 复制代码
 public class MyLinkedList<T> implements IList<T> {

    private static class ListNode<T>{
        T val;
        ListNode<T> next;
        ListNode<T> prev;

        public ListNode(T val) {
            this.val = val;
        }
    }
    ListNode<T> head;
    ListNode<T> last;
    int size = 0; 
  • 这里的设计思路与单链表相似,仅仅多了prev和last两个成员变量,这两个变量的意义在上面也提到过

插入节点

java 复制代码
  @Override
    public void addFirst(T data) {
        ListNode<T> newHead = new ListNode<>(data);
        //插入节点考虑头为空的情况
        if(head == null){
           head = newHead;
           last = newHead;
        }else{
            newHead.next = head;
            head.prev = newHead;
            head = newHead;
        }
        size++;
    }

    @Override
    public void addLast(T data) {
        ListNode<T> newNode = new ListNode<>(data);
        if (head == null){
            head = newNode;
            last = newNode;
        }else {
            last.next = newNode;
            newNode.prev = last;
            last = newNode;
        }
        size++;
    }

    @Override
    public void addIndex(int index, T data) {
        if(index < 0 || index > size){
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        if (index == 0){
            this.addFirst(data);
        }else if(index == size){
            this.addLast(data);
        }
        else{
            ListNode<T> newNode = new ListNode<>(data);
            //此处可优化遍历,看index离head近离还是last近
            ListNode<T> preNode = node(index - 1);
            //先连接后面的
            preNode.next.prev = newNode;
            newNode.next = preNode.next;
            //再连接前面的
            preNode.next = newNode;
            newNode.prev = preNode;
            size++;
        }

    }

    private ListNode<T> node(int index) {
        if(index < (size >> 1)){
            ListNode<T> x = head;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
            return x;
        }else{
            ListNode<T> x = last;
            for (int i = size - 1; i > index; i++) {
                x = x.prev;
            }
            return x;
        }
    }
  • 在双向链表的插入中,比单链表要多维护一个引用last的更新,所以对头结点是否为空(没有节点)格外的关心。
  • 在双向链表的插入操作中,连接环节需要确保以下双向链接正确建立:
    • 头插新节点的 next 指向原头节点原头节点的 prev 指向新节点
    • 尾插新节点的 prev 指向原尾节点原尾节点的 next 指向新节点
    • 指定位置插入 (在 prevNode 与 nextNode 之间):需依次完成
      • 新节点的 prev 指向 prevNode,
      • 新节点的 next 指向 nextNode,
      • prevNode.next 指向新节点,
      • nextNode.prev 指向新节点。
  • 在指定插入中,还需要注意,只有一个节点时的插入也需要特殊处理,因为此时后一个节点是为空的,直接用后面的逻辑会造成空指针异常
  • 除此之外,指定插入中的遍历链表环节也可以进行优化:
    • 分析index的位置更接近于head还是last,来决定从head开始正向遍历,还是从last开始反向遍历。

删除节点 :

java 复制代码
  @Override
    public void removeFirst() {
        //处理无节点的情况
        if(head == null){
            throw new IndexOutOfBoundsException("remove fail");
        }
        //只有一个节点的情况:
        if (size == 1){
            head = null;
            last = null;
        }else{
            head = head.next;             // 头节点后移
            head.prev = null;
        }
        size--;
    }

    @Override
    public void removeLast() {
        //处理无节点的情况
        if(head == null){
            throw new IndexOutOfBoundsException("remove fail");
        }
        //处理只有一个节点的情况:
        if(size == 1){
            head = null;
            last = null;
        }else{
            ListNode<T> preNode = last.prev;
            preNode.next = null;
            last = preNode;
        }
        size--;

    }
    @Override
    public void remove(T key) {
        if (head == null) return;
        if (Objects.equals(head.val, key)) {
            removeFirst();
            return;
        }
        if (Objects.equals(last.val, key)) {
            removeLast();
            return;
        }
        ListNode<T> cur = head.next; // 跳过已检查的头
        while (cur != null && cur != last) { // 也可不加 last 判断,因为 last 已排除
            if (Objects.equals(cur.val, key)) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                size--;
                return;
            }
            cur = cur.next;
        }
    }

    @Override
    public void removeAll(T key) {
        // 1. 循环删除头节点
        while (head != null && Objects.equals(head.val, key)) {
            head = head.next;
            if (head != null) head.prev = null;
            else last = null;
            size--;
        }
        if (head == null) return; // 全部删完

        // 2. 循环删除尾节点(此时头已确定非匹配)
        while (last != null && Objects.equals(last.val, key)) {
            last = last.prev;
            if (last != null) last.next = null;
            // 注意:头已经在步骤1中处理,不会因为删除尾而变空(因为 head != null)
            size--;
        }

        // 3. 遍历中间节点(不包括头尾)
        ListNode<T> cur = head.next;
        while (cur != null && cur != last) {
            if (Objects.equals(cur.val, key)) {
                cur.prev.next = cur.next;
                cur.next.prev = cur.prev;
                size--;
            }
            cur = cur.next;
        }
    }
  • 无头双向非循环链表中,删除节点不仅要对无节点的情况进行判断,还要对只有一个节点的情况进行特殊处理(因为要维护last引用)
  • 在后两个删除方法中,对头尾删除进行了复用,剩下的就是遍历中间节点然后进行连接即可

5、LinkedList源码剖析

这里仅仅只对部分封装设计进行分析:

java 复制代码
 public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    transient int size = 0;

    /**
     * Pointer to first node.
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     */
    transient Node<E> last;
    
    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;
        }
    }
 }
  • 可以看到,这里对三个成员变量都用transient 进行修饰,都不想对其进行默认的序列化。
    • 防止了写入大量与业务数据无关的JVM内部信息(对象布局,GC元数据等)
    • 防止了因默认的反序列化的过程想要恢复节点之间的引用关系失败导致的一系列后果。
  • 其内部类节点的构造选择将所有成员统一传入,不允许在外部一个个修改引用。

java 复制代码
public void addFirst(E e) {
     linkFirst(e);
 }
public void addLast(E e) {
    linkLast(e);
}
public boolean add(E e) {
    linkLast(e);
    return true;
}
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

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++;
   }

   /**
    * 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++;
  }
  void linkBefore(E e, Node<E> succ) {
       // assert succ != null;
       final Node<E> pred = succ.prev;
       final Node<E> newNode = new Node<>(pred, e, succ);
       succ.prev = newNode;
       if (pred == null)
           first = newNode;
       else
           pred.next = newNode;
       size++;
       modCount++;
   }
   
 Node<E> node(int index) {
      // assert isElementIndex(index);

      if (index < (size >> 1)) {
          Node<E> x = first;
          for (int i = 0; i < index; i++)
              x = x.next;
          return x;
      } else {
          Node<E> x = last;
          for (int i = size - 1; i > index; i--)
              x = x.prev;
          return x;
      }
  }
 
  • 可以看到实际外部可调用的方法又调用了一层,而不是直接实现。
  • 源码插入的逻辑实现 ,看到用final进行修饰了变量,这是为了防止传入构造函数之前,引用被修改。
  • 这里的指定插入也实现了遍历优化,上面的模拟实现已经介绍到。

6、链表的性质:

从以上那么多的分析,我们不难看出链表的优点有些正好弥补了顺序表的缺点,在以下给各位进行总结

6.1、链表的优点

  • 内存分配方面链表无内存浪费,不会因预留过大而浪费,也不会因容量不足而扩容(顺序表的扩容也是一种非常大的消耗)
  • 插入与删除方面 : 对于频繁增删的场景(如任务队列、LRU缓存),链表效率远超数组
    • 虽然从结构上看,链表的增删有明显优势,但是在Java中的具体实现上,LinkedList 和 ArrayList的增删效率其实区别不大,具体的原因,我们之前就已总结。
  • 空间利用率方面 : 因为其结构的特性,可以利用细小的内存碎片来创建节点
  • 扩展性方面 :面对不确定的数据量,不需要过多成本就可以进行扩展
    至此链表章节结束,下一章将会继续看栈和堆

6.2、链表的缺点

  • 查找遍历方面:因底层结构导致无法实现随机访问,查找数据只能从头进行遍历
  • 内存开销方面每个节点都要额外储存一两个引用
  • 缓存性能:不适配cpu的缓存行机制,cpu缓存命中率低
  • 内存碎片 :频繁的删除节点也会导致内存碎片增多

  • 总的来说,在现代工程实践中,由于CPU缓存和内存连续性的巨大优势,顺序表,往往是默认选择。链表仅在特定场景(如超大频率插入删除、需要常数量级合并)下才胜出。
相关推荐
唐青枫1 小时前
Java MyBatis 实战指南:XML 映射、动态 SQL 与数据访问层设计
java·mybatis
码语智行1 小时前
MQTT 配置、依赖与使用说明
java·物联网·mt
_日拱一卒1 小时前
LeetCode:39组合总和
java·算法·leetcode·职场和发展
无限进步_1 小时前
【Linux】进程状态、僵尸与孤儿、进程调度
linux·运维·服务器·开发语言·数据结构·算法
郝学胜-神的一滴1 小时前
力扣 662 :二叉树最大宽度
java·数据结构·c++·python·算法·leetcode·职场和发展
仙俊红1 小时前
反射到底解决什么问题?
java·开发语言
小森林之主1 小时前
凌晨3点的闹钟:分布式定时任务设计实战
java·redis·任务调度·cron·分布式定时任务
yaoxin5211232 小时前
430. Java 日期时间 API - 时间计算 Temporal 包
java·前端·python
小欣加油2 小时前
leetcode169 多数元素
数据结构·c++·算法·leetcode·职场和发展