写在前面

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

02.线性表之链表LinkedList - 图1

链表:数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构,简称链表。

链表的前驱和后继

数据结构中,一组数据中的每个个体被称为“数据元素”(简称“元素”)。
另外,对于具有“一对一”逻辑关系的数据,我们一直在用“某一元素的左侧(前边)或右侧(后边)”这样不专业的词,其实线性表中有更准确的术语:

  • 某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”;
  • 某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”;
    02.线性表之链表LinkedList - 图2

单链表

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

节点

在上图中无法提现出元素之间的逻辑关系,对此,链表的解决方案是,每个数据元素在存储时都配备一个指针,用于指向自己的直接后继元素。表示一个节点如下:

  1. //节点信息
  2. class Node {
  3. E data;
  4. Node next;
  5. public Node(E element, Node next) {
  6. this.data = element;
  7. this.next = next;
  8. }
  9. public Node(E data) {
  10. this.data = data;
  11. }
  12. }

02.线性表之链表LinkedList - 图4
因此,在链表中,每个数据的存储有以下2部分组成

  • 数据本身,其所在的区域叫做数据域

  • 指向后继元素的指针,叫指针域
    02.线性表之链表LinkedList - 图5

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

单链表—-增

02.线性表之链表LinkedList - 图7

  1. /**
  2. * 头部添加节点
  3. *
  4. * @param e
  5. */
  6. public void add(E e) {
  7. //头结点
  8. Node cur = new Node(e, list);
  9. list = cur;
  10. size++;
  11. }
  12. /**
  13. * 指定位置添加节点
  14. *
  15. * @param index
  16. * @param e 0 1 2 3 4
  17. */
  18. public void add(int index, E e) {
  19. //越界检查
  20. checkElementIndex(index);
  21. Node preNode = list;
  22. for (int i = 0; i < index - 1; i++) {
  23. //找到插入位置的前一个节点
  24. preNode = preNode.next;
  25. }
  26. Node node = new Node(e);
  27. node.next = preNode.next;
  28. preNode.next = node;
  29. size++;
  30. }

单链表—-删

02.线性表之链表LinkedList - 图8

  1. /**
  2. * 删除头部节点
  3. */
  4. public void remove() {
  5. if (list != null) {
  6. Node node = list;
  7. list = node.next;
  8. //GC
  9. node.next = null;
  10. size--;
  11. }
  12. }
  13. /**
  14. * 删除指定位置节点
  15. * 1 2 3 4 5
  16. * 0 1 2 3 4
  17. *
  18. * @param index
  19. */
  20. public void remove(int index) {
  21. checkElementIndex(index);
  22. Node preNode = list;
  23. for (int i = 0; i < index - 1; i++) {
  24. //找到指定位置元素的前一个节点
  25. preNode = preNode.next;
  26. }
  27. //指定位置的节点
  28. Node next = preNode.next;
  29. preNode.next = next.next;
  30. //GC
  31. next = null;
  32. size--;
  33. }
  34. /**
  35. * 删除最后一个节点
  36. */
  37. public void removeLast() {
  38. if (list != null) {
  39. //当前节点
  40. Node cur = list;
  41. //最后一个节点的前一个节点
  42. Node preNode = list;
  43. while (cur.next != null) {
  44. preNode = cur;
  45. cur = cur.next;
  46. }
  47. preNode = null;//此时cur已经为null
  48. size--;
  49. }
  50. }

单链表—-改

02.线性表之链表LinkedList - 图9

  1. /**
  2. * 修改指定索引的元素
  3. *
  4. * @param index
  5. * @param e
  6. */
  7. public void set(int index, E e) {
  8. checkElementIndex(index);
  9. Node cur = list;
  10. for (int i = 0; i < index; i++) {
  11. cur = cur.next;
  12. }
  13. cur.data = e;
  14. }

单链表—-查

02.线性表之链表LinkedList - 图10

  1. /**
  2. * 获取头部节点
  3. */
  4. public E get() {
  5. if (list != null) {
  6. return list.data;
  7. } else {
  8. return null;
  9. }
  10. }
  11. /**
  12. * 获取指定位置的元素
  13. *
  14. * @param index
  15. * @return
  16. */
  17. public E get(int index) {
  18. checkElementIndex(index);
  19. Node cur = list;
  20. for (int i = 0; i < index; i++) {
  21. cur = cur.next;
  22. }
  23. return cur.data;
  24. }

完整代码

  1. /**
  2. * 单链表
  3. *
  4. */
  5. public class SingleLinkedList<E> {
  6. int size = 0;
  7. /**
  8. * 指向第一个节点的指针
  9. */
  10. Node list;
  11. /**
  12. * 头部添加节点
  13. *
  14. * @param e
  15. */
  16. public void add(E e) {
  17. //头结点
  18. Node cur = new Node(e, list);
  19. list = cur;
  20. size++;
  21. }
  22. /**
  23. * 指定位置添加节点
  24. *
  25. * @param index
  26. * @param e 0 1 2 3 4
  27. */
  28. public void add(int index, E e) {
  29. checkElementIndex(index);
  30. Node preNode = list;
  31. for (int i = 0; i < index - 1; i++) {
  32. //找到插入位置的前一个节点
  33. preNode = preNode.next;
  34. }
  35. Node node = new Node(e);
  36. node.next = preNode.next;
  37. preNode.next = node;
  38. size++;
  39. }
  40. /**
  41. * 删除头部节点
  42. */
  43. public void remove() {
  44. if (list != null) {
  45. Node node = list;
  46. list = node.next;
  47. //GC
  48. node.next = null;
  49. size--;
  50. }
  51. }
  52. /**
  53. * 删除指定位置节点
  54. * 1 2 3 4 5
  55. * 0 1 2 3 4
  56. *
  57. * @param index
  58. */
  59. public void remove(int index) {
  60. checkElementIndex(index);
  61. Node preNode = list;
  62. for (int i = 0; i < index - 1; i++) {
  63. //找到指定位置元素的前一个节点
  64. preNode = preNode.next;
  65. }
  66. //指定位置的节点
  67. Node next = preNode.next;
  68. preNode.next = next.next;
  69. //GC
  70. next = null;
  71. size--;
  72. }
  73. /**
  74. * 删除最后一个节点
  75. */
  76. public void removeLast() {
  77. if (list != null) {
  78. //当前节点
  79. Node cur = list;
  80. //最后一个节点的前一个节点
  81. Node preNode = list;
  82. while (cur.next != null) {
  83. preNode = cur;
  84. cur = cur.next;
  85. }
  86. preNode = null;//此时cur已经为null
  87. size--;
  88. }
  89. }
  90. /**
  91. * 修改指定索引的元素
  92. *
  93. * @param index
  94. * @param e
  95. */
  96. public void set(int index, E e) {
  97. checkElementIndex(index);
  98. Node cur = list;
  99. for (int i = 0; i < index; i++) {
  100. cur = cur.next;
  101. }
  102. cur.data = e;
  103. }
  104. /**
  105. * 获取头部节点
  106. */
  107. public E get() {
  108. if (list != null) {
  109. return list.data;
  110. } else {
  111. return null;
  112. }
  113. }
  114. /**
  115. * 获取指定位置的元素
  116. *
  117. * @param index
  118. * @return
  119. */
  120. public E get(int index) {
  121. checkElementIndex(index);
  122. Node cur = list;
  123. for (int i = 0; i < index; i++) {
  124. cur = cur.next;
  125. }
  126. return cur.data;
  127. }
  128. class Node {
  129. E data;
  130. Node next;
  131. public Node(E element, Node next) {
  132. this.data = element;
  133. this.next = next;
  134. }
  135. public Node(E data) {
  136. this.data = data;
  137. }
  138. public Node() {
  139. }
  140. @Override
  141. public String toString() {
  142. return "Node{" +
  143. "data=" + data +
  144. ", next=" + next +
  145. '}';
  146. }
  147. }
  148. /**
  149. * 判断参数是否为现有元素的索引 即边界
  150. *
  151. * @param index
  152. */
  153. private void checkElementIndex(int index) {
  154. if (!(index >= 0 && index < size))
  155. throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
  156. }
  157. @Override
  158. public String toString() {
  159. Node node = list;
  160. for (int i = 0; i < size; i++) {
  161. System.out.print(node.data + " ");
  162. node = node.next;
  163. }
  164. return super.toString();
  165. }
  166. public static void main(String[] args) {
  167. SingleLinkedList<Integer> list = new SingleLinkedList<>();
  168. list.add(5);
  169. list.add(4);
  170. list.add(3);
  171. list.add(2);
  172. list.add(1);
  173. list.toString();
  174. System.out.println();
  175. list.add(2, 5);
  176. list.toString();
  177. list.removeLast();
  178. System.out.println();
  179. list.toString();
  180. list.set(1, 1);
  181. System.out.println();
  182. list.toString();
  183. System.out.println();
  184. System.out.println(list.get());
  185. }
  186. }

双链表LinkedList源码分析

双链表,指各节点之间的逻辑关系是双向的。

02.线性表之链表LinkedList - 图11

节点

02.线性表之链表LinkedList - 图12
因此,在双向链表中各节点包含以下 3 部分信息

  • 指针域:用于指向当前节点的直接前驱节点;
  • 数据域:用于存储数据元素。
  • 指针域:用于指向当前节点的直接后继节点;
  1. private static class Node<E> {
  2. E item;
  3. Node<E> next;
  4. Node<E> prev;
  5. Node(Node<E> prev, E element, Node<E> next) {
  6. this.item = element;
  7. this.next = next;
  8. this.prev = prev;
  9. }
  10. }

如果理解了单链表,双项链表其实也没有太多的差别,主要在于限制条件。不仅仅是双向链表,还有很多分类,比如静态链表,动态链表,循环链表等等。这里可以就增删给出对应的过程,源码可以自己去研究研究。

类的继承关系

02.线性表之链表LinkedList - 图13

双链表—-增

02.线性表之链表LinkedList - 图14

  1. public void add(int index, E element) {
  2. checkPositionIndex(index);
  3. //如果索引和size相等直接尾部插入
  4. if (index == size)
  5. linkLast(element);
  6. else
  7. linkBefore(element, node(index));
  8. }
  9. /**
  10. * first 指向第一个节点的指针
  11. * @param e 要插入的元素
  12. * @param succ index位置的节点
  13. * a1 a3
  14. * 在a3索引出插入新的元素 a2
  15. * a1 a2 a3
  16. */
  17. void linkBefore(E e, Node<E> succ) {
  18. // succ:原a3的前置节点a1
  19. final Node<E> pred = succ.prev;
  20. //pred(a1) e(a2) succ(a3)形成新的节点
  21. //即把e(a2)prev指向pred(a1)节点,把e(a2)next指向succ(a3)节点
  22. final Node<E> newNode = new Node<>(pred, e, succ);
  23. //把succ(a3)的prev指向新的节点newNode
  24. succ.prev = newNode;
  25. //pred为空代表newNode为首节点
  26. if (pred == null)
  27. first = newNode;
  28. else
  29. //a1的next节点由a3变为a2
  30. pred.next = newNode;
  31. size++;
  32. modCount++;
  33. }

双链表—-删

02.线性表之链表LinkedList - 图15

  1. public E remove(int index) {
  2. checkElementIndex(index);
  3. return unlink(node(index));
  4. }
  5. /**
  6. * Unlinks non-null node x.
  7. */
  8. E unlink(Node<E> x) {
  9. // assert x != null;
  10. //获取该节点的值
  11. final E element = x.item;
  12. //获取该节点的next节点
  13. final Node<E> next = x.next;
  14. //获取该节点的prev节点
  15. final Node<E> prev = x.prev;
  16. //把该节点的前节点的next指向该节点的next节点,并清除该节点的prev指向
  17. if (prev == null) {
  18. first = next;
  19. } else {
  20. prev.next = next;
  21. x.prev = null;
  22. }
  23. //把该节点的next节点的prev指向该节点的prev节点,并清除该节点的next指向
  24. if (next == null) {
  25. last = prev;
  26. } else {
  27. next.prev = prev;
  28. x.next = null;
  29. }
  30. x.item = null;//gc清除
  31. size--;
  32. modCount++;
  33. return element;
  34. }

总结

前面讲过ArrayList源码分析及扩容机制,如果你看了应该知道不管是用ArrayList还是LinkedList主要是看场景,LinkedList增删快,因为只用调整指向即可,对于ArrayList而言却要移动整个数组,但是如果说是在尾部插入的话,使用两者都可以。而查找和修改却要ArrayList只需要知道下标即可,而对于LinkedList却要通过循环查找。

对于LinkendList,其中还有很多方法,例如addFirst,addLast,remove等,如果你学会了单链表,其实双链表也是一样的,主要在于思维。

参考

拓展延伸

  • ArrayList和LinkedList有什么区别?
  • 什么时候用ArrayList,什么时候用LinkedList?