!!本篇所选题目及解题思路均来自代码随想录 (programmercarl.com)
一 203.移除链表元素
题目要求:给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回新的头节点。203. 移除链表元素 - 力扣(LeetCode)
我们的目标是要寻找val等于目标值的节点,那么我们就要遍历这个链表,找到该节点,之后让该节点的上一个节点指向它的下一个节点,即可实现“删除”。但是我们知道,单向链表只能指向它的下一个节点,那么我们该如何找到它的上一个节点呢?这里我能可以定义一个指针cur,让cur指向该元素的前一个节点,那么cur.next = cur.next.next 即实现了该节点的上一个节点指向它的下一个节点。因此我们可以得出cur.next = head;也就是说cur是头结点的前一个节点。解决了“删除”的问题,接下来思考如何实现寻找这个节点。也就是说,满足val等于目标值这个条件,翻译过来就是cur.next.val == targrt;(别忘了cur是当前节点的前一个节点,cur.next才是该节点)。也就是满足条件时执行cur.next = cur.next.next 操作,不满足条件时cur就移向下一个节点,继续遍历。最后一个问题,如何判断遍历何时结束?不难判断出,当cur指向链表的最后一个节点时,循环就该结束了(此时这个节点的val已经在cur指向它前一个节点时就对比过了),而cur.next为null也无法得到cur.next.val了。也就是说,我们的循环判断条件就是cur.next != null。
整体思路结束,接下来来考虑一些特殊点,比如各种临界值。首先是当target==head.val的情况,满足cur.next.val == targrt条件;之后是target等于最后一个节点的值的情况,我们已经谈论过了;第三是链表长度为0的情况,此时head为null,即满足cur.next为null,退出循环;最后是有连续节点满足val等于target的情况,这时我们就会发现,如果有连续值满足条件,那么第二个节点就会被跳过对比,如何解决这个问题呢?我们可以在进行完cur.next = cur.next.next操作后,先不急着将cur向后移动(cur = cur.next),而是让它在原位置不动,这样下一轮比较cur.next.val时,cur.next就是第二个节点啦。
最后瞅一眼返回值,是返回头节点吗?如果直接返回head,那么如果head.val刚好等于target,已经被我们删除了,该怎么办?这时候就要请出我们的老朋友——虚拟头节点了(不熟也没关系,多刷几道链表题目之后就会发现,基本上有链表的地方就有它)。在最开始,设置一个虚拟头节点指向head,即p.next = head;无论头结点被删掉多少个,p.next都会指向它的下一项元素,也就是p始终都是“首个节点”,最后直接返回p.next即可。
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode p = new ListNode();
p.next = head;
ListNode cur = p;
while(cur.next != null) {
if(cur.next.val == val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return p.next;
}
}
代码随想录上提供了另一种不适用虚拟头节点的思路,这里直接附上代码,不再具体讲解。其实思路与上面几乎一致,只是多了一个对头节点是否需要删除的判断,就像上面说过的,如果head.val刚好等于target,已经被我们删除了,该如何找到链表的头部?
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head != null && head.val ==val) {
head = head.next;
}
ListNode cru = head;
while(cru != null && cru.next != null){
if(cru.next.val == val) {
cru.next = cru.next.next;
} else{
cru = cru.next;
}
}
return head;
}
}
关于此方法的具体讲解,以及该题目的视频讲解,其他语言版本代码,大家可以到代码随想录中自行查看。代码随想录 (programmercarl.com)https://www.programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
二 206.反转链表
题目描述:给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。206. 反转链表 - 力扣(LeetCode)
这道题乍一看很难(反正我是毫无思路),看到有一种暴力解法是将链表中的元素存储在数组中,反转数组,再放回到链表里,感觉可行但是我没具体尝试(也太暴力了一点......).听完卡尔哥的讲解之后我的感受只有两个字:秒啊。
回归正题,翻转链表我们很自然就能想到让下一个元素指向上一个元素,问题在于,在单向链表中,如何获得上一个元素的位置呢?这里又要用到虚拟头节点了。我们定义一个指针pre指向虚拟头节点,也就是head的前一个元素,再定义一个指针cur指向head,让cur.next指向pre,即可完成下一个元素指向上一个元素,之后我们只要向后移动pre和cur,循环上面的操作即可。是不是很完美?错!这种做法存在一个很明显的错误,当cur.next指向pre之后,head后面的节点就失去了指向它的头节点,如图,那么cur如何如我们所愿向后移动呢?
这时候,我们就需要清楚我们的第三个指针temp出场了。我们在执行cur.next = pre之前,先令p指向cur.next,如此一来,就可保留住cur下一个节点的位置,我们就能随意改变cur.next的指向了。当我们完成cur.next = pre的操作后,只要让cur = temp,就可以让cur指向它原先的下一个节点了。
大体思路完成,接下来思考实现细节。
1.什么时候终止循环?
当循环进行到这一步时,整个反转操作就完成了,再往下走(cur = null)也就没有了意义,所以可以得到,临界条件为cur!=null;
2.返回值是什么?
搞不明白就画图,在最后一步操作执行完时,指针情况是这样的,此时cur!=null条件不满足,结束循环。原先的最后一个节点就是现在的头节点,也就是pre指针所指向的节点,因此我们返回pre。
3.链表为空或只有一个节点时,是否需要特殊讨论?
链表为空时,head也为空,此时自然满足cur=null的条件,不进入循环,返回pre,为空。符合。
链表只有头节点时,cur != null,完成第一轮交换,之后pre指向了head,cur执行null,退出循环,返回pre(head),满足。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = new ListNode();
ListNode pre = new ListNode();
cur = head;
pre= null;
while(cur != null ) {
ListNode temp = new ListNode();
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
关于该题目的视频讲解,其他语言版本代码,大家可以到代码随想录中自行查看。
代码随想录 (programmercarl.com)https://www.programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html
三 707.设计链表
题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:
val
和next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果是双向链表,则还需要属性
prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。实现
MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。707. 设计链表 - 力扣(LeetCode)
这道题非常非常非常麻烦,但是其实难度不是很高,建议大家可以做完其他链表题目后再翻回来做这道题,可能更容易一些。这里直接给出代码及相应注释,不再具体讲解,关于该题目的视频讲解以及其他语言版本代码,大家可以到代码随想录中自行查看。代码随想录 (programmercarl.com)https://www.programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
// 设计链表结构
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
// 初始化
class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
//包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
if (index == 0) {
head = head.next;
return;
}
ListNode pred = head;
for (int i = 0; i < index ; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}