结束了数组,我们开始学习链表。今天一共有两道题目,一道是LeetCode.203移除链表元素,另一道是LeetCode.206反转链表.这两道题都不是很难,但是我认为是很有用的两道题目,通过这两道题目,可以进一步掌握双指针思路,并且对递归的写法加深理解。
LeetCode.203移除链表元素
这道题首先要读懂题目所说头结点到底是什么,是头结点还是首元结点,通过对示例的分析,我们可以知道,这里所说的头结点是首元结点的意思。
链表的移除操作,一般都是将待移除的结点的前驱结点的指针域指向待移除的结点的后继结点。但是首元结点有一处细节需要注意,如果首元结点就是需要移除的元素,首元结点没有前驱结点,所以我们可以直接将头结点向后移动,直到移动到不是待删除的结点停止,开始常规移除操作。
public static ListNode removeElements(ListNode head,int val){
while(head!=null&&head.val==val){
head=head.next;
}
ListNode p=head;
while(p!=null&&p.next!=null){
if (p.next.val==val){
p.next=p.next.next;
}else {
p=p.next;
}
}
return head;
}
这里有几处需要注意:1. 当首元结点不为空并且就是待移除的结点时,删除操作就是将头结点向后移动。2. 在处理完头结点后,准备向后遍历链表时,我们需要一个临时的指针来操作,因为最后需要返回头结点,如果操作头结点,到最后无法返回头结点的正确位置。
这道题更为简单,也更接近数据结构课程学习的头结点的方法是使用虚拟头结点,既然这里的所谓的头结点其实是首元结点,那我们就自己设置一个虚拟的头结点,来统一实际的首元结点和后面的所有结点的操作。
public static ListNode ipRemoveElements(ListNode head,int val){
ListNode dummyhead=new ListNode();
dummyhead.next=head;
ListNode p=dummyhead;
while(p.next!=null){
if (p.next.val==val){
p.next=p.next.next;
}else {
p=p.next;
}
}
return dummyhead.next;
}
LeetCode.206反转链表
这道题本身并不难,我第一遍使用了集合容器来存储链表中的元素,然后颠倒顺序再放回链表中,非常简单。
暴力解法
public static ListNode reverseList(ListNode head){
ListNode dummyhead=new ListNode();
dummyhead.next=head;
ListNode p=dummyhead;
List<Integer> list=new ArrayList<>();
while(p.next!=null){
p=p.next;
list.add(p.val);
}
ListNode q=dummyhead;
for (int i = list.size()-1; i>=0; i--) {
q=q.next;
q.val= list.get(i);
}
return dummyhead.next;
}
需要注意的是,本道题的头结点其实也是首元结点,所以为了统一操作,仍然使用了虚拟头结点。
双指针法
在我第一遍做这道题的时候,我有大概的双指针思路,但是因为觉得如果将结点的指针域反转,指向后继结点改成指向前驱结点,会导致链表断开,无法找到后续结点,也没有仔细思考这个问题,就转向使用集合存储来解决这道题。但其实这个问题很简单,使用一个临时变量来存储其后继结点,就不用担心找不到后面的链表结点了。
public static ListNode ipReverseList(ListNode head){
ListNode pre=null;
while(head!=null){
ListNode temp=head.next;
head.next=pre;
pre=head;
head=temp;
}
return pre;
}
双指针的具体思路是:首先设置前驱结点pre=null,然后将pre和head依次向后遍历,每次遍历时,使head指向pre,定义临时变量temp来存储head的后继结点防止断链。注意这里最后返回的是pre不是head。
递归
双指针法中使用了循环,可以改写称递归函数。这里着重学习循环改写递归的方法。
public static ListNode reverse(ListNode head,ListNode pre){
if (head==null){
return pre;
}
ListNode temp=head.next;
head.next=pre;
pre=head;
head=temp;
reverse(head,pre);
return reverse(head,pre);
}
public static ListNode recursionReverseList(ListNode head){
return reverse(head,null);
}
在recursionReverseList函数中,我们调用递归函数并返回递归函数的返回值。
递归函数的两个参数,一个是head,一个是pre。而起始条件是head=head,pre=null,所以在函数中,第一次进入递归函数的参数为(head,null)。
双指针法中,循环的条件是head!=null ,那么转换为结束条件,就是head==null ,返回pre。接着在递归函数中写出双指针法的循环内的操作,就可以完成递归函数。
总结一下,循环改递归。1. 循环最开始的初始条件,作为第一次调用递归函数的参数值。2. 循环的继续条件的反条件,作为递归的结束条件。3. 将循环中的操作写道递归函数中。