链表实现/移除节点/链表相加/链表部分翻转/链表改序/链表去重/链表划分/链表的环/链表公共节点问题/链表复制。
简介
线性表是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。数组 immutable length 和 no holes allowed 的特性带来的结果就是当 resize array 的时候需要 copy 原有的所有元素,当删除一个不在末尾的元素时,又要 shift 很多元素。而 Linked list 解决了这个问题,它的代价是需要额外的 4 bytes(32bit machine) 来储存指向下一个 node 的 reference,另外不允许随机访问元素。
线性表的两种存储方式
- 顺序存储结构:随机读取,访问时是 O(1)
- 链式存储结构:插入和删除 O(1),访问时最坏是 O(n)
线性表的分类(根据指针域)
- 单向链表(Singly linked list)
- 双向链表(Doubly linked list)
- 循环链表(Circular linked list)
这一篇主要讲的是链表(linked list)。链表是一种常见的线性数据结构。单向链表(singly linked list),每个节点有一个 next 指针指向后一个节点,还有一个成员变量用以存储数值;双向链表(doubly Linked List),多了一个 prev 指针指向前一个节点。与数组类似,搜索链表需要O(n)的时间复杂度,但是链表不能通过常数时间 O(1) 读取第 k 个数据。链表的优势在于能够以较高的效率在任意位置插入或删除一个节点。
Complexity
Linked list
addFirst: O(1)
insertBefore or insertAfter: O(n)
delete: O(n)
search: O(n)
Array
locate: O(1)
insert/delete: O(N)
search: not sorted -> linear search => O(N), sorted -> binary search => O(logN)
iteration: O(N)
基本策略
涉及头节点
当涉及对头节点的操作,考虑创建哑节点
修改单向链表的操作
考虑哪个节点的next指针会受到影响,则需要修正该指针;
反转链表
要把反转后的最后一个节点(即第一个节点)指向 null
删除某个节点
- 由于需要知道前继节点的信息,而前继节点可能会导致表头产生变化,所以需要一些技巧 Dummy Node
- 全部操作结束后,判断是否有环;若有,则置其中一端为 null
快慢指针
快速找出未知长度单链表的中间节点/涉及在链表中寻找特定位置
- 设置两个指针 *fast 和 *slow 都指向头节点
- *fast 移动速度是 *slow 的两倍
- *fast 指向末尾节点时,*slow 正好就在中间
判断单链表是否有环
- 设置两个指针 *fast 和 *slow 都指向头节点
- *fast 移动速度是 *slow 的两倍
- 如果 *fast == null 说明该单链表不是循环链表
- 如果 *fast == *slow 说明该链表是循环链表
找倒数第 N 个节点
- 设置两个指针 fast 和 slow 都指向头节点
- *fast 先移动 N 步,然后两个指针一起前进
- *fast 到达末尾时,*slow 即为倒数第 N 个节点
检验有效性
访问某个节点 cur.next 时,要检验 cur 是否为 null。(同理,访问 cur.next.next,检验 cur.next)
链表实现
Singly-linked list Implementation
|
|
Leetcode 实例
移除节点(19.Remove Nth Node From End of List)
Problem
Given a linked list, remove the nth node from the end of list and return its head.
For example,
Given linked list: 1->2->3->4->5, and n = 2.
After removing the second node from the end, the linked list becomes 1->2->3->5.
Note:
Given n will always be valid.
Try to do this in one pass.
Solution
|
|
链表相加(2.445. Add Two Numbers; 369. Plus One Linked List)
Problem(I)
You are given two linked lists representing two non-negative numbers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.
Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Similar problems (M) Multiply Strings (E) Add Binary (M) Add Two Numbers (M) Plus One Linked List
66.Plus One 67.Add Binary
43. Multiply Strings
Solution
|
|
注意考虑两个数位数不同的情况。
因为两位数相加进位最多影响后一位,不会影响 i+2 位,所以发现一个链表为空后,直接结束循环,最后只用进位和较长链表的当前节点相加,之后较长链表的 i+2 位直接照搬。
用这个结构可以实现大整数的计算。
Problem(II)
You are given two non-empty linked lists representing two non-negative integers. The most significant digit comes first and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.
You may assume the two numbers do not contain any leading zero, except the number 0 itself.
Follow up:
What if you cannot modify the input lists? In other words, reversing the lists is not allowed.
Example:
Solution
|
|
Problem(III)
Given a non-negative integer represented as non-empty a singly linked list of digits, plus one to the integer.
You may assume the integer do not contain any leading zero, except the number 0 itself.
The digits are stored such that the most significant digit is at the head of the list.
Example:
Solution
|
|
链表的翻转(206.92.Reverse Linked List)
Problem(I)
Reverse a singly linked list.
Hint:
A linked list can be reversed either iteratively or recursively. Could you implement both?
初步思考
每次走到最后一个数,把它放到最前面
时间复杂度 $O(n^2)$
头插法
|
|
时间复杂度 $O(n)$,空间复杂度 $O(1)$
要注意的是,把翻转后的最后一个节点(即原来的第一个节点)指向 Null(None)。
访问某个节点 cur.next,要检验 cur 是否为 None.
|
|
迭代法
|
|
更简单。
递归
|
|
Problem(II)
Reverse a linked list from position m to n. Do it in-place and in one-pass.
For example:
Given 1->2->3->4->5->NULL, m = 2 and n = 4,
return 1->4->3->2->5->NULL.
Note:
Given m, n satisfy the following condition:
1 ≤ m ≤ n ≤ length of list.
oup, 最后要返回的是 oup.next 指针
dummy 指针,在原来的 m-1 位置
cur 指针,在原来的 n 位置
reverse,在原来的 m 位置
dummy 指针,空转 m-1 次,找到第 m-1 个节点,即开始翻转的第一个结点的前一个;
利用 cur, reverse 按完全翻转的方法翻转[m,n]部分
最后修改两个指针,dummy.next 指向 reverse,dummy.next.next 指向第 n+1 个节点。
Solution
|
|
链表改序(143. Reorder List)
Problem
Given a singly linked list L: L0→L1→…→Ln-1→Ln,
reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→…
You must do this in-place without altering the nodes’ values.
For example,
Given {1,2,3,4}, reorder it to {1,4,2,3}.
Solution
|
|
排序链表去重(82.83. Remove Duplicates from Sorted List I&II)
Problem(I)
Given a sorted linked list, delete all duplicates such that each element appear only once.
For example,
Given 1->1->2, return 1->2.
Given 1->1->2->3->3, return 1->2->3.
Solution
|
|
注意用到 head.next 一定要判断前一个节点 head 是否为空,同理,head.next.next 判断 head.next 是否为空。
Problem(II)
Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.
For example,
Given 1->2->3->3->4->4->5, return 1->2->5.
Given 1->1->1->2->3, return 2->3.
Solution
|
|
考虑的 bad case: [1,1],[1,1,1],所以最后的 cur.next = None 不能少,否则还会返回 [1]
链表的合并(21. Merge Two Sorted Lists)
快速排序对链表结构适用,然而不是所有排序都适合使用链表存储,如堆排序,不断寻找数组的 n/2 和 n 位置,用链表不大方便。
Problem
Merge two sorted linked lists and return it as a new list. The new list should be made by splicing together the nodes of the first two lists.
Solution
Recursive
时间复杂度 O(N),空间复杂度 O(1)
链表的划分(89.Partition List)
Problem
Given a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x.
You should preserve the original relative order of the nodes in each of the two partitions.
For example,
Given 1->4->3->2->5->2 and x = 3,
return 1->2->2->4->3->5.
Solution
用两个指针 left,right,小于 x 的用 left,大于 x 的用 right,最后连接 left,right
这里要注意的是最后 right 要指向空,不然考虑 case [2,1],会陷入[1,2,1,2…]的死循环中,因为 right_cur.next 指向了 head,形成了环。
链表的环 (141.142.Linked List Cycle I & II)
Problem(I)
Given a linked list, determine if it has a cycle in it.
Follow up:
Can you solve it without using extra space?
Solution
|
|
Problem(II)
Given a linked list, return the node where the cycle begins. If there is no cycle, return null.
Note: Do not modify the linked list.
Follow up:
Can you solve it without using extra space?
Solution
|
|
单链公共节点问题
Problem
Write a program to find the node at which the intersection of two singly linked lists begins.
假设两个链表长度为 m,n,认为 m > n,两链表的第一个公共节点到尾节点一定是重合的。于是,可以分别遍历两个链表得到链表长度 m,n, 长链表空转 m-n 次,然后两链表齐头并进,同步遍历,直到找到公共节点。时间复杂度为 $O(m+n)$
如果链表存在环,则需要用快慢指针的方式计算公共节点。两个指针,每次分别移动 1 个/2 个节点。
Solution
|
|
一个从代码层面来讲的简洁版本。
链表复制(138. Copy List with Random Pointer)
Problem
A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null.
Return a deep copy of the list.
Solution
|
|
参考链接:
编程起跑线 第 5 课 链表
Implementing a Singly Linked List in Python