基于节点的数据结构—链表

数组存储:连续的内存空间,并且预留一部分空间方便扩展。这样会大大降低内存的使用率。
链表 - 图1
如上图降低电影院入座率一样。

链表

功能与数组类似——用于存储一系列相同类型的数组。只是存储方式不同。

节点

链表中每个元素我们称之为节点。
链表 - 图2
节点由两部分组成——节点内容和下一个节点地址
与数组的区别就是多了下一个节点的地址。
为什么需要下一个节点地址?

  • 数组中由于连续内存空间存储,可通过start_adress+item_size*i来获取元素位置。
  • 链表中是分散储存的,所以要保存下一个节点位置。

好处:数据分散存储在内存中,达到内存最大利用率。
节点类

  1. public class Node {
  2. private int content;
  3. private Node next;
  4. public Node() {
  5. }
  6. public Node(int content, Node next) {
  7. this.content = content;
  8. this.next = next;
  9. }
  10. public int getContent() {
  11. return content;
  12. }
  13. public void setContent(int content) {
  14. this.content = content;
  15. }
  16. public Node getNext() {
  17. return next;
  18. }
  19. public void setNext(Node next) {
  20. this.next = next;
  21. }
  22. }

代码实现链表:

  1. public class Demo {
  2. // 创建链表
  3. public static Node createLinkedList(int[] array) {
  4. if (array.length==1){
  5. Node root = null;
  6. return new Node(array[0],root);
  7. }
  8. int[] newarray = new int[array.length-1];
  9. for (int i=1;i<array.length ;i++ ){
  10. newarray[i-1] = array[i] ;
  11. }
  12. return new Node(array[0],createLinkedList(newarray));
  13. }
  14. public static void main(String[] args) {
  15. int[] array = {9, 2, 4};
  16. Node root = createLinkedList(array);
  17. System.out.println(root.getContent());
  18. }
  19. }

答案:

  1. public class Demo {
  2. // 创建链表
  3. public static Node createLinkedList(int[] array) {
  4. // 创建一个根节点
  5. Node root = null;
  6. // 从末尾元素开始依次创建Node节点
  7. for (int i = array.length - 1; i >= 0; i--) {
  8. Node node = new Node(array[i], null);
  9. // 创建两个节点的连接关系
  10. if (root != null) {
  11. node.setNext(root);
  12. root = node;
  13. } else {
  14. root = node;
  15. }
  16. }
  17. return root;
  18. }
  19. public static void main(String[] args) {
  20. int[] array = {9, 2, 4};
  21. Node root = createLinkedList(array);
  22. System.out.println(root.getContent());
  23. }
  24. }

链表的读取与查找

读取

通过第一个节点的地址,依次遍历找到第三个节点地址。
链表 - 图3

时间复杂度

读取第一个O(1),最坏读取最后一个需要N步,所以为O(N)。

查找

链表 - 图4
时间复杂度:O(N)

模拟实现JAVA中的LinkedList(底层是用链表实现的)

  1. public class YKDLinkedList {
  2. private Node root = null;
  3. int size = 0;
  4. public YKDLinkedList() {
  5. }
  6. // 数组创建一个链表
  7. public static Node createLinkedLNode(int[] array) {
  8. // 创建一个根节点
  9. Node root = null;
  10. // 从末尾元素开始依次创建Node节点
  11. for (int i = array.length - 1; i >= 0; i--) {
  12. Node node = new Node(array[i], null);
  13. // 创建两个节点的连接关系
  14. if (root != null) {
  15. node.setNext(root);
  16. root = node;
  17. } else {
  18. root = node;
  19. }
  20. }
  21. return root;
  22. }
  23. public YKDLinkedList(int[] array) {
  24. this.root = YKDLinkedList.createLinkedLNode(array);
  25. this.size = array.length;
  26. }
  27. // 获取长度
  28. public int size() {
  29. return this.size;
  30. }
  31. // 获取某个索引节点内容
  32. public int get(int index) {
  33. int i = 0;
  34. Node node = this.root;
  35. // 依次往前推进
  36. while (i < index) {
  37. node = node.getNext();
  38. i++;
  39. }
  40. return node.getContent();
  41. }
  42. // 查找某个值的索引值,默认不存在为-1
  43. public int find(int value) {
  44. Node node = this.root;
  45. // index 保存当前的索引
  46. int index = 0;
  47. // 依次遍历链表,找到内容等于value的node,返回index
  48. while (node != null && node.getNext() != null) {
  49. if (node.getContent() == value) {
  50. return index;
  51. }
  52. node = node.getNext();
  53. index++;
  54. }
  55. return -1;
  56. }
  57. public static void main(String[] args) {
  58. int[] array = {9, 2, 4};
  59. YKDLinkedList linkedList = new YKDLinkedList(array);
  60. System.out.println(linkedList.find(2));
  61. System.out.println(linkedList.find(5));
  62. System.out.println(linkedList.get(2));
  63. }
  64. }

链表的插入

头部插入

链表 - 图5

中间插入

链表 - 图6

尾部插入

链表 - 图7

代码实现

  1. public class YKDLinkedList {
  2. private Node root = null;
  3. int size = 0;
  4. public YKDLinkedList() {
  5. }
  6. // 数组创建一个链表
  7. public static Node createLinkedLNode(int[] array) {
  8. // 创建一个根节点
  9. Node root = null;
  10. // 从末尾元素开始依次创建Node节点
  11. for (int i = array.length - 1; i >= 0; i--) {
  12. Node node = new Node(array[i], null);
  13. // 创建两个节点的连接关系
  14. if (root != null) {
  15. node.setNext(root);
  16. root = node;
  17. } else {
  18. root = node;
  19. }
  20. }
  21. return root;
  22. }
  23. public YKDLinkedList(int[] array) {
  24. this.root = YKDLinkedList.createLinkedLNode(array);
  25. this.size = array.length;
  26. }
  27. // 获取长度
  28. public int size() {
  29. return this.size;
  30. }
  31. // 获取某个索引节点内容
  32. public int get(int index) {
  33. int i = 0;
  34. Node node = this.root;
  35. // 依次往前推进
  36. while (i < index) {
  37. node = node.getNext();
  38. i++;
  39. }
  40. return node.getContent();
  41. }
  42. // 查找某个值的索引值,默认不存在为-1
  43. public int find(int value) {
  44. Node node = this.root;
  45. // index 保存当前的索引
  46. int index = 0;
  47. // 依次遍历链表,找到内容等于value的node,返回index
  48. while (node != null && node.getNext() != null) {
  49. if (node.getContent() == value) {
  50. return index;
  51. }
  52. node = node.getNext();
  53. index++;
  54. }
  55. return -1;
  56. }
  57. // 末尾添加元素
  58. public boolean add(int value) {
  59. return this.add(this.size - 1, value);
  60. }
  61. // 头部插入元素
  62. public boolean addFirst(int value) {
  63. return this.add(-1, value);
  64. }
  65. // 插入元素
  66. public boolean add(int index, int value) {
  67. if (index < -1 || index > this.size - 1){
  68. return false;
  69. }
  70. if (index == -1) {
  71. if (this.root != null) {
  72. this.root = new Node(value, this.root);
  73. } else {
  74. this.root = new Node(value, null);
  75. }
  76. } else {
  77. Node pre = this.root;
  78. while (index > 0) {
  79. if (pre.getNext() == null) {
  80. return false;
  81. }
  82. pre = this.root.getNext();
  83. index--;
  84. }
  85. if (pre == null) {
  86. return false;
  87. }
  88. Node newNode = new Node(value, pre.getNext());
  89. pre.setNext(newNode);
  90. }
  91. this.size++;
  92. return true;
  93. }
  94. // 链表中的元素字符串形式
  95. public String toString() {
  96. if (this.root == null) {
  97. return "";
  98. }
  99. StringBuilder str = new StringBuilder();
  100. Node node = this.root;
  101. while (node != null) {
  102. str.append(node.getContent()).append(" ");
  103. node = node.getNext();
  104. }
  105. return str.toString();
  106. }
  107. public static void main(String[] args) {
  108. YKDLinkedList linkedList = new YKDLinkedList();
  109. boolean result = linkedList.add(9);
  110. if (!result) {
  111. System.out.println("9插入失败");
  112. return;
  113. }
  114. result = linkedList.add(4);
  115. if (!result) {
  116. System.out.println("4插入失败");
  117. return;
  118. }
  119. result = linkedList.add(0, 2);
  120. if (!result) {
  121. System.out.println("2插入失败");
  122. return;
  123. }
  124. result = linkedList.addFirst(10);
  125. if (!result) {
  126. System.out.println("10插入失败");
  127. return;
  128. }
  129. System.out.println(linkedList.toString());
  130. }
  131. }

链表删除

头部删除

链表 - 图8

中间删除

链表 - 图9

尾部删除

链表 - 图10

代码实现

  1. public class YKDLinkedList {
  2. private Node root = null;
  3. int size = 0;
  4. public YKDLinkedList() {
  5. }
  6. // 数组创建一个链表
  7. public static Node createLinkedLNode(int[] array) {
  8. // 创建一个根节点
  9. Node root = null;
  10. // 从末尾元素开始依次创建Node节点
  11. for (int i = array.length - 1; i >= 0; i--) {
  12. Node node = new Node(array[i], null);
  13. // 创建两个节点的连接关系
  14. if (root != null) {
  15. node.setNext(root);
  16. root = node;
  17. } else {
  18. root = node;
  19. }
  20. }
  21. return root;
  22. }
  23. public YKDLinkedList(int[] array) {
  24. this.root = YKDLinkedList.createLinkedLNode(array);
  25. this.size = array.length;
  26. }
  27. // 获取长度
  28. public int size() {
  29. return this.size;
  30. }
  31. // 获取某个索引节点内容
  32. public int get(int index) {
  33. int i = 0;
  34. Node node = this.root;
  35. // 依次往前推进
  36. while (i < index) {
  37. node = node.getNext();
  38. i++;
  39. }
  40. return node.getContent();
  41. }
  42. // 查找某个值的索引值,默认不存在为-1
  43. public int find(int value) {
  44. Node node = this.root;
  45. // index 保存当前的索引
  46. int index = 0;
  47. // 依次遍历链表,找到内容等于value的node,返回index
  48. while (node != null && node.getNext() != null) {
  49. if (node.getContent() == value) {
  50. return index;
  51. }
  52. node = node.getNext();
  53. index++;
  54. }
  55. return -1;
  56. }
  57. // 末尾添加元素
  58. public boolean add(int value) {
  59. return this.add(this.size - 1, value);
  60. }
  61. // 头部插入元素
  62. public boolean addFirst(int value) {
  63. return this.add(-1, value);
  64. }
  65. // 插入元素
  66. public boolean add(int index, int value) {
  67. if (index < -1 || index > this.size - 1){
  68. return false;
  69. }
  70. if (index == -1) {
  71. if (this.root != null) {
  72. this.root = new Node(value, this.root);
  73. } else {
  74. this.root = new Node(value, null);
  75. }
  76. } else {
  77. Node pre = this.root;
  78. while (index > 0) {
  79. if (pre.getNext() == null) {
  80. return false;
  81. }
  82. pre = this.root.getNext();
  83. index--;
  84. }
  85. if (pre == null) {
  86. return false;
  87. }
  88. Node newNode = new Node(value, pre.getNext());
  89. pre.setNext(newNode);
  90. }
  91. this.size++;
  92. return true;
  93. }
  94. // 删除最后一个元素
  95. public boolean removeLast() {
  96. return this.remove(this.size - 1);
  97. }
  98. // 删除第一个元素
  99. public boolean removeFirst() {
  100. return this.remove(0);
  101. }
  102. // 插入元素
  103. // index = 0, 表示删除第 1 个元素
  104. // index = n,表示删除第 n+1 个元素
  105. public boolean remove(int index) {
  106. if (index < 0 || index > this.size - 1){
  107. return false;
  108. }
  109. // 判断整个列表为空情况,删除失败
  110. if (this.root == null) {
  111. return false;
  112. }
  113. if (index == 0) {
  114. // 删除第一个元素,情况比较简单,注意必须先保留 this.root,因为紧接着 this.root 会被修改
  115. Node node = this.root;
  116. this.root = node.getNext();
  117. node.setNext(null);
  118. } else {
  119. // 判断越界问题
  120. if(this.size < index + 1){
  121. return false;
  122. }
  123. // 遍历获取index的前置节点
  124. Node pre = this.root;
  125. while (index > 1) {
  126. pre = pre.getNext();
  127. index--;
  128. }
  129. // 修改next指针,同样大家需要先保存 pre.next,因为紧接着下一步会修改 pre.next 指针
  130. Node next = pre.getNext();
  131. pre.setNext(next.getNext());
  132. next.setNext(null);
  133. }
  134. this.size--;
  135. return true;
  136. }
  137. // 链表中的元素字符串形式
  138. public String toString() {
  139. if (this.root == null) {
  140. return "";
  141. }
  142. StringBuilder str = new StringBuilder();
  143. Node node = this.root;
  144. while (node != null) {
  145. str.append(node.getContent()).append(" ");
  146. node = node.getNext();
  147. }
  148. return str.toString();
  149. }
  150. public static void main(String[] args) {
  151. int[] array = {10, 9, 2, 4};
  152. YKDLinkedList linkedList = new YKDLinkedList(array);
  153. boolean result = linkedList.removeFirst();
  154. if (!result) {
  155. System.out.println("removeFirst失败");
  156. return;
  157. }
  158. result = linkedList.removeLast();
  159. if (!result) {
  160. System.out.println("removeLast失败");
  161. return;
  162. }
  163. result = linkedList.remove(1);
  164. if (!result) {
  165. System.out.println("remove(1)失败");
  166. return;
  167. }
  168. System.out.println(linkedList.toString());
  169. }
  170. }

链表VS数组

链表 - 图11

image.png

复杂场景

我们手里有一堆手机号码,我们希望写一个算法删除无效的手机号码。
具体逻辑:我们每次读取一个手机号码,判断这个手机号码是否是11位,并且满足正则表达式的要求。
遍历
无论是数组还是链表都要进行N次遍历

删除
数组:每次删除都需要移动后面的元素,所以每次删除的时间复杂度为O(N)。
链表:则是O(1)。

总结

在频繁插入和删除的情况下,链表有绝对的优势。

链表经典算法题—多指针使用

如何查找单向链表中倒数第K个节点。

假设链表中:9,2,4,5,6,8,0,3,10
当k为3和6时,结果为0和5。
单向链表——导致节点只知道自己后面的节点,那该如何获取倒数第K个节点呢。

暴力法

  1. 第一次循环,获取链表所有节点个数,设为N
  2. 第二次循环,只需要推进N-k次即可。

代码如下:

  1. // 找寻倒数第 k 个节点内容
  2. public static int findK(Node node, int k) {
  3. // 第一步,计算链表的长度
  4. int size = 0;
  5. Node p = node;
  6. while (p != null) {
  7. p = p.getNext();
  8. size++;
  9. }
  10. // 第二步,推进 size - k 步
  11. Node c = node;
  12. int index = size - k;
  13. while (index > 0) {
  14. c = c.getNext();
  15. index--;
  16. }
  17. return c.getContent();
  18. }

双指针法

  1. 先申明一个先驱指针。为了探索链表的边界。我们将先驱指针移动k-1位。移动后我们开始申明一个新的指针实际指针,用来寻找倒数第k个节点。
  2. 同时移动两个指针
  3. 结束条件:先驱指针执行到链表结尾位置,这是,实际指针刚好指向倒数第K个位置。
    代码实现
    ```java

public class Demo {

// 找寻倒数第 k 个节点内容 public static int findK(Node node, int k) { Node preNode = node; for (int i=0;i<k-1 ;i++ ){ preNode = preNode.getNext(); } Node nowNode = node; while(preNode.getNext()!=null){ preNode = preNode.getNext(); nowNode = nowNode.getNext(); } return nowNode.getContent(); }

// 数组创建一个链表 public static Node createLinkedLNode(int[] array) { // 创建一个根节点 Node root = null; // 从末尾元素开始依次创建Node节点 for (int i = array.length - 1; i >= 0; i—) { Node node = new Node(array[i], null); // 创建两个节点的连接关系 if (root != null) { node.setNext(root); root = node; } else { root = node; } } return root; }

public static void main(String[] args) { int[] array = {9, 2, 4, 5, 6, 8, 0, 3, 10}; Node root = createLinkedLNode(array); int result = findK(root, 3); System.out.println(“k = 3 result: “ + result); result = findK(root, 6); System.out.println(“k = 6 result: “ + result); } }

  1. <a name="UMq6V"></a>
  2. #### 双指针变形——获取中心节点
  3. ```java
  4. public class Demo {
  5. // 找寻倒数第 k 个节点内容
  6. public static int findCenter(Node node) {
  7. // 创建先驱指针
  8. Node pioneer = node;
  9. // 创建实际指针
  10. Node current = node;
  11. // 每次遍历需要实际指针移动一步,先驱指针移动二步
  12. while(pioneer.getNext() != null){
  13. current = current.getNext();
  14. pioneer = pioneer.getNext();
  15. if(pioneer.getNext() != null){
  16. pioneer = pioneer.getNext();
  17. }
  18. }
  19. return current.getContent();
  20. }
  21. // 数组创建一个链表
  22. public static Node createLinkedLNode(int[] array) {
  23. // 创建一个根节点
  24. Node root = null;
  25. // 从末尾元素开始依次创建Node节点
  26. for (int i = array.length - 1; i >= 0; i--) {
  27. Node node = new Node(array[i], null);
  28. // 创建两个节点的连接关系
  29. if (root != null) {
  30. node.setNext(root);
  31. root = node;
  32. } else {
  33. root = node;
  34. }
  35. }
  36. return root;
  37. }
  38. public static void main(String[] args) {
  39. int[] array1 = {9, 2, 4, 5, 6, 8, 0, 3, 10};
  40. Node root1 = createLinkedLNode(array1);
  41. int result = findCenter(root1);
  42. System.out.println("array1 result: " + result);
  43. int[] array2 = {9, 2, 4, 5, 6, 7, 8, 0, 3, 10};
  44. Node root2 = createLinkedLNode(array2);
  45. result = findCenter(root2);
  46. System.out.println("array2 result: " + result);
  47. }
  48. }

有环链表

链表 - 图13
检查链表是否有环

  1. public class Demo {
  2. // 查询链表是否有环,true是有环,false是无环
  3. public static boolean hasCircle(Node node) {
  4. // 移动先驱指针
  5. if (node == null || node.getNext() == null) {
  6. return false;
  7. }
  8. // first 每次前进一步
  9. Node first = node;
  10. // second 每次前进两步
  11. Node second = node.getNext();
  12. // 核心逻辑:如果链表中有环,两个指针用不同速度运行,最终second会追上first,也就是 first == second。
  13. while (first != null && second != null && first != second) {
  14. first = first.getNext();
  15. second = second.getNext();
  16. if (second != null) {
  17. second = second.getNext();
  18. }
  19. }
  20. // 如果相等,并且不为空,则表示有环
  21. if (first != null && first == second) {
  22. return true;
  23. }
  24. return false;
  25. }
  26. // 数组创建一个链表
  27. public static Node createLinkedLNode(int[] array) {
  28. // 创建一个根节点
  29. Node root = null;
  30. // 从末尾元素开始依次创建Node节点
  31. for (int i = array.length - 1; i >= 0; i--) {
  32. Node node = new Node(array[i], null);
  33. // 创建两个节点的连接关系
  34. if (root != null) {
  35. node.setNext(root);
  36. root = node;
  37. } else {
  38. root = node;
  39. }
  40. }
  41. return root;
  42. }
  43. public static void main(String[] args) {
  44. testCaseA();
  45. testCaseB();
  46. testCaseC();
  47. }
  48. // 无环
  49. public static void testCaseA() {
  50. int[] array1 = {9, 2, 4, 5, 6, 8, 0, 3, 10};
  51. Node root1 = createLinkedLNode(array1);
  52. boolean result = hasCircle(root1);
  53. System.out.println("caseA has circle: " + result);
  54. }
  55. // 有环: 案例1
  56. public static void testCaseB() {
  57. Node node0 = new Node();
  58. node0.setContent(10);
  59. Node node1 = new Node(3, node0);
  60. Node node2 = new Node(0, node1);
  61. Node node3 = new Node(8, node2);
  62. Node node4 = new Node(6, node3);
  63. Node node5 = new Node(5, node4);
  64. Node node6 = new Node(4, node5);
  65. Node node7 = new Node(2, node6);
  66. Node node8 = new Node(9, node7);
  67. node0.setNext(node8);
  68. boolean result = hasCircle(node8);
  69. System.out.println("caseB has circle: " + result);
  70. }
  71. // 有环: 案例2
  72. public static void testCaseC() {
  73. Node node0 = new Node();
  74. node0.setContent(4);
  75. Node node1 = new Node(10, node0);
  76. Node node2 = new Node(3, node1);
  77. Node node3 = new Node(0, node2);
  78. Node node4 = new Node(8, node3);
  79. Node node5 = new Node(6, node4);
  80. Node node6 = new Node(5, node5);
  81. node0.setNext(node6);
  82. Node node7 = new Node(2, node0);
  83. Node node8 = new Node(9, node7);
  84. node0.setNext(node8);
  85. boolean result = hasCircle(node8);
  86. System.out.println("caseC has circle: " + result);
  87. }
  88. }

双向链表

链表 - 图14

特点

  1. 对于每个节点,有三部分组成,prev(指向前一个节点),next(指向下一个节点),content(节点内容)
  2. 对于双向链表,为了方便遍历,我们需要同时保留第一个和最后一个节点。

    LinkedList

    阅读LinkedList.java的源码,发现实际上就是利用双向链表实现的。

    1. public class LinkedList<E>
    2. extends AbstractSequentialList<E>
    3. implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    4. {
    5. transient int size = 0;
    6. /**
    7. * Pointer to first node.
    8. * Invariant: (first == null && last == null) ||
    9. * (first.prev == null && first.item != null)
    10. */
    11. transient Node<E> first;
    12. /**
    13. * Pointer to last node.
    14. * Invariant: (first == null && last == null) ||
    15. * (last.next == null && last.item != null)
    16. */
    17. transient Node<E> last;
    18. /**
    19. * Constructs an empty list.
    20. */
    21. public LinkedList() {
    22. }
    23. ......
    24. }