LinkedList 详解:核心特性、常用接口与总结(附图解)
本文聚焦 Java 集合框架中的LinkedList,从「什么是链表」讲起,用图解方式直观展示 LinkedList 的底层结构,系统梳理所有常用接口的作用与返回值,最后给出模拟代码实现并且对比 ArrayList 给出整体总结。适合作为学习笔记或面试速查。
目录
- 一、从 ArrayList 的缺陷说起
- 二、链表:LinkedList 的底层原理(图解)
- 三、LinkedList 简介
- 四、LinkedList 的构造方式
- 五、LinkedList 常用接口(作用 + 返回值)
- 六、LinkedList 的三种遍历方式
- 七、ArrayList 和 LinkedList 的区别
- 八、模拟代码实现
- 九、总结
一、从 ArrayList 的缺陷说起
ArrayList 底层使用数组存储元素。由于其底层是一段连续空间,在任意位置插入或删除元素时,需要将后序元素整体往前或往后搬移,时间复杂度为O(n),效率较低。
因此:ArrayList 不适合做任意位置插入和删除较多的场景。为此,Java 集合中又引入了LinkedList,即链表结构。
二、链表:LinkedList 的底层原理(图解)
2.1 什么是链表
链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的——元素存储在一个个独立的节点(Node)中,节点之间靠引用串起来。
与顺序表对比着看更直观:
一句话总结:顺序表是「物理连续、逻辑也连续」;链表是「物理不连续、逻辑连续」。
2.2 链表的 8 种结构
实际中链表的结构非常多样,以下三种情况组合起来共有2 × 2 × 2 = 8 种链表结构:
虽然结构很多,但重点掌握两种:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据,实际中更多是作为其他数据结构的子结构(如哈希桶、图的邻接表),并且是笔试面试的高频考点;
- 无头双向链表:Java 集合框架中 LinkedList 的底层实现就是无头双向循环链表。
2.3 LinkedList 的底层结构:无头双向循环链表
每个节点(Node)包含三个部分:prev(前驱引用)| item(数据)| next(后继引用),同时额外维护first和last两个引用。由于链表没有将元素存储在连续的空间中,因此在任意位置插入或删除元素时,不需要搬移元素,只需修改引用的指向,效率较高。
三、LinkedList 简介
在集合框架中,LinkedList 也实现了List 接口。它的核心特性如下:
| 特性 | 说明 |
|---|---|
| 实现了 List 接口 | 拥有 List 的全部通用方法 |
| 底层是双向链表 | 元素存储在独立节点中,通过引用连接 |
| 不支持随机访问 | 没有实现 RandomAccess 接口,按下标访问只能从头遍历,效率 O(n) |
| 插入删除效率高 | 任意位置插入和删除元素时效率比较高(改引用即可,时间复杂度为 O(1))¹ |
| 适合频繁插删的场景 | 与 ArrayList 互补 |
¹ 精确地说:「改引用」这一步是 O(1),但定位到目标位置仍需 O(n) 遍历;不过相比 ArrayList 插入时还要整体搬移元素,LinkedList 依然优势明显。
此外,LinkedList 还实现了Deque(双端队列)接口,所以它既能当列表用,也能当栈、队列用:
四、LinkedList 的构造方式
publicstaticvoidmain(String[]args){// 1. 构造一个空的 LinkedListList<Integer>list1=newLinkedList<>();// 2. 使用其他集合构造 LinkedList(拷贝构造)List<String>list2=newjava.util.ArrayList<>();list2.add("JavaSE");list2.add("JavaWeb");list2.add("JavaEE");// list3 构造好之后,与 list2 中的元素一致List<String>list3=newLinkedList<>(list2);}| 构造方法 | 作用 |
|---|---|
LinkedList() | 构造一个空的LinkedList |
LinkedList(Collection<? extends E> c) | 使用其他集合中的元素构造 LinkedList(拷贝构造) |
注意:与 ArrayList 不同,LinkedList没有「指定初始容量」的构造方法——链表按需创建节点,不存在预分配容量的概念。
五、LinkedList 常用接口(作用 + 返回值)
5.1 List 通用接口
| 方法 | 作用 | 返回值 |
|---|---|---|
boolean add(E e) | 尾插(在链表末尾添加元素) | 添加成功返回true |
void add(int index, E e) | 在index位置插入元素 | 无返回值 |
E remove(int index) | 删除index位置的元素 | 返回被删除的元素 |
boolean remove(Object o) | 删除第一次出现的指定元素 | 删除成功返回true,不存在返回false |
E get(int index) | 获取index位置的元素 | 返回该位置的元素 |
E set(int index, E e) | 将index位置的元素设置为e | 返回被替换的旧元素 |
boolean contains(Object o) | 检测元素o是否存在 | 存在返回true,否则返回false |
int indexOf(Object o) | 从前往后找o第一次出现的位置 | 找到返回下标,未找到返回 -1 |
int lastIndexOf(Object o) | 从后往前找o第一次出现的位置 | 找到返回下标,未找到返回 -1 |
int size() | 获取链表中有效元素个数 | 返回元素个数 |
void clear() | 清空链表 | 无返回值 |
List<E> subList(int from, int to) | 用 list 中[from, to)之间的元素构造一个新的 List 返回 | 返回子列表 |
5.2 LinkedList 特有的首尾操作(来自 Deque 双端队列接口)
这是 LinkedList 区别于 ArrayList 的最大亮点——头尾操作全部 O(1):
| 方法 | 作用 | 返回值 |
|---|---|---|
void addFirst(E e) | 头插,将元素插到链表头部 | 无返回值 |
void addLast(E e) | 尾插,将元素插到链表尾部 | 无返回值 |
E remove() | 删除第一个元素(内部调用的就是removeFirst()) | 返回被删除的元素;链表为空时抛异常 |
E removeFirst() | 删除第一个元素 | 返回被删除的元素;链表为空时抛异常 |
E removeLast() | 删除最后一个元素 | 返回被删除的元素;链表为空时抛异常 |
E getFirst() | 获取第一个元素(不删除) | 返回首个元素;链表为空时抛异常 |
E getLast() | 获取最后一个元素(不删除) | 返回末尾元素;链表为空时抛异常 |
boolean offer(E e) | 尾插(等价add(e),队列风格) | 成功返回true |
boolean offerFirst(E e) | 头插(队列风格) | 成功返回true |
boolean offerLast(E e) | 尾插(队列风格) | 成功返回true |
E poll() | 删除第一个元素(队列风格) | 返回被删除元素;链表为空返回null而不抛异常 |
E pollFirst()/E pollLast() | 删除首 / 尾元素 | 同上,空链表返回null |
E peek() | 获取第一个元素(不删除) | 空链表返回null,不抛异常 |
E peekFirst()/E peekLast() | 获取首 / 尾元素(不删除) | 空链表返回null |
void push(E e) | 头插(栈风格,等价addFirst(e)) | 无返回值 |
E pop() | 删除第一个元素(栈风格,等价removeFirst()) | 返回被删除元素;空链表抛异常 |
⚠️两组方法的区别务必记牢:
remove(int index)返回被删除的元素,remove(Object o)返回boolean——重载语义不同;- 「抛异常组」(
removeFirst/getFirst/pop…)与「返回特殊值组」(pollFirst/peekFirst/offer…)功能相同,区别仅在于链表为空时前者抛异常、后者返回null或false。
5.3 接口使用示例
publicstaticvoidmain(String[]args){LinkedList<Integer>list=newLinkedList<>();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);// 在 index 位置插入元素 elem:在起始位置插入 0list.add(0,0);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,否则返回 falseif(!list.contains(1)){list.add(0,1);}list.add(1);System.out.println(list);System.out.println(list.indexOf(1));// indexOf(elem): 从前往后找到第一个 1 的位置System.out.println(list.lastIndexOf(1));// lastIndexOf(elem): 从后往前找第一个 1 的位置intelem=list.get(0);// get(index): 获取指定位置元素list.set(0,100);// set(index, elem): 将 index 位置的元素设置为 elemSystem.out.println(list);// subList(from, to): 用 list 中 [from, to) 之间的元素构造一个新的 List 返回List<Integer>copy=list.subList(0,3);System.out.println(list);System.out.println(copy);list.clear();// 将 list 中元素清空System.out.println(list.size());}六、LinkedList 的三种遍历方式
publicstaticvoidmain(String[]args){LinkedList<Integer>list=newLinkedList<>();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());// 1. foreach 遍历for(inte:list){System.out.print(e+" ");}System.out.println();// 2. 使用迭代器遍历 --- 正向遍历ListIterator<Integer>it=list.listIterator();while(it.hasNext()){System.out.print(it.next()+" ");}System.out.println();// 3. 使用反向迭代器 --- 反向遍历ListIterator<Integer>rit=list.listIterator(list.size());while(rit.hasPrevious()){System.out.print(rit.previous()+" ");}System.out.println();}💡 由于 LinkedList没有实现 RandomAccess 接口,用「for 循环 + 下标
get(i)」遍历的代价是每次get都从头找,整体为O(n²),不推荐。推荐foreach或迭代器遍历。
七、ArrayList 和 LinkedList 的区别
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态顺序表(连续数组) | 无头双向循环链表 |
| 随机访问 | 支持(实现了 RandomAccess),get(i)为O(1) | 不支持,get(i)需从头遍历,为O(n) |
| 任意位置插入 / 删除 | 需整体搬移元素,O(n) | 只需修改引用,O(1)¹ |
| 首尾操作 | 尾插均摊 O(1),头插 O(n) | 头插、尾插都是O(1) |
| 内存占用 | 连续空间,有一定冗余容量 | 每个节点额外维护 prev / next 两个引用,占用更大 |
| 扩容 | 需要申请新空间 + 拷贝数据 | 无需扩容,按需创建节点 |
| 适用场景 | 查询多、增删少 | 任意位置增删多、查询少 |
¹ 同第三节的说明:定位节点本身是 O(n),此处指节点间的插入 / 删除操作。
一句话选型:以「查」为主选 ArrayList,以「增删」为主选 LinkedList。
八、模拟代码实现
packagelinkedlist;classNode{publicStringvalue;publicNodenext;publicNode(Stringv){value=v;}}publicclassMyLinkedList{privateNodehead=null;publicvoidaddFirst(Stringv){NodenewNode=newNode(v);//需要完成的两个步骤newNode.next=head;head=newNode;}publicvoidaddLast(Stringv){if(head==null){addFirst(v);return;}NodenewNode=newNode(v);Nodecur=head;while(cur.next!=null){cur=cur.next;}cur.next=newNode;}publicStringtoString(){StringBuilderstr=newStringBuilder();str.append("[");Nodecur=head;//打印的话要遍历所有 而不是走到最后就结束了while(cur!=null){str.append(cur.value);if(cur.next==null)break;str.append(",");cur=cur.next;}str.append("]");returnstr.toString();}publicintsize(){intsize=0;Nodecur=head;while(cur!=null){cur=cur.next;size++;}returnsize;}publicvoidadd(intindex,Stringv){if(index<0||index>size()){thrownewIndexOutOfBoundsException();}NodenewNode=newNode(v);Nodeprev=null;Nodecur=head;while(index-->0){prev=cur;cur=cur.next;}if(prev==null){addFirst(v);}else{prev.next=newNode;newNode.next=cur;}}publicbooleancontains(Stringv){if(size()==0)returnfalse;Nodecur=head;while(cur!=null){if(cur.value.equals(v))returntrue;cur=cur.next;}returnfalse;}publicvoidremove(intindex){if(index<0||index>size()){thrownewIndexOutOfBoundsException();}Nodeprev=null;Nodecur=head;for(inti=0;i<index;i++){prev=cur;cur=cur.next;}if(prev==null){head=head.next;return;}prev.next=cur.next;return;}publicvoidremove(Stringv){if(size()==0)return;Nodecur=head;Nodeprev=null;while(cur!=null){if(cur.value.equals(v)){if(prev==null){head=head.next;return;}prev.next=cur.next;return;}prev=cur;cur=cur.next;}}publicintindexOf(Stringv){Nodecur=head;intindex=0;while(cur!=null){if(cur.value.equals(v)){returnindex;}cur=cur.next;index++;}return-1;}publicvoidremoveAllKey(Stringkey){Nodeprev=null;Nodecur=head;while(cur!=null){if(cur.value.equals(key)){if(prev==null){head=head.next;cur=head;continue;}prev.next=cur.next;cur=prev.next;continue;}prev=cur;cur=cur.next;}}publicvoidclear(){//垃圾回收机制 只要没引用指向 就会自动释放内存head=null;}publicstaticvoidmain(String[]args){MyLinkedListlist=newMyLinkedList();list.addFirst("Hello");list.addFirst("World");list.addLast("Hello");list.addLast("Hello");list.addLast("World");list.add(2,"gogogo");list.add(0,"hel");System.out.println(list);list.removeAllKey("Hello");System.out.println(list);}}九、总结
- LinkedList 本质:底层是无头双向循环链表的 List 实现,同时实现了
List、Deque等接口; - 核心特性:物理存储不连续、不支持随机访问(未实现 RandomAccess)、头尾插入删除效率极高(O(1))、无需扩容;
- 关键接口:
add/remove/get/set/contains/indexOf/lastIndexOf/size/clear/subList,以及特有的一整套首尾操作(addFirst/addLast/removeFirst/removeLast/getFirst/getLast/poll/peek/push/pop),需牢记各自作用与返回值; - 返回值易错点:
remove(int index)返回被删除元素,remove(Object o)返回 boolean;removeFirst()/getFirst()/pop()空链表抛异常,pollFirst()/peekFirst()空链表返回 null;
- 遍历建议:用foreach或迭代器,不要用「for + get(i)」(O(n²));
- 与 ArrayList 的选型:查询多选 ArrayList,任意位置增删多选 LinkedList;两者是互补关系而非替代关系。
如果这篇文章对你有帮助,欢迎点赞 👍 + 收藏 ⭐ + 关注,后续会继续更新 Java 集合框架系列文章!