写在前面
在日常开发中,一般在对于List的场景,基本上都是通过ArrayList去封装数据的,而对于链表LinkedList相对来说用的比较少。对我而言,好像ArrayList熟练度高一些,所以基本上也很少用LinkedList,也就是在面试的时候去背过八股文。

链表:数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构,简称链表。
链表的前驱和后继
数据结构中,一组数据中的每个个体被称为“数据元素”(简称“元素”)。
另外,对于具有“一对一”逻辑关系的数据,我们一直在用“某一元素的左侧(前边)或右侧(后边)”这样不专业的词,其实线性表中有更准确的术语:
- 某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”;
- 某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”;

单链表
概念:单链表,用于存储逻辑关系为 “一对一” 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。HashMap在1.7就是通过单链表来解决hash冲突的。

节点
在上图中无法提现出元素之间的逻辑关系,对此,链表的解决方案是,每个数据元素在存储时都配备一个指针,用于指向自己的直接后继元素。表示一个节点如下:
//节点信息class Node {E data;Node next;public Node(E element, Node next) {this.data = element;this.next = next;}public Node(E data) {this.data = data;}}

因此,在链表中,每个数据的存储有以下2部分组成
数据本身,其所在的区域叫做数据域
指向后继元素的指针,叫指针域

上图所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中

单链表—-增

/*** 头部添加节点** @param e*/public void add(E e) {//头结点Node cur = new Node(e, list);list = cur;size++;}/*** 指定位置添加节点** @param index* @param e 0 1 2 3 4*/public void add(int index, E e) {//越界检查checkElementIndex(index);Node preNode = list;for (int i = 0; i < index - 1; i++) {//找到插入位置的前一个节点preNode = preNode.next;}Node node = new Node(e);node.next = preNode.next;preNode.next = node;size++;}
单链表—-删

/*** 删除头部节点*/public void remove() {if (list != null) {Node node = list;list = node.next;//GCnode.next = null;size--;}}/*** 删除指定位置节点* 1 2 3 4 5* 0 1 2 3 4** @param index*/public void remove(int index) {checkElementIndex(index);Node preNode = list;for (int i = 0; i < index - 1; i++) {//找到指定位置元素的前一个节点preNode = preNode.next;}//指定位置的节点Node next = preNode.next;preNode.next = next.next;//GCnext = null;size--;}/*** 删除最后一个节点*/public void removeLast() {if (list != null) {//当前节点Node cur = list;//最后一个节点的前一个节点Node preNode = list;while (cur.next != null) {preNode = cur;cur = cur.next;}preNode = null;//此时cur已经为nullsize--;}}
单链表—-改

/*** 修改指定索引的元素** @param index* @param e*/public void set(int index, E e) {checkElementIndex(index);Node cur = list;for (int i = 0; i < index; i++) {cur = cur.next;}cur.data = e;}
单链表—-查

/*** 获取头部节点*/public E get() {if (list != null) {return list.data;} else {return null;}}/*** 获取指定位置的元素** @param index* @return*/public E get(int index) {checkElementIndex(index);Node cur = list;for (int i = 0; i < index; i++) {cur = cur.next;}return cur.data;}
完整代码
/*** 单链表**/public class SingleLinkedList<E> {int size = 0;/*** 指向第一个节点的指针*/Node list;/*** 头部添加节点** @param e*/public void add(E e) {//头结点Node cur = new Node(e, list);list = cur;size++;}/*** 指定位置添加节点** @param index* @param e 0 1 2 3 4*/public void add(int index, E e) {checkElementIndex(index);Node preNode = list;for (int i = 0; i < index - 1; i++) {//找到插入位置的前一个节点preNode = preNode.next;}Node node = new Node(e);node.next = preNode.next;preNode.next = node;size++;}/*** 删除头部节点*/public void remove() {if (list != null) {Node node = list;list = node.next;//GCnode.next = null;size--;}}/*** 删除指定位置节点* 1 2 3 4 5* 0 1 2 3 4** @param index*/public void remove(int index) {checkElementIndex(index);Node preNode = list;for (int i = 0; i < index - 1; i++) {//找到指定位置元素的前一个节点preNode = preNode.next;}//指定位置的节点Node next = preNode.next;preNode.next = next.next;//GCnext = null;size--;}/*** 删除最后一个节点*/public void removeLast() {if (list != null) {//当前节点Node cur = list;//最后一个节点的前一个节点Node preNode = list;while (cur.next != null) {preNode = cur;cur = cur.next;}preNode = null;//此时cur已经为nullsize--;}}/*** 修改指定索引的元素** @param index* @param e*/public void set(int index, E e) {checkElementIndex(index);Node cur = list;for (int i = 0; i < index; i++) {cur = cur.next;}cur.data = e;}/*** 获取头部节点*/public E get() {if (list != null) {return list.data;} else {return null;}}/*** 获取指定位置的元素** @param index* @return*/public E get(int index) {checkElementIndex(index);Node cur = list;for (int i = 0; i < index; i++) {cur = cur.next;}return cur.data;}class Node {E data;Node next;public Node(E element, Node next) {this.data = element;this.next = next;}public Node(E data) {this.data = data;}public Node() {}@Overridepublic String toString() {return "Node{" +"data=" + data +", next=" + next +'}';}}/*** 判断参数是否为现有元素的索引 即边界** @param index*/private void checkElementIndex(int index) {if (!(index >= 0 && index < size))throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);}@Overridepublic String toString() {Node node = list;for (int i = 0; i < size; i++) {System.out.print(node.data + " ");node = node.next;}return super.toString();}public static void main(String[] args) {SingleLinkedList<Integer> list = new SingleLinkedList<>();list.add(5);list.add(4);list.add(3);list.add(2);list.add(1);list.toString();System.out.println();list.add(2, 5);list.toString();list.removeLast();System.out.println();list.toString();list.set(1, 1);System.out.println();list.toString();System.out.println();System.out.println(list.get());}}
双链表LinkedList源码分析
双链表,指各节点之间的逻辑关系是双向的。

节点

因此,在双向链表中各节点包含以下 3 部分信息
- 指针域:用于指向当前节点的直接前驱节点;
- 数据域:用于存储数据元素。
- 指针域:用于指向当前节点的直接后继节点;
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;}}
如果理解了单链表,双项链表其实也没有太多的差别,主要在于限制条件。不仅仅是双向链表,还有很多分类,比如静态链表,动态链表,循环链表等等。这里可以就增删给出对应的过程,源码可以自己去研究研究。
类的继承关系

双链表—-增

public void add(int index, E element) {checkPositionIndex(index);//如果索引和size相等直接尾部插入if (index == size)linkLast(element);elselinkBefore(element, node(index));}/*** first 指向第一个节点的指针* @param e 要插入的元素* @param succ index位置的节点* a1 a3* 在a3索引出插入新的元素 a2* a1 a2 a3*/void linkBefore(E e, Node<E> succ) {// succ:原a3的前置节点a1final Node<E> pred = succ.prev;//pred(a1) e(a2) succ(a3)形成新的节点//即把e(a2)prev指向pred(a1)节点,把e(a2)next指向succ(a3)节点final Node<E> newNode = new Node<>(pred, e, succ);//把succ(a3)的prev指向新的节点newNodesucc.prev = newNode;//pred为空代表newNode为首节点if (pred == null)first = newNode;else//a1的next节点由a3变为a2pred.next = newNode;size++;modCount++;}
双链表—-删

public E remove(int index) {checkElementIndex(index);return unlink(node(index));}/*** Unlinks non-null node x.*/E unlink(Node<E> x) {// assert x != null;//获取该节点的值final E element = x.item;//获取该节点的next节点final Node<E> next = x.next;//获取该节点的prev节点final Node<E> prev = x.prev;//把该节点的前节点的next指向该节点的next节点,并清除该节点的prev指向if (prev == null) {first = next;} else {prev.next = next;x.prev = null;}//把该节点的next节点的prev指向该节点的prev节点,并清除该节点的next指向if (next == null) {last = prev;} else {next.prev = prev;x.next = null;}x.item = null;//gc清除size--;modCount++;return element;}
总结
前面讲过ArrayList源码分析及扩容机制,如果你看了应该知道不管是用ArrayList还是LinkedList主要是看场景,LinkedList增删快,因为只用调整指向即可,对于ArrayList而言却要移动整个数组,但是如果说是在尾部插入的话,使用两者都可以。而查找和修改却要ArrayList只需要知道下标即可,而对于LinkedList却要通过循环查找。
对于LinkendList,其中还有很多方法,例如addFirst,addLast,remove等,如果你学会了单链表,其实双链表也是一样的,主要在于思维。
参考
拓展延伸
- ArrayList和LinkedList有什么区别?
- 什么时候用ArrayList,什么时候用LinkedList?
