【初阶数据结构】链表经典OJ(8道)

目录

1. 删除链表中等于给定值 val 的所有结点。 OJ链接

2. 反转一个单链表。OJ链接

3. 输入一个链表,输出该链表中倒数第k个结点。OJ链接

4. 链表的回文结构。OJ链接

5. 输两个链表,找出它们的第一个公共结点。OJ链接

6. 给定一个链表,判断链表中是否有环。 OJ链接

7.给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL OJ链接

8.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。要求返回这个链表的深度拷贝。OJ链接


1. 删除链表中等于给定值 val 的所有结点。 OJ链接

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* pcur = head;
    ListNode* newtail,*newhead ;
    newtail = newhead = NULL;

    while(pcur)//挨个判断链表节点是否满足 Node.val == val 的节点
    {
        if(pcur->val != val)
        {
            if(newhead == NULL)//将首个满足 Node.val == val 的节点作为头节点赋给newhead;
            {
                newhead = newtail = pcur;
            }
            else//将之后满足条件的节点串联在头节点后
            {
                newtail->next = pcur;
                newtail = pcur;
            }
        }
        pcur = pcur->next;
    }
    if(newhead != NULL)//如果原链表存在满足条件的节点构成新链表,将尾节点的next指针置为空指针。
    newtail->next = NULL;
    return newhead;// 返回新的头节点 
}

2. 反转一个单链表。OJ链接

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    ListNode* newnode = NULL;
    ListNode* pcur = head;
    if(pcur == NULL)//原链表为空,直接返回空
    return pcur;
    while(pcur->next)
    {
        ListNode* p = pcur->next;//创建新的指针保存当前节点的下一个节点
        pcur->next = newnode;//将当前节点的next指针指向newnode保存的上一个节点
                             //pcur为头节点时,newnode为空,则原链表头节点
                             //反转后作为新尾节点next指针正好指向空指针
        newnode = pcur;//newnode保存当前指针
        pcur = p;//pcur指向向下一个节点
    }
    pcur->next = newnode;//将最后一个节点的next指针指向它的原上一个指针,则链表反转完成
    return pcur;//返回新链表头节点
}

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接

本体笔者使用快慢指针的方法,通过创建两个指针,一个slow,一个fast,slow循环一次走到一步(slow做到下一个节点),fast一次走两个步(fast走到下下个节点),由于速度之比是1:2,因此当fast走到尾的时候,slow正好就在中间节点。但是节点的个数有奇偶之分,那对于这两种不同情况,这种方法是否都适用呢?

1.首先当链表节点个数为偶数时,中间节点只有一个,就在正中间,当fast走到尾节点时,slow就在该节点处,我们根据fast->next为空结束循环,直接返回slow即可。

2.首先当链表节点个数为奇数时,中间节点有两个,我们需要返回下一个中间节点,这种情况下,快慢指针也可以解决,当slow指针走到前一个中间节点时,fast才走到尾节点的前一个节点,但是需要注意的是,当slow走到下一个中间节点时,fast会走到尾节点的下一个指针,即fast会变成空指针,我们根据fast为空结束循环,直接返回slow。其实在这个情况下,尾节点的next指向的NULL指针可以视为一个新节点。这是节点个数为奇数的情况就可以视为偶数的情况了(即长度/2得到中间节点)。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
    ListNode* slow = head;//创建快慢指针
    ListNode* fast = head;
    while(fast && fast->next)//fast为空对应节点个数为奇数的情况
    {                        //fast->next为空对应节点个数为偶数的情况
        slow = slow->next;//慢指针走一步
        fast = fast->next->next;//快指针走两步
    }
    return slow;//返回中间节点
}

3. 输入一个链表,输出该链表中倒数第k个结点。OJ链接

对于单向链表,我们无法直接获取链表的尾节点,同时本题还要求返回倒数第几个节点,这就要求我们能够判断相对位置。因此针对倒数第k个节点相对于尾节点的相对距离固定,本题笔者依旧创建两个指针slow、fast,指向头结点,根据要求求倒数第k个节点,我们让fast从头结点先走K步,之后我们再让slow,fast一起一次一步往前走,这是slow、fast之间就有固定的距离差即K个步。当fast走到尾节点next指针指向的NULL时,slow即为所求的倒数第K个节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


int kthToLast(struct ListNode* head, int k){
    struct ListNode* slow = head,*fast = head;//快满指针
    while(k--)//要找倒数第K个节点,则fast先走k步(即K个节点)
    {
        fast = fast->next;
    }
    while(fast)//fast走到NULL循环结束
    {
        fast = fast->next;//快慢指针一起走
        slow = slow->next;
    }
    return slow->val;//返回该节点的值。
}

4. 链表的回文结构。OJ链接

对于这一题,如果是数组的回文判断那么就非常简单,但是根据代码的注释部分可知,我们这题是基于单链表来完成的。首先,由于单链表自身缺陷,我们无法由节点直接找到它的前一个节点,及时存一个尾节点指针也不行。因此,我们这题无法直接从左右向中间比较。其次,根据题目要求空间复杂度O(1)的要求,我们也无法开辟数组来按序存储链表节点的值,再按照数组内的位置反过来循环遍历节点。

本题笔者首先通过前文讲到的快慢指针的方法,找到回文结构那个分界点即中间节点。当然,读者可能会疑惑节点的数量不是会有奇偶之分吗?这里我们同前文的题目一样,只返回后一个中间节点。这时,我们以中间节点为头,将中间节点及之后的节点反转。

需要注意的是我们讲中间节点及之后所有的节点反转,中间节点的前一个节点的next指针依然是指向中间节点的,中间节点的next指针反转后却会指向NULL.

对于奇数,我们指针A、Newhead,挨个遍历比较,如果有对应节点值不相等,我们直接返回false,原链表不是回文结构,当两个指针都走到空,则原链表是回文结构。

对于偶数,需要注意的是,A、和Newhead不会同时走到NULL,我们是根据Newnode为空为结束条件。

注:笔者目前并不会C++,但是牛客网该题不直接支持C语言,因此笔者在原C++代码上直接按C完成代码,若代码对读者造成误导,可以直接跳过。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
ListNode* Mid(ListNode* head)//快慢指针找到中间节点
{
    struct ListNode*slow = head,*fast = head;
    while(fast&&fast->next)
    {
        slow = slow->next; 
        fast = fast->next->next;
    }
    return slow;
}
ListNode* Reverse(ListNode* head)//将输入节点及往后节点反转
{
    struct ListNode*cur= head,*newhead = NULL;
    while(cur)
    {
        struct ListNode* n = cur->next;
        cur->next = newhead;
        newhead = cur;
        cur = n;
    }
    return newhead;

}
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        struct ListNode* mid = Mid(A);//找到中间节点
        struct ListNode* Newhead = Reverse(mid);//将中间节点及往后节点反转,返回反转之后的新头结点
        while(A&&Newhead)//
        {
            if(A->val != Newhead->val)
            {
                return false;//出现不符合条件的节点直接返回
            }
            A = A->next;
            Newhead = Newhead->next;//两个链表一起往后挨个走
        }
        return true;//所有节点都比较完
    }
};

5. 输两个链表,找出它们的第一个公共结点。OJ链接

需要注意的是,对于相交的单链表,一个节点定义时就没有两个next的指针,因此,一但出现相交节点,两个链表必然合二为一,不会出现相交后又分开的情况。 同时,将链表反转的思路也不可用。

不过如果直接循环比较链表节点,由于相交节点之前的节点个数存在的不同情况,我们如果直接比较会存在错位的情况,永远无法得出正确结果。因此在比较之前,我们需要先循环遍历两个链表,得出两个链表的长度,让长的链表先走两个链表长度差步,这样两个链表到相交节点前长度相等。

这样后,我们就可以直接进行循环遍历比较了。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    int len1 = 0,len2 = 0;
    struct ListNode *HeadA = headA,*HeadB = headB;

    while(HeadA)//遍历得出headA的长度(节点数)
    {
        HeadA = HeadA->next;
        len1++;
    }

    while(HeadB)//遍历得出headB的长度(节点数)
    {
        HeadB = HeadB->next;
        len2++;
    }

    int gap = abs(len1 - len2);//长度差(长的链表比短的链表多出的节点个数)
    struct ListNode *longer = headA,*shoter = headB;//为了减少代码量,使用假设法

    if(len2 > len1)//根据测的长度检验假设是否正确,不正确更新一下
    {
        longer = headB;
        shoter = headA;
    }

    while(gap--)//长的链表先走gap长度差步,这样长短链表就可以视为一样长
    {
        longer = longer->next;
    }

    while(longer&&shoter)//长短链表任意链表能到空,说明比较完链表
    {
        if(longer == shoter)
        {
            return longer;
        }
        longer = longer->next;
        shoter = shoter->next;
    }

    return NULL;//比较完都没有相较的节点,两个原链表不相交
}

6. 给定一个链表,判断链表中是否有环。 OJ链接

判断链表是否存在环,我们这里创建快慢指针,slow每次走一步,fast每次走两步,fast先进环,slow后进环,当slow后进环,不管fast与slow之间的距离为多少,fast、slow之间的速度差恒为1,因此,这就成了一个速度差恒定的追击问题。

每追击一次距离减1,距离为0就追上,因此如果存在环,fast一定能追上slow,如果不存在环,fast会走到链表尾,fast会为空。

拓展:在上述方法中,slow是走一步,fast走两步,那么如果fast走三步、四步、五步、n步呢?

以fast走三步为例:slow,fast速度差为2

看起来,如果同时存在N是奇数且C是偶数(C-1是奇数),那么就永远追不上了,那么现在我们不妨在追问一下,这种情况真的能存在吗?接下来,我们跟据两者的速度,距离关系尝试证明一下。

综上当slow走一步、fast走三步也一定能追上。

fast走四步速度差为三时,也是类似的证明过程。笔者在此便不过多赘述了。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {//快慢指针

    struct ListNode *slow = head;//慢指针走一步
    struct ListNode *fast = head;//快指针走两步

    while(fast&&fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(fast == slow)//快慢指针相遇存在环
        {
            return true;
        }
    }

    return false ;//快慢指针无法相遇不存在环
}

7.给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL OJ链接

该题在上一题的基础上要求再返回开始入环的第一个节点。

方法1:在创建meet指针记录fast、slow相遇的位置,并从当前节点就序往下走,创建head指针从链表第一个节点开始向后遍历,最后,head,meet一定会在开始入环的第一个节点相遇,这时再返回meet。但是,为什么head,meet一定会在开始入环的第一个节点相遇呢?笔者接下来论证一下。

我们可以看到L=(x-1)*C+C-N,因此,当head走完L的路程,mee会走完(X-1)圈回到fast、slow相遇的位置,再走C-N走到开始入环的第一个节点与head相遇.

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow = head; //快慢指针
    struct ListNode *fast = head;

    while(fast&&fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        
        if(fast == slow)//是否判断存在环
        {
           struct ListNode *meet = slow;//如果存在环,在相遇出创建meet

           while(meet != head)//meet与head相遇出处链表开始入环的第一个节点
           {

            meet = meet->next;
            head = head->next;//head从第一个有效节点向开始入环的第一个节点走

           }

           return meet;//meet与head相遇出处链表开始入环的第一个节点

        }
    }

    return NULL ;//不存在环
    
}

方法二:我们可以从slow、fast相遇处将环解开,这时返回链表开始入环的第一个结点问题,就转变成前文求两个链表的相加节点问题。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    int len1 = 0,len2 = 0;
    struct ListNode *HeadA = headA,*HeadB = headB;

    while(HeadA)//遍历得出headA的长度(节点数)
    {
        HeadA = HeadA->next;
        len1++;
    }

    while(HeadB)//遍历得出headB的长度(节点数)
    {
        HeadB = HeadB->next;
        len2++;
    }

    int gap = abs(len1 - len2);//长度差(长的链表比短的链表多出的节点个数)
    struct ListNode *longer = headA,*shoter = headB;//为了减少代码量,使用假设法

    if(len2 > len1)//根据测的长度检验假设是否正确,不正确更新一下
    {
        longer = headB;
        shoter = headA;
    }

    while(gap--)//长的链表先走gap长度差步,这样长短链表就可以视为一样长
    {
        longer = longer->next;
    }

    while(longer&&shoter)//长短链表任意链表能到空,说明比较完链表
    {
        if(longer == shoter)
        {
            return longer;
        }
        longer = longer->next;
        shoter = shoter->next;
    }

    return NULL;//比较完都没有相较的节点,两个原链表不相交
}
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow = head; //快慢指针
    struct ListNode *fast = head;

    while(fast&&fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        
        if(fast == slow)//是否判断存在环
        {
           struct ListNode *meet = slow;//如果存在环,在相遇出创建meet
           struct ListNode *newhead = meet->next;
           meet->next = NULL;//断开环
           
           return getIntersectionNode(newhead,head);;//返回交链表,没有返回空

        }
    }

    return NULL ;//不存在环
    
}

8.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。要求返回这个链表的深度拷贝。OJ链接

本题深拷贝属于C++中概念,简单来说就是创建原链表的复制链表。

本题的主要的难点在于如何实现新链表的random的指针指向关系与原链表相似,需要注意的是,

新链表的random指向不是指向原链表中的节点,而是新的节点,得到的新链表与原链表出指针指向的空间不同,其他的val值,指针指向的相对位置关系都一致。除此之外因为random指针指向随机,因此如果直接做,创建新的链表的random,我们还需要求出其指向的节点在整个链表中的相对位置关系。因此不论是循环得出位置信息还是数组存储位置信息之类,时间空间复杂度会很大。

笔者的方法是在原链表节点后面开辟它的对应的复制节点,即将复制节点插入到原链表中,这时对于random指针,如果random指向NULL,那么后面的复制节点random指向NULL,如果random指向一个链表中的随机节点,复制节点random就 指向原节点random指向节点的next下一个节点(copy->random  = cur->random->next),这是复制节点就指向其对应的前一个节点。例如图中复制,就指向原13节点random指向的原7节点的next指向的复制7节点。

random处理完后,我们只需要将复制节点都拿下来,组装成新链表,并将原链表恢复原样就可以了。

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */

struct Node* copyRandomList(struct Node* head) {

    struct Node* cur = head;

    while(cur)//在原链表节点的后面插入对应的复制节点
    {
        struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->val = cur->val;
        newnode->next = cur->next;
        cur->next = newnode;
        cur = newnode->next;
    }

    cur = head;//cur在原链表的第一个有效节点
    struct Node* copy = head->next;//copy在第一个复制节点

    while(cur)//复制原链表的random指针的指向关系
    {
        copy = cur->next;//需要注意的是cur走完原链表,指向NULL,那么后面就没有复制节点
                        //因此移动copy指针,一定要放在开头,cur不为空移动copy
        if(cur->random == NULL)
        {
            copy->random  = cur->random ;
        }
        else
        {
            copy->random  = cur->random->next ;
        }

        cur = copy->next;//cur移动到复制节点的下一个原节点
                         //copy = cur->next;放在这就会出现cur为NULL是解引用的情况

    }

    cur = head;
    struct Node*newhead = NULL;
    struct Node*newtail = NULL;

    while (cur)//将复制节点从链表上解下来组成新链表,并恢复原链表
    {
        copy = cur->next;
        struct Node*next =  copy->next;

        if(newtail == NULL)
        {
            newhead = newtail = copy;
        }
        
        else
        {
            newtail->next = copy;
            newtail = newtail->next;
        }
        cur->next = next;
        cur = next;
    }

    return newhead;
}
  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZLRRLZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值