经典链表OJ(接口型)

一些链表OJ思路

快慢指针法

1.链表的中间节点

在这里插入图片描述
在这里插入图片描述
通过上图分析,当链表为奇数个时slow指针指向的位置恰好是链表中间位置。为偶数个时当fast为空时,slow指针指向的位置恰好是第二个中间节点。

注意while循环成立的条件是fast和fast->next指针不为空

2.链表中倒数第k个节点

在这里插入图片描述
在这里插入图片描述
这串代码k–是fast先走k步的情况,设链表数为n,那么倒数第k个节点其实是正数第n-k+1个节点,fast先走k步后用while循环判断当fast等于空时停止链表的移动并返回slow,fast移动到空时就相当于slow移动了n-k+1中的+1次。
同理fast先走k-1次,倒数第k个节点相当于正数第n-(k-1)个节点,用while循环当fast->next不等于空时链表依次向后移动,最后返回slow。

反转链表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
法一:设置三个指针,先将n1置空,由于n2的指针指向发生改变,所以需要n3记录原链表n2的下一个节点,然后依次向后挪到一位,直到n2为空时,n1已经是反转后链表中最后一个节点,返回n1。

法二:取节点头插到新链表
在这里插入图片描述
设置三个指针,因为头插后cur的指针指向改变,所以需用next提前记录原链表中cur的下一个节点,每次头插完后更新newhead和cur位置,当cur不为空时,用next记录cur下一个节点,循环结束后返回newhead,比法一好在不需要单独判断头指针为空的情况。

合并两个有序链表

在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    //判断头指针为空的情况
    if(list1==NULL)
    {
        return list2;
    }
    if(list2==NULL)
    {
        return list1;
    }
    struct ListNode*n1=list1,*n2=list2,*head=NULL,*tail=NULL;
    while(n1&&n2)
    {
        if(n1->val<n2->val)
        {
            if(head==NULL)
            {
                head=tail=n1;
            }
            else
            {
                tail->next=n1;
                tail=tail->next;
            }
            n1=n1->next;//不管是否为头节点,n1都需要后移一位
        }
        else
        {
            if(head==NULL)
            {
                head=tail=n2;
            }
            else
            {
                tail->next=n2;
                tail=tail->next;
            }
            n2=n2->next;
        }
    }
       //判断有指针还未完全链接的情况
       if(n1)
       {
        tail->next=n1;//不需要在移动tail,节点已链接完
       }
       if(n2)
       {
        tail->next=n2;
       }
       return head;
    
}

法一

思路:通过n1、n2两指针代替参数指针依次遍历比较,取小的尾插。需注意判断参数指针为空的情况,以及最后剩余的一个指针的链接问题。

法二(哨兵节点)

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode*n1=list1,*n2=list2,*guard=NULL,*tail=NULL;
    guard=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
    tail->next=NULL;
    while(n1&&n2)
    {
        if(n1->val<n2->val)
        {
                tail->next=n1;
                tail=tail->next;
                n1=n1->next;
        }
        else
        {
                tail->next=n2;
                tail=tail->next;
                n2=n2->next;
        }
    }
       //判断有指针还未完全链接的情况
       if(n1)
       {
        tail->next=n1;//不需要在移动tail,节点已链接完
       }
       if(n2)
       {
        tail->next=n2;
       }

       struct ListNode*head=guard->next;
       free(guard);
       return head;
    
}

用malloc为guard和tail指针开辟结构体大小的空间,二者指向同一块空间,都将他们的next节点置空,防止参数指针为空时guard->next造成非法访问,guard->next作为新头节点返回,tail通过遍历完成合并操作。

优势在于尾插时不需要给头指针赋值,直接插入就行,并且不需要单独考虑头指针为空情况。

链表分割

在这里插入图片描述

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        struct ListNode*gguard,*gtail,*lguard,*ltail;
        gguard=gtail=(struct ListNode*)malloc(sizeof(struct ListNode));
        lguard=ltail=(struct ListNode*)malloc(sizeof(struct ListNode));
        gtail->next=NULL;//前缀g代表great,接受大的节点,l代表less,反之
        ltail->next=NULL;

        struct ListNode*cur=pHead;
        while(cur)
        {
            if(cur->val<x)
            {
                ltail->next=cur;
                ltail=ltail->next;
            }
            else 
            {
                gtail->next=cur;
                gtail=gtail->next;

            }
            cur=cur->next;
        }
        ltail->next=gguard->next;//链接两个重新排列后的链表
        gtail->next=NULL;//尾插不会改变下一个节点,手动置空
         pHead=lguard->next;//重置头指针
         free(gguard);
         free(lguard);
        return pHead;
    }
};

由于编译器没有c语言的模式,用的cpp框架,代码是用c语言实现。
思路:小于参数值的尾插到一个链表,大于等于的尾插到另一个链表,最后将两个链表链接起来。运用哨兵位节点可有效避免多种插入时空指针的讨论情况

需要注意一点,尾插时是不改变下一个节点指向的,所以在链接两个链表之后,gtail作为新链表中其实是指向原链表中它的下一个节点,也就是说肯定指向新链表中某一个节点,所以必须将它手动置空,否则会造成循环问题。

链表的回文结构

在这里插入图片描述

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
   //找到中间节点
struct ListNode*middleNode(struct ListNode*head)
{
    struct ListNode*slow,*fast;
    slow=fast=head;
    while(fast&&fast->next)//判断节点数为奇数或偶数
    {
        fast=fast->next->next;
        slow=slow->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;
}
class PalindromeList {
public:
    bool chkPalindrome(ListNode* head) {
        //找到中间节点
        struct ListNode*mid=middleNode(head);
         //对中间节点往后开始逆置
        struct ListNode*rhead=reverseList(mid);
        //前半段和后半段比较
        while(head&&rhead)
        {
            if(head->val!=rhead->val)
            {
                return false;
            }
            head=head->next;
            rhead=rhead->next;
        }
        return true;
    }
};

思路:找到中间节点,从中间节点开始,对后半段逆置,然后依次比较。
可以通过快慢指针法找到中间节点(涵盖了节点数为奇偶的情况,详见以目录),反转链表有两种方式,一种通过三指针直接改变节点指针方向,我用的另一种取节点头插,可以避免讨论头指针为空的情况(详见目录),然后对原节点和从中间节点反转后的节点依次比较,当其中一个头节点遍历到空时停止,详见代码及注释。

相交链表

在这里插入图片描述

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *tailA=headA,*tailB=headB;
    int lenA=1,lenB=1;
    //分别求两个链表的长度
    while(tailA->next)
    {
        tailA=tailA->next;
        lenA++;
    }
     while(tailB->next)
    {
        tailB=tailB->next;
        lenB++;
    }
    if(tailA!=tailB)//如果两个尾节点不相等,一定不相交,直接返回空
    {
        return NULL;
    }
    //长的走差距步
    int gap=abs(lenA-lenB);//计算绝对值
    struct ListNode*longlist=headA,*shortlist=headB;
    if(lenA<lenB)//用ifelse语句一样,判断链表长度
    {
        longlist=headB;
        shortlist=headA;
    }
    while(gap--)
    {
        longlist=longlist->next;
    }
    //两个指针同时走,第一个地址相同的节点就是交点
    while(longlist!=shortlist)
    {
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
    return longlist;
}

思路:先分别求两个链表的长度,再让长的链表走差距步(相差的节点数),最后两个链表统一出发,第一个地址相同的节点就是交点。

需注意第一步遍历完两个链表后,要对两个链表的尾指针进行判断,相等代表有交点,不相等直接返回空结束。这种思路比较容易想到

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    if (headA == NULL || headB == NULL) {
        return NULL;
    }
    struct ListNode *pA = headA, *pB = headB;
    while (pA != pB) {
        pA = pA == NULL ? headB : pA->next;
        pB = pB == NULL ? headA : pB->next;
    }
    return pA;
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/intersection-of-two-linked-lists/solutions/811625/xiang-jiao-lian-biao-by-leetcode-solutio-a8jn/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这是一种非常巧妙的思路,大家可以想明白吗,我就不过多赘述,可以上官网看看。相比上一种较难想到

环形链表问题

概念:尾结点指针不指向空,指向该链表中的某一个节点。
先来一道简单题引入

1.环形链表证明

在这里插入图片描述

bool hasCycle(struct ListNode *head) {
    struct ListNode *fast,*slow;
    fast=slow=head;
    while(fast&&fast->next)//快指针走两步,所以断言两次
    {
        slow=slow->next;
        fast=fast->next->next;

        //判断环中追击相遇问题
        if(slow==fast)
        {
            return true;
        }
    }
    return false;

}

快慢指针思想,快指针先进环,待得慢指针进环后转化为追击问题。

  • 思考:追击过程中能否相遇

主要看快指针每次比慢指针多走多少位移,也就是每一次移动的缩小距离,再就是看他们所在的环结构的长度是奇数还是偶数,若是每次缩小距离的奇偶性和环结构的长度的奇偶性相同(奇数可能还要考虑是否为某些数的倍数,不过顶多循环几次,是有限的),就能相遇。

2.返回入口点

在这里插入图片描述
做这个题首先需要了解并证明一个结论:一个指针从相遇点走,一个指针从起始点走,最终会在环形链表的入口点相遇。

在这里插入图片描述
先假设各个距离参数如图所示,环长为c,0<=X<=C-1(X等于0代表slow刚进环时fast恰好绕环一圈在入口点相遇,X等于C-1代表slow刚进环时fast恰好绕一圈并多走一步,是他们环中最大相差距离)。


相遇时slow走的距离为L+X,slow进环前fast已经走了n圈(n>=1),所以fast走得距离为L+n*C+X。由以下证明结合上图可得。
在这里插入图片描述

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *fast,*slow;
    fast=slow=head;
    while(fast&&fast->next)//快指针走两步要断言两次
    {
        slow=slow->next;
        fast=fast->next->next;
        if(fast==slow)//变成追击相遇问题
        {
            //运用结论,一个指针从相遇点走,另一个起始点走
            //最终会在入口点相遇
            struct ListNode *meet=slow;//fast也行
            struct ListNode *start=head;
            while(start!=meet)
            {
                start=start->next;
                meet=meet->next;
            }
            return meet;//start也行
        }
    }
    return NULL;
}

该题还有另一种思路,思路难度小,代码量大
在这里插入图片描述
两个链表,一个从起始点到相遇点,一个从相遇点next到相遇点,可以将相交链表中的代码作为该题函数调用的定义。

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *fast,*slow;
    fast=slow=head;
    while(fast&&fast->next)//快指针走两步要断言两次
    {
        slow=slow->next;
        fast=fast->next->next;
        if(fast==slow)//变成追击相遇问题
        {
            //转化成list1和list2求交点
            struct ListNode *list1=head;
            struct ListNode *meet=fast;//slow也行
            struct ListNode *list2=meet->next;
            meet->next=NULL;//将环分割成两个链表求交点
            return getIntersectionNode(list1, list2);
        }
    }
    return NULL;
}

结语

通过以上例题发现除环形链表外的大部分题目,其实都在换种方式用头插、尾插等方式解决问题,一般少用具体查找和删除(涉及改变前一个和后一个节点的指向,需要分类讨论稍复杂),要熟悉哨兵位头节点以及各种置空条件的判断,而且这些题目都是单向的,要对单链表增删查改的掌握程度有一定要求。

  • 希望对大家有所帮助,感谢支持
    请添加图片描述
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值