由于单链表本身存在一定的缺陷,很多OJ题都在考察单链表,所以我这篇文章为大家分享一下力扣(LeetCode)力扣官网上面的单链表的OJ题,包括我自己的解题思路和我认为容易想错的点(我曾经错过的点).一共是九个个题目,这篇文章先分享四个题目,分别是1.移除链表元素(对应力扣第203题)力扣203题 ----- 2.反转链表力扣206题-----3.链表的中间结点力扣876题-----4.链表倒数第n个结点力扣19题,这个地方我们都先用C语言来实现
要删除链表中所有满足node->val等于val的结点,我们就需要遍历链表.当我们找到val对应的结点时(我称为cur),我们要做的就是要找到这个结点的前一个结点prev,然后将prev->next指向cur->next也就是cur结点的后面一个结点.但是我们说单链表有一个缺陷就是它是单向的,只能找到它的下一个结点,所以这个地方我们应该定义两个变量,一个变量用于判断结点对应的值是不是val,宁一个变量用于保存上一个结点.这样我们的大致思路就出来了,然后我们还面临一个问题是一个链表中可能出现多个结点的值都为val,所以当我们找到一个结点后不能直接把cur指向空指针,应该让cur指向下一个位置接着遍历链表直到找到链表的最后一个结点.我们分析到这儿就可以开始着手尝试写一下代码了
这个地方报错中出现了null pointer 是空指针的意思,我们仔细思考不难发现,当我们第一个结点对应的val等于val的时候,这个时候prev是空指针,但是我们删除当前结点写了prev->next,这代表我们对空指针解引用了,所以会报错.所以这个题有一个特殊情况需要特殊讨论,当第一个结点对应的值为val的时候,我们直接进行头删,并且重新赋值一个开头的点.这样就可以通过了!
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* prev = NULL;//m为n指向n的前一个结点
while (cur)//遍历链表也可以写为cur!=NULL
{
if (cur->val == val)//删除此结点
{
if (cur == head)//头删
{
cur = head->next;
free(head);
head = cur;
}
else
{
prev->next = cur->next;
free(cur);
cur = prev->next;
}
}
else//找到val值对应的地址(遍历链表)
{
prev = cur;
cur = cur->next;
}
}
return head;
}
我们可以发现,链表最开始第一个结点存的第二个结点的地址,第二个结点存的第三个结点的地址以此类推,但是我们反转链表之后
可以发现,以前的第二个结点存放以前第一个结点的地址,以前的第三个结点存放以前第二个结点的地址,相当于把链表存放地址的箭头给倒过来
我们可以想到的写法是定义一个cur来遍历链表,再定义一个prev指向前一个结点用来把箭头倒转,我们很容易这样写出这样的代码
但是这样写真的对吗?我们一边画图一边分析可以发现这是一个死循环,因为当我们head指向上一个结点的地址时,当我们再次往后遍历时会找不到下一个结点了.所以我们在定义变量的时候应该考虑定义三个变量,分别是当前这个结点(cur),前一个结点(prev)和后一个结点(nextone),这样定义就不会有问题了现在我们来实现一下
根据上面的画图分析,我们可以写出这样的代码
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head;//前一个结点
struct ListNode* prev = NULL;//当前结点
struct ListNode* nextone = head->next;//下一个结点
while (cur != NULL)
{
cur->next = prev;//把箭头反转指向前一个
prev = cur;
nextone = nextone->next;
cur = nextone;
}
head = prev;
return head;
}
但是这个地方我们上交代码会发现问题,当当循环走到倒数第二个结点时,这时cur是最后一个结点,但是nextone是NULL,然后对nextone解引用nextone=nextone->next相当于对空指针解引用,会报错.所以这个地方我们想优化一下代码.如下
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head;//定义三个结点,前中后
struct ListNode* prev = NULL;//加入不加限制条件下这个地方如果定义struct ListNode* next=head->next,然后循环里面写next=next_>next的话,
while (cur != NULL) //当循环走到倒数第二个结点时,这时cur是最后一个结点,但是next是NULL,然后对next解引用next=next->next相当于
{ //对空指针解引用,会报错
struct ListNode* nextone = cur->next;//在循环里面定义后直接赋值就不会出现对nextone解引用了
cur->next = prev;
prev = cur;
cur = nextone;
}
head = prev;//把头重新给最后一个结点prev
return head;
不难发现,当结点个数是奇数个的时候返回中间的结点,当结点为偶数个的时候返回右边一半的第一个结点,我们要做的就是把要返回的结点前面的结点free掉,然后再把要返回的结点设置为新的head.而且我们可以观察到奇数个结点需要删除的结点一共有总结点数除以2个(不是数学的除法,是c语言中两个整型的除法),偶数个结点需要删除的结点恰好也是总结点数除以2个.所以我们可以先定义一个count来记录整个链表的结点个数,然后再定义一个变量来接受count除以2的值(也就是我们要删除的结点).然后我们再创建一个循环来进行头删.
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* cur=head;
struct ListNode* m=head;
int count=0;
while(cur)//找链表结点总数
{
cur=cur->next;
count++;
}
int n=(count/2);//n为几个就需要头删几个
while(n-->0)//头删
{
m=head->next;
free(head);
head=m;
}
return head;
}
我们知道单链表不能直接找到除了头以外的其他结点,我们需要遍历链表才能找到需要找到的结点.这个题涉及到删除结点,根据我们之前三个题的经验,我们知道可能会定义一个prev变量指向当前结点的上一个结点.要找到倒数第n个位置我第一个想到的方法就是先遍历链表看看一共有多少个结点(共有count个),然后倒数第n个结点的位置可以由链表总结点数减去n减去1次(也就是count-n-1)循环找到我们可以来实现一下代码看看分析的对不对.
根据上面的分析我们很容易可以写出以下代码:
struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
int count=1,i=0;
struct ListNode* phead=head;//这个变量用于求出链表总结点个数
struct ListNode* m=head;
struct ListNode* prev=NULL;
while(phead!=NULL)//区别phead!=NULL和phead->next!=NULL有什么区别
{
phead=phead->next;
count++;
}
while(i++<count-n-1)//找到倒数第n个结点
{
prev=m;
m=m->next;
}
//中间删除
prev->next=m->next;
free(m);
m=NULL;
return head;
}
我们提交上面的代码后会报错,但是我们不用慌张,先看看报错的类型
可以看见报错的是一个空指针的问题,并且我们在报错下面可以看见当用例为链表只有一个元素时,这时代码会过不去.因为这时prev为空指针,而我们删除部分写的prev->next是对空指针解引用了,和之前的题目相似,会报错.所以这个地方我们还有考虑一种特殊情况就是当倒数第n个结点为头时.那我们来优化一下代码
struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
int count=1,i=0;
struct ListNode* phead=head;//这个变量用于求出链表总结点个数
struct ListNode* m=head;
struct ListNode* prev=NULL;
while(phead!=NULL)//区别phead!=NULL和phead->next!=NULL有什么区别
{
phead=phead->next;
count++;
}
while(i++<count-n-1)//找到倒数第n个结点
{
prev=m;
m=m->next;
}
if(m==head)//头删
{
head=m->next;
free(m);
m=NULL;
}
else//中间删除
{
prev->next=m->next;
free(m);
m=NULL;
}
return head;
}
做题的总结:做完这四道单链表的题我们不难发现,这一部分的题目如果直接上手写代码很容易出错,所以我建议大家在做这一部分题时先画图思考,画图真的很重要!,有了大致的思路后我们再去尝试实现代码,往往第一次提交都会有错,这时不要慌张,要有一个作为程序员的基本素养,先看看力扣上报的错的用例,然后是是第几行有问题,大概是个什么问题(如上面有两个题都为空指针问题).如果你还是不理解可以去编译器下调试一下,这里有一些调试技巧,就是人为的在main函数里面定义单链表,定义的内容和题目的尝试内容相同,然后力扣写代码的地方会有系统自带的注释,把它复制到编译器的时候记得把它取消注释.刚开始做链表的题时想不到思路很正常,多练习一些题目就有经验了.题目的总结:当我们遇见题目需要删除某个结点时,脑子里要想到一些特殊情况,比如头删.当我们定义两个变量不能解决问题时,那就定义三个甚至四个,不要觉得多定义一个变量会占用很多空间,只要定义常数个变量空间复杂度都为O(1)!