文章目录
前言
单链表的特点就是只能由上一个节点找到下一个节点,这样会使得我们在解决单链表问题时需要格外细心。
这里有一些在解决单链表问题时需要注意的情况和一些解决方法。
1注意是否引用了空指针
我们在解决单向链表的问题的时候,由于只能通过上一个节点找到下一个节点,所以我们通常会设置多个指针来保存节点的地址,但是在使用这些指针的时候我们需要注意不能引用了空指针。
下面两个题目可以帮助我们认识到什么时候任意出现空指针,以及如何避免。
1.1删除链表中指定的节点
**题目链接:删除链表中指定的元素
思路:
这道题中,我们需要移除链表中特定的元素。我们可以利用两个指针:一个指针cur
指向当前节点,一个指针pre
指向前一个节点。
注意:怎样控制循环?
当我们只需要删除链表中的一个节点的时候,我们只需要判断当前节点是否满足条件,满足条件就执行删除,删除后就结束循环。
但是这道题不是只删除一个节点,也存在一个链表中的多个节点需要删除的情况:所以只有当检查完所有的节点后才能结束循环。
struct ListNode *cur = head;
while(cur)//遍历链表,遍历完后会退出循环。
{
//执行操作
cur = cur->next;
}
注意:这里我们使用pre指针指向前一个节点的时候,不能引用空指针。
当我们使用两个链表是遍历指针的时候,我们需要重点在意:不能在前一个指针为空指针的时候去访问它。
即当我们访问到第一个节点的时候,由于该节点此时没有前一个节点,所以pre指针还是一个空指针。
如果我们要删除第一个节点的话,我们就只能改变头指针的值,不能改变pre指针的值。
解题:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* pre = NULL;
struct ListNode* cur = head;
while (cur)//当cur指针为NULL,就说明已经遍历完链表。
{
if (cur->val == val)
{
struct SListNode* next = cur->next;
if(pre == NULL)//pre为空指针的情况,说明这是第一个节点
{
head = next;
}
else
{
pre->next = next;
}
free(cur);
cur = next;
}
else
{
pre = cur;
cur = cur->next;//每一次cur都会指向后面一个节点
}
}
return head;
}
1.2反转一个链表
这道题的链接:反转链表
这道题需要我们反转一个链表,并且返回这个新链表第一个节点的地址:
这道题有两个解法,但是实质都是相同的:改变各个节点中next
指针的值。
方法一
让每一个节点中的指针指向前一个节点
我们可以利用三个指针,一个指针(命名为
cur
)用来遍历整个链表,一个指针(命名为prev
)来指针前一个节点,一个指针
next
来指向该节点的后一个节点。
每一次迭代到一个节点,就让这个节点的的
next
指针指向前一个节点的地址,然后让prev
指针指向当前节点,当前节点指向后一个节点,依次迭代,直到cur
指针的值为空指针就结束循环。
解题
struct ListNode* reverseList(struct ListNode* head){
if(head == NULL)
{
return NULL;
}
struct ListNode *prev = NULL;
struct ListNode *cur = head;
struct ListNode *next = cur->next;
while(cur)
{
cur->next = prev;
prev = cur;
cur = next;
if(next)//如果next指针为空指针,那么久说明cur已经是最后一个节点了
next = next->next;
}
return prev;
}
注意:
这个方法需要我们保存下一个节点的地址,每一次循环我们都会让next
指针指向下一个节点*,但是当next
已经是空指针的时候,访问next->next
就会造成引用空指针,所以我们需要在next
指针为空指针
的时候就结束循环。
我们也可以利用方法二来避免引用空指针的情况
方法二
依次插入到一个新的链表中
创建一个新的链表头,然后遍历原链表,每一次把头节头插到新的链表中,然后返回新链表的头节点,
这个方法仍然需要两个指针,一个指针用来指向当前迭代的节点,一个指针用来指向当前节点的后一个节点。
struct ListNode* reverseList(struct ListNode* head){
struct ListNode *newhead = NULL;
if(head == NULL)
{
return NULL;
}
struct ListNode*cur = head;
struct ListNode*next = NULL;
while(cur)
{
next = cur->next;
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
2.利用带哨兵位的链表(带头链表)
上面我们注意到,每一次如果我们要向一个位置的链表的尾部插入一个节点的时候,都需要判断这个链表是否是空链表。
向空链表尾部插入节点和向非空链表尾部插入节点的区别:
那么有没有一个办法可以让我们避免掉这个情况,让我们不用区分两种情况呢?
这个方法就是: 我们创建一个头节点(我们称为哨兵位),然后再向后插入新的节点,最后返回哨兵位后面一个节点的地址,这个地址就是我们需要得到的链表的头指针。
这里利用两个例题来使用带哨兵位的链表:
2.1.合并两个有序链表
该题的链接是:合并两个有序链表
思路:
这道理我们得到了两个有序的链表,我们需要把他们按大小顺序合并到一个链表中
我们可以先创建一个头节点和两个指针,这两个指针指向这个节点,然后再创建两个指针,分别指向两个链表的头节点。
比较list1和list2 指向的节点中的值得大小,我们将较小的节点插入到newhead
节点的后面,然后让对应的cur指针指向连标志哦你的下一个节点,同时tail指针指向新插入的节点。
注意:
什么时候循环停止。
当其中一个链表已经遍历完,循环就结束了。我们返回的是
newhead
指向的下一个节点的地址
newhead
是我们创建的哨兵位
该题解法:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
//创建一个新的链表节点和一个指向这个节点的指针
struct ListNode*newhead = (struct ListNode *)malloc(sizeof(struct ListNode));
newhead->next = NULL;
//创建一个尾指针指向新的头节点
struct ListNode*tail = newhead;
while(list1 && list2)
{
if(list1->val <list2->val)
{
tail->next = list1;
list1 = list1->next;
tail = tail->next;
}
else
{
tail->next = list2;
list2 = list2->next;
tail = tail->next;
}
}
if(list1)//如果其中一个链表已经遍历完,
// 那么剩下的链表的全部节点可以直接插入到尾节点后面。
{
tail ->next = list1;
}
if(list2)
{
tail ->next = list2;
}
return newhead ->next;
}
2.2 将一个链表按某个条件分割成多个部分
该题的链接是:链表分割
思路:
我们可以根据他提供x= 5
来将链表分为两类,然后将这个链表分割成为两个链表,一部分小于x,一部分大于x。
然后将两个链表收尾链接起来,就得到了答案:
这道题我们也可以利用通过添加哨兵位头节点来简化解题步骤:
先创建两个头节点,一个头节点用来作为小于x的节点集合的头节点,一个头节点用来作为大于等于X的节点集合的头节点。
下面是这道题的解法:
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
// write code here
//创建两个带头的链表
struct ListNode *greater = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode *less = (struct ListNode*)malloc(sizeof(struct ListNode));
greater->next=less->next = NULL;
struct ListNode *tail_less = less;
struct ListNode *tail_greater = greater;//创建了两个指针来保存两个新节点的地址。
struct ListNode *cur = pHead;//一个指针来遍历原链表
while(cur)
{
if(cur->val <x)
{
tail_less->next = cur;
tail_less = tail_less->next;
}
else
{
tail_greater->next = cur;
tail_greater = tail_greater->next;
}
cur = cur->next;
}
tail_greater->next = NULL;//保证链表最后一个节点的next指针为空指针。
tail_less->next = greater->next;
struct ListNode *list = less->next;
return list;
}
};
注意:
当我们得到更新后的整个链表时,我们需要把最后一个节点的
next
指针赋为NULL
。
原因:我们不能保证新链表的最后一个节点时原链表中的最后一个节点。
3.找到链表中特殊位置的节点
有些情况下我们需要去寻找链表中比较特殊的位置的一个点:
比如:
在一个普通单向链表中,找到这个链表中间的那个节点,或者是找到倒数第K个节点(k小于链表的长度);
在相交链表中找到第一个相交节点。
什么是相交链表:
两个链表中的某两个节点指向的是同一个节点的地址。
解决之类问题的方法:
我们可以利用快慢指针的方法-----创建两个指针,通过改变两个指针之间的距离来找到特殊位置的节点。
下面我们利用快慢指针来解决一些经典的问题:
3.1 找到链表的中间节点
该题的链接是:找到链表的中间节点
思路:这道题要求我们找到中间节点,我们可以设置快慢指针,快指针每次走两步,慢指针每次走一步,当快指针遍历完整个链表的时候,慢指针指向的刚好就是我们要找的中间位置的节点:
注意:
怎样控制循环条件?
由于每次
fast
都会向后移动两个节点,但是我们无法保证fast
是最后一个节点(如果fast
是最后一个节点,那么fast->next
就是一个空指针,fast->next->next
就是在访问一个空指针,会发生错误),所以我们判断退出循环的条件应该是while(fast && fast->next)
这样可以保证遍历链表完后会退出循环。
该题的解法:
struct ListNode* middleNode(struct ListNode* head){
//利用快慢指针
struct ListNode*fast = head;
struct ListNode*slow = head;
while(fast&&fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;//此时慢指针指向的节点就是中间位置的节点。
}
补充:用这个方法可以找到一些其他位置:
例如三分之一位置的节点(我们设置每次快指针走的距离是慢指针走的距离的三倍,当快指针遍历完链表的时候,慢指针就指向了第三分之一位置的节点,但是我们设置循环条件的时候就应该设置为:
while(fast && fast->next && fast->next->next)//控制循环的条件
{
//循环的内容
fast = fast->next->next->next;
slow = slow->next;
}
//退出循环后,slow指针指向的节点就是三分之一位置的节点。
3.2找到链表中倒数第K个节点
该题的链接是:找到倒数第K个节点
思路:
这道题我们仍然使用快慢指针的方式:
现在我们确定了我们需要返回的节点和最后一个节点的相对距离为K,我们只需要让两个指针之间的距离也为K,即让快指针先走K步,然后两个指针一起以相同的速度向后移动,当快指针成为空指针的时候,慢指针指向的就是倒数第K个节点
注意:
在两个指针一起向后移动的循环中,循环一定要等到fast指向NULL的时候才能停止。
考虑K不合理的情况(K大于节点的个数)—这种情况下返回NULL 如果需要在之遍历一次链表的情况下判断出K是否合理,可以用这样的方法:
while(k--)
{
if(fast->next)
fast = fast->next;
else
return NULL;
}
//如果K的值大于节点的个数,
//那么在多次迭代后fast->next一定会等于NULL,此时返回NULL即可。
完整的解法:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
//用快慢指针:一个指针先走k步,然后两个指针一起向后迭代,
//当快指针等于空指针的时候,慢指针指向的就是倒数第k个节点。
//但是要注意不能让k 超过链表的长度
struct ListNode*fast = pListHead;
struct ListNode*slow = pListHead;
while(k--)
{
if(fast != NULL)
fast = fast->next;
else
return NULL;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
3.3 相交链表
该题的链接是:找到相交链表的交点
思路:
首先要判断两个链表是否相交:如果两个链表的最后一个节点的地址相等,那么这两个节点就相交
反之,这两个链表不相交,所以我们需要创建两个尾指针,用来遍历这两个链表,最后比较这两个尾指针的地址
struct ListNode* tail_headA = headA;
struct ListNode* tail_headB = headB;
//找到链表A的最后一个节点
while(tail_headA)
{
tail_headA = tail_headA->next;
}
//找到链表B的最后一个节点
while(tail_headB)
{
tail_headB = tail_headB->next;
}
//比较地址,如果地址不相等,就返回空指针。
if(tail_headA != tail_headB)
{
return NULL;
}
如果地址相等,说明这两个链表相交,我们还需要求出第一个交点:
我们找出两个链表的长度,由于两个链表从第一个交点开始,后面的内容是相等的,所以我们可以通过求出两个链表的长度差K,然后设置快慢指针,快指针指向长链表的头节点,慢指针指向短链表的头节点,先让快指针向后移动K个节点,
然后两个指针一起向后移动,当两个指针相等的时候,这两个指针所指向的就是第一个节点的地址。
这道题的解法:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
//遍历链表判断是否相交
struct ListNode* tail_headA = headA;
struct ListNode* tail_headB = headB;
int lenA = 1;
int lenB = 1;
while(tail_headA)//遍历A链表的时候计算出该链表的长度,同时找到该链表的为节点
{
++lenA;
tail_headA = tail_headA->next;
}
while(tail_headB)
{
++lenB;
tail_headB = tail_headB->next;
}
if(tail_headA != tail_headB)//判断两个链表是否相交
{
return NULL;
}
int sub = abs(lenA-lenB);//计算链表长度的差值
struct ListNode*longNode = headA;
struct ListNode*shortNode = headB;
if(lenA < lenB)
{
shortNode = headA;
longNode = headB;
}//找到较长的链表
while(sub--)
{
longNode = longNode->next;
}
while(longNode != shortNode)//两个指针一起向后移动,当循环退出的时候,指针所指向的地址就是第一个交点的地址。
{
longNode = longNode->next;
shortNode = shortNode->next;
}
return longNode;
}