LinkedList集合的常用方法及底层解析
文章目录
前言
LinkedList集合属于线性链表结构(双向链表)
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的
优缺点
优点:删除和插入效率高
缺点:查询效率低
注意:使用LinkedList集合特有的方法不能使用多态
一、常用方法
public void addFirst(E e);将特定的元素插入此链表的开头
public void addLast(E e);将指定的元素添加到此列表的结尾
public E getFirst();返回此链表的第一个元素
public E getLast();返回此列表的最后一个元素
public E removeFirst();删除并返回此链表的第一个元素
public E removeLast();删除并返回此链表的最后一个元素
public E pop();从此列表所表示的堆栈出弹出一个元素
public void push(E e);将元素推入此列表所表示的堆栈
public boolean isEmpty();判断此链表是否包含元素
public class Demo2LinkedList {
public static void main(String[] args) {
//创建一个LInkedList集合 特点:增删快,查询慢
LinkedList<String> link=new LinkedList<>();
//使用addFirst方法向链表添加一个头元素
link.addFirst("Arvin");
link.add("Kevin");
System.out.println(link);
//addLast方法添加一个未元素
link.addLast("KIki");
link.add("Tom");
System.out.println(link);
//removeFirst方法删除链表的头元素
link.removeFirst();
System.out.println(link);
//removeLast方法删除链表的尾元素
link.removeLast();
System.out.println(link);
//pop方法从堆栈内弹出一个索引元素
System.out.println( link.pop());
//pus方法将元素推入所表示的堆栈
link.push("Arvin");
System.out.println(link);
//isEmpty方法判断链表集合是否为空
System.out.println(link.isEmpty());
//getFirst获取链表集合的第一个元素
System.out.println(link.getFirst());
}
}
二、构造方法
无参构造
/**
* 创建一个空的集合
*/
public LinkedList() {
}
含参构造
public LinkedList(Collection<? extends E> c) {
//首先调用 this() 创建一个空的集合
this();
//链表生成方法
addAll(c);
}
addAll ( c )方法
public boolean addAll(int index, Collection<? extends E> c) {
//检查 index 索引位置 默认为 0
checkPositionIndex(index);
//底层System.copyOf方法 ,将集合 浅拷贝成一个数组
Object[] a = c.toArray();
//获取数组长度
int numNew = a.length;
//长度为0 直接返回false
if (numNew == 0)
return false;
//定义两个节点
//pred 来存储前一个节点的引用
//succ存储当前节点
Node<E> pred, succ;
//如果索引等于size则从末尾开始添加节点
if (index == size) {
succ = null;
pred = last;
} else {//否则从索引处添加
succ = node(index);
pred = succ.prev;
}
//将数组的元素遍历依次添加
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//根据pred ——》将集合元素 的prev指向pred
Node<E> newNode = new Node<>(pred, e, null);
//如果pred为空,说明这是个空链表
if (pred == null)
//则以 newNode prev=pred 的节点为头
first = newNode;
else
//否则,将pred的下一个引用指向 这个节点即可
pred.next = newNode;
//更新pred节点
pred = newNode;
}
if (succ == null) {
//pred 节点为最后一个节点
last = pred;
} else {//pred不是最后一个节点
//pred的下一个引用指向succ(尾节点)
pred.next = succ;
//尾节点的上一个引用指向pred即可
succ.prev = pred;
}
//原来的size+新添加的数组长度 就是新链表的size
//当 这个方法作为LinkedList的含参构造调用时,size为 0
size += numNew;
modCount++;
return true;
}
三、add方法
直接在 链表的尾部添加即可
底层具体实现逻辑
public boolean add(E e) {
linkLast(e);
return true;
}
linkLast(e);
void linkLast(E e) {
//获取当前链表尾部的节点
final Node<E> l = last;
//创建一个新节点
//将新节点的prev(上一个节点引用)指向last
final Node<E> newNode = new Node<>(l, e, null);
//newNode 节点成为尾部节点
last = newNode;
//尾部节点为空,说明这个是个空链表
if (l == null)
//直接将这个 节点作为第一个节点
first = newNode;
else
//否则,直接使原尾节点的下一个引用指向 新节点即可
l.next = newNode;
size++; //大小自增1
modCount++;//记录修改次数+1
}
四、get方法
底层会判断查询的索引有没有 超过链表长度的一半,如果没有
直接从头开始遍历链表节点,直到找到索引节点。
如果过半,则会从链表的尾部开始向前遍历(双向链表)找到索引节点。
public E get(int index) {
//检查索引的合理性
checkElementIndex(index);
//遍历查找指定索引节点
return node(index).item;
}
node(index)方法
Node<E> node(int index) {
// assert isElementIndex(index);
//size >> 1 右按位与1 相当于除以2
//这里判断索引有没有超过长度的一半
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;
}
}
五、 remove方法
按下标删,也是先根据index找到Node,然后去链表上拿掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上拿掉这个Node。
索引删除节点
Integer remove = list.remove(2);
public E remove(int index) {
//检查索引
checkElementIndex(index);
//unlink掉节点
return unlink(node(index));
}
首先调用node(index)方法找到节点
在使用 unlink方法拿掉节点
unlink方法
E unlink(Node<E> x) {
// assert x != null;
//拿到这个节点的值
final E element = x.item;
//记录这个节点的下一个引用
final Node<E> next = x.next;
//记录这个节点的上一个引用
final Node<E> prev = x.prev;
if (prev == null) {//如果这个节点是头节点
first = next;//直接将firs换作这个节点的下一个引用
} else {
//将这个节点的上一个引用指向这个节点的next 指向这个节点的next
prev.next = next;
//断开这个节点的prev
x.prev = null;
}
//判断这个节点是不是尾节点
if (next == null) {
//是尾节点直接将 尾节点替换为 这个节点的上一个节点
last = prev;
} else {
//将这个节点下一个节点的prev引用指向这个节点 的prev
next.prev = prev;
//断开这个节点的next
x.next = null;
}
x.item = null;//节点值清空
size--;//长度 -1
modCount++; //修改次数 +1
return element;
}
按元素删除这个节点
Boolean remove2 = list.remove(new Integer(3));
public boolean remove(Object o) {
//这个 元素为 null
if (o == null) {
//遍历这个链表
for (Node<E> x = first; x != null; x = x.next) {
//比较元素 是否等于null
if (x.item == null) {
//匹配到目标则 依旧调用 unlink方法 拿掉这个节点
unlink(x);
return true;
}
}
} else {
//拿掉与这个 元素值相等的节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
总结
原文链接:原文链接
1、增删改查
● 链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。对比ArrayList是通过System.arraycopy完成批量增加的。增加一定会修改modCount。
● 通过下标获取某个node 的时候,(add select),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率
● 删也一定会修改modCount。 按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上unlink掉这个Node。
● 改也是先根据index找到Node,然后替换值。改不修改modCount。
● 查本身就是根据index找到Node。
● 所以它的CRUD操作里,都涉及到根据index去找到Node的操作。
2、遗忘点
LinkedList最大的好处在于头尾和已知节点的插入和删除时间复杂度都是o(1)。但是涉及到先确定位置再操作的情况,则时间复杂度会变为o(n)。
当然,每个节点都需要保留prev和next指针也是经常被吐槽是浪费了空间。
3、offer与add的区别
offer属于 offer in interface Deque
add 属于 add in interface Collection。
当队列为空时候,使用add方法会报错,而offer方法会返回false。
作为List使用时,一般采用add / get方法来 压入/获取对象。
作为Queue使用时,才会采用 offer/poll/take等方法作为链表对象时,offer等方法相对来说没有什么意义这些方法是用于支持队列应用的。