Java | LinkedList

Java8 中 LinkedList内部基于双向链表结构实现。

链表是由一系列节点组成的数据结构,每个节点包含了存储数据的数据域 和存储下一个节点地址的指针域;

链表不需要按顺序存储数据,而存储单元可以是连续的,也可以是不连续的。

链表在插入的时候可以达到O(1)的复杂度,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

双向链表的每个数据节点都有两个指针,分别指向其直接后继和直接前驱节点,所以从双向链表中的任意一个节点开始,都可以访问它的直接前驱节点和直接后继节点。
 

接下来看下 LinkedList类继承关系(IDEA中快捷键Ctrl + Alt + U查看),如下:

public class LinkedList<E> extends AbstractSequentialList<E> 
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable{......}

如图所示:

类继承关系咋的一看较为复杂,一个个来看下:


 LinkedList 继承了 AbstractSequentialList,而AbstractSequentialList是List接口的基础实现类,继承自AbstractList。不过与其功能相反。AbstractList表示随机访问元素的列表,AbstractSequentialList表示顺序访问元素的列表。AbstractSequentialList改写了AbstractList中获取和修改元素的实现,通过ListIterator完成元素的增删改查。

 Deque接口表示一个双端队列,即同时支持在队列的两端执行元素插入或者删除操作,可以当做是先进先出FIFO队列Queue使用,也可以当做后进先出LIFO队列Stack使用。同Queue,操作队列两端元素的方法都有两种形式,一种是操作失败抛出异常,一种是返回null或者false,且都不支持插入null元素。

注意Dequeue继承自Queue接口,而Queue接口继承自Collection接口,表示一个先进先出队列,所有的新元素都插入到队尾,从队首获取元素。Queue接口有6个核心方法,两两成对,add和offer表示添加元素到队尾,remove和poll是移除并返回队首的元素,element和peek方法时返回队首的元素但不移除,前者操作失败时如队列已满添加失败或者队列为空移除获取队首元素失败会抛出异常,后者不会,会返回null或者false。正是因为Queue接口返回null表示队列为空所以不允许插入null的元素,即使实现类支持插入null元素。

 LinkedList实现了List接口和Deque(双端队列)接口,所以既可以作为链表(常用方法有 add()、remove()、get())使用,也可以作为队列(常用方法有 offer()、poll()、peek())使用(FIFO)。并且 LinkedList 内部是双向链表结构,从链表的任一端都可以遍历,所以 LinkedList 还可以作为堆栈(常用的方法有 push()、pop())使用。

再看下LinkedLis内部定义的基础属性:

// 元素个数
transient int size = 0;
// 链表第一个元素
transient Node<E> first;
// 链表最后一个元素
transient Node<E> last;

// 注意下,申明first & last时,类型都是Node,那么Node在LinkedList内部作业是什么?
// Node 是 LinkedList 的私有静态内部类,作为链表结构的基本元素,可以看作是链条上的一个节点。
// 一个 Node 对象中除了存储元素的值外,还存储着前一个 Node 和后一个 Node 的引用。
// 之前有说过双向链表的每个数据节点都有两个指针,分别指向其直接后继和直接前驱节点, 再来瞧瞧 next % prev ;
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;
	}
}

调用add()方法时, 注意 new LinkedList().add()时,默认调用linkLast()方法,即将指定元素追加在链表尾部。

// 再来看下调用add()方法往LinkedList添加元素时,默认将指定元素追加在链表尾部 ,过程如下:
public boolean add(E e) {
	linkLast(e);
	return true;
}
// 注意调用addLast()添加元素时,也是调用了linkLast(),再看下 linkLast() 方法的具体操作:
// 将新元素添加到链表尾部 
void linkLast(E e) {
	final Node<E> l = last;
	final Node<E> newNode = new Node<>(l, e, null);
	// 将新元素赋值为last
	last = newNode;
	// 判断last是否为null,如果是则表示链表为空
	if (l == null)
		// 将新元素赋值为first,表示第一个元素
		first = newNode;
	else
		// 如果链表不为null,则将原来尾部元素直接后继节点指向新元素,表示将新元素插入到链表尾部 
		l.next = newNode;
	// 链表内元素自增
	size++;
	modCount++;
}

而linkFirst()方法表示将指定元素添加到链表头部,过程如下:

// 将新元素添加到链表头部
 private void linkFirst(E e) {
	final Node<E> f = first;
	final Node<E> newNode = new Node<>(null, e, f);
	// 将新元素赋值为 first 
	first = newNode;
	// 判断原来的头部元素first是否为null,如果是则表示链表为空
	if (f == null)
		// 如果为空,则将新元素赋值为last 
		last = newNode;
	else
		// 如果不为空,则将原来头部元素直接前驱节点指向新元素,表示将新元素添加到头部。
		f.prev = newNode;
	// 链表内元素自增
	size++;
	modCount++;
}

再来看看将指定元素添加到目标索引index处时,又做了什么?

	
// 再来看看将指定元素添加到目标索引index处时,又做了什么
public void add(int index, E element) {
	// 先检查该索引index是否存在,即 index >= 0 && index <= size; 
	checkPositionIndex(index);
    // 如果index 等于当前链表大小,则将该元素添加到链表尾部
	if (index == size)
		linkLast(element);
	else
		// 如果不相等,则将该元素添加到某个元素前面
		linkBefore(element, node(index));
}

// 再来具体看下linkBefore()过程: 将目标元素添加到指定节点元素前面
void linkBefore(E e, Node<E> succ) {
	// assert succ != null;
	final Node<E> pred = succ.prev;
	// 建立newNode 与前后节点元素的关联
	final Node<E> newNode = new Node<>(pred, e, succ);
	succ.prev = newNode;
	// 当前一个元素为null时,则表明succ为头部元素
	if (pred == null)
		// 因此将新元素设置为头部元素
		first = newNode;
	else
		// 如果不为null,则将前一个元素的直接后继节点指向当前新元素。
		pred.next = newNode;
	size++;
	modCount++;
}

然后再来看下 调用addAll()方法时,方法内部又是怎样的过程?


// 再来看看addAll()方法中内部又是怎样的过程?
public boolean addAll(Collection<? extends E> c) {
	return addAll(size, c);
}
// 将目标集合的所有元素添加到到链表index处
public boolean addAll(int index, Collection<? extends E> c) {
	// check Index
	checkPositionIndex(index);
	// 将新的集合转为数组对象,并得到数组长度
	Object[] a = c.toArray();
	int numNew = a.length;
	// 如果集合为空则直接返回,不做任何操作
	if (numNew == 0)
		return false;
	// 申明变量
	Node<E> pred, succ;
	// 如果size == index,则说明将新集合数据添加到链表尾部
	if (index == size) {
		// succ 为目标索引处的节点,添加到尾部时,置为null
		succ = null;
		// 但是将原链表的尾部元素赋值给pred
		pred = last;
	} else {
		// 当index < size时,先查询到index索引处节点元素并赋值给succ
		succ = node(index);
		// 然后获取succ的前驱节点指针,并赋值给pred
		pred = succ.prev;
	}
	// 总之上述if 代码段就是获取当前集合索引处的节点元素信息

	// 获取到index索引处节点信息后开始添加数据
	for (Object o : a) {
		@SuppressWarnings("unchecked") 
		E e = (E) o;
		// 为数据对象a中每个对象o 构建节点信息
		Node<E> newNode = new Node<>(pred, e, null);
		if (pred == null)
			// 如果pred为空即空链表时候,将第一个对象o设置为链表头部元素
			first = newNode;
		else
			// 当链表不为空时,则将新节点添加到当前index索引处节点的后继节点处,即index索引节点的尾部
			pred.next = newNode;
		// pred设置为最后一个添加的节点元素
		pred = newNode;
	}

	// 如果succ = null 则说明index == size,两种情况, 第一原始链表为空,第二从原始链表尾部添加元素
	if (succ == null) {
		// 满足succ == null时则满足两种情况,因此集合c的最后一个元素添加完毕后,将其设置为链表尾部元素
		last = pred;
	} else {
		// 如果不是,则	succ = node(index); 那么基于索引处节点和集合最后一个元素添加完毕后重新构建链表链结构。
		pred.next = succ;
		succ.prev = pred;
	}
	// 修改变动后链表大小
	size += numNew;
	modCount++;
	return true;
}

上述操作都是往链表中添加元素,接下来再来看看往链表中删除元素时,过程又是怎样的?

实际上当涉及到LinedList删除节点元素即调用remove时,涉及到三个方法,即unlinkFirst()、unlinkLast()、unlink();

移除first元素:


// 移除first元素
private E unlinkFirst(Node<E> f) {
	// 假设f为头部元素且不为空
	// assert f == first && f != null;
	final E element = f.item;
	final Node<E> next = f.next;
	// 将头元素引用置为null
	f.item = null;
	f.next = null; // help GC
	// 将下一个元素值为头部元素
	first = next;
	// 如果next为null则说明该队列只有一个元素,删除后需要将last也设置为null
	if (next == null)
		last = null;
	else
		// 否则的话将下一个元素其实就是新的头部元素的前驱节点置为null
		next.prev = null;
	// 链表内元素自减
	size--;
	modCount++;
	return element;
}

 移除掉last元素:

private E unlinkLast(Node<E> l) {
	// 同样假设l为last 且 last不为null
	// assert l == last && l != null;
	final E element = l.item;
	final Node<E> prev = l.prev;
	// 将last 引用和后继节点置为null
	l.item = null;
	l.prev = null; // help GC
	// 将last元素的前一个元素置为新的last,即新的尾部元素
	last = prev;
	// 如果前一个元素为null则表示当前链表只有l这一个元素,
	if (prev == null)
		// 删除后fisrt = null 
		first = null;
	else
		// 如果不为null,则将l的前一个元素的直接后继指针置为null,表示前一个元素为新的尾部元素
		prev.next = null;
	// 链表元素自减
	size--;
	modCount++;
	return element;
}

当移除某个节点时,实际上是遍历整个链表,先看下具体的remove()方法实现:

public boolean remove(Object o) {
	if (o == null) {
		for (Node<E> x = first; x != null; x = x.next) {
			if (x.item == null) {
				unlink(x);
				return true;
			}
		}
	} else {
		// 如果对象不为空,则遍历链表结构,调用unlink()方法移除指定的某个所有对象信息
		for (Node<E> x = first; x != null; x = x.next) {
			if (o.equals(x.item)) {
				unlink(x);
				return true;
			}
		}
	}
	return false;
}

再来看看移除指定节点是,内部调用的unlink() 详细过程:


// 再来看看unlink() 详细过程:
E unlink(Node<E> x) {
	// assert x != null;
	// 获取节点x自身以及前后节点信息
	final E element = x.item;
	final Node<E> next = x.next;
	final Node<E> prev = x.prev;
	// 注意链表中有两个指针,分别为直接前驱指针和直接后驱指针,
	// 如果prev == null 说明x的直接前继节点为null,表示x为头部元素
	if (prev == null) {
		// 如果头部元素则直接将x的直接后继节点设置为新的头部元素
		first = next;
	} else {
		// 如果不为null,则将要删除的x节点的下一个节点链接上x节点的上一个节点。
		// 实际上就是将x节点断开, 将x节点上下两个节点链在一起。
		prev.next = next; 
		// 将x的直接前驱指针置为null
		x.prev = null;
	}
	// 直接后继指针的操作:
	// 如果next == null 则表示x为链表中的尾部元素
	if (next == null) {
		// 如果x是尾部元素,则移除x时直接将尾部元素设置为x的前一个元素
		last = prev;
	} else {
		// 如果x不是尾部元素,则将x元素的前驱指针赋值x直接后继节点的前驱指针
		next.prev = prev;
		x.next = null;
	}
	// 最后将x元素引用置为null
	x.item = null;
	// 链表元素自减
	size--;
	modCount++;
	return element;
}

 

最后再来看看LinkedList中是如何查询数据的, 过程如下:

通过get()方法获取指定索引位置元素:

// 再来看下LinkedList内如何查询数据的:
public E get(int index) 
	// 先检查index条件即: index >= 0 && index < size;
	checkElementIndex(index);
	return node(index).item;
}

// 再来看下node()方法:很有意思的一段查询方法,
// 然后为什么说LinkedList查询效率要比ArrayList要低,ArrayList是根据下标随机访问,而LinkedList通过遍历链表方式查询数据
Node<E> node(int index) {
	// assert isElementIndex(index);
	// 如果index 小于链表整个容量size大小的一半, 则从链表的前一半且从链表头部元素开始遍历查询,找到了即返回。
	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;
	}
}

 

顺序查询某元素索引位置,仅返回顺序第一次查找到的索引,没有找到则返回-1 ,过程如下:

// 顺序查询某元素索引位置,仅返回顺序第一次查找到的索引,没有找到则返回-1 
public int indexOf(Object o) {
	int index = 0;
	if (o == null) {
		for (Node<E> x = first; x != null; x = x.next) {
			if (x.item == null)
				return index;
			index++;
		}
	} else {
		for (Node<E> x = first; x != null; x = x.next) {
			if (o.equals(x.item))
				return index;
			index++;
		}
	}
	return -1;
}

 

 倒叙查询某元素索引位置,仅返回倒叙第一次查找到的索引,没有找到则返回-1 ,过程如下:

// 倒叙查询某元素索引位置,仅返回倒叙第一次查找到的索引,没有找到则返回-1 
public int lastIndexOf(Object o) {
	int index = size;
	if (o == null) {
		for (Node<E> x = last; x != null; x = x.prev) {
			index--;
			if (x.item == null)
				return index;
		}
	} else {
		for (Node<E> x = last; x != null; x = x.prev) {
			index--;
			if (o.equals(x.item))
				return index;
		}
	}
	return -1;
}

 

LinkedList 和 ArrayList 的区别LinkedList 是基于双向链表实现的,ArrayList 是基于数组实现的。
LinkedList 添加、插入、删除元素速度更快,而 ArrayList 查询速度更快。
LinkedList 使用 Node 来存储数据,每个 Node 中不仅存储元素的值,还存储了前一个 Node 的引用和后一个 Node 的引用,占用内存更多,而 ArrayList 使用 Object 数组来存储数据,占用内存相比会更少。
LinkedList 和 ArrayList 都是线程不安全的,可以使用 Collections 中的方法在外部封装一下。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值