当前位置:   article > 正文

对LinkedList ,单链表和双链表的理解

对LinkedList ,单链表和双链表的理解
 一.ArrayList的缺陷
 二.链表
 三.链表部分相关oj面试题
     四.LinkedList的模拟实现
     五.LinkedList的使用
     六.ArrayList和LinkedList的区别
一.ArrayList的缺陷:
1. ArrayList底层使用 数组 来存储元素,如果不熟悉可以来再看看: ArrayList与顺序表-CSDN博客
由于其底层是一段连续空间,当 ArrayList 任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后 搬移,时间复杂度为 O(n) ,效率比较低,因此 ArrayList 不适合做任意位置插入和删除比较多的场景 。因此: java集合中又引入了 LinkedList,即链表结构
二.链表
1.链表的概念及结构:链表是一种 物理存储结构上非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的引用链接次序实现的就像一个火车。
注意:1链表在逻辑上是连续的,在物理结构上不一定连续。
            2.节点一般都是从堆上申请出来的
          3.从堆上申请出来空间,是有它的分配规律和策略的,两次申请出来的可能连续也可能不续
2.链表的分类
单向或者双向循环和非循环带头和不带头就可以组合出8种类型的链表
虽然有这么多的链表的结构,但是我们重点掌握两种:
(1) 无头单向非循环链表:结构简单, 一般不会单独用来存数据。实际中更多是 作为其他数据结构的子结构,哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
(2)无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表
 

3.无头单向非循环链表实现
自己定义的类和包:


这里可以把方法先写在一个接口中,再通过MyLinkList实现接口,这样写可能更好,代码好复用。
我们
MyLinkList类中:我们要先抽象出每一个节点,每一个节点就是一个对象,我们可以写一个产生节点对象的模板:(这里可以定义一个静态 内部类,来抽象出节点模板):
 
  1. public class MyLinkList {
  2. public int data;
  3. public MyLinkList.Node next;
  4. //静态内部类
  5. public static class Node {
  6. public int data;//0
  7. public Node next;//引用类型默认值为NULL
  8. public Node(int data) {
  9. this.data = data;
  10. }
  11. }
  12. }

(1)头插方法:
  1. public void addFirst(int data) {
  2. //第一次插入节点(链表为空)
  3. if (this.head == null) {
  4. Node node = new Node(data);//链表头为空时(head == null),整了链表的头引用为 node
  5. this.head = node;
  6. return;
  7. }
  8. //链表不为空,单链表插入要先绑后面
  9. Node node = new Node(data);
  10. node.next = this.head;
  11. head = node;//把node的引用给head,然head变成新的头
  12. }

(2)尾插法:

  1. public void addList(int data) {
  2. //第一次插入时
  3. if (this.head == null) {
  4. Node node = new Node(data);
  5. head = node;
  6. return;
  7. }
  8. Node node = new Node(data);
  9. Node cur = this.head;//cur从头开始
  10. /*这里注意cur不可以先走到空,如果cur走到null,那么cur的next就是cull*/
  11. while (cur.next != null) {
  12. cur = cur.next;
  13. }
  14. //出来时cur==null,就尾插
  15. cur.next = node;
  16. }

(3)打印单链表:这里我们可以写一个,重载方法display2,可以让链表从返回的某个节点开始打印;

  1. //打印单链表
  2. public void display2(Node nodeH) {
  3. Node cur = this.head;//cur从头开始
  4. cur = nodeH;
  5. while (cur != null) {
  6. System.out.print(cur.data + " ");
  7. cur = cur.next;
  8. }
  9. System.out.println();
  10. }
  11. public void display() {
  12. Node cur = this.head;//cur从头开始
  13. while (cur != null) {
  14. System.out.print(cur.data + " ");
  15. cur = cur.next;
  16. }
  17. System.out.println();
  18. }

(4)查找链表中是否包含某一数据节点:

  1. //查找是否包含关键字Key,是否在链表中
  2. public boolean contains(int key) {
  3. Node cur = this.head;
  4. while (cur != null) {
  5. if (cur.data == key) {
  6. return true;
  7. }
  8. cur = cur.next;
  9. }
  10. return false;
  11. }

 
(5)清空链表:
 
  1. public void clear() {
  2. Node cur = head;
  3. while (cur != null) {
  4. //注意定义一个,变量记住置为空的,后驱节点
  5. Node curN = cur.next;
  6. cur.next =null;//引用类型必须制空
  7. cur = curN;
  8. }
  9. //最后把头节点手动置为null
  10. head = null;
  11. }

(6).返回链表的长度:

  1. public int size() {
  2. Node cur = this.head;
  3. int count = 0;//count不能为1,如果是空链表,count=1返回就,寄了
  4. while (cur != null) {
  5. cur = cur.next;
  6. count++;
  7. }
  8. return count;
  9. }

(7)任意位置插入:这里我画了个图来理解:

  1. //任意位置插入(第一个数据节点为0号下标)
  2. public void addIndex(int index, int data) {
  3. //相当于头插
  4. if (index == 0) {
  5. addFirst(data);
  6. return;
  7. }
  8. //相当于尾插
  9. if (index == this.size()) {
  10. addList(data);
  11. return;
  12. }
  13. //正常插入方法:
  14. /**
  15. * 1. 先找到index前一个节点的地址->定义一个cur走index-1步
  16. * 2.画图插入
  17. */
  18. //先找到index前一个节点的地址
  19. Node cur = searchIndex(index);
  20. //插入
  21. Node node = new Node(data);
  22. /**
  23. * 这里注意,先绑后面(node = cur.next;),因为单链表前一个节点负责,单独的维护后一个节点,前一个节点的引用被覆盖(cur节点)
  24. * 那么原本和cur节点连接的节点就找不到了
  25. */
  26. node.next = cur.next;
  27. cur.next = node;
  28. }
  29. //找到index前一个节点的地址的方法
  30. private Node searchIndex(int index) {
  31. //index下标位置检验
  32. if (index < 0 || index > this.size()) {
  33. throw new RuntimeException("下标位置不合法");
  34. }
  35. Node cur = this.head;
  36. while (index-1 != 0/*走index-1步*/) {
  37. cur = cur.next;
  38. index--;
  39. }
  40. return cur;//返回走index-1步后的,cur类型地址
  41. }

(8)删除指定位置节点:
  1. //找key节点的前驱
  2. private Node searchPrev(int key) {
  3. Node prev = this.head;
  4. while(prev.next != null) {
  5. if (prev.next.data == key) {
  6. return prev;
  7. }else {
  8. prev = prev.next;//继续往后走
  9. }
  10. }
  11. return null;
  12. }
  13. //删除第一次出现关键字为key的节点
  14. public void remove(int key) {
  15. /** 1. 找到,要删除节点del的前驱
  16. * 2. 找到要删除的节点del
  17. * 3. 删除节点
  18. */
  19. //空节点直接返回
  20. if (this.head == null) {
  21. return;
  22. }
  23. //头节点直接删除
  24. if (this.head.data == key) {
  25. head = head.next;
  26. return;//这里注意别忘记了
  27. }
  28. //1. 找到,要删除节点del的前驱
  29. Node prev = searchPrev(key);
  30. if (prev == null) {
  31. throw new RuntimeException("没有你要删除的节点,请考量要删除的节点");
  32. }
  33. //2. 找到要删除的节点del
  34. Node del = prev.next;
  35. //3. 删除节点
  36. prev.next = del.next;
  37. }

(9)只遍历一遍链表,删除所有指定的节点:这里我画了一个图可以帮助理解:定义一个一直往后走快指针,和一个,不需要时往后走判断是否要删除慢指针

  1. //遍历单链表一遍,删除所有值为key的节点
  2. public void removeAllKey(int key) {
  3. /** 1.定义一个快指针 cur : cur指针一直往后走;
  4. * 2.定义一个慢指针 prev: prev指针,只有cur遇到要删除的数据时,prev指针才往后走,不然保持不动
  5. * 3.注意最后不要漏了,head头节点
  6. */
  7. // 1.定义一个 cur指针 : cur指针一直往后走
  8. // 2.定义一个 prev指针: prev指针,只有cur遇到要删除的数据时,prev指针才往后走,不然保持不动
  9. Node cur = this.head.next;//
  10. Node prev = this.head;
  11. while (cur != null) {
  12. if (cur.data == key) {
  13. //cur.data == key,时只有cur指针都在走,因为要遍历删除数据
  14. prev.next = cur.next;
  15. cur = cur.next;
  16. }else {
  17. //cur.data != key,两个指针都在动,prev指针,指向cur指针
  18. prev = cur;
  19. cur = cur.next;
  20. }
  21. }
  22. // 3.注意最后不要漏了,head头节点
  23. if (this.head.data == key) {
  24. this.head = this.head.next;
  25. }
  26. }

 三.链表部分相关oj面试题:(分享一些我认为比较重要的)
1.  反转一个单链表:我录了视频方便理解: 反转一个链表-CSDN直播

反转一个链表

  1. class Solution {
  2. public ListNode reverseList(ListNode head) {
  3. ListNode cur = head;
  4. if(head == null) {
  5. return null;
  6. }
  7. head = null;
  8. while(cur != null) {
  9. ListNode curN = cur.next;
  10. cur.next = head;
  11. head = cur;
  12. cur = curN;
  13. }
  14. return head;
  15. }
  16. }


2.给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点:

理解视频:找到链表中间节点-CSDN直播

找到链表中间节点

  1. class Solution {
  2. public ListNode middleNode(ListNode head) {
  3. if(head == null) {
  4. return null;
  5. }
  6. ListNode fast = head;//快指针一次走2步
  7. ListNode slow = head;//慢指针一次走一步
  8. //条件不可以交换:(fast != null && slow.next != null),fast可能开始就为null
  9. while (fast != null && fast.next != null) {
  10. fast = fast.next.next;
  11. slow = slow.next;
  12. }
  13. return slow;
  14. }
  15. }

3.将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的:

理解视频:合并两个有序链表-CSDN直播

合并两个有序链表


 

  1. class Solution {
  2. public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
  3. ListNode headH = new ListNode(-1);
  4. ListNode tmp = headH;//tmp用来遍历两个链表
  5. while(list1 != null && list2 != null) {
  6. //哪个节点数据小,就接在tmp后面
  7. if(list1.val < list2.val) {
  8. tmp.next = list1;
  9. list1 = list1.next;
  10. tmp = tmp.next;
  11. }else {
  12. tmp.next = list2;
  13. list2 = list2.next;
  14. tmp = tmp.next;
  15. }
  16. }
  17. //当其中一个链表遍历完,就直接接上另一个链表的后半部分
  18. if(list1 != null) {
  19. tmp.next = list1;
  20. }
  21. if(list2 != null) {
  22. tmp.next = list2;
  23. }
  24. return headH.next;
  25. }
  26. }

4.链表的回文结构:

这里有两个点要注意:1.从后往前用slow走,因为偶数节点,fast指针会走到null,无法往前走

 2.回文时偶数情况下,A的下一个节点是slow节点,并且两个节点的val相等。这个时候就要直接返回ture

理解视频:链表的回文结构-CSDN直播

链表的回文结构

  1. public class PalindromeList {
  2. public boolean chkPalindrome(ListNode A) {
  3. // write code here
  4. if (A == null) {
  5. return true;
  6. }
  7. // write code here
  8. ListNode fast = A;
  9. ListNode slow = A;
  10. //1.找到中间节点
  11. while (fast != null && fast.next != null) {
  12. fast = fast.next.next;
  13. slow = slow.next;
  14. }
  15. //2.翻转链表
  16. ListNode cur = slow.next;
  17. while (cur != null) {
  18. ListNode curN = cur.next;
  19. cur.next = slow;
  20. slow = cur;
  21. cur = curN;
  22. }
  23. //3.判断回文
  24. //让A往后走,slow往前走直到;A.val==slow.val
  25. //注意:回文时会有偶数情况下,A的下一个节点是slow节点,并且两个节点的val相等。这个时候就要直接返回ture
  26. while (A != slow) {
  27. if (A.val != slow.val) {
  28. return false;
  29. }
  30. //到这里A.val == slow.val
  31. //A.val == slow.val前提下,偶数情况下,A的下一个节点是slow节点,并且两个节点的val相等。这个时候就要直接返回ture
  32. if (A.next == slow) {
  33. return true;
  34. }
  35. A = A.next;
  36. slow = slow.next;
  37. }
  38. return true;
  39. }
  40. }


 

5.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前:

注意:这里我的方法是,改完后,链表数据从小到大的,而做题在牛客网是,要求反过来(但是方法都一样)

理解视频:链表分割-CSDN直播

链表分割

  1. //链表的分割
  2. public Node partition(Node pHead, int x) {
  3. Node as = null;
  4. Node ae = null;
  5. Node bs = null;
  6. Node be = null;
  7. Node cur = pHead;
  8. while (cur != null) {
  9. if (cur.data > x) {
  10. //第一次插入
  11. if (as == null) {
  12. as = ae = cur;
  13. }else {//第N次插入
  14. ae.next = cur;
  15. ae = ae.next;
  16. }
  17. } else {
  18. //第一次插入
  19. if (bs == null) {
  20. bs = be = cur;
  21. }else{//第N次插入
  22. be.next = cur;
  23. be = be.next;
  24. }
  25. }
  26. cur = cur.next;
  27. }
  28. //当一个链表为空时,返回
  29. if(as == null) {
  30. return bs;
  31. }
  32. //如果到这里as!= null
  33. //连接两部分
  34. ae.next = bs;
  35. //注意,第二部分结尾不为空时,要手动把第二部分最后一个节点,手动制空
  36. if(bs != null) {
  37. be.next = null;
  38. }
  39. //最后返回as
  40. return bs;
  41. }

6.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环返回空 :

方法是:第一次相遇点,到入口点的距离,等于起始点到入口点的距离

这里我画了这个图的推到:

  1. public class Solution {
  2. public ListNode detectCycle(ListNode head) {
  3. ListNode fast = head;
  4. ListNode slow = head;
  5. // 方法:第一次相遇点,到入口点的距离,等于起始点到入口点的距离
  6. while(fast != null && fast.next != null) {
  7. fast = fast.next.next;
  8. slow = slow.next;
  9. if(fast == slow) {
  10. break;
  11. }
  12. }
  13. /**
  14. 1.走到这里,要么不满足{(fast != null && fast.next != null)}
  15. 就是没有环;
  16. 2. 要么就是有环
  17. */
  18. //没有环
  19. if(fast == null || fast.next == null) {
  20. return null;
  21. }
  22. /**
  23. 有环:让slow以和fast以相同的速度,从起始点到入口点,
  24. fast从第一次相遇的成环点走到入口点
  25. */
  26. slow = head;//把slow返回起始点
  27. while(slow != fast) {
  28. slow = slow.next;
  29. fast = fast.next;
  30. }
  31. return slow;
  32. }
  33. }


7.输入两个链表,找出它们的第一个公共结点:

方法:先找到哪个链表长,再让长的链表他们的差值步最后两个链表一起走,直到他们第一次相遇。

  1. public class Solution {
  2. public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
  3. //1.先分别求出两个链表的长度
  4. ListNode pl = pHead1;
  5. ListNode ps = pHead2;
  6. int lenA = 0;
  7. int lenB = 0;
  8. while (pl != null) {
  9. lenA++;
  10. pl = pl.next;
  11. }
  12. while (ps != null) {
  13. lenB++;
  14. ps = ps.next;
  15. }
  16. //注意pl和ps,指向了null,要赋值回来
  17. pl = pHead1;
  18. ps = pHead2;
  19. //2.求差值
  20. int len = lenA - lenB;
  21. if (len < 0) {
  22. pl = pHead2;
  23. ps = pHead1;
  24. len = lenB - lenA;//len变为为正数
  25. }
  26. //现在知道pl指向长的链表,ps指向短的链表
  27. //3.操作两个链表pl和ps,长的链表(pl)先走链表的差值,然后再一起走直到相交
  28. while (len != 0) {
  29. pl = pl.next;
  30. len--;
  31. }
  32. //两个链表分别都走,直到他们相遇
  33. while (pl != ps) {
  34. pl = pl.next;
  35. ps = ps.next;
  36. }
  37. if (pl == null) {
  38. //pl,ps为空,也不可能相交
  39. return null;
  40. }
  41. return pl;
  42. }
  43. }


 

 四.LinkedList的模拟实现:无头双向链表实现
 
1.写的类和包:
其实 无头双向链表,就比单链表多了一个,可以指向前一个节点的引用域,并且尾节点也被一个引用记录着。这样任意位置插入就不用记录节点了。
2.实现:
这里注意一下删除双链表指定位置Remove的节点 :可以优化一下代码,先删除头节点,之后尾节点和中间任意位置节点,有重复代码,(cur.prev.next = cur.next)可以共用;
  1. public class MyLinkList implements IList{
  2. static class ListNode{
  3. public int val;
  4. public ListNode prev;
  5. public ListNode next;
  6. public ListNode(int val) {
  7. this.val = val;
  8. }
  9. }
  10. public ListNode head;//头节点
  11. public ListNode last;//尾节点
  12. @Override
  13. public void addFirst(int data) {
  14. ListNode node = new ListNode(data);
  15. if (head == null) {
  16. head = last = node;
  17. }else {
  18. //所有的插入优先绑定后面
  19. node.next = head;
  20. head.prev = node;
  21. head = node;
  22. }
  23. }
  24. @Override
  25. public void addLast(int data) {
  26. ListNode node = new ListNode(data);
  27. if (head == null) {
  28. head = last = node;
  29. }else {
  30. last.next = node;
  31. node.prev = last;
  32. last = last.next;
  33. }
  34. }
  35. @Override
  36. public void addIndex(int index, int data) {
  37. int len = size();
  38. if (index > len || index < 0) {
  39. return;
  40. }
  41. if (index == len) {
  42. addLast(data);
  43. }
  44. if (index == 0) {
  45. addFirst(data);
  46. }
  47. ListNode cur = findIndex(index);
  48. ListNode node = new ListNode(data);
  49. node.next = cur;
  50. cur.prev.next = node;
  51. node.prev = cur.prev;
  52. cur.prev = node;
  53. }
  54. private ListNode findIndex(int index) {
  55. ListNode cur = head;
  56. while (index != 0) {
  57. cur = cur.next;
  58. index--;
  59. }
  60. return cur;
  61. }
  62. @Override
  63. public boolean contains(int key) {
  64. ListNode cur = head;
  65. while (cur != null) {
  66. if (cur.val == key) {
  67. return true;
  68. }
  69. cur = cur.next;
  70. }
  71. return false;
  72. }
  73. @Override
  74. public void remove(int key) {
  75. ListNode cur = head;
  76. while (cur != null) {
  77. if (cur.val == key) {
  78. if (cur == head) {
  79. //当只有一个节点要删除时
  80. if (head == null) {
  81. cur.next.prev = null;
  82. }
  83. head = head.next;
  84. //删除就走人
  85. return;
  86. }else {
  87. cur.prev.next = cur.next;//优化后,删除中间和尾巴的代码
  88. if (cur == last) {
  89. last = cur.prev;
  90. }else {
  91. cur.next.prev = cur.prev;
  92. }
  93. //删除就走人
  94. return;
  95. }
  96. }
  97. cur = cur.next;
  98. }
  99. }
  100. @Override
  101. public void removeAllKey(int key) {
  102. ListNode cur = head;
  103. while (cur != null) {
  104. if (cur.val == key) {
  105. if (cur == head) {
  106. //当只有一个节点要删除时,cur.next.prev = null会为空,所以加上if判断
  107. if (head == null) {
  108. cur.next.prev = null;
  109. }
  110. head = head.next;
  111. //删除不能走人,接着删除后面。
  112. }else {
  113. cur.prev.next = cur.next;//优化后,删除中间和尾巴的代码
  114. if (cur == last) {
  115. last = cur.prev;
  116. }else {
  117. cur.next.prev = cur.prev;
  118. }
  119. //删除不能走人,接着删除后面。
  120. }
  121. }
  122. cur = cur.next;
  123. }
  124. }
  125. @Override
  126. public int size() {
  127. ListNode cur = head;
  128. int len = 0;
  129. while (cur != null) {
  130. len++;
  131. cur = cur.next;
  132. }
  133. return len;
  134. }
  135. @Override
  136. public void display() {
  137. ListNode cur = head;
  138. while (cur != null) {
  139. System.out.print(cur.val + " ");
  140. cur = cur.next;
  141. }
  142. System.out.println();
  143. }
  144. @Override
  145. public void clear() {
  146. ListNode cur = head;
  147. while (cur != null) {
  148. ListNode curN = cur.next;
  149. cur.next = null;
  150. cur.prev = null;
  151. cur = curN;
  152. }
  153. //注意head和last节点在链表中还被引用着
  154. head = last = null;
  155. }
  156. }

五.LinkedList的使用:
1.什么是LinkedList:
LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
2.  在集合框架中,LinkedList也实现了List接口,具体如下:

总结
1. LinkedList实现了List接口
2. LinkedList的底层使用了双向链表
3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
5. LinkedList比较适合任意位置插入的场景

 
3. LinkedList也有有参数和二无参数的构造方法:
4.方法的使用表参考:
  1. public class Test {
  2. public static void main(String[] args) {
  3. List<Integer> list = new LinkedList<>();
  4. list.add(1);
  5. list.add(2);
  6. list.add(3);
  7. list.add(4);
  8. System.out.println(list);
  9. ArrayList<Integer> list1 = new ArrayList<>();
  10. list1.add(11);
  11. list1.add(12);
  12. list1.add(13);
  13. System.out.println("==============");
  14. list.addAll(list1);
  15. System.out.println(list);
  16. }
  17. }

输出:

5.LinkedList的遍历:ListIteratorIterator的一个子类,可以专门用来打印链表

代码如下:

  1. public class Test {
  2. public static void main(String[] args) {
  3. List<Integer> list = new LinkedList<>();
  4. list.add(1);
  5. list.add(2);
  6. list.add(3);
  7. list.add(4);
  8. System.out.println(list);
  9. ArrayList<Integer> list1 = new ArrayList<>();
  10. list1.add(11);
  11. list1.add(12);
  12. list1.add(13);
  13. System.out.println("foreach遍历");
  14. for (Integer x:list) {
  15. System.out.print(x + " ");
  16. }
  17. System.out.println();
  18. System.out.println("迭代器遍历历");
  19. Iterator<Integer> it = list.iterator();
  20. while (it.hasNext()) {
  21. System.out.print(it.next() + " ");
  22. }
  23. /**
  24. * ListIterator是Iterator的一个子类,可以专门用来打印链表
  25. */
  26. System.out.println();
  27. System.out.println("使用迭代器遍历---正向遍历");
  28. ListIterator<Integer> it1 = list.listIterator();
  29. while (it1.hasNext()) {
  30. System.out.print(it1.next() + " ");
  31. }
  32. System.out.println();
  33. System.out.println("使用反向迭代器---反向遍历");
  34. ListIterator<Integer> it2 = list.listIterator(/*这里要传链表的长度*/ list.size());
  35. while (it2.hasPrevious()) {
  36. System.out.print(it2.previous() + " ");
  37. }
  38. }
  39. }


 

 六.ArrayList和LinkedList的区别:

                                                                                 

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/843750
推荐阅读
相关标签
  

闽ICP备14008679号