数据结构:链表的一些经典的OJ题目,环形链表问题

news2024/12/26 13:28:12

文章目录

  • 写在前面
  • 链表OJ调试技巧
  • 移除链表元素
  • 反转链表
  • 链表的中间节点
  • 链表中倒数第K个节点
  • 链表分割问题
  • 环形链表


写在前面

本篇为本人学习链表的过程中遇到的典型OJ题,于是整理出来分享思路和便于后续重新学习,每个标题均可跳转至对应习题,大多为Leetcode


链表OJ调试技巧

Leetcode中只能看到函数体,不能看到链表的具体情况,因此调试存在困难,自己搭建链表又过于繁琐,这里介绍一种很方便的链表调试技巧

原理如下

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

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

int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	assert(n1);
	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	assert(n2);
	struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	assert(n3);
	struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
	assert(n4);
	struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
	assert(n5);

	n1->val = 1;
	n2->val = 2;
	n3->val = 3;
	n4->val = 4;
	n5->val = 5;

	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	n5->next = NULL;
}

我们只需要创建出每一个节点,接着把这些节点都连在一起,手动创建一个链表,链表中需要多少个元素自己搭建,这样可以便于调试,解决OJ中遇到的问题~~

介绍完调试的方法,正式总结链表中的OJ题目


移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

在这里插入图片描述

1.遍历,是val的删掉

一种暴力解决的方法,图示如下

在这里插入图片描述

代码实现如下

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


struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* prev=NULL;
    struct ListNode* cur=head;
    while(cur)
    {
        if(cur->val==val)
        {
            if(prev)
            {
                prev->next=cur->next;
                free(cur);
                cur=prev->next;
            }
            else
            {
                cur=head->next;
                free(head);
                head=cur;
            }
        }
        else
        {
            prev=cur;
            cur=cur->next;
        }
    }
    return head;
}

2.遍历链表,把不是val的拿下来尾插

图解过程如下:

在这里插入图片描述

代码如下所示

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* cur=head;
    struct ListNode* newnode = NULL;
    struct ListNode* tail = NULL;
    while(cur)
    {
        if(cur->val!=val)
        {
            if(tail==NULL)
            {
                newnode=tail=cur;
            }
            else
            {
                tail->next=cur;
                tail=tail->next;
            }

            cur=cur->next;
        }
        else
        {
            struct ListNode* del=cur;
            cur=cur->next;
            free(del);
        }
    }

    if(tail)
        tail->next=NULL;

    return newnode;
}

反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

在这里插入图片描述

下面介绍两种思路,均能解决问题

1.改变节点指向

在这里插入图片描述
定义三个节点(思考为什么定义三个而不是两个?)而后通过指针遍历把cur指向的节点方向指向前一个,然后通过迭代到末尾,当cur为空退出循环

代码实现如下

/**
 * 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* prev=NULL;
    struct ListNode* cur=head;
    struct ListNode* next=head->next;
    
    while(cur)
    {
        cur->next=prev;

        prev=cur;
        cur=next;
        if(next)
            next=next->next;
    }

    return prev;
}

2.将每一个节点都头插

是一个新方法,只需要把新创建一个头,然后把每一个节点都头插进来即可,下面为原理图

在这里插入图片描述

代码实现如下

struct ListNode* reverseList(struct ListNode* head)
{
	struct ListNode* cur = head;
	struct ListNode* rhead = NULL;

	while (cur)
	{
		struct ListNode* next = cur->next;
		
		//头插
		cur->next = rhead;
		rhead = cur;

		//迭代
		cur = next;
	}
	return rhead;
}

关于代码简洁性:

代码简洁程度取决于特殊情况的处理,例如,假设这里把next定义在while循环外,那么假设参数为NULL,就需要对特殊情况做处理,此时代码就多了if条件,整体来说简洁程度就降低了

简洁程度需要对特殊情况的处理足够好,才能足够精炼解决问题


链表的中间节点

给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

在这里插入图片描述
1.遍历链表求长度

代码实现如下

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


struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* plist=head;
    int flag=0;
    while(plist)
    {
        while(plist->next!=NULL)
        {
            plist=plist->next;
            flag++;
        }
        break;
    }
    flag+=1;
    flag=flag/2+1;
    for(int i=1;i<flag;i++)
    {
        head=head->next;
    }
    return head;
}

2.快慢指针法

定义两个指针,一个指针是慢指针,每次走一步;一个指针是快指针,每次走两步。当快指针走到末尾时,慢指针走的位置恰好为整个链表的一半

相比起思路1来说,时间复杂度只是O(N),从时间复杂度来讲,这个方法更优

具体实现原理如下图所示

在这里插入图片描述

代码实现如下

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个节点

描述
输入一个链表,输出该链表中倒数第k个结点。

输入:1,{1,2,3,4,5}
输出:{5}

在这里插入图片描述

1.遍历求长度,根据k求结果

代码思路和上题十分类似,十分简单

/**
 * 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
    struct ListNode* cur=pListHead;
    struct ListNode* prev=pListHead;
    int flag=0;
    while(cur)
    {
        cur=cur->next;
        flag++;
    }
    if(k>flag)
    {
        return NULL;
    }
    for(int i=0;i<flag-k;i++)
    {
        prev=prev->next;
    }
    return prev;
}

2.仿照上题,快慢指针的方法解决

和上题相比,略有不同的一点是,上面的快指针每次走的是一样的,而对于这个题来说,假设K=4,那么就令快指针每次走四格,其余思路和上题类似

总结原理:这里既然要求的是倒数第k个,那么就令fast指针和slow指针之间差k个,然后两个一起走,当fast指针到末尾的时候,slow所在的位置就是fast位置减去它们一开始的距离差,也就是倒数k个的位置

整体思路是十分巧妙的,其实原理和方法1类似,都是找到最后一个元素,然后再找前面倒数的元素,但相比起来使用快慢指针的方法可以让时间复杂度缩短到最少

代码实现如下

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    struct ListNode* fast=pListHead;
    struct ListNode* slow=pListHead;
    for(int i=0;i<k;i++)
    {
        if(fast)
            fast=fast->next;
        else
         return NULL;
    }
    while(fast)
    {
        fast=fast->next;
        slow=slow->next;
    }
    return slow;
}

这里思考
第一次走k步和走k-1步有什么区别?


链表分割问题

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

由于题目描述中提到,不能改变原来的数据顺序,那么也就意味着只能尾插,因为尾插不会破坏数据顺序

于是,我们要做的实际上就是把小于val的单独拿出来,大于等于val的单独拿出来,创建两个节点让拿出来的这两份数据分别进行尾插,最后再实现链表的合并,即可解决这个问题

代码的实现也相对简单,代码实现如下

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        struct ListNode* lesshead,*lesstail,*greaterhead,*greatertail;
        lesshead=lesstail=(struct ListNode*)malloc(sizeof(struct ListNode));
        greaterhead=greatertail=(struct ListNode*)malloc(sizeof(struct ListNode));

        struct ListNode* cur=pHead;
        while(cur)
        {
            if(cur->val<x)
            {
                lesstail->next=cur;
                lesstail=lesstail->next;
            }
            else 
            {
                greatertail->next=cur;
                greatertail=greatertail->next;
            }
            cur=cur->next;
        }

        //链表合并
        lesstail->next=greaterhead->next;
        greatertail->next=NULL;
        pHead=lesshead->next;
        free(lesshead);
        free(greaterhead);
        return pHead;
    }
};

环形链表

环形链表是链表中的一类题,先看环形链表的基本题型

  1. 如何判断一个链表是否带环?
  • 可以使用快慢指针解法,快指针一次走两步,慢指针一次走一步,如果slow指针和fast指针在环中相遇,就说明链表带环,否则快指针独自走完就说明不带环
  1. 快指针一次走两步,慢指针一次走一步,这样的情况下快慢指针一定会相遇吗?
  • 是的,一定会相遇,fast一次走两步,slow一次走一步,假设进环后它们之间相差的距离为N,那么它们两个每走一次,之间的距离就会少1,因此最后一定会相遇
  1. 快指针一次走三步,慢指针一次走一步,这样的情况下快慢指针一定会相遇吗?
  • 不一定,这和它们之间的初始距离还有环的长度有关,假设进环的时候它们之间的距离为N,那么快慢指针每走一次,它们两个之间的距离就少2,如果这里的N为偶数,就会相遇,如果N为奇数,在此次追击中会错过,此时会进入下一次追击,假设环的长度为C,那么此时它们之间的距离为C-1,那么C-1如果为偶数快慢指针还能相遇,如果C-1为奇数快慢指针无法相遇
  1. 慢指针一次走一步,快指针一次走n步(n>2)可以吗?
  • 假设快指针每次走三步,慢指针每次走一步,此时快指针先进环,慢指针后进环,假设慢指针进环的时候,快指针的位置如图所示,则此时按照上述方法进行绕环移动,每次快指针走三步,慢指针走一步,永远不会相遇,快指针刚好把慢指针套圈了,因此不可以
  • 只有快指针走两步,慢指针走一步才可以,因为环的最小长度是1,即使套圈了两个指针也在相同的位置

  1. 给定一个带环的链表,如何求链表开始入环的第一个节点?
  • 思路:一个指针从相遇点走,一个指针从起始点走,两个指针会在入口相遇
  • 假设快指针一次走两步,慢指针一次走一步,那么快指针走的路程是慢指针走的路程的两倍,从起始点到进环口的距离设为L,进环口到相遇点之间的距离为X,环的长度为C,fast进入环后绕环n圈(假设初始为1)
  • 慢指针走的路程为L+X,快指针走的路程为L+X+n* C,因此化简后L=n* C-X通常n为1,因此L=C-X,因此从上述结论成立

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

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

相关文章

未注册商标乱用R标识,后果很严重!

在生活中&#xff0c;如果你留心观察的话会发现&#xff0c;很多品牌会在商标的右上角或右下角印一个标识。这个代表什么含义呢&#xff1f;哪些商标可以使用&#xff1f;私自乱用会有什么后果&#xff1f; 其实&#xff0c;标识代表的是“注册商标”&#xff0c;即这个商标是…

PostMan调用metersphere接口 ,copy完事~

获取token接口&#xff1a; http://192.****:8081/signin &#xff0c;接下来就可以调用其他功能的接口了 例&#xff1a;创建账户&#xff0c;将获取到的access_token放置在接口请求的token中 其他接口调用同上

SpringBoot笔记:SpringBoot集成Dataway

文章目录 1、什么是 Dataway?2、主打场景3、技术架构4、整合SpringBoot4.1、maven 依赖4.2、初始化脚本4.3、整合 SpringBoot 5、Dataway 接口管理6、Mybatis 语法支持7、小结 1、什么是 Dataway? 官网地址&#xff1a;https://www.hasor.net/docs/guides/quickstart Da…

Linux虚拟机安装JDK

目录 第一章、xshell工具和xftp的使用1.1&#xff09;xshell下载与安装1.2&#xff09;xshell连接1.3&#xff09;xftp下载安装和连接 第二章、安装Java的JDK2.1&#xff09;关闭防火墙&#xff0c;传输jdk压缩包到Linux虚拟机22&#xff09;配置java环境变量 第一章、xshell工…

有效实践:优化项目工作中的文件协同

在项目工作过程中&#xff0c;每天都会产出大量文件&#xff0c;团队需要频繁进行文件协同工作。文件协同在现代办公生活中扮演着至关重要的角色。文件协同可以帮助团队成员共享信息、协调工作、提高工作效率。 然而传统的文件协同方式不仅效率低下&#xff0c;而且容易出现错漏…

无法加载 DLL“xxxx.dll”: 找不到指定的模块。 (异常来自 HRESULT:0x8007007E)。

目录 1、DLL引用的问题 1、DLL引用的问题 这两篇文章说的很好&#xff0c;[C# DllImport用法和路径问题(https://blog.csdn.net/spw55381155/article/details/83624733)和无法加载 DLL“xxxx.dll”: 找不到指定的模块 但我折腾了2天&#xff0c;还是没有解决。后来请同事帮忙…

解决allure-report下index.html文件打开空白显示数据

问题描述 Darren洋在把jmeter生成的.jtl文件转化为allure测试报告html页面的过程中发现&#xff0c;allure测试报告一直显示空白无数据的情形&#xff0c;且其他配置无误的情况下。 解决办法 其实在allure-report下index.html文件是不能直接打开的&#xff0c;出现页面都是load…

2023年受人欢迎的低代码开发平台大盘点

随着企业对于降低成本和加快软件开发的需求增加&#xff0c;低代码开发平台逐渐成为一种受欢迎的选择。这些平台提供了拖放界面和预置组件&#xff0c;使得开发人员可以用更少的代码创建复杂的应用软件。低代码开发平台不仅有助于企业加速数字化转型&#xff0c;而且还能打破业…

读书笔记——《走出荒野》

前言 之所以接触到这本书&#xff0c;是因为在电子书软件上看到奥巴马推荐书籍&#xff0c;其中这一本书感觉很特别&#xff0c;而且评分还不错&#xff0c;所以就决定看一看。刚开始看了下介绍&#xff0c;说是一个徒步旅行爱好者的自传&#xff0c;估计书内也就写写旅途的风…

无涯教程-Lua - 函数声明

函数是一起执行任务的一组语句&#xff0c;您可以将代码分成单独的函数。 Lua语言提供了程序可以调用的许多内置方法。如方法 print()打印在控制台中作为输入传递的参数。 定义函数 Lua编程语言中方法定义的一般形式如下- optional_function_scope function function_name(…

【Golang 接口自动化05】使用yml管理自动化用例

目录 YAML 基本语法 对象&#xff1a;键值对的集合(key:value) 数组&#xff1a;一组按顺序排列的值 字面量&#xff1a;单个的、不可再分的值&#xff08;数字、字符串、布尔值&#xff09; yml 格式的测试用例 定义yml文件 创建结构体 读取yml文件中的用例数据 调试…

一起学算法(顺序表篇)

概念&#xff1a; 1.顺序表的定义 用一段地址连续的存储单元依次存储数据的线性表被称为数据表&#xff0c;在Java中顺序表一般是数组或者是ArrayList实现的 先把代码放这里&#xff0c;接下来一一给大家进行讲解&#xff1a; public class SeqList {private Object[] data;…

HTML 初

前言 HTML的基本骨架 HTML基本骨架是构建网页的最基本的结果。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0">…

Chisel-Strike:一款功能强大的.NET异或XOR加密CobaltStrike Aggressor实现

## 关于Chisel-Strike Chisel-Strike是一款功能强大的.NET异或XOR加密CobaltStrike Aggressor实现&#xff0c;该工具主要针对的是 Chisel&#xff08;一个通过HTTP实现的快速TCP/UDP信道&#xff09;&#xff0c;旨在实现运行速度更快的代理以及更加高级的Socks5功能。 根据…

新能源汽车下半场,“哪吒”们还有机会吗?

【潮汐商业评论/原创】 “现在油价太贵了&#xff0c;上班都快开不起车了。同事Andy买了辆新能源&#xff0c;在家充满电也就40多块钱&#xff0c;可以跑500多公里&#xff0c;我加200的油&#xff0c;还没人家跑得多。已经在考虑要不要换辆新能源汽车了。”Allen无奈道。 在…

运营商的风控难题该如何破解?

一、运营商难题 01 黑产养卡 这个产业是运营商独有的难题&#xff1a;部分虚拟运营商走线上渠道吸引用户效果不理想&#xff0c;为盲目追求用户数字&#xff0c;便利用线下渠道养卡&#xff0c;即兜售给卡贩子&#xff0c;由此滋生了非实名卡、黑卡等乱象。 “养卡”又称“假…

接口自动化测试-Postman+Newman+Git+Jenkins实战集成(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、Postman 创建…

100个精选Python实战项目案例,在线无偿分享

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 随着 Python 语言的流行&#xff0c;越来越多的人加入到了 Python 的大家庭中。 为什么这么多人学 Python &#xff1f; 我要喊出那句话了&#xff1a;“人生苦短&#xff0c;我用 Python&#xff01;”&#xff0c; 正是…

PyTorch(安装及卸载)

目录 1. 安装 2. 卸载 参考文献 为什么用PyTorch&#xff1a;简单来说&#xff0c;19年之前tensorflow是大哥&#xff0c;19年tensorflow和PyTorch双龙并行&#xff0c;20年之后PyTorch一往无前。宗旨&#xff0c;哪个用的人多用哪个。 1. 安装 1. 先打开Anaconda Prompt&…

专为工业环境设计的高性能网络设备(4G工业路由器)

4G工业路由器顾名思义它是一种专为工业环境设计的高性能网络设备。它采用了4G通信技术&#xff0c;可以实现高速稳定灵活拓展的无线网络连接&#xff0c;满足工业领域对数据传输的需求。这种路由器具有坚固耐用的外壳&#xff0c;能够抵抗恶劣的工作环境&#xff0c;如高低温、…