好题分享(2023.11.5——2023.11.11)

好题分享(2023.11.5——2023.11.11)_第1张图片

目录

前情回顾:

前言:

题目一:补充《移除链表元素》

题目二:《反转链表》

解法一:三指针法

解法二:头插法 

题目三: 《相交链表》

题目四:《合并两个有序数列》

题目五:《链表中倒数第K个节点》

题目六:《链表的分割》

题目七:《链表的回文结构》 

题目八:《环形链表(一)》 

 题目九:《环形链表(二)》 

由题目八引出结论:

对于第九题的算法:

总结:

题目十:《随机链表的复制》

1.先拷贝各个节点,再连接起来

2.对random进行赋值

3.断开连接

总结:


前情回顾:

我们在上一篇好题分析中,分析了以下几题:

《合并两个有序数组》《移除链表元素》《链表的中间节点》

上一篇的好题分析的blog在

好题分析(2023.10.29——2023.11.04)-CSDN博客

前言:

本次好题分享,我们将对《移除链表元素》进行一种算法即思路进行补充,因为在之后的题目,都是围绕此算法来实现的

同时,我们还将对Leecode和牛客网上的众多题目进行分析:

《反转链表》《相交链表》《环形链表(一)》《环形链表(二)》《随机链表的复制》《合并两个有序链表》

《链表中倒数第K个节点》《链表分割》《链表的回文结构》

题目一:补充《移除链表元素》

 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第2张图片​ 

我们在上一篇blog中,我们是利用三个指针来进行操作,在删除节点前,保存该节点前后的节点,再进行修改指向的操作,此算法的不足之处,就在于如果我们要删除的元素位于头结点,那么这个时候就要考虑另外一种情况了。

接下来的算法将可以省去上述步骤,且在之后的刷题中会更高效!

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 
 //创建“新链表”
struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* newhead = NULL,*cur = head,*tail = NULL;
    while(cur)
    {
        //不是val节点的拿下来尾插
        if(cur->val != val)
        {
            //尾插
            if(tail==NULL)
            {
                newhead = tail =cur;
            }
            else
            {
                tail->next = cur;
                tail = tail->next;
            }
            cur = cur->next;
        }
        else
        {
            cur =cur->next;
        }
    }
    if(tail)
    {
        tail->next = NULL;
    }
    return newhead;
}

 该算法是先创建一个新链表,将需要的节点连接到新链表中,再返回新创建的指针。

好题分享(2023.11.5——2023.11.11)_第3张图片

 刚开始由于newnode指向的是NULL,即该链表为空,所以我们需要进行尾插操作,同时tail也指向新链表的第一个节点。

即:

好题分享(2023.11.5——2023.11.11)_第4张图片

但是要注意的是,此时我们newnode中的头结点,它的next是指向原链表中所在位置的下一个节点的,并非指向NULL,即:

好题分享(2023.11.5——2023.11.11)_第5张图片​ 

你可能会觉得代码中该部分是多余的

好题分享(2023.11.5——2023.11.11)_第6张图片​ 

但是这一步缺失这种算法的关键。

如果我们这时候的cur指向了题目描述中的val,此时cur就会跳过该节点。

并指向下一个节点,那么如果没有tail->next = cur;这一行代码

那么新链表中的tail将直接指向题目描述中的val。

所以我们必须加上此代码!!! 好题分享(2023.11.5——2023.11.11)_第7张图片

当遇到此时的情况时,如果我们直接放回newnode,就会是一个不完整的链表,因为尾结点并没有指向NULL,所有我们务必要在最后加上一句:tail->next = NULL;

 这种创建新链表的思想需要我们去学习,在之后的题目中尤为重要!

题目二:《反转链表》

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第8张图片

对于本题目我将给出两种算法,为了展示出创建新链表该种算法的优势。 

解法一:三指针法

 如果我们尝试常规解法来实现,那我们我们就务必需要保存前后节点的位置。

所以该算法实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 
 //三指针法
struct ListNode* reverseList(struct ListNode* head) {
    if(head==NULL)
    {
        return NULL;
    }
    
    struct ListNode* n1 = NULL,*n2 = head,*n3 = head->next;
    if(n3 == NULL)
    {
        return n2;
    }

    while(n2 != NULL)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3)
            n3 = n3->next;
    }
    return n1;
}

好题分享(2023.11.5——2023.11.11)_第9张图片

好题分享(2023.11.5——2023.11.11)_第10张图片​ 

好题分享(2023.11.5——2023.11.11)_第11张图片​ 

​ 

这道题还要注意,如果链表只要一个节点或者无节点时的返回值!

解法二:头插法 

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

 //头插法
struct ListNode* reverseList(struct ListNode* head) {
    if(head == NULL)
    {
        return NULL;
    }

    struct ListNode* newnode = NULL;
    struct ListNode* cur = head;
    struct ListNode* next = head->next;
    
    while(next)
    {
        cur->next = newnode;
        newnode = cur;
        cur = next;
        next = next->next;
    }
    cur -> next = newnode;
    newnode = cur;
    return newnode; 

}

 好题分享(2023.11.5——2023.11.11)_第12张图片

反转的本质,就是在新链表中头插 。

那么我们就可以将cur->next指向newnode;

newnode再指向此时的cur,这就完成了头插操作

再将cur = next,

next = next -> next;

因为我们改变来cur->next的指向,所以我们对于保存cur下一个节点的位置就尤为重要!

好题分享(2023.11.5——2023.11.11)_第13张图片

好题分享(2023.11.5——2023.11.11)_第14张图片​ 

完成操作后,我们最好可以将head = NULL,避免出现野指针。

题目三: 《相交链表》

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第15张图片

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

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode* curA = headA, *curB = headB;
    int countA = 0;
    int countB = 0;
    //判断是否相交
    while(curA->next)
    {
        curA = curA->next;
        countA++;
    }   

    while(curB->next)
    {
        curB = curB->next;
        countB++;
    }   
    if(curA != curB)
    {
        return NULL;
    }

    //找出相交的起始节点
    int n = abs(countA - countB);
    if(countA > countB)
    {
        while(n)
        {
            headA = headA->next;
            --n;
        }
        while(headA != headB)
        {
            headA = headA->next;
            headB = headB->next;
        }
        return headA;
    }
    else
    {
        while(n)
        {
            headB = headB->next;
            --n;
        }
        while(headA != headB)
        {
            headA = headA->next;
            headB = headB->next;
        }
        return headB;
    }


}

对于该题目,我们提供一种算法:

1.先判断它们是否相交。

2.找到第一个公共节点。

相交好判断

我们可以先遍历,找到它们最后一个节点,如果节点相同,就说明它们相交!

因为相交链表只能是“Y”型相交,而绝对不可能是“X”型的 !

那么对于第二步,

我们可以先求出两个链表分别有多长,再相减求绝对值,得出相差步。

再使得短的链表的curA先走相差步,这样两个指针就是从同一位置起步了

再一起同时走一步,直到它们相等,如果这时两指针相等,则就说明相等的节点就是第一个公共节点!

好题分享(2023.11.5——2023.11.11)_第16张图片

让curB走向差步:

好题分享(2023.11.5——2023.11.11)_第17张图片​ 

在同时走一步:

好题分享(2023.11.5——2023.11.11)_第18张图片​ 

如此就可以得到公共节点!

题目四:《合并两个有序数列》

 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第19张图片​ 

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{
    struct ListNode* cur1 = list1,*cur2 = list2;
    struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
    newnode->next = NULL;
    struct ListNode* next = NULL;
    struct ListNode* tail = newnode;
    while(cur1 && cur2)
    {
        if((cur1->val <= cur2->val))
        {
            next = cur1->next;
            tail->next = cur1;
            cur1 = next;
            tail = tail->next;
        }
        else
        {
            next = cur2->next;
            tail->next = cur2;
            cur2 = next;
            tail = tail->next;
        }
    }
    if(cur1==NULL)
    {
        tail->next = cur2;
    }
    else
    {
        tail->next = cur1;
    }
    return newnode->next;
}

对于该题目,我建议使用新链表法,同时我们最好创建一个哨兵位的头结点。

具体为什么我这里建议使用新链表法,想要反驳的同学下去可以自己动手创建一个无哨兵位的解法,相信你一定会被绕晕的。

鉴于我们已经学会了前面两道题目的算法,这道题相信大家一定很好理解!

好题分享(2023.11.5——2023.11.11)_第20张图片

好题分享(2023.11.5——2023.11.11)_第21张图片​ 

 好题分享(2023.11.5——2023.11.11)_第22张图片

 好题分享(2023.11.5——2023.11.11)_第23张图片

如此循环,当cur1或者cur2为NULL时,则直接让tail->next指向cur1或cur2.

即:

好题分享(2023.11.5——2023.11.11)_第24张图片

题目五:《链表中倒数第K个节点》

 链表中倒数第k个结点_牛客题霸_牛客网

 好题分享(2023.11.5——2023.11.11)_第25张图片

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

/**
 * 
 * @param pListHead ListNode类 
 * @param k int整型 
 * @return ListNode类
 */
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    if(pListHead == NULL)
    {
        return NULL;
    }

    int count = 0;
    struct ListNode* tmp = pListHead;
    while(tmp)
    {
        count++;
        tmp = tmp->next;
    }

    int n = count - k;
    if(n<=0 && k > count)
    {
        return NULL;
    }
    while(n--)
    {
        
        pListHead = pListHead->next;
    }
    return pListHead;

}

该题目的算法实现还是好理解 

我们先统计该链表存在多少个节点。

然后再减去所谓的倒数第K个。

就可以得到正数第几个n,

再将pListHead进行遍历,同时n--

再返回此时的pListHead

思路还是好理解的,在这里我就不进行画图展示了。

题目六:《链表的分割》

链表分割_牛客题霸_牛客网

好题分享(2023.11.5——2023.11.11)_第26张图片​ 

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
#include 
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x)
     {
        // write code here
        struct ListNode* head1,*tail1,*head2,*tail2;
        head1 = tail1 = (struct ListNode*)malloc(sizeof(struct ListNode));
        head2 = tail2 = (struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* cur = pHead;
        while(cur)
        {
            if(cur->valnext = cur;
                tail1 = tail1->next;
                
            }
            else 
            {
                tail2->next = cur;
                tail2 = tail2->next;
            }
            cur= cur->next;
        }

        tail1->next = head2->next;
        //防止成环
        tail2->next = NULL;
        pHead = head1->next;
        free(head1);
        free(head2);
        return pHead;
    }
};

 对于该题目的算法思路,要注意的有以下几点:

1.我们在此选择开辟两个哨兵位的头节点创建链表,小于等于x的到第一个链表,大于x的到第二个链表。

2.再将小链表的最后一个节点的next,链接到第二个哨兵位头节点的next。

3.同时还要注意成环问题!

好题分享(2023.11.5——2023.11.11)_第27张图片

 好题分享(2023.11.5——2023.11.11)_第28张图片

如此一直循环:

好题分享(2023.11.5——2023.11.11)_第29张图片​ 

到这里时我们就可以直接将tail1->next = head2->next;

即:

好题分享(2023.11.5——2023.11.11)_第30张图片

此时一定不要忘记将tail2->next = NULL;

因为此时的tail2->next是指向的tail1指向的节点的

好题分享(2023.11.5——2023.11.11)_第31张图片​ 

如果这里不注意的话,就会形成一个环!!!

好题分享(2023.11.5——2023.11.11)_第32张图片​ 

如此才是正确的全部思路。

题目七:《链表的回文结构》 

链表的回文结构_牛客题霸_牛客网

 好题分享(2023.11.5——2023.11.11)_第33张图片

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        // write code here

        //找中间节点
        ListNode* fast = A;
        ListNode* slow = A;
        while(fast && fast->next)
        {
            fast = fast->next->next;
            slow = slow->next;
        }

        //逆序
        ListNode* next = slow ->next;
        ListNode* rehead = slow;
        rehead->next = NULL;
        while(next)
        {
        slow = next;
        next = slow->next;
        slow->next = rehead;
        rehead = slow;
        }

        while(A && rehead)
        {
            if(A->val == rehead->val)
            {
                A = A->next;
                rehead = rehead->next;
            }
            else
            {
                return false;
            }
        }
        return true;
    }
};

 由于牛客网对于本题目只要C++一种格式,但是没关系,C++兼容C语言。

所有我们就按照我们的写法即可:

 好题分享(2023.11.5——2023.11.11)_第34张图片

第一步,利用快慢指针找到中间节点!

即:

好题分享(2023.11.5——2023.11.11)_第35张图片​ 

此时的该节点就为中间节点。

再逆序中间节点后面的全部节点

利用新链表法!

先创建个指针rehead

好题分享(2023.11.5——2023.11.11)_第36张图片​ 

在创建的时候,提前将slow指向的节点拿下来,进行置空操作,即:

好题分享(2023.11.5——2023.11.11)_第37张图片

 再进行一个经典的头插操作即while循环里面的全部操作:

好题分享(2023.11.5——2023.11.11)_第38张图片

如此就可以进行全部逆序的操作,则此时的图为:

好题分享(2023.11.5——2023.11.11)_第39张图片​ 

我们只需要分别将A和rehead每次挪动一步,再判断是否为相等即可。

如果一直相等直到循环结束,就return true

如果出现不想等的,直接return false

题目八:《环形链表(一)》 

 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第40张图片

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
bool hasCycle(struct ListNode *head) {
    struct ListNode* fast,*slow;
    fast = slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;
        }
    }
    return false;
}

对于一个带环链表,我们遇到该种题目应当先考虑考虑快慢指针算法。

如图:

好题分享(2023.11.5——2023.11.11)_第41张图片

fast一次走两步,slow一次走一步,当它们相遇的时候,一定是在圆环内!

例:

好题分享(2023.11.5——2023.11.11)_第42张图片

好题分享(2023.11.5——2023.11.11)_第43张图片

好题分享(2023.11.5——2023.11.11)_第44张图片

从上述可以看出,再进行第三次循环的时候,fast和slow就相遇,就说明此时的链表是带环链表。

 题目九:《环形链表(二)》 

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第45张图片

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow = head;
    struct ListNode* meet = NULL;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            meet = slow;
            while(head != meet)
            {
                head = head->next;
                meet = meet->next;
            }
            return head;
        }
    }

    return NULL;
}

这道题与上题类似,但区别在于现在已经告诉你这是一个环形链表,需要你求出进入环的第一个节点。

对于解决该问题的方法,此方法偏向于数学思维思路。

我们由图来分析。

由题目八引出结论:

 我们在题目八中分析过,如果我们利用快慢指针,就可以求出它们是否再圆环内。

但是当快指针一次走三步呢?

那么还可以追上吗?

这就不得不利用数学思维来解决此问题。

 好题分享(2023.11.5——2023.11.11)_第46张图片

我们可以将链表抽象为如图内容。

那么当fast一次三步时,怎么判断它们是否相遇:

好题分享(2023.11.5——2023.11.11)_第47张图片

当slow刚进入环内时,fast肯定比slow快3倍,走过的距离肯定也多三倍。

但是一旦它们都在环内时,距离就会一直-2.

假设一开始的距离为n

好题分享(2023.11.5——2023.11.11)_第48张图片

此时的-1就代表了它们的距离相差1,意思就是:

好题分享(2023.11.5——2023.11.11)_第49张图片

那么就不难的出以下结论:

假设C为圆的周长,此时它们距离就是C-1。

如果C-1为偶数,那么随着它们-2的操作,最后会相遇。

但如果C-1是奇数,那么随着它们-2的操作,最后还需要再来一圈才可以相遇!

即:

1.若n为偶数,直接就可以追上

2.若n是奇数,C是奇数,得过两圈才能追上。

3.如果n是奇数,C是偶数,永远都追不上。

好题分享(2023.11.5——2023.11.11)_第50张图片

但是第3条是不成立的,因为我们可以得出公式:

L为直线长

N是两指针在刚开始的距离,

3L = L +n*C - N

即2L = n*C - N

等号左边百分百为偶数,

而右边永远等于奇数。

所以它们不相等,即该结论永远不成立!

对于第九题的算法:

 而对于该题目,我们只需要知道此时的fast只走一步,而它们绝对可以在圆环内相遇:

好题分享(2023.11.5——2023.11.11)_第51张图片

我们在它们相遇的地方创建个指针,指向该地方,后面我们就会用到此指针:

好题分享(2023.11.5——2023.11.11)_第52张图片

我们在创建个未知数X,代表第一个环节点到相遇点, 

 通过该图,我们可以知道,slow走过的路程 = fast的路程/2

所以就有2(L+X) = L + X + n*C

假设这个环足够小,那么我的fast很有可能在圆环内走很多圈,但我们假设就走过一圈。

则就有以下算式:

2(L+X) =L + X + C

L = C - X

通过以上的推导:

我们知道了以下:

好题分享(2023.11.5——2023.11.11)_第53张图片

L== Y

那我们就可以让meet和head同时走一步,直到它们相遇,因为相遇的地方就是圆环的第一个节点。

这样就可以求出来第一个节点了!

总结:

本题目设计到的数学思维和思路会使很多同学绕不清楚,还有转不过弯来。

对于这部分的解法下来需要好好熟练掌握并学习学习,在这里我就不再多讲了。

题目十:《随机链表的复制》

 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

好题分享(2023.11.5——2023.11.11)_第54张图片

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

struct Node* copyRandomList(struct Node* head) {
     if (head == NULL) {
        return NULL;
    }

	struct Node* cur = head;
    struct Node* copy = NULL;
    while(cur)
    {
        copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val = cur->val;
        copy->next = cur->next;
        cur->next = copy;
        cur = cur->next->next;
    }
    copy = head->next;
    struct Node* tmp = NULL;
    cur = head;
    while(cur)
    {
        tmp = cur->next;
        tmp->random = (cur->random != NULL)?cur->random->next : NULL;
        cur = cur->next->next;
    }

    tmp = copy;
    while(tmp->next != NULL)
    {
        tmp->next = tmp->next->next;
        tmp = tmp->next;
    }
    return copy;
}

本道题目较难,我在这里先讲解以下思路:

1.先拷贝各个节点,再连接起来

好题分享(2023.11.5——2023.11.11)_第55张图片

好题分享(2023.11.5——2023.11.11)_第56张图片

2.对random进行赋值

好题分享(2023.11.5——2023.11.11)_第57张图片

 这里格外要注意,如果此时原radom是NULL,就要另外讨论。

如果不为空,则将cur->radom->next赋给copy

这一句代码则是精华所在!

3.断开连接

 将拷贝好的各个节点互相链接,并且返回。

总结:

本周的好题分享设计到的知识面较广,我们在做题过程中难免会遇到一些难以实现的操作,但是只要我们沉下心来一点点的了解和学习,相信一定会有进步!

记住“坐而言不如起而行!”

Action speak louder than words!

你可能感兴趣的:(数据结构,开发语言,c语言,算法,经验分享,Leecode)