05.LinkedList与链表

发布于:2025-08-06 ⋅ 阅读:(11) ⋅ 点赞:(0)

目录

1. ArrayList的缺陷

2. 链表

2.1 链表的概念以及结构

2.2 链表的实现

3.LinkedList的模拟实现

4.LinkedList的使用

4.1 什么是LinkedList

4.2 LinkedList的使用

4.2.1. LinkedList的构造

4.2.2. LinkedList的其他常用方法介绍

4.2.3. LinkedList的遍历

5. ArrayList和LinkedList的区别

详细解释差异点

底层数据结构:


1. ArrayList的缺陷

在上一节中,我们学习了ArrayList的基本用法,以及其特点。

那么,现在我们来思考一个问题,ArrayList有缺陷吗?

在上一节,我们学到ArrayList底层其实是一段连续的空间

也就是说,在ArrayList任意位置插入或者删除元素时,我们都需要将后序元素整体往前移或者往后移,这样一来,时间复杂度就是O(n),效率是较低的。

综上所述,ArrayList是不适合用于任意位置插入和删除操作比较多的场景的。

因此,Java集合中引入了LinkedList结构,也就是链表结构

2. 链表

2.1 链表的概念以及结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

ps:

  • 从上图可以看出,链式结构在逻辑上是连续的,但在物理上不一定连续
  • 现实中的节点一般都是从堆上申请出来的。
  • 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可以连续,也可能不连续。

而链表的结构其实是非常多样的。

1.单向或双向链表

2.带头或不带头链表

3.循环或非循环链表

尽管有这么多种链表结构,但我们重点只需掌握两种便OK。

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

  • 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表

2.2 链表的实现

这里,我们来简单实现一个无头单向非循环链表

import com.sun.xml.internal.bind.v2.model.core.EnumLeafInfo;

/**
 * @Author @fiber-cloud
 * 手写List
 * @Date 2025/7/27 10:50
 */
public class MySingleList {

    static class ListNode{
        public int val;
        public ListNode next;

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

    public ListNode head;


    //返回整个链表的长度
    public int size(){
        if (this.head == null){
            return 0;
        }
        ListNode cur = this.head;
        int count = 0;
        while (cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

    //打印链表中的数据,默认从头开始
    public void display(){
        ListNode cur = this.head;
        while (cur != null){
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.println();
    }

    /**
     * 从指定节点newHead开始打印链表
     */
    public void display(ListNode cur){
        ListNode cur1 = cur;
        while (cur1 != null){
            System.out.print(cur1.val+" ");
            cur1 = cur1.next;
        }
        System.out.println();
    }
    //逆序打印
    public void display2(ListNode cur){
        if (cur == null){
            return;
        }
        if (head.next == null){
            System.out.print(head.val + " ");
            return;
        }
        display2(head.next);
        System.out.print(head.val + " ");
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int val){
        ListNode cur = this.head;
        while (cur != null){
            if(cur.val == val){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
    //头插法
    public void addFirst(int data){
        ListNode cur = new ListNode(data);
        cur.next = head;
        head = cur;
    }
    //尾插法
    public void addLast(int data){
        ListNode cur = new ListNode(data);
        ListNode temp = this.head;
        if (temp == null){
            this.head = cur;
        }else {
            while (temp.next != null){
                temp = temp.next;
            }
            temp.next = cur;
        }
    }

    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data) throws IndexWrongFulException {
        //先检查输入的数据是否合法
        if (index < 0 || index > size()){
            System.out.println("index位置不合法");
            throw new IndexWrongFulException("index位置不合法");
        }

        if (index == 0){
            addFirst(data);
            return;
        }
        if (index == size()){
            addLast(data);
            return;
        }

        ListNode cur = findIndexSubOne(index);
        ListNode node = new ListNode(data);

        node.next = cur.next;
        cur.next = node;

    }

    //走index-1步
    private ListNode findIndexSubOne(int index) {
        ListNode cur = this.head;
        while (index-1 != 0){
            cur = cur.next;
            index-- ;
        }
        return cur;
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key){
        //判断链表是否为空
        if (this.head == null){
            return;
        }
        if (this.head.val == key){
            this.head = this.head.next;
            return;
        }

        ListNode cur = findPrevOfKey(key);
        if (cur == null){
            System.out.println("没有你要删除的数据");
            return;
        }
        ListNode temp = cur.next;
        cur.next = temp.next;
    }
    //找到key的前驱节点
    private ListNode findPrevOfKey(int key) {
        ListNode cur = this.head;
        while (cur.next != null){
            if (cur.next.val == key){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }


    //删除整个链表中所有值为key的节点
    public void removeAllKey(int key){
        //判断链表是否为空
        if (this.head == null){
            return;
        }
        ListNode cur = this.head.next;
        ListNode prev = this.head;

        while (cur != null){
            if (cur.val ==  key){
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
        }
        if (this.head.val == key){
            this.head = this.head.next;
        }
    }

    //倒数第K个节点
    public ListNode FindKthToTail(int k){
        if (k <= 0|| k >size()){
            return null;
        }

        //快慢指针
        ListNode slow = this.head;
        ListNode fast = this.head;

        //fast先走k-1步
        while (k-1 != 0){
            fast = fast.next;
            if (fast == null){
                return null;
            }
            k--;
        }

        while (fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }



    //链表分割
    public ListNode partition(ListNode pHead, int x) {
        ListNode bs = null;
        ListNode be = null;

        ListNode as = null;
        ListNode ae = null;

        ListNode cur = pHead;
        //遍历链表
        while (cur != null){
           if (cur.val < x){
               //第一次进行插入节点
               if (bs == null ){
                   bs = cur;
                   be = cur;
               }else {
                   be.next = cur;
                   be = be.next;
               }
           }else {
               if (as == null){
                   as = cur;
                   ae = cur;
               }else {
                   ae.next = cur;
                   ae = ae.next;
               }
           }
           cur = cur.next;
        }

        //第一个段里面没有数据
        if (bs == null){
            return as;
        }
        be.next = as ;

        if (as != null){
            ae.next = null;
        }
        return bs;
    }


    //链表的回文结构
    public boolean chkPalindrome(ListNode head) {
       if (head == null){
          return false;
       }
       if (head.next == null){
           return true;
       }


        ListNode curNext = null;
        //1.找到链表的中间节点
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }

        // 2.翻转中间节点后面的链表
        ListNode cur = slow.next;
        while (cur != null){
            curNext = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curNext;
        }
        // 3.一个从头开始 一个从尾部开始
        cur = head;
        while (head != slow){
            if (head.val != slow.val){
                return false;
            }
            if (head.next == slow){
                return true;
            }
            head = head.next;
            slow = slow.next;
        }
        return true;
    }


    //相交链表
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //1.求链表长度
        ListNode pl = headA;//永远指向最长的链表
        ListNode ps = headB;//永远指向最短的链表

        int lenA = 0;
        while (pl != null){
            lenA++;
            pl = pl.next;
        }
        int lenB = 0;
        while (ps != null){
            lenB++;
            ps = ps.next;
        }


        pl = headA;
        ps = headB;

        int len = lenA - lenB;
        if (len < 0){
            pl = headB;
            ps = headA;
            len = lenB - lenA;
        }

        //2. 让最长的链表 先走len步
        while (len != 0){
            pl = pl.next;
            len--;
        }
        // 3.pl和ps 现在在相同的起始位置
        while (pl != ps){
            pl = pl.next;
            ps = ps.next;
        }
        // 走到这里,pl和ps相等了,pl == ps == null
        if (pl == null){
            return null;
        }
        return pl;
    }

    //环形链表
    public boolean hasCycle(ListNode head) {
        //追及相遇问题
        if (head == null){
            return false;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow){
                return true;
            }
        }
        return false;
    }

    //环形链表Ⅱ
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow){
                ListNode temp = head;
                while (temp != slow){
                    temp = temp.next;
                    slow = slow.next;
                }
                return temp;
            }
        }
        return null;
    }


}

3.LinkedList的模拟实现

我们前面说到,LinkedList的底层其实是一个双向链表,这里我们来模拟实现一下。

/**
 * @Author @fiber-cloud
 * @Date 2025/7/28 20:32
 * 模拟实现LinkedList
 */
public class MyLinkedList {
    //LinkedList是一个双向链表
    // prev指向的是前一个节点的地址
    // next指向的是后一个节点的位置
    static class ListNode{
        public int val;
        public ListNode prev;
        public ListNode next;


        public ListNode(int val){
            this.val = val;
        }
    }
    public ListNode head;//标记双向链表的头部
    public ListNode tail;//标记双向链表的尾巴


    //从头到尾打印双向链表的每一个节点的值
    public void display1(){
        ListNode cur = this.head;
        while (cur != null){
            System.out.print(cur.val + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    //得到单链表的长度
    public int size() {
        int count = 0;
        ListNode cur = this.head;
        while (cur != null){
            cur = cur.next;
            count++;
        }
        return count;
    }

    //清除链表
    public void clear() {
        ListNode cur = this.head;
        while (cur != null){
            ListNode curNext = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = curNext;
        }
        head = null;
        tail = null;
    }

    //头插法
    public void addFirst(int data) {
        ListNode d = new ListNode(data);
        //当前链表只有一个节点
        if (this.head == null){
            this.head = d;
            this.tail = d;
        }else {
            d.next = this.head;
            this.head.prev = d;
            head = d;
        }

    }
    //尾插法
    public void addLast(int data) {
        ListNode node = new ListNode(data);
        if (this.head == null){
            this.head = node;
            this.tail = node;
        }else {
            tail.next = node;
            node.prev = tail;
            tail = node;
        }
    }


    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data) throws IndexWrongFulException {
        //判断index的输入是否合法
        if (index < 0 ||  index > size()){
            throw new IndexWrongFulException("index位置不合法!");
        }
        //判断特殊位置,头插、尾插
        if (index == 0){
            addFirst(data);
            return;
        }
        if (index == size()){
            addLast(data);
            return;
        }
        //在index插入值,找到index位置
        ListNode cur = findIndexListNode(index);
        ListNode node = new ListNode(data);

        node.next = cur;
        cur.prev.next = node;
        node.prev = cur.prev;
        cur.prev = node;

    }

    private ListNode findIndexListNode(int index) {
        ListNode cur = this.head;
        while (index != 0){
            cur = cur.next;
            index -- ;
        }
        return cur;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key) {
        ListNode cur = this.head;
        //链表为null
        if (this.head == null){
            return false;
        }else {
            while (cur != null){
                if (cur.val == key){
                    return true;
                }else {
                    cur = cur.next;
                }
            }
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key) {
        ListNode cur = this.head;
        while (cur != null){
            if (cur.val == key){
                if (cur == head){
                    head = head.next;
                    if (head != null){
                        head.prev = null;
                    }else {
                        tail = null;
                    }
                }else {
                    cur.prev.next = cur.next;
                    if (cur.next != null) {
                        cur.next.prev = cur.prev;
                    } else {
                        this.tail = cur.prev;
                    }
                }
                return;
            }
            cur = cur.next;
        }
    }
    //删除所有值为key的节点
    public void removeAllKey(int key) {
        ListNode cur = head;
        while (cur != null){
            if (cur.val == key){
                if (cur == head){
                    head = head.next;
                    if (head != null){
                        head.prev = null;
                    }else {
                        tail = null;
                    }
                }else {
                    cur.prev.next = cur.next;
                    if (cur.next != null) {
                        cur.next.prev = cur.prev;
                    } else {
                        this.tail = cur.prev;
                    }
                }
            }
            cur = cur.next;
        }
    }

}

4.LinkedList的使用

4.1 什么是LinkedList

LinkedList (Java Platform SE 8 )

在集合框架中,LinkedList也实现了List接口,具体如下:

ps:

  1. LinkedList实现了List接口
  2. LinkedList的底层使用了双向链表
  3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
  4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)

4.2 LinkedList的使用

4.2.1. LinkedList的构造

方法

解释

LinkedList()

无参构造

public LinkedList(Collection<? extends E> c)

使用其他集合容器中元素构造List

public static void main(String[] args) {
    // 构造一个空的LinkedList
    List<Integer> list1 = new LinkedList<>();
    
    List<String> list2 = new java.util.ArrayList<>();
    
    list2.add("JavaSE");
    list2.add("JavaWeb");
    list2.add("JavaEE");
    
    // 使用ArrayList构造LinkedList
    List<String> list3 = new LinkedList<>(list2);
}

4.2.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

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());
    System.out.println(list);

    
    // 在起始位置插入0
    list.add(0, 0); // add(index, elem): 在index位置插入元素elem
    System.out.println(list);

    
    list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
    list.removeFirst(); // removeFirst(): 删除第一个元素
    list.removeLast(); // removeLast(): 删除最后元素
    list.remove(1); // remove(index): 删除index位置的元素
    System.out.println(list);

    // contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
    if(!list.contains(1)){
        list.add(0, 1);
    }
    list.add(1);
    System.out.println(list);
    System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置
    System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置
    int elem = list.get(0); // get(index): 获取指定位置元素
    list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
    System.out.println(list);
    
    // subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
    List<Integer> copy = list.subList(0, 3); 
    System.out.println(list);
    System.out.println(copy);
    list.clear(); // 将list中元素清空
    System.out.println(list.size());
}

4.2.3. LinkedList的遍历

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();
    
    // 使用反向迭代器---反向遍历
    ListIterator<Integer> rit = list.listIterator(list.size());
    while (rit.hasPrevious()){
        System.out.print(rit.previous() +" ");
    }
    System.out.println();
}

5. ArrayList和LinkedList的区别

特性

ArrayList

LinkedList

底层数据结构

动态数组 (可自动扩容的数组)

双向链表 (每个节点包含数据、前驱、后继引用)

随机访问性能 (get/set)

极快 O(1)。

直接通过索引计算内存偏移量访问。

慢 O(n)。

需要从头或尾遍历链表找到索引位置。

头部插入/删除性能

慢 O(n)。

需要移动插入点之后的所有元素。

快 O(1)。

只需修改头节点和前几个节点的引用。

尾部插入/删除性能

快 O(1) (摊销)。

通常只需在数组末尾操作,偶尔触发扩容。

快 O(1)。

只需修改尾节点引用。

中间插入/删除性能

慢 O(n)。

需要移动插入点之后的所有元素。

相对慢 O(n)。

需要遍历找到位置,但找到后操作快 O(1) (只需修改相邻节点引用)。

内存占用

较低。

主要是存储数据的数组。可能有少量未使用的尾部空间。

较高。

每个元素都需要额外的空间存储前驱和后继引用。

空间局部性/CPU缓存

好。

元素在内存中连续存储,利于CPU缓存预取。

差。

元素分散在内存各处,缓存不友好。

迭代性能

快。

顺序访问数组元素非常高效。

快。

顺序访问链表节点也很高效。

内存回收

删除元素时,内部数组对应位置置 null,空间不会立即释放,后续可复用或被GC回收。

删除节点后,节点对象及其引用会被GC回收。

详细解释差异点

底层数据结构:

  • ArrayList: 基于一个可动态调整大小的数组。当数组容量不足时,会创建一个更大的新数组(通常是原容量的1.5倍),并将旧数组的数据复制过去。元素在内存中是连续存储的
  • LinkedList: 基于双向链表。每个元素(节点)包含三个部分:实际存储的数据 (item)、指向下一个节点的引用 (next)、指向前一个节点的引用 (prev)。元素在内存中是分散存储的,通过指针连接。

随机访问性能:

  • ArrayList: 访问任意索引 i 的元素 (get(i), set(i, element)) 非常快,时间复杂度是 O(1)。因为它可以通过简单的公式 base_address + i * element_size 直接计算出元素在内存中的位置。
  • LinkedList: 访问索引 i 的元素需要从头节点(或尾节点,如果 i 靠近尾部)开始遍历链表,直到找到第 i 个节点。时间复杂度是 O(n)。对于大型列表,随机访问性能很差。

插入/删除性能:

  • ArrayList:
  • 尾部插入 (add(e)): 通常是 O(1)(摊销时间)。大部分情况下直接在数组末尾添加元素。只有当数组满了需要扩容时,复制操作是 O(n),但扩容是偶尔发生的,平均(摊销)下来还是 O(1)。
  • 头部/中间插入 (add(0, e), add(i, e)) 或删除 (remove(0), remove(i)): 非常慢,时间复杂度是 O(n)。因为需要将插入点/删除点之后的所有元素向后/向前移动一位来腾出空间/填补空缺。插入点越靠前,需要移动的元素越多。
  • LinkedList:
  • 头部/尾部插入/删除 (addFirst(e), addLast(e), removeFirst(), removeLast() 或 add(0, e), add(size(), e), remove(0), remove(size()-1)): 非常快,时间复杂度是 O(1)。因为只需要修改头节点/尾节点及其相邻节点的引用即可。
  • 中间插入/删除 (add(i, e), remove(i)): 时间复杂度是 O(n)。但是! 这个 O(n) 的时间主要用于遍历链表找到索引 i 对应的节点。一旦找到了目标节点(位置),实际的插入或删除操作本身只需要 O(1),因为只需要修改目标节点前后相邻节点的引用即可,无需移动大量数据。如果应用程序能利用迭代器在遍历过程中进行插入删除(如 ListIterator.add(e) / ListIterator.remove()),则可以避免查找位置的 O(n) 开销,实现 O(1) 的操作。

内存占用:

  • ArrayList: 主要占用内存的是存储数据的数组。数组可能会有一些未使用的尾部空间(为了减少频繁扩容)。每个元素本身只占用存储其数据所需的空间(不考虑对象头等开销)。
  • LinkedList: 除了存储元素数据本身,每个节点还需要额外的空间(通常是两个引用,32位JVM上每个引用4字节,64位JVM通常也是4字节或8字节)来存储指向前驱节点和后继节点的引用。对于存储小对象(如 Integer, String)的链表,这些引用的额外开销可能比数据本身还大。

空间局部性与CPU缓存:

  • ArrayList: 元素在内存中连续存储。现代CPU的缓存机制对这种连续访问模式非常友好,可以高效地预取数据到高速缓存中,显著提升顺序访问(迭代)和随机访问的速度。
  • LinkedList: 元素分散在堆内存的不同位置。遍历链表时,访问下一个节点通常意味着访问一个全新的、可能不在缓存中的内存地址(指针追逐)。这会导致大量的缓存未命中(Cache Miss),降低访问速度,即使时间复杂度是 O(n),实际耗时也可能比遍历相同大小的 ArrayList 慢很多。

网站公告

今日签到

点亮在社区的每一天
去签到