Java集合中的 LinkedList

本文主要是在单向不带头链表的基础上,探讨并实现的双向不带头链表 LinkedList。

一、认识 LinkedList

说明:

1、在集合框架中,LinkedList 也实现了List接口;

2、LinkedList 的底层使用了双向链表;

3、LinkedList 没有实现 RandomAccess 接口,因此 LinkedList 不支持随机访问;

4、LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)

5、LinkedList比较适合任意位置插入的场景。

在具体实现 LinkedList 的方法前,我们需要注意,单向链表不能回退找前一个节点,但是双向链表可以通过 prev 引用找到前一个节点,因此定义的临时变量比单向链表少。

二、LinkedList 的模拟实现

与单向链表的功能一致。其中 display()、size()、contains() 方法一模一样,而其他方法因为不用考虑前一个节点怎么获得的问题,因此需要重写画图思考如何实现。

重点是删除节点的方法:

java 复制代码
public class MyLinkedList{
    static class ListNode{
        public int val;
        public ListNode prev;
        public ListNode next;

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

    public ListNode head;
    public ListNode last;   // 标志最后一个节点

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

    public int size(){
        int count = 0;
        ListNode cur = head;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

    public boolean contains(int key){
        ListNode cur = head;
        while(cur != null){
            if(cur.val == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }
    
    public void addFist(int data){
        ListNode newNode = new ListNode(data);
        if(head == null){
            head = last = newNode;
            return;
        }
        head.prev = newNode;
        newNode.next = head;
        head = newNode;
    }

    public void addLast(int data){
        ListNode newNode = new ListNode(data);
        if(head == null){
            head = last = newNode;
            return;
        }
        last.next = newNode;
        newNode.prev = last;
        last = newNode;
    }

    public void addIndex(int index, int data) throws IllegalIndexException{  // 自己编写的异常处理
        int len = this.size();  // 两次使用size()时用变量进行存储可提高效率
        if(index < 0 || index > len){
            throw new IllegalIndexException("指定位置不合法!!!");
        }

        if(index == 0){
            addFist(data);
            return;
        }
        if(index == len){
            addLast(data);
            return;
        }

        ListNode newNode = new ListNode(data);
        ListNode cur = findIndex(index);
        newNode.next = cur;
        cur.prev.next = newNode;
        newNode.prev = cur.prev;
        cur.prev = newNode;
    }

    private ListNode findIndex(int index) {
        ListNode cur = head;
        while (index != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

    public void remove(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{
                    cur.prev.next = cur.next;  // 中间和结尾的删除
                    if (cur.next == null){     // 尾节点的删除
                        last = last.prev;
                    }else{
                        cur.next.prev = cur.prev;  // 中间节点的删除
                    }
                }
                return;   // 删除完毕,循环结束
            }
            cur = cur.next;  // cur.val != 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{
                    cur.prev.next = cur.next;
                    if (cur.next == null){
                        last = last.prev;
                    }else{
                        cur.next.prev = cur.prev;
                    }
                }
                //return;   // 比remove少了该语句,
            }
            cur = cur.next;  // cur.val != key 就前进
        }
    }

    public void clear() {
        ListNode cur = head;
        while (cur != null){
            ListNode perNode = cur.next;
            cur.prev = null;
            cur.next = null;
            cur = perNode;
        }
        head = last = null;
    }
}

三、LinkedList 的方法

与 ArrayList 一致。此处不再赘述。

方法 解释
size() 获取有效元素个数
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 subList(int fromIndex, int toIndex) 截取部分 list

四、ArrayList 顺序表 与 LinkedList 链表的不同

不同点 ArrayList LinkedList
存储方式 物理上一定连续,即逻辑上相邻的元素在内存中也相邻。 逻辑相邻的元素物理位置不相邻
随机访问 通过下标直接访问元素,时间复杂度为O(1) 不支持随机访问,查询节点数据的时间复杂度为O(N)
插入/删除 头部或中间插入/删除元素时,需移动其他元素,最坏情况下时间复杂度为O(N) 只需要修改指针指向,操作时间复杂度为O(1)
扩容机制 需预分配连续空间,扩容成本高且可能造成空间浪费 按需分配空间,动态管理内存,无扩容成本
适用场景 适用于数据量稳定、频繁随机访问的场景(如缓存数据) 适用于频繁插入/删除操作且数据量不确定的场景(如动态列表)
空间利用率 一次性申请大量空间,可能造成空间浪费 按需分配,空间利用率更高