【基础算法总结】链表篇

news2024/11/24 12:30:14

目录

  • 一, 链表常用技巧和操作总结
  • 二,算法原理和代码实现
    • 2.两数相加
    • 24.两两交换链表中的节点
    • 143.重排链表
    • 23.合并k个升序链表
    • 25.k个一组翻转链表
  • 三,算法总结

一, 链表常用技巧和操作总结

有关链表的算法题也是一类常见并且经典的题型,这些题要使用数据结构中我们手搓的链表结构,也要使用STL中的 list 容器

下面是几点常用操作的总结
(1) 善于画图
(2) 引入虚拟头结点
方便处理边界情况,就是当没节点第一次头插或尾插的时候的那个判断,引入虚拟头结点就可以避免这个判断(写成 ptail = newHead)
(3) 不要吝啬空间,大胆定义变量。
(4) 快慢双指针的使用
主要应用是判环,找链表中环的入口,找链表中倒数第 n 个节点
(5) 解决链表类的题,一般是:创建一个新节点,尾插操作,头插操作

我们在学习数据结构时也做过许多有关链表类的经典题目,请浏览:【C/数据结构与算法】10道链表经典OJ。
里面的一些题目与本篇文章的题目也是有关联的,这篇文章算是进阶篇

二,算法原理和代码实现

2.两数相加

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法原理:

这道题是一道简单的模拟题,模拟两数相加即可
定义两个指针 cur1和cur2用于遍历两个链表,再定义一个整数 t 用于进位。用 t 和每个节点的值相加,取出 t 的个位创建新节点
在这里插入图片描述

细节问题:

(1) 这里要注意的细节就是链表是逆序的,其实这也是方便我们操作,因为我们可以直接从个位开始相加
(2) 1. 循环条件:cur1 || cur2 || t
这里要或t是原因:当两个指针都走完时,t里可能还有进位,还需要尾插

(3) 要销毁哨兵位头节点

代码实现:

class Solution 
{
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
    {
        ListNode* cur1 = l1, *cur2 = l2;
        ListNode* newHead = new ListNode(), *ptail = newHead; // 虚拟头节点
        int t = 0; // 记录进位
        
        while(cur1 || cur2 || t)
        {

            if(cur1) 
            {
                t += cur1->val;
                cur1 = cur1->next;
            }

            if(cur2) 
            {
                t += cur2->val;
                cur2 = cur2->next;
            }
            
            ListNode* newNode = new ListNode(t % 10);
            ptail->next = newNode;
            ptail = newNode;
            ptail->next = nullptr;
            t /= 10;
        }

        ListNode* pcur = newHead->next;
        delete newHead;

        return pcur;
    }
};

24.两两交换链表中的节点

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法原理:

本题也是一道模拟题,重点是画图,看图写代码!!! 定义四个指针
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/dd888c8a2f4e4bf098ccdb8d4b4c7392.png
根据交换后的链表,进行指针的修改(绿色字),一直循环迭代
结束条件:
(1) 偶数个节点时:
cur 已经指向空了,next 和 nnext 也无效了,此时结束循环。
在这里插入图片描述
(2) 奇数个节点时:
next 已经指向空了, nnext 无效了,此时也结束循环。
在这里插入图片描述

代码实现:

class Solution 
{
public:
    ListNode* swapPairs(ListNode* head) 
    {
        if(head == nullptr) return nullptr;

        ListNode* newHead = new ListNode(), *prev = newHead;
        ListNode* cur = head, *next = cur->next;
        ListNode* nnext = nullptr;

        if(next) nnext = next->next;

        prev->next = cur;

        while(cur && next)
        {
            // 交换节点
            prev->next = next;
            next->next = cur;
            cur->next = nnext;

            // 修改指针
            prev = cur;
            cur = prev->next;
            if(cur) next = cur->next;
            if(next) nnext = next->next;
        }

        ListNode* pcur = newHead->next;
        delete newHead;

        return pcur;
    }
};

143.重排链表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法原理:

这道题比较有综合性,通过分析可以发现:
重排后的链表 = 合并两个链表(原链表的前半段 + 原链表的后半段逆置)

在这里插入图片描述
所以这道题的思路是:
(1) 找到链表的中间节点
(2) 把后半部分逆序
(3) 合并两个链表

代码实现:

class Solution 
{
public:
    // 找中间节点
    ListNode* FindMiddleNode(ListNode* head)
    {
        ListNode* slow = head, *fast = head, *prev = head;
        while(fast && fast->next)
        {
            prev = slow;
            slow = slow->next;
            fast = fast->next->next;
        }
        prev->next = nullptr; // 与后半段断开

        return slow;
    }

    // 逆置后半部分链表
    ListNode* reverseList(ListNode* head)
    {
        ListNode* phead = head, *ptail = nullptr;
        while(phead)
        {
            ListNode* next = phead->next;
            // 头插法
            phead->next = ptail;
            ptail = phead;
            phead = next;
        }

        return ptail;
    }

    void reorderList(ListNode* head) 
    {
        // 处理特殊情况
        if(head->next == nullptr || head->next->next == nullptr) return;

        ListNode* mNode = FindMiddleNode(head);
        ListNode* rNode = reverseList(mNode);

        // 合并两个链表
        ListNode* newHead = new ListNode(), *ptail = newHead;
        ListNode* n1 = head, *n2 = rNode;
        while(n1 && n2)
        {
            ptail->next = n1;
            ptail = ptail->next;
            n1 = n1->next;

            ptail->next = n2;
            ptail = ptail->next;
            n2 = n2->next;
        }

        if(n1) ptail->next = n1;
        if(n2) ptail->next = n2;

        delete newHead;
    }
};

23.合并k个升序链表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法原理:

解法1:暴力解法,利用"合并两个有序链表"的思想。先把第一个和第二个链表进行合并,得到一个新链表,再用新链表和第三个链表进行合并…。
假设有 k 个链表,每条链表的平均节点有 n 个,则时间复杂度为:n(k-1) + n(k-2) + n(k-3) + … + n --> O(n * k^2)。大概率超时

解法2:利用优先级队列进行优化
假设有 k 个链表,建一个大小为 k 的小根堆,把所有链表的第一个节点都扔进小根堆中,堆顶节点就是最小值,取出堆顶节点和虚拟头结点进行链接,再移到下一个节点…
合并链接+找出最小值的节点总的时间复杂度为:O(n * k * logk)

易错/细节问题:

(1) 优先级队列的第三个模板参数不能直接写成 greater<ListNode * >,因为这样比较的是地址,要写一个仿函数!!
(2) 把链表的所有头节点都入队列时,要注意传递的链表为空的处理

代码实现:

class Solution
{
    struct cmp
    {
        bool operator()(const ListNode* l1, const ListNode* l2)
        {
            return l1->val > l2->val;
        }
    };
public:
    ListNode* mergeKLists(vector<ListNode*>& lists)
    {
    	// 建小根堆
        priority_queue<ListNode*, vector<ListNode*>, cmp> pq;
		
		// 把所有头结点进小根堆
        for (auto l : lists)
            if (l) pq.push(l);

		// 合并k个有序链表
        ListNode* newhead = new ListNode(), * ptail = newhead;
        while (!pq.empty())
        {
            ListNode* pcur = pq.top();
            pq.pop();
            ptail->next = pcur;
            ptail = pcur;
            if (pcur->next) pq.push(pcur->next);
        }

        ListNode* ret = newhead->next;
        delete newhead;

        return ret;
    }
};

解法3:分治-递归
和归并分治的思想一样:
在这里插入图片描述
假设有 k 个链表,那么就有 logk 层,递归回退时每个链表的每个节点都执行 logk 次合并,每个链表的平均节点有 n 个,所以时间复杂度为:O(n * k * logk)

易错/细节问题:

(1) 有两个递归出口:区间不存在和区间里只有一个链表
(2) 合并两个有序链表时,链表为空的情况

代码实现:

class Solution 
{
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        return merge(lists, 0, lists.size()-1);
    }

    ListNode* merge(vector<ListNode*>& lists, int left, int right)
    {
        if(left > right) return nullptr;
        if(left == right) return lists[left];

        // 找中点
        int mid = left + (right - left) / 2;
        // [left, mid] [mid+1, right]

        // 合并左右两边
        ListNode* l1 = merge(lists, left, mid);
        ListNode* l2 = merge(lists, mid+1, right);

        // 合并两个有序链表
        return mergeTwoList(l1, l2);
    }

    ListNode* mergeTwoList(ListNode* l1, ListNode* l2)
    {
        // 处理特殊情况
        if(l1 == nullptr) return l2;
        if(l2 == nullptr) return l1;

        ListNode head; // 把虚拟节点开在栈上
         head.next = nullptr;
        ListNode* ptail = &head;
        while(l1 && l2)
        {
            if(l1->val < l2->val)
            {
                ptail->next = l1;
                ptail = ptail->next;
                l1 = l1->next;
            }
            else
            {
                ptail->next = l2;
                ptail = ptail->next;
                l2 = l2->next;
            }
        }

        if(l1) ptail->next = l1;
        if(l2) ptail->next = l2;

        return head.next;
    }
};

25.k个一组翻转链表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法原理:

这也是一道模拟题,算法流程比较简单
(1) 先求出需要逆序多少组:n
(2) 重复 n 次,长度为 k 的链表的逆序即可 。
(3) 把不需要逆序的接上。
在这里插入图片描述

细节问题:

(1) 这里需要记录第n次头插的开始位置,每次进行新一轮逆序时都要记住该位置。
(2) 如果使用while循环,则每逆序完一组后 k 要重新赋值,不然就一直都是k==0了

代码实现:

class Solution
{
public:
    ListNode* reverseKGroup(ListNode* head, int k)
    {
        // 先求出要逆序多少组
        int n = 0;
        ListNode* phead = head;
        while (phead)
        {
            phead = phead->next;
            n++;
        }
        n /= k;

        // 重复 n 次长度为 k 的链表逆序
        phead = head;
        ListNode* newhead = new ListNode(), * prev = newhead;
        for (int i = 0; i < n; i++)
        {
            ListNode* tmp = phead; // 记录每一组的起点
            for (int j = 0; j < k; j++)
            {
                ListNode* next = phead->next;
                phead->next = prev->next;
                prev->next = phead;
                phead = next;
            }
            prev = tmp;
        }

        // 把不需要翻转的接上
        prev->next = phead;

        ListNode* ret = newhead->next;
        delete newhead;

        return ret;
    }
};

三,算法总结

通过上面的若干道题目可以发现,大多数链表类的题目本质上是模拟题,最重要的就是画图,分清各个指针指向哪里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

案例-猜数字游戏

文章目录 效果展示初始画面演示视频 代码区 效果展示 初始画面 演示视频 猜数字游戏 代码区 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width,…

【题解】【动态规划,最长上升子序列LIS】—— [CSP-J 2022] 上升点列

【题解】【动态规划&#xff0c;最长上升子序列LIS】—— [CSP-J 2022] 上升点列 [CSP-J 2022] 上升点列题目描述输入格式输出格式输入输出样例输入 #1输出 #1输入 #2输出 #2 提示 1.题意解析2.AC代码 [CSP-J 2022] 上升点列 通往洛谷的传送门 题目描述 在一个二维平面内&am…

GAMES101(19节,相机)

相机 synthesis合成成像&#xff1a;比如光栅化&#xff0c;光线追踪&#xff0c;相机是capture捕捉成像&#xff0c; 但是在合成渲染时&#xff0c;有时也会模拟捕捉成像方式&#xff08;包括一些技术 动态模糊 / 景深等&#xff09;&#xff0c;这时会有涉及很多专有名词&a…

确保接口安全:六大方案有效解决幂等性问题

文章目录 六大方案解决接口幂等问题什么是接口幂等&#xff1f;天然幂等不做幂等会怎么样&#xff1f; 解决方案1&#xff09;insert前先select2&#xff09;使用唯一索引3&#xff09;去重表加悲观锁4&#xff09;加乐观锁之版本号机制5&#xff09;使用 Redisson 分布式锁6&a…

银河麒麟系统内存清理

银河麒麟系统内存清理 1、操作步骤2、注意事项 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 当银河麒麟系统运行较长时间&#xff0c;内存中的缓存可能会积累过多&#xff0c;影响系统性能。此时&#xff0c;你可以通过简单的命令来清理这…

问:LINUXWINDOWS线程CPU时间如何排序?

Linux 在Linux上&#xff0c;你可以使用ps命令结合sort命令来查看和排序进程或线程的CPU使用时间。 查看进程的CPU使用时间并按时间排序 使用ps命令的-o选项可以自定义输出格式&#xff0c;-e选项表示显示所有进程&#xff0c;--sort选项用于排序。 ps -e -o pid,tid,comm,…

排序大全(干货)

目录 1. 插入排序步骤&#xff1a; 2.选择排序思路&#xff1a;每次从待排序列中选出一个最小值&#xff0c;然后放在序列的起始位置&#xff0c;直到全部待排数据排完即可。实际上&#xff0c;我们可以一趟选出两个值&#xff0c;一个最大值一个最小值&#xff0c;然后将其放…

【springboot】整合LoadBalancer

目录 问题产生背景解决方案&#xff1a;实现LoadBalancer1. 添加依赖2. 配置文件3. 使用LoadBalancer4. 使用 RestTemplate 进行服务调用5. 测试 问题产生背景 以下是一个购物车项目&#xff0c;通过调用外部接口获取商品信息&#xff0c;并添加到购物车中&#xff0c;这段代码…

如何使用ssm实现中学生课后服务的信息管理与推荐+vue

TOC ssm766中学生课后服务的信息管理与推荐vue 第一章 绪论 1.1 选题背景 目前整个社会发展的速度&#xff0c;严重依赖于互联网&#xff0c;如果没有了互联网的存在&#xff0c;市场可能会一蹶不振&#xff0c;严重影响经济的发展水平&#xff0c;影响人们的生活质量。计算…

查缺补漏----I/O中断处理过程

中断优先级包括响应优先级和处理优先级&#xff0c;响应优先级由硬件线路或查询程序的查询顺序决定&#xff0c;不可动态改变。处理优先级可利用中断屏蔽技术动态调整&#xff0c;以实现多重中断。下面来看他们如何运用在中断处理过程中&#xff1a; 中断控制器位于CPU和外设之…

SpringBoot开发:古典舞在线交流平台的架构与实现

第三章 系统分析 3.1 可行性分析 需要使用大部分精力开发的古典舞在线交流平台为了充分降低开发风险&#xff0c;特意在开发之前进行可行性分析这个验证系统开发是否可行的步骤。本文就会从技术角度&#xff0c;经济角度&#xff0c;还有操作角度等进行综合阐述。 3.1.1技术可行…

排序01 多目标模型

引入 使用机器学习方法对指标做预估&#xff0c;再对预估分数做融合。融合方法&#xff1a;加权和方法给不同指标赋予不同的权重&#xff0c;权重是做A/B test调试得到的。还有更好地融合方法。 多目标模型 排序模型的输入是各种各样的特征&#xff0c;用户特征主要是用户id和…

易趋(EasyTrack)资深顾问唐颖受邀为第四届中国项目经理大会演讲嘉宾

全国项目经理专业人士年度盛会 易趋&#xff08;EasyTrack&#xff09;资深顾问唐颖女士受邀为PMO评论主办的全国项目经理专业人士年度盛会——2024第四届中国项目经理大会演讲嘉宾&#xff0c;演讲议题为“隐形翅膀——数字化项目管理助力项目经理鹏程万里”。大会将于10月26-…

古典舞在线互动:SpringBoot平台设计与功能实现

第三章 系统分析 3.1 可行性分析 需要使用大部分精力开发的古典舞在线交流平台为了充分降低开发风险&#xff0c;特意在开发之前进行可行性分析这个验证系统开发是否可行的步骤。本文就会从技术角度&#xff0c;经济角度&#xff0c;还有操作角度等进行综合阐述。 3.1.1技术可行…

Prometheus之Pushgateway使用

Pushgateway属于整个架构图的这一部分 The Pushgateway is an intermediary service which allows you to push metrics from jobs which cannot be scraped. The Prometheus Pushgateway exists to allow ephemeral and batch jobs to expose their metrics to Prometheus. S…

扩散引导语言建模(DGLM):一种可控且高效的AI对齐方法

随着大型语言模型(LLMs)的迅速普及,如何有效地引导它们生成安全、适合特定应用和目标受众的内容成为一个关键挑战。例如,我们可能希望语言模型在与幼儿园孩子互动时使用不同的语言,或在撰写喜剧小品、提供法律支持或总结新闻文章时采用不同的风格。 目前,最成功的LLM范式是训练…

使用python基于DeepLabv3实现对图片进行语义分割

DeepLabv3 介绍 DeepLabv3 是一种先进的语义分割模型&#xff0c;由 Google Research 团队提出。它在 DeepLab 系列模型的基础上进行了改进&#xff0c;旨在提高图像中像素级分类的准确性。以下是 DeepLabv3 的详细介绍&#xff1a; 概述DeepLabv3 是 DeepLab 系列中的第三代…

无人机控制和飞行、路径规划技术分析

无人机控制和飞行、路径规划技术是现代无人机技术的核心组成部分&#xff0c;它们共同决定了无人机的性能和应用范围。以下是对这些技术的详细分析&#xff1a; 一、无人机控制技术 无人机控制技术主要涉及飞行控制系统的设计、传感器数据的处理以及指令的发送与执行。飞行控…

新闻推荐系统开发:Spring Boot实践指南

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…

厂商资源分享网站

新华三&#xff08;H3C&#xff09;是一家中国知名的网络设备供应商&#xff0c;提供网络设备、网络解决方案和云计算服务。公司成立于2003年&#xff0c;是华为公司和惠普公司合资的企业&#xff0c;总部位于中国深圳。 华为&#xff08;Huawei&#xff09;是一家全球知名的电…