与数组类似,链表也是一种线性数据结构。下面是单链表的例子:
链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。
单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。
结点结构
单链表中节点的典型定义:
// Definition for singly-linked list.
public class SinglyListNode {
int val;
SinglyListNode next;
SinglyListNode(int x) { val = x; }
}
707. 设计链表
class MyLinkedList {
//头结点
private Node head;
//链表长度
private int N;
//结点类
private class Node {
int val;
Node next;
public Node(int val, Node next) {
this.val = val;
this.next = next;
}
}
/**
* Initialize your data structure here.
*/
public MyLinkedList() {
// 初始化头结点
// head = new Node(0, null);
// 初始化结点个数
N = 0;
}
/**
* Get the value of the index-th node in the linked list. If the index is invalid, return -1.
*/
public int get(int index) {
if (index < 0 || index >= N) {
return -1;
}
//通过循环,从头结点开始往后找,依次找index次,就可以找到对应元素
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node.val;
}
/**
* Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
*/
public void addAtHead(int val) {
//新建一个结点保存val值,且next指向head
Node newNode = new Node(val, head);
head = newNode;
N++;
}
/**
* Append a node of value val to the last element of the linked list.
*/
public void addAtTail(int val) {
if (N == 0) {
addAtHead(val);
}
//找到当前最后一个结点
Node node = head;
while (node.next != null) {
node = node.next;
}
//创建新结点,保存元素val
Node newNode = new Node(val, null);
node.next = newNode;
N++;
}
/**
* Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
*/
public void addAtIndex(int index, int val) {
if (index <= 0 || N == 0) {
addAtHead(val);
} else if (index <= N) {
// 找到index位置前一个结点
Node pre = head;
for (int i = 0; i < index - 1; i++) {
pre = pre.next;
}
// 找到index位置的结点
Node cur = pre.next;
//创建新结点,并且新结点需要指向原来i位置的结点
Node newNode = new Node(val, cur);
//原来i位置的前一个结点指向新结点即可
pre.next = newNode;
//元素个数+1
N++;
}
}
/**
* Delete the index-th node in the linked list, if the index is valid.
*/
public void deleteAtIndex(int index) {
if (index == 0) {
Node nextHead = head.next;
head = nextHead;
} else if (index < 0 || index >= N) {
return;
} else {
//找到i位置的前一个结点
Node pre = head;
for (int i = 0; i < index - 1; i++) {
pre = pre.next;
}
//要找到i位置的结点
Node cur = pre.next;
//要找到i位置的下一个结点
Node nextNode = cur.next;
//前一个结点指向后一个结点
pre.next = nextNode;
}
//元素个数-1
N--;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
感觉我写得不太行,我看了下别人的题解,用的是虚拟头尾节点。意思是在头结点数据域里面不放有实际意义的值,方便删除等操作(因为不需要将头结点与其他结点分开讨论了)。但我自己做的时候真的没想到啊,还傻傻地每次都if,else……不过,我还是觉得我这种更适合新手理解啊~
链表环问题的安全选择:
一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
141. 环形链表
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode slowNode = head, fastNode = head.next;
while (slowNode != fastNode) {
if (slowNode.next != null && fastNode.next != null && fastNode.next.next != null) {
slowNode = slowNode.next;
fastNode = fastNode.next.next;
} else {
return false;
}
}
return true;
}
虽然通过了,但感觉我对这个链表的条件判断不够准确,看看大佬的:
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode l1 = head, l2 = head.next;
while (l1 != null && l2 != null && l2.next != null) {
if (l1 == l2) {
return true;
}
l1 = l1.next;
l2 = l2.next.next;
}
return false;
}
官方题解有一个用哈希表的方法,也很值得学习。
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<ListNode>();
while (head != null) {
if (!seen.add(head)) {
return true;
}
head = head.next;
}
return false;
}
}
142. 环形链表 II
最容易想到的当然是哈希表:
法1:哈希表
Set<ListNode> seen = new HashSet<>();
while (head != null) {
if (!seen.add(head)) {
return head;
}
head = head.next;
}
return null;
}
法2:快慢指针
我知道要用这个做,但我想了半小时都没做出来,看了别人的思路,原来是有数学关系在里面的啊,那就简单了。
图源@Krahets
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
ListNode n = head;
while (n != slow) {
slow = slow.next;
n = n.next;
}
return n;
}
}
return null;
}
160. 相交链表
法1:哈希表
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
HashSet<ListNode> seen = new HashSet<>();
while (headA != null) {
if (!seen.add(headA)) {
break;
}
headA = headA.next;
}
while (headB != null) {
if (!seen.add(headB)) {
return headB;
}
headB = headB.next;
}
return null;
}
法2:暴力法
ListNode temp = headB;
while (headA != null) {
while (headB != null) {
if (headA == headB) {
return headA;
}
headB = headB.next;
}
headA = headA.next;
headB = temp;
}
return null;
法3:双指针
利用了相同速度的长度差。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA, B = headB;
while (A != B) {
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return A;
}
19. 删除链表的倒数第N个节点
fast指针先行n步,然后两指针以相同速度共同前进,在fast指针刚好走到最后一个元素时一起停下脚步,此刻,slow站着的地方就是被删除元素的前一个元素,然后进行删除操作。
涉及到删除操作的要注意头结点要单独拿出来讨论。
法1:双指针
// 双指针
ListNode slow = head, fast = head;
for (int i = 0; i < n; i++) {
if (fast != null) {
fast = fast.next;
}
}
if (fast == null) {
head = slow.next;
return head;//不要return null啊,返回的是新的头结点,可能为null。
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return head;
法2:ArrayList法
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode node = head;
List<ListNode> list = new ArrayList<>();
while (node != null) {
list.add(node);
node = node.next;
}
int len = list.size();
if (len == n) {
head = head.next;
return head;
}
ListNode pre = list.get(len - n - 1);
pre.next = list.get(len - n).next;// list.get(len - n + 1)会索引越界
return head;
}
双指针模版:
// Initialize slow & fast pointers
ListNode slow = head;
ListNode fast = head;
/**
* Change this condition to fit specific problem.
* Attention: remember to avoid null-pointer error
**/
while (slow != null && fast != null && fast.next != null) {
slow = slow.next; // move slow pointer one step each time
fast = fast.next.next; // move fast pointer two steps each time
if (slow == fast) { // change this condition to fit specific problem
return true;
}
}
return false; // change return value to fit specific problem
做这种题比数组更容易出错,
要注意的点:
复杂度分析:
空间复杂度:如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。
时间复杂度:
我们需要分析运行循环的次数。
在前面的查找循环示例中,假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。
显然,M <= N 。所以我们将循环运行 N 次。对于每次循环,我们只需要常量级的时间。因此,该算法的时间复杂度总共为 O(N)。
自己分析其他问题以提高分析能力。别忘了考虑不同的条件。如果很难对所有情况进行分析,请考虑最糟糕的情况。
在上一章中,我们介绍了如何在链表中使用双指针技巧。本章节中,我们将从如何反转单链表开始并进一步探索更多经典问题。
一种解决方案是按原始顺序迭代结点,并将它们逐个移动到列表的头部。
时间复杂度为 O(N),其中 N 是链表的长度。我们只使用常量级的额外空间,所以空间复杂度为 O(1)
206. 反转链表
法1:迭代
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
法2:递归
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode cur = reverseList(head.next);
head.next.next = head;
head.next = null;
return cur;
}
203. 移除链表元素
法1:正常删除法
public ListNode removeElements(ListNode head, int val) {
// 删除开头可能存在的所有结点,要用while循环
while (head != null && head.val == val) {
head = head.next;
}
ListNode pre = head;
while (pre != null && pre.next != null) {
if (pre.next.val == val) {
pre.next = pre.next.next;
} else {// 考虑到可能需要删除连续的结点,所以这里的else不能省
pre = pre.next;
}
}
return head;
}
法2:递归
if(head==null)
return null;
head.next=removeElements(head.next,val);
if(head.val==val){
return head.next;
}else{
return head;
}
328. 奇偶链表
把奇数项结点先连接起来,其实可以理解成删除偶数项结点,但偶数项结点也要连起来,最后拼接在奇数项结点尾部。
public ListNode oddEvenList(ListNode head) {
if (head == null) {
return null;
}
ListNode odd = head, even = odd.next, evenHead = odd.next;
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
234. 回文链表
最容易想的当然是利用集合这一工具。
法1:ArrayList法
public boolean isPalindrome(ListNode head) {
List<ListNode> list = new ArrayList<>();
ListNode node = head;
while (node != null) {
list.add(node);
node = node.next;
}
int l = 0, r = list.size() - 1;
while (l < r) {
if (list.get(l++).val != list.get(r--).val){
return false;
}
}
return true;
}
递归法和快慢指针法似乎比较麻烦,暂时没想出来,先留着。
与单链表不同的是,双链表的每个结点中都含有两个引用字段。
添加操作
与单链表类似,添加操作的时间和空间复杂度都是 O(1)
删除操作
从双链表中删除一个现有的结点 cur,我们可以简单地将它的前一个结点 prev 与下一个结点 next 链接起来。
与单链表不同,使用“prev”字段可以很容易地在常量时间内获得前一个结点。
因为我们不再需要遍历链表来获取前一个结点,所以时间和空间复杂度都是O(1)
707. 设计链表
但是删除给定结点(包括最后一个结点)时略有不同。
如果你需要经常添加或删除结点,链表可能是一个不错的选择。
如果你需要经常按索引访问元素,数组可能是比链表更好的选择。
21. 合并两个有序链表
我一看题目,就想起了以前的合并有序数组,然后想这不是很简单吗。写到这里突然想起来,这不是数组啊,是链表,这样做下去返回的merged会是null的。
ListNode node1 = l1, node2 = l2;
ListNode merged = null;
while (node1 != null && node2 != null) {
if (node1.val <= node2.val) {
merged = node1;
node1 = node1.next;
} else {
merged = node2;
node2 = node2.next;
}
merged = merged.next;
}
赶紧换回链表的思维,链表里很有意思的一点是,到处都是引用,你得在脑子里时刻记住你定义和操作的变量是指向哪里的。脑子里想这个容易乱,所以最好还是画图,把链表结构和引用名称写上去。
虚拟头结点的val值无意义,它只是一个工具,有了它我们就可以直接使用.next来拼接。如果这道题改一下,改成把两个有序链表拼接在一个已知链表后,那我们不需要dummyHead了,因为我们可以直接在原来链表里.next。
有了dummyHead,dummyHead指向的我们归并的结果链表头。那为什么还要cur呢,cur其实可以理解成链表指针。最好还是自己dubug一下,看看各种引用的指向自己就懂了。最后来思考一些有趣的问题:return之后,l1,l2,l3,l4,l5,l6他们的地址值变了吗?如果没变,那他们里面的内容属性是怎么变的呢?
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(4);
l1.next = l2;
l2.next = l3;
l3.next = null;
ListNode l4 = new ListNode(1);
ListNode l5 = new ListNode(3);
ListNode l6 = new ListNode(4);
l4.next = l5;
l5.next = l6;
l6.next = null;
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode cur = dummyHead;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
cur.next = l1;
cur = cur.next;
l1 = l1.next;
} else {
cur.next = l2;
cur = cur.next;
l2 = l2.next;
}
}
if (l1 == null) {
cur.next = l2;
} else {
cur.next = l1;
}
return dummyHead.next;
}
2. 两数相加
两数相加,最重要的是要处理进位问题。/10获得进位,%10获得实际存入相应位的值。
这个和上一道题一样,都要创建新的链表头,而且区别是这道题不能像上一道题一样,直接cur.next指向l1或者l2,因为这不是排序,而是每次加上一个数都新建一个结点值来保存sum。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode res = new ListNode(0);
ListNode cur = res;
int carry = 0;
while (l1 != null || l2 != null) {
int a = l1 == null ? 0 : l1.val;
int b = l2 == null ? 0 : l2.val;
int sum = a + b + carry;
carry = sum / 10;
cur.next = new ListNode(sum % 10);
cur = cur.next;
if (l1 != null)
l1 = l1.next;
if (l2 != null)
l2 = l2.next;
}
if (carry > 0) {
cur.next = new ListNode(carry);
}
return res.next;
}
需要注意的是,头结点是个位所以进位默认为0,而每次循环加上的进位其实是上一次循环的进位。最后要注意的是空出的位要记得补0。循环结束后如果有进位,就再拼接一个val为carry的链表。
430. 扁平化多级双向链表
想不出来啊。。一开始题目都没看懂,看了一下别人的答案,都是用栈和二叉树什么的来做的,我还没系统学过,先理解一下别人的代码,对我以后学栈应该有帮助。
法1:借助栈
public Node flatten(Node head) {
if (head == null) return head;
Node pseudoHead = new Node(0, null, head, null);
Node curr, prev = pseudoHead;
Deque<Node> stack = new ArrayDeque<>();
stack.push(head);
while (!stack.isEmpty()) {
curr = stack.pop();
prev.next = curr;
curr.prev = prev;
if (curr.next != null) stack.push(curr.next);
if (curr.child != null) {
stack.push(curr.child);
// don't forget to remove all child pointers.
curr.child = null;
}
prev = curr;
}
// detach the pseudo node from the result
pseudoHead.next.prev = null;
return pseudoHead.next;
}
作者:LeetCode
链接:https://leetcode-cn.com/problems/flatten-a-multilevel-doubly-linked-list/solution/bian-ping-hua-duo-ji-shuang-xiang-lian-biao-by-lee/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
感觉有点像我以前看过的二叉树前序遍历啊,等学完了回来看看。
138. 复制带随机指针的链表
这道题看懂不难,复制链表也不难,难就难在怎么处理随机指针。
法一:
这道题我一开始也没想出思路,不过我看了https://leetcode-cn.com/problems/copy-list-with-random-pointer/solution/liang-chong-shi-xian-tu-jie-138-fu-zhi-dai-sui-ji-/的思路,自己也会实现了,思路很重要啊。
简单来说,就是在原链表每个结点后面复制结点,比如原来是A-B-C,复制后的链表就就变成A-A’-B-B’-C-C’,复制完后还要设置A’B’C’这三个结点的random属性,新结点的ranodom指向就是原结点的random指向的next。最后再把新链表提取出来(A‘-B’-C’),当然不要忘了还原原链表(A-B-C)。其实也很简单啊,就是不懂为啥自己想不出来。
但我觉得这道题很有意思,算是小小的综合题,能做出来了说明对链表的结构掌握得还行了。
public Node copyRandomList(Node head) {
// 1.每个原结点后面复制一个结点
if (head == null) return null;
Node l1 = head;
while (l1 != null) {
Node newNode = new Node(l1.val);
newNode.next = l1.next;
l1.next = newNode;
l1 = l1.next.next;
}
l1 = head;
// 2.设置新结点的random引用
while (l1 != null && l1.next != null) {
if (l1.random != null)//别漏掉这句,因为random可能为null
l1.next.random = l1.random.next;
l1 = l1.next.next;
}
l1 = head;
// 3.把新链表提取出来
Node res = new Node(0);
Node cur = res;
while (l1 != null && cur != null) {
cur.next = l1.next;
cur = cur.next;
l1.next = cur.next;//我一开始漏掉了这句,会出现错误,所以要把原链表还原回来
// Next pointer of node with label 7 from the original list was modified.
l1 = l1.next;
}
return res.next;
}
法二:哈希表
解答过程来自上面的链接,代码是我自己写的。
我们用哈希表来解决这个问题
首先创建一个哈希表,再遍历原链表,遍历的同时再不断创建新节点
我们将原节点作为key,新节点作为value放入哈希表中
第二步我们再遍历原链表,这次我们要将新链表的next和random指针给设置上
从上图中我们可以发现,原节点和新节点是一一对应的关系,所以
- map.get(原节点),得到的就是对应的新节点
- map.get(原节点.next),得到的就是对应的新节点.next
- map.get(原节点.random),得到的就是对应的新节点.random
所以,我们只需要再次遍历原链表,然后设置: 新节点.next-> map.get(原节点.next) 新节点.random ->
map.get(原节点.random) 这样新链表的next和random都被串联起来了
最后,我们然后map.get(head),也就是对应的新链表的头节点,就可以解决此问题了。
public Node copyRandomList1(Node head) {
// 1.新建一个Map容器存储新旧结点
Map<Node, Node> map = new HashMap<>();
Node ptr = head;
while (ptr != null) {
map.put(ptr, new Node(ptr.val));
ptr = ptr.next;
}
// 2.设置旧结点的next和random属性
ptr = head;
while (ptr != null) {
map.get(ptr).next = map.get(ptr.next);
map.get(ptr).random = map.get(ptr.random);
ptr = ptr.next;
}
// 3.提取新链表,而且发现并不需要像上面的方法那样还原原链表
/* ptr = head;
Node resNode = new Node(0);
Node cur = resNode;
while (ptr != null && cur != null) {
cur.next = map.get(ptr);
cur = cur.next;
ptr = ptr.next;
}
return resNode.next;*/
return map.get(head);
}
发现用了HashMap后代码量少了很多,而且步骤也简单了。
61. 旋转链表
这道题还是不难的,题目也在暗示你要弄出一个环,然后把与k对应的位置结点指向null就成了。
(吐槽:这道中等题想了几分钟就想出来了,前面两道想了好久。。)
做题步骤:
public ListNode rotateRight(ListNode head, int k) {
if (head == null) return null;
ListNode l1 = head;
int count = 1;
while (l1.next != null) {
l1 = l1.next;
count++;
}
l1.next = head;
for (int i = 0; i < count - k % count; i++) {
l1 = l1.next;
}
ListNode headTemp = l1.next;
l1.next = null;
return headTemp;
}
链表专题到这里就结束了,最后一题建造了一个环,然后又把这个环拆散。就像我们的学习,学完了后能把知识连起来,又可以拆散来朝着不同的方向不断延伸。
接下来继续学习队列和栈吧!
最近很喜欢听Medtner,比较他与同学拉赫玛尼诺夫和斯克里亚宾,他是不为人知的那一位作曲家。但我喜欢他作品宏大的构思,精致的结构和内敛的感情。感觉糅合了贝多芬、拉赫玛尼诺夫、勃拉姆斯的听感,让人着迷。分享一下他的第一部作品:f小调钢琴奏鸣曲,op.5,我认为它宏大不输brahms的op.5,反而更加细腻柔情,虽然coda没有勃氏那般激情狂野,但最后再次出现首乐章的主题让我十分感动。我个人听出的是美好而苦涩的追忆似水年华。
http://music.163.com/song?id=546504279&userid=88476473