上一篇文章我们详细讲解了一下单链表的基础知识,这一篇我们就根据上一篇的内容做一些OJ题进行巩固。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/remove-linked-list-elements/description/
根据上一篇的思路,我们通常是引入一个prev,那么就是prev->next=cur->next;free(cur);cur=prev->next;但这里注意不要写成cur=cur->next;因为我们已经free(cur),我们可以再定义cur,但是不能进行访问,这是空指针访问,会产生野指针。
这里还有一个要注意的地方,就是prev->next=cur->next;如果我们第一个元素就要删除,那么prev就是NULL,就会产生空指针访问,会产生野指针。
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* prev = NULL;//定义prev,cur
struct ListNode* cur = head;
while (cur)
{
if (cur->val != val)//没有到达目标元素
{
prev = cur;
cur = cur->next;//prev,cur各往下走一位
}
else
{
if (prev == NULL)//防止野指针
{
head = cur->next;//prev本来就是辅助作用,prev为null用不了,那就只能从自己身上解决了
free(cur);
cur = head;
}
else
{
prev->next = cur->next;
free(cur);
cur = prev->next;
}
}
}
return head;
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
removeElements(n1, 7);
}
快速手搓链表,快速调试分析。
不是val的值,就尾插到新链表。
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* newHead = NULL, * tail = NULL;
struct ListNode* cur = head;
while (cur)
{
if (cur->val != val)
{
if (tail == NULL)
{
newHead = tail = cur;//空tail没有next
}
else
{
tail->next = cur;
tail = tail->next;
}
cur=cur->next;//cur要往下走
}
else
{
struct ListNode* next = cur->next;
free(cur);
cur = next;
}
}
if (tail)
{
tail->next = NULL;//看注释
}
return newHead;
}
if(tail)这条语句,因为链表终止是尾节点->next=NULL,所以要写上tail->next = NULL;同时也是为了防止野指针的产生。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/middle-of-the-linked-list/
这道题用快慢指针的方法解决。快慢指针,顾名思义,就是一个指针走得快,一个指针走得慢,因为链表的一个终止条件就是xxxx->next=NULL,所以我们可以用快慢指针解决。
struct ListNode* middleNode(struct ListNode* head){
struct ListNode* slow,* fast=NULL;
slow=fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
链表中倒数第k个结点_牛客题霸_牛客网输入一个链表,输出该链表中倒数第k个结点。。题目来自【牛客题霸】https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&&tqId=11167&rp=2&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking
这道题也可以用快慢指针,只是用法和上一题不一样,我们快慢指针的核心思想是:通过快指针到达某个条件终止,让慢指针停在目的地。
这样就是fast先走k步,fast和slow拉开k个距离,因为倒数第k个结点到null距离k个,这样slow和fast一起走,当fast到达null时停止,slow也停止,正好到达目的位置。
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
if(pListHead==NULL)//防止引入空链表
{
return NULL;
}
struct ListNode* slow,* fast=NULL;
slow=fast=pListHead;
while(k--)
{
if(fast==NULL)//防止k过大导致fast指向空或者next产生野指针
{
return NULL;
}
fast=fast->next;
}
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/reverse-linked-list/description/
如图,我们取n1n2n3三个点位,n2目的是取出对应位置,n1是为了给n2衔接,n3是提供下一个点位,所以1和NULL就是一块地方,然后把n2指向n1,与原链表分割开,然后目标节点往下走一个。
struct ListNode* reverseList(struct ListNode* head)
{
if(head==NULL)
return NULL;
struct ListNode*n1,*n2,*n3;
n1=NULL;
n2=head;
n3=n2->next;
while(n2)
{
// 翻转
n2->next=n1; //step1
// 迭代
n1=n2; //step2
n2=n3; //step3
if(n3) //避免最后一步n3为NULL的时候n3->next产生野指针
n3=n3->next; //step4
}
return n1;
}
步骤:1.先翻转指向;2.把对应节点转入n1;3.引入下一个目标节点;4.更新下一个节点
把1的下一位提取出来,然后把newnode接在1->next,然后整体给newnode,然后取下一位。
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur=head,*newhead=NULL;
while(cur)
{
struct ListNode* next=cur->next;
cur->next=newhead;
newhead=cur;
cur=next;
}
return newhead;
}
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/merge-two-sorted-lists/description/
依次比较,较小的尾插。
truct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
if(list1==NULL)
return list2;
if(list2==NULL)
return list1;
struct ListNode* cur1=list1, *cur2=list2;
struct ListNode* head=NULL, *tail=NULL;
while(cur1&&cur2)
{
if(cur1->val < cur2->val)
{
if(head==NULL)
head=tail=cur1;
else
{
tail->next=cur1;
tail=tail->next;
}
cur1=cur1->next;
}
else
{
if(head==NULL)
head=tail=cur2;
else
{
tail->next=cur2;
tail=tail->next;
}
cur2=cur2->next;
}
}
if(cur1)
tail->next=cur1;
if(cur2)
tail->next=cur2;
return head;
}
head是记录头结点的位置,以便输出链表,tail是实现尾插。
哨兵位的特点就是我开辟一块空间,然后接上tail,这样的话我们打印出来guard->next,这样的话我们头节点就有了,我们上个方法和该方法思路是一致的,就是记录上头结点,然后往后接,接好后输出头结点,如果没有的话只能输出尾结点。
没有guard的话,我们只能得到链表的尾部,所以要更新tail。
#include
#include
typedef int SLTDataType;
typedef struct ListNode
{
SLTDataType val;
struct ListNode* next;
}ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
if (list1 == NULL)
return list2;
if (list2 == NULL)
return list1;
struct ListNode* cur1 = list1, * cur2 = list2;
struct ListNode* guard = NULL, * tail = NULL;
guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
tail->next = NULL;
while (cur1 && cur2)
{
if (cur1->val < cur2->val)
{
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
else
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
}
if (cur1)
tail->next = cur1;
if (cur2)
tail->next = cur2;
//return guard->next;不这么写是为了防止内存泄漏
tail=guard->next;
free(guard);
return tail;
}
void SLTPrint(ListNode* phead)
{
ListNode* cur = phead;
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
int main()
{
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n6 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->next = n2;
n2->next = n3;
n3->next = NULL;
n4->next = n5;
n5->next = n6;
n6->next = NULL;
n1->val = 1;
n2->val = 2;
n3->val = 4;
n4->val = 1;
n5->val = 3;
n6->val = 4;
ListNode* list = mergeTwoLists(n1, n4);
SLTPrint(list);
}
上面是一个完整的代码,不是OJ体,但是我们要通过此代码彻底理解哨兵位的用法。
首先我们开辟一个guard和一个tail,此时他们指向同一块内存空间。这个时候开始进行比较,但这里就有一个细节,guard->next是什么时候确定的,为什么guard不随tail一起变,他们什么时候内存分割开来。
通过调试窗口,在第一次执行完tail=tail->next的时候,guard和tail的地址就不一样了guard不变,tail换到了next地址上,也就是说,第一次执行后,guard->next就已经被记录了下来,然后tail继续连接数据更新迭代,最后将guard->next交给tail,使其从头部输出,最后free(guard),完成哨兵位解法。所以最关键的就是guard->next(头结点)是什么时候被确定下来的。
//哨兵位,避免了第一个节点为空的情况
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
if(list1==NULL)
return list2;
if(list2==NULL)
return list1;
struct ListNode* cur1=list1, *cur2=list2;
struct ListNode* guard=NULL, *tail=NULL;
guard=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
tail->next=NULL;
while(cur1&&cur2)
{
if(cur1->val < cur2->val)
{
tail->next=cur1;
tail=tail->next;
cur1=cur1->next;
}
else
{
tail->next=cur2;
tail=tail->next;
cur2=cur2->next;
}
}
if(cur1)
tail->next=cur1;
if(cur2)
tail->next=cur2;
tail=guard->next;
free(guard);
return tail;
}
哨兵位解法的优势在于不用考虑头结点为空的情况。因为哨兵位的存在,第一个节点必然存在(guard->next),对比与正常解法,正常解法第一个节点(第一个输入对象)一定是空的,所以就要单独考虑,不能直接->next产生野指针。
链表分割_牛客题霸_牛客网现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的。题目来自【牛客题霸】https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId=8&&tqId=11004&rp=2&ru=/activity/oj&qru=/ta/cracking-the-coding-interview/question-ranking
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
ListNode* lGuard,* gGuard,* lTail,* gTail;
lGuard=lTail=(ListNode*)malloc(sizeof(ListNode));
gGuard=gTail=(ListNode*)malloc(sizeof(ListNode));
lTail->next=NULL;
gTail->next=NULL;
ListNode* cur=pHead;
while(cur)
{
if(cur->valnext=cur;
lTail=lTail->next;
}
else
{
gTail->next=cur;
gTail=gTail->next;
}
cur=cur->next;
}
lTail->next=gGuard->next;//小链表和大链表链接
pHead=lGuard->next;//注意,这里不能用lGuard接收,因为后面会free掉
gTail->next=NULL;//gTail->next必须置空,一方面要避免空指针访问,另一方面下面细讲。
free(gGuard);
free(lGuard);
return pHead;
}
};
这里有一个很隐蔽的小bug,就是这个gTail->next=NULL;注意,这行代码必须写,否则会产生bug
为什么会产生这种错误呢?一开始我想到的是野指针问题,但我很快否定了这一观点。我没有对这个空指针进行访问,为什么会报错呢?而且是内存超限问题。
如图,我们只是将pHead的节点复制到了对应的新链表上,也就是说,4和7的下一个点位都还是原先的点位,所以7->next=3,3->next=4,4->next=5,5->next=7,7->next=3,这样的话就形成了一条循环链,就会产生内存超限。所以要gTail->next=NULL。
链表的回文结构_牛客题霸_牛客网对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为。题目来自【牛客题霸】https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=49&&tqId=29370&rp=1&ru=/activity/oj&qru=/ta/2016test/question-ranking
总的来说,这一题就是快慢指针和反转链表两题的结合,相对简单。
class PalindromeList {
public:
struct ListNode* middleNode(struct ListNode* head){
struct ListNode* slow,* fast=NULL;
slow=fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head, * newhead = NULL;
while (cur)
{
struct ListNode* next = cur->next;
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
bool chkPalindrome(ListNode* head) {
struct ListNode* mid=middleNode(head);//这里mid是slow,不是头节点
struct ListNode* rhead=reverseList(mid);//这里是把slow后面的内容逆置
while(head&&rhead)
{
if(head->val!= rhead->val)
{
return false;
}
else
{
head=head->next;
rhead=rhead->next;
}
}
return true;
}
};
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/intersection-of-two-linked-lists/description/
这里先理清一个点,就是交叉链表到底是怎么交叉。注意,一个节点只能有一个next,而多个节点的next可以指向同一个节点。故如下结构就不可能出现:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode* tailA=headA,*tailB=headB;
int lenA=1,lenB=1;
//记录出tailA的长度
while(tailA->next)
{
tailA=tailA->next;
lenA++;
}
//记录出tailB的长度
while(tailB->next)
{
tailB=tailB->next;
lenB++;
}
//如果尾都不交叉,那么就不可能交叉
if(tailA!=tailB)
return NULL;
int gap=abs(lenA-lenB);//两个链表的长度差
struct ListNode* longlist=headA,*shortlist=headB;
if(lenAnext;
}
while(longlist!=shortlist)//寻找交叉节点
{
longlist=longlist->next;
shortlist=shortlist->next;
}
return longlist;
}
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/linked-list-cycle/description/
bool hasCycle(struct ListNode *head) {
struct ListNode* slow,* fast;
slow=fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
return true;
}
return false;//如果没有环,那么就会出现fast->next=NULL或者fast=NULL,走出循环
}
证明:假设slow进环的时候fast和slow之间的距离为N,slow进环后,fast开始追击slow,slow走一步,fast走两步,他们之间距离缩小1。
fast和slow的距离: N,N-1,N-2......2,1,0 必然能追击上。
(1).N为偶数:距离N,N-2,N-4......4,2,0
(2).N为奇数:距离N,N-2,N-4......3,1,-1(相当于进入一次新的循环,永远追不上)
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/linked-list-cycle-ii/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* slow,*fast;
fast=slow=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow == fast)
{
struct ListNode* meet=slow;
struct ListNode* start=head;
while(meet!=start)//核心代码,下面讲解
{
meet=meet->next;
start=start->next;
}
return meet;
}
}
return NULL;
}
如图,我们对slow和fast走的距离进行分析。
slow:L+X, fast:L+n*C+X
因为fast一次两步,slow一次一步,所以fast=2*slow,得到L=n*C-X,故如果一个指针从相遇点走,一个指针从起始点走,则其二者必然相遇,在入口点相遇。
上述十道题都是很经典的链表OJ题,读者不妨一试,以加深对链表的了解。