暑期算法训练.11

发布于:2025-08-01 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

47. 力扣203 移除链表元素

47.1 题目解析:

​编辑 

47.2 算法思路:

47.3 代码演示:

​编辑 

48. 力扣2.两数相加

48.1 题目解析:

​编辑 

48.2 算法思路;

48.3 代码演示:

48.4 总结反思:

49. 力扣24 两两交换链表中的节点

49.1 题目解析:

49.2 算法思路:

49.3 代码演示:

​编辑

49.4 总结反思

50. 力扣 143 重排链表

50.1 题目解析:

50.2 算法思路:

 

50.3 代码演示:

​编辑

50.4 总结反思

51 力扣 25 k个1组翻转链表

51.1题目解析:

51.2 算法思路:

​编辑 

 

51.3 代码演示:

 ​编辑

51.4 总结反思


今天咱们讲链表,以及链表常用操作总结:

就是咱们接下来的题目,基本上都会用到这几种方法:

1.画图!!!(这个画图特别重要),因为画图直观加形象加便于我们理解

2.咱们在做题的时候,可以引入虚拟节点:

【1】虚拟头结点可以简化边界的处理。

【2】可以统一操作,不需要再单独的对头节点进行特殊操作。为什么这么说呢?是因为

那么接下来咱们通过一道题目来进行实际的探查:

47. 力扣203 移除链表元素

47.1 题目解析:

 

题目很简单。就是删除值为val的某个节点

47.2 算法思路:

就是遍历整个链表,找到值为val的那个元素,然后再跳过这个元素即可。

47.3 代码演示:

 

//不带虚拟头结点
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 需要单独处理头节点,确保头结点不为空,并且里面的数值也不可以是空,头结点是个有效的头结点
        while (head != nullptr && head->val == val) {
            head = head->next;
        }
        // 再处理其他节点
        ListNode* cur = head;
        while (cur != nullptr && cur->next != nullptr) {
            if (cur->next->val == val) {
                cur->next = cur->next->next;
            }
            else {
                cur = cur->next;
            }
        }
        return head;
    }
};

这个是不带虚拟头结点的版本,可以看出来,咱们需要单独的处理一下头结点,然后再是处理其他的节点。并且,如果这样的话,步骤就比较繁琐,而且不容易统一处理方式

//带虚拟头结点的版本
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummy = new ListNode(0); // 虚拟头节点指向head
        dummy->next = head;
        ListNode* cur = dummy;
        while (cur->next != nullptr) {
            if (cur->next->val == val) {
                cur->next = cur->next->next; // 统一删除逻辑,直接跳过这个节点即可,也是单向链表通常的删除逻辑
            }
            else {
                cur = cur->next;
            }
        }
        return dummy->next; // 新头节点
    }
};

这个是带虚拟头结点的版本,是不是很爽,基本可以统一处理节点了(包括头节点)。

所以,虚拟头节点的好处就是:

1. 避免头节点的特殊处理

  • 问题:在链表操作中,如果直接操作头节点(如删除、反转、插入等),通常需要额外判断:

    • 如果删除头节点,需要重新指定 head

    • 如果插入新节点到头部,需要更新 head

  • 虚拟头结点的作用

    • 提供一个统一的“前驱节点”,使得头节点和其他节点的操作逻辑一致。

    • 无论原头节点如何变化,dummy->next 始终指向新头节点。

2. 简化代码逻辑

  • 没有虚拟头结点:需要单独处理头节点,代码分支增多。

  • 有虚拟头结点:所有节点(包括原头节点)都有前驱节点,可以用统一逻辑处理。

 

在C++中,虚拟头节点一般这样创建:

ListNode* dummy = new ListNode(0); // 值任意,一般用0
dummy->next = head; // 指向原头节点
// ... 操作链表
return dummy->next; // 返回新头节点

3.不要吝啬空间,该创建的时候就得创建,这样可以省去不少的麻烦:

想必大家多多少少都做过这种题目吧,就是断开两个节点之间的链接,让第三个节点链接上去。那么此时,咱们如果不定义一个next节点变量,直接就有两个变量(prev,cur)去连接的话,此时就得注意一下顺序了。只能按照咱们图中所指的顺序来进行链接。

prev->next->prev=cur;
cur->next=prev->next;
prev->next=cur;
cur->prev=prev;

如果前两行与后两行颠倒了,先执行后两行,那么这样的话,会发现找不到后面那个节点了,也会导致cur->next=cur,自己指向自己,死循环。所以,这个时候就体现了,再定义一个新变量的必要性。再定义一个新变量,就不需要再担心执行的顺序了,管你先执行那个,都可以。

4.快慢指针

一般用于链表中环的判断,找到链表中环的入口,找到链表中倒数第n个节点。

而咱们链表的常用操作就是1.创建一个新的节点,2.尾插 3.头插(这个头插法在逆序链表中还是比较常用的)

这个是尾插,还是比较简单的。

这个是头插。相对于尾插来说,还是复杂一点的。

48. 力扣2.两数相加

48.1 题目解析:

 

这个题目有个关键的地方就是逆序操作,咱们可以直接从头开始加(因为这个是逆序存储的),即2,4,3 逆序存储为 3,4,2。5,6,4 逆序存储为4,6,5 。那么3,4,2与4,6,5相加,由于是从最低位开始加,那么逆序之后,对应的就从链表的头开始相加,非常的方便。

48.2 算法思路;

直接模拟两数相加即可

 

以上就是这个题目的所有的算法思路。

48.3 代码演示:

struct ListNode {
    int val;
    ListNode* next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode* next) : val(x), next(next) {}

};
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* cur1 = l1; ListNode* cur2 = l2;
        ListNode* newnode = new ListNode(0);//创建一个虚拟头结点,记录最终结果
        int t = 0;
        ListNode* prev = newnode;
        while (cur1 || cur2 || t)
        {
            if (cur1)
            {
                t += cur1->val;
                cur1 = cur1->next;
            }
            if (cur2)
            {
                t += cur2->val;
                cur2 = cur2->next;
            }
            prev->next = new ListNode(t % 10);//新建一个节点才能去存储数字,因为此时newnode后面并没有节点。
            prev = prev->next;
            t /= 10;
        }
        ListNode* next = newnode->next;
        delete newnode;
        return next;
    }
};

48.4 总结反思:

这道题目充分的利用了上面的知识,所以还得具备充分的知识储备才可以。

49. 力扣24 两两交换链表中的节点

49.1 题目解析:

本题交换节点需要注意的是,不可以只交换节点里面的数值,必须得连着节点一起交换才可以。

49.2 算法思路:

OK,算法思路就是上面我写的这些,大家好好的参悟一下

49.3 代码演示:

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (head == nullptr || head->next == nullptr) return head;//若是链表为空,或者是链表只有一个元素,直接返回。注意,这里判断的标准时head,因为head才是真正的头结点,而不是newHead。判断这个的目的是为了预防下面的cur->next(cur为空,cur就是头结点,链表为空)与next->next(链表只有一个元素,此时next为空),空指针解引用
        ListNode* newHead = new ListNode(0);
        newHead->next = head;//head才是真正的头结点
        ListNode* prev = newHead;
        ListNode* cur = prev->next; ListNode* next = cur->next; ListNode* nnext = next->next;
        while (cur && next)
        {
            //更新节点
            prev->next = next;
            next->next = cur;
            cur->next = nnext;
            //交换指针
            prev = cur;
            cur = nnext;
            if (cur) next = cur->next;
            if (next) nnext = next->next;
        }
        ListNode* kk = newHead->next;
        delete newHead;
        return kk;
    }
};

49.4 总结反思

其实这道题目也是用到了上面咱们所说的这些算法,大家还是得好好的参悟一下

50. 力扣 143 重排链表

50.1 题目解析:

这个题目其实是让你按照一定得顺序重新排一下链表

 

那么其实题目就是这样的,所以咱们可以分为三个步骤

1.找到链表的中间节点

使用快慢指针(一定要画图,考虑使用slow得落点在哪里)

2.把后面的部分逆序

使用双指针(或者三指针),或者头插法(推荐)完成逆序

3.合并两个有序链表,(双指针),按照一定得顺序合并

50.2 算法思路:

 关于slow的落点,咱们直接砍掉slow后面的部分给逆序即可。不过需要引入虚拟头结点。一开始得先头插进第二个链表:

 

 

 

 

50.3 代码演示:

class Solution {
public:
    void reorderList(ListNode* head) {
        if (head == nullptr || head->next == nullptr || head->next->next == nullptr) return;
        //先找到中间节点
        ListNode* slow = head; ListNode* fast = head;
        while (fast && fast->next)//注意循环条件是这个,不是slow<fast
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        //之后使用头插法对slow后面的部分进行逆序
        ListNode* head2 = new ListNode(0);

        ListNode* cur = slow->next;
        slow->next = nullptr;
        while (cur)
        {
            ListNode* next = cur->next;//再定义一个变量存储这个cur,就是为了防止顺序问题
            cur->next = head2->next;
            head2->next = cur;
            cur = next;
        }
        //之后进行合并链表
        ListNode* kk = new ListNode(0);
        ListNode* prev = kk;
        ListNode* cur1 = head;
        ListNode* cur2 = head2->next;
        while (cur1)
        {
            //先合并第一个链表
            prev->next = cur1;
            cur1 = cur1->next;
            prev = prev->next;
            //再合并第二个链表
            if (cur2)
            {
                prev->next = cur2;

                prev = prev->next;
                cur2 = cur2->next;
            }
        }

        delete head2;
        delete kk;


    }
};

这样的代码量,放在整个链表对应的题目里面,算是比较多的了

50.4 总结反思

重点的东西还是开篇所讲的那些东西,还是希望大家要好好的研读,最好记住。

51 力扣 25 k个1组翻转链表

51.1题目解析:

其实这一题就考了一个模拟,并且也用到了逆序

51.2 算法思路:

 

 

 

51.3 代码演示:

 

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {

        //求出需要逆序多少组
        int n = 0;
        ListNode* cur = head;
        while (cur)
        {
            cur = cur->next;
            n++;
        }
        n /= k;

        //重复n次,将长度为k的链表逆序即可
        ListNode* newHead = new ListNode(0);
        ListNode* prev = newHead;
        ListNode* cur1 = head;
        for (int i = 0; i < n; i++)
        {
            ListNode* tmp = cur1;//把一开始的cur1的位置给存起来,由于是逆序存储,所以说后面的tmp的位置,就是下一组逆序头插的位置
            for (int j = 0; j < k; j++)//不可写成whilw(k--)
                /*k 被修改后没有恢复:内层 while (k--) 会直接修改 k 的值,导致后续循环(如果有)的 k 不正确。例如:

                第一次循环 k = 3,执行完 while (k--) 后 k = -1。

                如果还有第二次循环,k 已经变成 - 1,导致内层循环不执行,逻辑错误。

                n 被修改后不影响逻辑,但 k 被修改会导致后续组的反转次数错误。*/
            {
                ListNode* next = cur1->next;
                cur1->next = prev->next;
                prev->next = cur1;
                cur1 = next;
            }
            prev = tmp;
        }
        //剩下的直接接在后面即可
        prev->next = cur1;
        ListNode* cur2 = newHead->next;
        delete newHead;
        return cur2;
    }
};

这个地方,作者一开始写的时候,出现了一个失误,给大家看一下:

这个是错误的:
while(n--)  // 直接修改n的值
{
    ListNode* tmp = cur1;
    while(k--)  // 直接修改k的值
    {
        // 反转逻辑
    }
    prev = tmp;
}

问题:

  1. k 被修改后没有恢复:内层 while(k--) 会直接修改 k 的值,导致后续循环(如果有)的 k 不正确。例如:

    • 第一次循环 k=3,执行完 while(k--) 后 k=-1

    • 如果还有第二次循环,k 已经变成 -1,导致内层循环不执行,逻辑错误。

  2. n 被修改后不影响逻辑,但 k 被修改会导致后续组的反转次数错误。

这个是正确的:
for(int i=0; i<n; i++)  // 不修改n
{
    ListNode* tmp = cur1;
    for(int j=0; j<k; j++)  // 不修改k
    {
        // 反转逻辑
    }
    prev = tmp;
}

改进点:

  1. 使用 for 循环代替 while 循环

    • for(int j=0; j<k; j++) 不会修改 k 的值,确保每次反转都是 k 个节点。

    • for(int i=0; i<n; i++) 也不会修改 n,但即使修改 n 也不会影响逻辑。

  2. 避免了 k 被错误修改

    • 由于 k 保持不变,每次都能正确反转 k 个节点。

 

51.4 总结反思

今天的知识基本都用到了我在开篇所讲的那些知识

本篇完................. 

 

 

 

 

 

 


网站公告

今日签到

点亮在社区的每一天
去签到