【数据结构与算法】学完链表,快来做做这些题吧

在下面的所有题目中,链表都是单链表,且链表节点的声明如下:

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

移除链表元素

题目描述

在这里插入图片描述

思路分析

大眼一看这题很简单,

无非是遍历一遍链表,

把与 val 相同的节点删除掉,

细节就是删掉这个节点后还要把前后两个节点连接起来。

那么就要同时找到前后两个节点。

此时会有两种情况:

  1. 这是最基本的一种情况,此时只需要再用一个指针 prev 标记前一个节点即可,当遍历到需要删除的节点时,用 next 记住它下一个位置,然后连接 prevnext,释放当前节点,从 next 节点继续走。
    在这里插入图片描述

  2. 当链表第一个节点就需要删除时,prev 还是个空指针,此时直接动头指针 head ,先标记下一个,然后释放掉头节点,然后 head 指向下一个。即便链表所有节点都需要删除也没问题。
    在这里插入图片描述

代码实现
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;
    while(cur)
    {
        //头节点不需要删除时
        if(head->val != val)
        {
            struct ListNode* next = cur->next;
            //当前节点不需要删除时
            if(cur->val != val)
            {
                prev = cur;
                cur = next;
            }
            //当前节点需要删除时
            else 
            {
                prev->next = next;
                free(cur);
                cur = next;
            }
        }

        //头节点需要删除时
        else
        {
            struct ListNode* next = cur->next;
            free(cur);
            head = next;
            cur = next;
        }
    }
    
    return head;
}

反转链表

题目描述

在这里插入图片描述

思路分析

这里提供三种思路:

  1. 逆置 val
  2. 翻转 next,即让 next 指向前一个而非下一个;
  3. 依次取最后一个节点进行头插。

下面只讲解第三种思路:

依次取最后一个节点进行头插的话,实际执行起来效率是很低的,因为每次找到最后一个节点都要遍历。

所以不妨转化思路,每次取第一个节点进行尾插,而为了避免遍历,每次取下来头结点之后插入到一个新链表中:

在这里插入图片描述

代码实现
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* newhead = NULL;
    struct ListNode* cur = head;
    while(cur)
    {
        struct ListNode* next = cur->next;
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    return newhead;
}

链表的中间结点

题目描述

在这里插入图片描述

思路分析

这是一个经典的快慢指针问题。

定义两个指针 slowfast

slow 每次走一步, fast 每次走两步,

fast 走到头,slow 也就走到了中间结点。

不过节点个数为奇数偶数时终止条件还不太一样:
在这里插入图片描述

代码实现
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while(fast != NULL && fast->next != NULL)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

链表中倒数第k个节点

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDLg1q5V-1653478157566)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523172459219.png#pic_center)]

思路分析

假想最后一个节点指向一个空节点,

那么倒数第 k 个节点与该空节点正好隔着 k 个节点。

所以如果能找到两个节点,

让这两个节点之间始终隔着 k 个节点,

当前一个节点走到空时,

返回后一个节点即可。

代码实现
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    for (int i=0; i<k; i++)
        fast = fast->next;
    while (fast != NULL)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

合并两个有序链表

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fMbP9IKf-1653478157566)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523172927875.png#pic_center)]

思路分析

这道题的思路很明了,

遍历两个链表,

每次取下小的那个节点尾插到新节点。

但这就有几种情况很棘手:

  1. 两个链表中有空链表,此时直接返回非空的即可,都为空则随便返回。
  2. 其中一个链表走到最后了,但另一个链表还没有,此时将未走完的那个链表接到新链表后面即可。
代码实现
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    if(list1 == NULL)
        return list2;
    if(list2 == NULL)
        return list1;
    struct ListNode* list3 = NULL;
    struct ListNode* phead = NULL;
    
    //先创建新链表的头节点
    if (list1->val < list2->val)
    {
        phead = list1;
        list1 = list1->next;
    }
    else
    {
        phead = list2;
        list2 = list2->next;
    }
    list3 = phead;
    /*上面代码如果觉得麻烦的话可以选择创建一个哨兵位,但最后别忘了释放掉。
    struct ListNode* list3 = NULL;
    struct ListNode* head = NULL;
    list3 = head = (struct ListNode*)malloc(sizeof(struct ListNode));
    list3->next = NULL;
    */
    
    //遍历两个链表
    while(list1 && list2)
    {
        if (list1->val < list2->val)
        {
            list3->next = list1;
            list1 = list1->next;
            list3 = list3->next;
        }
        else 
        {
            list3->next = list2;
            list2 = list2->next;
            list3 = list3->next;
        }
    }
    
    //其中一个链表走完
    if(list1)
        list3->next = list1;
    if(list2)
        list3->next = list2;
    
    return phead;
}

分割链表

题目描述

现有一链表的头指针 ListNode pHead*,给一定值 x,编写一段代码将所有小于 x 的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kanTvaoi-1653478157567)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523182959912.png#pic_center)]

思路分析

这道题最恶心的地方在于不改变原来数据的相对顺序。

那么这里提供一个很妙的思路。

将小于 x 的节点拿出来组成一个新链表,

将大于 x 的节点拿出来组成一个新链表,

再把两个链表连接起来返回即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KNizGqkD-1653478157567)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523184253548.png#pic_center)]

代码实现
ListNode* partition(ListNode* pHead, int x) 
{
    //创建大于x的链表
    struct ListNode* greaterList = NULL;
    struct ListNode* greaterTail = NULL;
    greaterList = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
    greaterList->next = greaterTail->next = NULL;

    //创建小于x的链表
    struct ListNode* lessList = NULL;
    struct ListNode* lessTail = NULL;
    lessList = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
    lessList->next = lessTail->next = NULL;

    while(pHead)
    {
        //节点的值小于x时
        if(pHead->val < x)
        {
            lessTail->next = pHead;
            lessTail = lessTail->next;
        }
        //节点的值大于x时
        else
        {
            greaterTail->next = pHead;
            greaterTail = greaterTail->next;
        }
        //找到下一个节点,继续循环
        pHead = pHead->next;
    }

    //连接两个链表
    lessTail->next = greaterList->next;
    greaterTail->next = NULL;
    pHead = lessList->next;

    //释放分配的空间
    free(greaterList);
    free(lessList);

    return pHead;
}

回文链表

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yIvUM7E3-1653478157567)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523185028916.png#pic_center)]

思路分析

判断链表的回文结构是不能和判断回文字符串一概而论的,

因为链表不能从后向前遍历。

所以这里的思路是先找到链表的中间结点,

然后翻转后半部分,

一个从头开始,一个从中间开始进行比较。

前两步处理都是前面做过的,

所以这道题是一道裁缝题(doge。

代码实现
bool isPalindrome(struct ListNode* head){
    //先找到中间结点
    struct ListNode* mid = head;
    struct ListNode* tmp = head;
    while(tmp && tmp->next)
    {
        tmp = tmp->next->next;
        mid = mid->next;
    }
    
    //翻转后半部分
    struct ListNode* newhead = NULL;
    while (mid)
    {
        struct ListNode* next = mid->next;
        mid->next = newhead;
        newhead = mid;
        mid = next;
    }
    
    //比较
    while(head && newhead)
    {
        if(head->val != newhead->val)
            return false;
        head = head->next;
        newhead = newhead->next;
    }
    return true;
}

相交链表

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wyqgAoZT-1653478157567)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523191250358.png#pic_center)]

思路分析

题目要求不能破坏链表的结构,

如果不限制这一点的话,

有一个简单暴力的方法:

先遍历一个链表,然后将每个节点都置为不可能出现的数值,再遍历另一个链表,如果有节点匹配上了那个超出数据范围的值,那么两个链表就相交,返回第一个匹配上的节点即可。

下面进行正经分析:

两个链表长度相同的话好说,同步遍历两个链表,当走到的节点位置相同时就返回该节点。

而两个链表长度不相同的话,如果能统一两个链表的起点,然后重复上面的步骤同样可以,所以现在问题就来到了怎么统一两个链表的起点。

首先遍历两个链表统计出两个链表的长度。此时可以先比较一下两个链表的尾结点,如果尾结点的地址不一致的话直接返回 false

然后动较长的那个链表,使它比较的起始位置向前移动,移动的次数为两个链表的长度差值。

同步之后开始比较即可。

代码实现
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    if(headA == NULL || headB == NULL)
        return NULL;

    //找到两个单链表的最后一个节点并比较
    //同时统计两个单链表的长度
    int cntA = 0;
    int cntB = 0;
    struct ListNode *curA = headA, *curB = headB;
    while (headA->next)
    {
        headA = headA->next;
        cntA++;
    }
    while (headB->next)
    {
        headB = headB->next;
        cntB++;
    }

    if (headA != headB)
        return NULL;

    //统一两个单链表的两个起点
    if (cntA > cntB)
        for (int i=0; i<cntA-cntB; i++)
            curA = curA->next;
    else if (cntA < cntB)
        for (int i=0; i<cntB-cntA; i++)
            curB = curB->next;

    //比较
    struct ListNode* tmpA = curA;
    struct ListNode* tmpB = curB;
    while(tmpA != tmpB && tmpA && tmpB)
    {
        tmpA = tmpA->next;
        tmpB = tmpB->next;
    }
    return tmpA;
}

环形链表

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MIz95udt-1653478157568)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523192822544.png#pic_center)]

思路分析

由于这道题没有限制不能改变链表结构,

所以上道题提到的标记法是可行的:

遍历链表,每到一个节点就将该节点存储的值改为数据范围之外的值,如果链表中有环的话一定会再走到值为数据范围之外的节点,该节点就是成环的第一个节点。

当然,如果用这个思路这道题就没意思了。

下面是正经思路:

还是采用快慢指针的方法,快指针一次走两个,慢指针一次走一个,如果链表中有环的话,两个指针一定会相交,证明如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pqJ5qiye-1653478157568)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523193814650.png#pic_center)]

代码实现
bool hasCycle(struct ListNode *head) 
{
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
            return true;
    }
    return false;
}

环形链表II

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tPFcC1wE-1653478157568)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523194254617.png#pic_center)]

思路分析

上一道题的升级版,

如果用暴力标记的话很容易就解决这道题,

但不是道题的灵魂所在。

下面是正经分析:

首先还是先验证链表是否有环,所以重复上一道题中移动快慢指针这一步骤,如果 fastslow 相遇的话,让一个指针从头开始,另一个指针从相遇点开始,两个指针同步前进,则两个指针一定会相遇,且该相遇点就是环开始的节点。证明如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZkfqALQ-1653478157568)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523202821201.png#pic_center)]

代码实现
struct ListNode *detectCycle(struct ListNode *head) 
{
    struct ListNode *fast = head, *slow = head;
    while (fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if (fast == slow)
        {
            struct ListNode *meet = fast;
            while (meet != head)
            {
                meet = meet->next;
                head = head->next;
            }
            return meet;
        }
    }
    return NULL;
}

复制带随机指针的链表

题目描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fxp5PDJl-1653478157569)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523203035710.png#pic_center)]

思路分析

这题挺复杂的。

你可能上来想先复制出来每个节点,

random 怎么办呢?

如果通过 val 去找 random 的指向时,

val 万一有重复呢?

所以复制一个链表单从它本身是做不到复制 random 的指向的。

那么就要借助原链表。

在原链表每个节点后面复制该节点:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jjGQd6U-1653478157569)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523204320160.png#pic_center)]

将复制的节点插入到原链表两两之间:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tL9dIr0O-1653478157569)(C:\Users\夜明\AppData\Roaming\Typora\typora-user-images\image-20220523204644671.png#pic_center)]

用一个 cur 从头开始遍历,

curnextrandom 就是对应拷贝节点的 random

currandomnext 就是拷贝节点的 random

如果 cur 的 random 不为 NULL ,

就连接 cur->next->randomcur->random->next

这样就完成了第一个拷贝节点的 random 的拷贝,

然后继续向下迭代即可 (cur=cur->next->next)

random 处理完成后再断开节点重新连接即可。

代码实现
struct Node* copyRandomList(struct Node* head) 
{
    if (head == NULL)
        return NULL;
        
    //插入拷贝节点
    struct Node* cur = head;
    while (cur)
    {
        struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->next = cur->next;
        newnode->val = cur->val;
        newnode->random = NULL; //random先置空,方便后面不用处理
        cur->next = newnode;
        cur = newnode->next;
    }

    //处理拷贝节点的random
    cur = head;
    while (cur)
    {
        struct Node* tmp = cur->random;
        if (tmp)
            cur->next->random = tmp->next;
        cur = cur->next->next;
    }

    //断开节点
    cur = head;
    struct Node* ret = cur->next;
    while (cur)
    {
        struct Node* copy = cur->next;
        struct Node* next = copy->next;
        cur->next = next;
        if (next)
            copy->next = next->next;
        cur = next;
    }

    return ret;
}

对链表进行插入排序

题目描述


在这里插入图片描述

在这里插入图片描述

思路分析

插入排序,可以创建一个新链表,

新链表的头 SortHead 指向原链表的头,

然后开始遍历原链表,

拿遍历到的节点与新链表中的每个节点进行比较,

直到找到合适的位置,然后插入。

插入的话有两种情况,

一种是上来就是最小的,直接头插,

另一种就是往里走一段再插,包含尾插和中间插。

如果是中间插则需要时刻记录前一个节点的地址,

下面用 p 来记录;

而如果尾插的话,p 实际指向最后一个节点,

就相当于在 p 和后面的空节点之间插入,

可以一块处理。

代码实现
struct ListNode* insertionSortList(struct ListNode* head)
{
    if (head == NULL || head->next == NULL)
        return head;
    struct ListNode* cur = head->next;
    struct ListNode* SortHead = head;
    SortHead->next = NULL;
    while (cur)
    {
        struct ListNode* next = cur->next;
        struct ListNode* p = NULL;
        struct ListNode* c = SortHead;
        while (c)
        {
            if (c->val > cur->val)
                break;
            else 
            {
                p = c;
                c = c->next;
            }
        }

        //头插
        if (p == NULL)
        {
            cur->next = c;
            SortHead = cur;
        }
        //尾插或中间插
        else
        {
            p->next = cur;
            cur->next = c;
        }
        cur = next;
    }
    return SortHead;
}

删除排序链表中的重复元素II

题目描述

在这里插入图片描述

思路分析

这道题比较麻烦。

就拿上图举例:

为了删除所有的 3

我要同时知道 24 两个节点的地址,

然后连接 24

继续删除 4 ,那就要知道 25 的地址,

然后连接 25

此时就需要三个指针:curprevnext

其中要比较 cur->calnext->val

如果二者相同,则 next 继续走,cur 不动,

直到 cur 走到头或二者不相等,

然后连接 prevnext ,继续进行迭代。

但是,如果 prev 为空呢?就如下面这种情况:

在这里插入图片描述

其实这种情况也好处理,

直接改变链表的头 head 就好了,

head 指向 next

代码实现
struct ListNode* deleteDuplicates(struct ListNode* head)
{
    if (head == NULL || head->next == NULL)
        return head;
    struct ListNode *prev = NULL, *cur = head, *next = head->next;
    
    //开始迭代
    while (next)
    {
        //遇到相同节点
        if (cur->val == next->val)
        {
            next = next->next;
            
            //next走到空时
            if (next == NULL)
            {
                //prev为空时
                if (prev == NULL)
                    head = next; 
                //prev不为空时
                else
                    prev->next = next;
                return head;
            }
            
            //next还没走到空时
            if (cur->val != next->val)
            {
                //prev为空时
                if (prev == NULL)
                    head = next; 
                //prev不为空时
                else
                    prev->next = next;
                cur = next;
                next = next->next;
            }
        }
        
        //继续迭代
        else 
        {
            prev = cur;
            cur = next;
            next = next->next;
        }
    }
    return head;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

LeePlace

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

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

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

打赏作者

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

抵扣说明:

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

余额充值