单链表
头结点:用来记录链表的基地址;
尾结点:指向的是一个空地址NULL;
插入、删除
- 数组在进行插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是O(n);
- 链表中插入、删除操作时,并不需要为了保持内存的连续性而搬移节点,因为链表的存储空间本身就不是连续的;所以只需要考虑相邻节点的指针改变,对应的时间复杂度为O(1);
边界的问题:
针对链表的插入、删除操作,需要对插入第一个节点和删除最后一个节点的情况进行特殊处理;
插入操作
在空链表插入第一个节点
if(head == null){ head = new_node; }
在其他位置插入节点
new_node.next = p.next; p.next = new_node;
删除链表的最后一个节点
if(head.next == null){ head = null; }
删除链表中的其他节点
解决:哨兵节点
p.next = p.next.next;
- 引入哨兵节点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵节点。我们也把这种有哨兵节点的链表叫带头节点;
- 引入哨兵节点统一了代码的逻辑实现,具体可以参考后面的删除节点问题。
查找
如果链表想要随机访问第k个元素,就没有数组那么高效了;因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个节点一个节点的依次遍历,直到找到相应的节点,所以时间复杂度为O(n)
循环链表
- 循环链表是特殊的单链表。单链表是尾结点指针指向空地址,表示最后的节点;而循环链表的尾结点指针指向链表的头节点。
- 著名问题:约瑟夫环(循环链表有n个节点,每次移除第m个节点,最终剩下的节点)
public int lastRemaining(int n, int m){ int res = 0; for(int i = 2; i <= n; i++){ res = (res + m) % i; } return res; }
双向链表
概念
单向链表只有一个方向,节点只有一个后续指针next指向后面的节点;双向链表支持两个方向,每个节点不止有一个后继指针next指向后面的节点,还有一个前驱指针prev指向前面的节点。
优势
双向链表在某些情况下的插入、删除操作要比单链表简单、高效
- 例如:删除给定指针指向的节点
- 单链表在指定节点的指针指向的位置插入或者删除的时间复杂度为O(1)。但是如果要删除指定的某个节点,需要知道其前驱节点,而单链表找到前驱节点需要从头节点开始遍历链表,直到p.next = q,所以时间复杂度为O(n);
- 而双向链表中的节点已经保存前驱节点的指针,所以只需要时间复杂度为O(1);
有序链表,双向链表的按值查询的效率更高
- 比如,我们可以记录上次查找的位置p,每次查询时,根据要查找的值与p的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。
应用
- Linkedhashmap的有序性就是通过双向链表实现;
- LRU算法;
- 判断回文链表;
- 将单链表变为双向链表。
练习
- 单链表反转
剑指 Offer II 024. 反转链表
剑指 Offer 24. 反转链表
class Solution { public ListNode reverseList(ListNode head) { // 固定格式 ListNode cur = head, pre = null; while(cur != null){ ListNode temp = cur.next; cur.next = pre; pre = cur; cur = temp; } return pre; } }
- 链表中环的检测
剑指 Offer II 022. 链表中环的入口节点
142. 环形链表 II
141. 环形链表
这里的判断条件是fast != null && fast.next != null;因为允许节点指向空节点,但是不允许空节点本身有指向,所以fast = fast.next.next是正确的
public class Solution { public ListNode detectCycle(ListNode head) { ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null){ fast = fast.next.next; slow = slow.next; if(fast == slow){ while(slow != head){ slow = slow.next; head = head.next; } return head; } } return null; } }
- 求链表的中间节点
如果是奇数是中间节点,如果是偶数是后一个中间节点;例如:2,3,4,5返回的是4
public ListNode findMiddle(ListNode head){ ListNode fast = head, slow = head; while(fast != null && fast.next != null){ fast = fast.next.next; slow = slow.next; } return slow; }如果是奇数是中间节点,如果是偶数是前一个中间节点;例如:2,3,4,5返回的是3
public ListNode findMiddle(ListNode head){ if(head == null){ return head; } ListNode fast = head, slow = head; while(fast.next != null && fast.next.next != null){ fast = fast.next.next; slow = slow.next; } return slow; }
- 删除链表倒数第n个节点
注意建立哨兵节点,主要是防止 p.next = p.next.next操作报错
19. 删除链表的倒数第 N 个结点
剑指 Offer II 021. 删除链表的倒数第 n 个结点
class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode sentinel = new ListNode(0, head); ListNode fast = head; ListNode slow = sentinel; while(fast != null){ if(n > 0){ n--; }else{ slow = slow.next; } fast = fast.next; } slow.next = slow.next.next; return sentinel.next; } }
2095. 删除链表的中间节点(如果是偶数,后一个为中间节点)
class Solution { public ListNode deleteMiddle(ListNode head) { ListNode sentinel = new ListNode(0, head); ListNode pre = sentinel; ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null){ fast = fast.next.next; pre = slow; slow = slow.next; } pre.next = pre.next.next; return sentinel.next; } }
237. 删除链表中的节点
面试题 02.03. 删除中间节点
class Solution { // 伪装成儿子、之后删除儿子 public void deleteNode(ListNode node) { node.val = node.next.val; node.next = node.next.next; } }
剑指 Offer 18. 删除链表的节点
class Solution { public ListNode deleteNode(ListNode head, int val) { ListNode sentinel = new ListNode(0, head); ListNode node = sentinel; while(node != null){ if(node.next.val == val){ break; } node = node.next; } node.next = node.next.next; return sentinel.next; } }
- 两个有序的链表合并
21. 合并两个有序链表
剑指 Offer II 078. 合并排序链表
剑指 Offer 25. 合并两个排序的链表
三种实现
- 在原有链表上合并(使用递归)
- 返回新建链表
- 使用优先队列处理多个有序链表
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 在原有链表上操作 if(l1 == null){ return l2; } if(l2 == null){ return l1; } if(l1.val < l2.val){ l1.next = mergeTwoLists(l1.next, l2); return l1; }else{ l2.next = mergeTwoLists(l1, l2.next); return l2; } } }
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 新建链表合并 if(l1 == null){ return l2; } if(l2 == null){ return l1; } ListNode head = new ListNode(); ListNode node = head; while(l1 != null && l2 != null){ if(l1.val < l2.val){ node.next = new ListNode(l1.val); node = node.next; l1 = l1.next; }else{ node.next = new ListNode(l2.val); node = node.next; l2 = l2.next; } } if(l1 != null){ node.next = l1; } if(l2 != null){ node.next = l2; } return head.next; } }
class Solution { public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if(l1 == null){ return l2; } if(l2 == null){ return l1; } Queue<ListNode> queue = new PriorityQueue<>((o1, o2) -> (o1.val - o2.val)); queue.offer(l1); queue.offer(l2); ListNode head = new ListNode(); ListNode node = head; while(!queue.isEmpty()){ ListNode temp = queue.poll(); node.next = temp; node = node.next; if(temp.next != null){ queue.offer(temp.next); } } return head.next; } }
- 拓展
剑指 Offer II 026. 重排链表
class Solution { public void reorderList(ListNode head) { //寻找前一个中间节点 if(head == null){ return; } ListNode slow = head, fast = head; while(fast.next != null && fast.next.next != null){ fast = fast.next.next; slow = slow.next; } //cur就是后半段节点 ListNode cur = slow.next; //断开前半段节点 slow.next = null; //翻转链表 ListNode pre = null; while(cur != null){ ListNode temp = cur.next; cur.next = pre; pre = cur; cur = temp; } //合并链表 ListNode node = head; while(head != null && pre != null){ ListNode l1 = node.next; ListNode l2 = pre.next; node.next = pre; node = l1; pre.next = node; pre = l2; } } }
剑指 Offer II 077. 链表排序(归并)
class Solution { public ListNode sortList(ListNode head) { if(head == null || head.next == null){ return head; } ListNode fast = head, slow = head; // 指向的是前一个中间 while(fast.next != null && fast.next.next != null){ fast = fast.next.next; slow = slow.next; } ListNode mid = slow.next; slow.next = null; // 前后半段 ListNode list1 = sortList(head); ListNode list2 = sortList(mid); return merge(list1, list2); } public ListNode merge(ListNode list1, ListNode list2){ ListNode cur = new ListNode(); ListNode node = cur; while(list1 != null && list2 != null){ if(list1.val < list2.val){ node.next = list1; list1 = list1.next; }else{ node.next = list2; list2 = list2.next; } node = node.next; } if(list1 != null){ node.next = list1; }else if(list2 != null){ node.next = list2; } return cur.next; } }
25. K 个一组翻转链表(使用递归、相当于反转链表的进阶)
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseKGroup(ListNode head, int k) { if(head == null){ return head; } //定位此组的后面的节点 ListNode position = head; for(int i = 0; i < k; i++){ if(position == null){ return head; } position = position.next; } ListNode pre = null, cur = head; while(cur != position){ ListNode temp = cur.next; cur.next = pre; pre = cur; cur = temp; } // cur是剩余的head节点 head.next = reverseKGroup(cur, k); return pre; } }