Skip to content

理想汽车一面面经

https://www.nowcoder.com/feed/main/detail/f6bcff8211ef4b66884d7f2621b396d1

Redis

1、Redis的数据结构?

  1. 字符串(String):
    • Redis 中的字符串是二进制安全的,可以存储文本、JSON、二进制数据等。
    • 可以执行字符串的增减操作,如追加、截取等。
    • 常用于缓存、计数器、分布式锁等场景。
  2. 哈希(Hash):
    • Redis 哈希是一个字段和值的映射表。
    • 用于存储对象,每个对象可以包含多个字段和字段值。
    • 常用于存储和查询具有多个属性的数据。
  3. 列表(List):
    • Redis 列表是一个有序的字符串元素集合。
    • 可以在列表的两端执行元素的插入和删除操作。
    • 常用于队列、栈、消息发布和订阅等。
  4. 集合(Set):
    • Redis 集合是一个无序的字符串元素集合。
    • 不允许重复的元素存在,且元素是唯一的。
    • 支持集合的交集、并集、差集等操作。
    • 常用于存储唯一值和执行集合操作。
  5. 有序集合(Sorted Set):
    • 有序集合是一个有序的字符串元素集合,每个元素都有一个分数(score)。
    • 元素根据分数排序,可以按照分数范围进行检索。
    • 常用于排行榜、范围查询等。
  6. 位图(Bitmap):
    • 位图是一个位序列,每个位可以设置为 0 或 1。
    • 支持位操作,如与、或、非等,可以执行高效的位运算。
    • 常用于标记和统计布尔值信息。
  7. 超级字符串(HyperLogLog):
    • 超级字符串是一种用于统计唯一元素的数据结构。
    • 通过估算基数(集合中不同元素的数量)来节省内存。
    • 常用于基数统计场景,如统计访问网站的独立访客数量。
  8. 地理空间数据(Geospatial Data):
    • Redis 支持地理空间数据的存储和查询,包括点、距离计算等功能。
    • 常用于地理位置服务、位置基础分析等。

2、Redis的哨兵机制?

Redis 的哨兵(Sentinel)是 Redis 的一个分布式系统,用于监控 Redis 实例的运行状态,并在主节点下线时自动完成故障转移。哨兵的工作包括监控、通知和决策三个主要方面,它可以帮助保证 Redis 集群的高可用性。

Redis 哨兵的主要功能和特点:

  1. 监控(Monitoring):哨兵会定期检查 Redis 实例的运行状态,包括主节点和从节点的健康状况、网络连接状态等。如果发现某个节点不可用,哨兵会记录日志并发送通知。
  2. 通知(Notification):当哨兵发现 Redis 实例状态发生变化时(比如主节点下线),它会向管理员发送通知,通知可以通过邮件、短信等方式实现。
  3. 决策(Decision Making):哨兵根据监控到的信息做出决策,比如当主节点下线时,哨兵会选举一个新的主节点,并通知其他节点切换到新的主节点。
  4. 自动故障转移(Automatic Failover):当主节点下线时,哨兵会协调从节点进行选举,选择一个从节点作为新的主节点,并通知其他从节点切换到新的主节点上,从而实现自动的故障转移。
  5. 配置管理(Configuration Management):哨兵负责管理 Redis 集群的配置信息,包括主从关系、故障转移策略等。

C++

1、介绍下虚函数?

在 C++ 中,虚函数是一种特殊的成员函数,它允许在派生类中重写基类中定义的函数。虚函数是实现多态性的关键机制之一,它允许在运行时根据对象的实际类型来调用相应的函数,而不是根据变量的声明类型来确定调用哪个函数。

以下是虚函数的特点和用法:

  1. 定义虚函数:在基类中,通过在函数声明前面加上 virtual 关键字来定义虚函数。例如:

    c++
    class Base {
    public:
        virtual void foo() {
            // 虚函数的实现
        }
    };
  2. 重写虚函数:在派生类中可以重写基类的虚函数,只需要在派生类中重新定义相同签名的函数即可。例如:

    c++
    class Derived : public Base {
    public:
        void foo() override {
            // 重写虚函数的实现
        }
    };
  3. 多态性:通过基类指针或引用调用虚函数时,实际调用的是指向或引用对象的实际类型的函数。这种行为被称为多态性,它允许在运行时动态地选择调用的函数。例如:

    c++
    Base* ptr = new Derived();
    ptr->foo();  // 调用 Derived 类中重写的 foo() 函数
  4. 虚函数表:为了实现虚函数的多态性,编译器会在每个对象中添加一个指向虚函数表(vtable)的指针。虚函数表是一个函数指针数组,存储了该类所有虚函数的地址。当调用虚函数时,实际上是通过虚函数表来查找并调用相应的函数。

  5. 纯虚函数:在基类中可以定义纯虚函数,即在函数声明后面加上 = 0,表示该函数没有实现,需要在派生类中进行实现。纯虚函数用于定义接口,派生类必须实现这些函数。例如:

    c++
    class AbstractBase {
    public:
        virtual void pureVirtualFunction() = 0;
    };

2、构造函数与析构函数?

  1. 构造函数:

    • 构造函数是一种特殊的成员函数,用于初始化对象的数据成员。
    • 构造函数的名称与类名相同,没有返回类型(包括 void),可以有参数,可以重载。
    • 当创建一个对象时,构造函数会被自动调用,用来初始化对象的状态。
    • 如果没有显式定义构造函数,编译器会提供一个默认的无参构造函数(如果没有其他构造函数的话)。
    • 构造函数可以有不同的形式,包括默认构造函数(无参构造函数)、带参数的构造函数、拷贝构造函数等。

    例如:

    c++
    class MyClass {
    public:
        // 默认构造函数
        MyClass() {
            // 构造函数的实现
        }
    
        // 带参数的构造函数
        MyClass(int value) {
            // 带参数的构造函数的实现
        }
    
        // 拷贝构造函数
        MyClass(const MyClass& other) {
            // 拷贝构造函数的实现
        }
    };
  2. 析构函数:

    • 析构函数是一种特殊的成员函数,用于在对象被销毁时执行清理工作。
    • 析构函数的名称是在类名前面加上一个波浪号 ~,没有返回类型,没有参数,不能重载。
    • 当对象生命周期结束时(比如对象离开其作用域、delete 一个动态分配的对象时),析构函数会被自动调用,用来释放对象占用的资源。
    • 如果没有显式定义析构函数,编译器会提供一个默认的析构函数,其行为是空的。

    例如:

    c++
    class MyClass {
    public:
        // 析构函数
        ~MyClass() {
            // 析构函数的实现
        }
    };

操作系统

1、讲讲poll和epoll?

pollepoll 都是 Linux 下用于 I/O 多路复用的机制,可以用于实现高性能的网络编程。它们都可以监视多个文件描述符,当其中任意一个文件描述符就绪时,就可以进行读写操作。

  1. poll
    • poll 是 Unix 系统提供的一种 I/O 多路复用机制,通过 poll 函数可以同时监听多个文件描述符的可读、可写和异常事件。
    • poll 使用一个 pollfd 结构体数组来存放要监视的文件描述符及其事件,每次调用 poll 函数时,都会遍历整个 pollfd 数组来检查文件描述符的状态。
    • poll 的缺点是在文件描述符数量较大时性能下降明显,因为每次调用 poll 都需要遍历整个 pollfd 数组。
  2. epoll
    • epoll 是 Linux 2.6 内核提供的一种高性能的 I/O 多路复用机制,通过 epoll 函数可以监视大量的文件描述符。
    • epoll 使用三个系统调用来完成工作:epoll_create 创建一个 epoll 实例,epoll_ctl 用于注册、修改或删除文件描述符的事件,epoll_wait 用于等待文件描述符的事件就绪。
    • epoll 的优点在于可以处理大量的文件描述符,而且在文件描述符数量增加时,性能下降较少,因为 epoll 是基于事件驱动的机制,只有在事件就绪时才会返回,而不需要每次都遍历整个文件描述符集合。

2、虚拟地址如何映射到物理内存的?

虚拟地址映射到物理内存的大致过程:

  1. 地址转换机制:
    • 当程序访问内存时,使用的是虚拟地址,这些虚拟地址需要被转换成物理地址才能访问实际的内存。
    • CPU 中的内存管理单元(MMU)负责地址转换的工作,它通过页表来实现虚拟地址到物理地址的映射。
  2. 页表:
    • 页表是一种数据结构,用于存储虚拟地址到物理地址的映射关系。
    • 页表将虚拟地址划分为多个页(通常大小为4KB),每个页都有对应的物理地址。
    • 操作系统会为每个进程维护一个独立的页表,用于管理该进程的虚拟地址空间。
  3. 地址转换过程:
    • 当程序访问内存时,CPU 会将虚拟地址发送给 MMU。
    • MMU 根据虚拟地址的高位索引页表,找到对应的页表项。
    • 页表项中存储着该虚拟地址对应的物理页框号和页内偏移量。
    • MMU 将物理页框号和页内偏移量组合成物理地址,然后发送给内存控制器进行访问。
  4. 页表的管理:
    • 页表通常会被存储在内存中,由操作系统负责管理。
    • 为了提高访问速度,一些系统会使用高速缓存(TLB)来缓存最近使用的页表项,减少访问内存的次数。
  5. 地址空间的划分:
    • 操作系统将虚拟地址空间划分为多个部分,比如代码段、数据段、堆、栈等。
    • 不同部分的虚拟地址可以映射到不同的物理内存区域,实现内存的灵活管理和保护。

3、页表默认大小是多少?

  1. 32位系统下的页表大小:
    • 在32位系统中,地址空间大小为2^32,即4GB。
    • 如果页的大小为4KB,那么一个页面可以包含2^12 = 4096 个字节。
    • 因此,一个页面可以包含 4096 / 4 = 1024 个页表项(假设每个页表项为4字节),即一个页面可以映射4MB的物理内存。
    • 因此,如果整个地址空间大小为4GB,那么需要 4GB / 4MB = 1024 个页表项来完成地址映射。
    • 由于每个页表项需要存储物理页框号和一些控制位,所以每个页表项通常需要4字节以上的空间。
  2. 64位系统下的页表大小:
    • 在64位系统中,地址空间大小为2^64,即16EB(1EB = 1024PB)。
    • 如果页的大小为4KB,那么一个页面可以包含2^12 = 4096 个字节。
    • 一个页面可以包含 4096 / 8 = 512 个页表项(假设每个页表项为8字节),即一个页面可以映射2MB的物理内存。
    • 因此,如果整个地址空间大小为16EB,那么需要 16EB / 2MB = 8M 个页表项来完成地址映射。
    • 由于每个页表项需要存储物理页框号和一些控制位,所以每个页表项通常需要8字节以上的空间。

4、谈一下namesapce?

在 C++ 中,namespace 是用来避免命名冲突和组织代码的一种机制。通过 namespace 可以将全局作用域划分为不同的区域,每个区域可以包含变量、函数、类等各种类型的标识符。

  1. 命名空间的定义:

    • 命名空间通过关键字 namespace 进行定义,后面跟着命名空间的名称和大括号 {}
    • 命名空间的名称可以是任何有效的标识符,遵循命名规则即可。
    • 命名空间可以嵌套定义,形成命名空间的嵌套结构。
    c++
    // 命名空间的定义
    namespace MyNamespace {
        // 命名空间中的内容
    }
  2. 命名空间的使用:

    • 在命名空间内部可以定义各种类型的标识符,包括变量、函数、类等。
    • 在命名空间外部可以通过 :: 运算符来访问命名空间中的标识符。
    c++
    // 定义在命名空间内部的标识符
    namespace MyNamespace {
        int myVariable;
        void myFunction() {
            // 函数的实现
        }
    }
    
    // 在命名空间外部访问命名空间中的标识符
    MyNamespace::myVariable = 10;
    MyNamespace::myFunction();
  3. 命名空间的作用:

    • 避免命名冲突:不同的命名空间可以包含相同名称的标识符,通过命名空间可以避免不同模块中的标识符冲突。
    • 组织代码:命名空间可以将相关的标识符组织在一起,提高代码的可读性和维护性。
    • 控制作用域:命名空间可以限定标识符的作用域,可以控制标识符的可见性。
  4. 匿名命名空间:

    在 C++ 中还有一种特殊的命名空间叫做匿名命名空间,它是一个无名的命名空间,用来限制变量、函数等的作用域,防止在其他文件中被访问。

    c++
    // 匿名命名空间中的变量和函数只在当前文件中可见
    namespace {
        int myVariable;
        void myFunction() {
            // 函数的实现
        }
    }

5、线程和进程区别?

  1. 定义:
    • 进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位。
    • 线程是进程的一部分,是 CPU 调度和分派的基本单位,是程序执行流的最小单元。
  2. 资源分配:
    • 进程拥有独立的地址空间、文件描述符、堆栈和数据段等资源,各个进程之间相互独立。
    • 线程与所属进程共享同一地址空间和其他资源,包括代码段、数据段和打开的文件等。
  3. 调度和切换:
    • 进程切换涉及到上下文切换,需要保存和恢复整个进程的状态,开销较大。
    • 线程切换只需保存和恢复线程的上下文,开销较小。
  4. 并发性:
    • 进程之间相互独立,需要通过进程间通信机制来实现数据共享和通信。
    • 线程之间共享同一进程的资源,可以直接访问共享变量,通信更加方便。
  5. 创建和销毁:
    • 创建和销毁进程的开销较大,涉及到系统调用和资源分配。
    • 创建和销毁线程的开销较小,通常由线程库来管理。
  6. 安全性:
    • 进程之间相互独立,一个进程的崩溃不会影响其他进程。
    • 线程共享同一进程的资源,一个线程的错误可能会影响到整个进程。

6、父进程与子进程有什么特点?

在操作系统中,当一个进程创建了另一个新的进程,新进程就成为了原进程的子进程。

  1. 进程间关系:

    父子进程之间存在一种层级关系,即子进程是由父进程创建的,父进程可以有多个子进程,但每个进程只有一个父进程(除了 init 进程)。

  2. 进程标识:

    • 在 Unix/Linux 系统中,每个进程都有一个唯一的进程标识符(PID),用于区分不同的进程。
    • 子进程的 PID 是在父进程调用 fork 函数后由操作系统分配的,而父进程的 PID 则保持不变。
  3. 资源继承:

    • 子进程会继承父进程的大部分资源,包括内存空间、文件描述符、信号处理方式等。
    • 子进程通常会复制父进程的地址空间,并且在 fork 后继续执行从 fork 处开始的代码。
  4. 执行环境:

    子进程在创建时会复制父进程的执行环境,包括代码段、数据段、堆栈等,但是它们在内存中是独立的。

  5. 通信机制:

    父子进程之间可以通过进程间通信(IPC)机制进行通信和同步,比如管道、共享内存、信号量等。

  6. 退出状态:

    当子进程退出时,会向父进程发送一个退出状态码,父进程可以通过这个状态码来获取子进程的退出状态,以便进行后续处理。

7、线程的调度方式,进程的调度方式有哪些?

线程的调度方式

  1. 抢占式调度:
    • 在抢占式调度中,操作系统可以随时中断当前正在执行的线程,并将 CPU 分配给其他线程执行。
    • 抢占式调度可以通过时钟中断、优先级调度等机制来实现,保证了高优先级任务的及时响应。
  2. 协作式调度:
    • 在协作式调度中,线程只有在主动放弃 CPU 使用权或者发生阻塞等情况下,才会由操作系统切换到其他线程。
    • 协作式调度的优点是简单高效,缺点是一个线程的错误可能导致整个系统的阻塞。

进程的调度方式

  1. 先来先服务调度(FCFS)

    • 特点:按照进程到达的顺序分配 CPU 时间片,是一种非抢占式调度算法。

    • 工作原理:按照进程到达的先后顺序,依次为每个进程分配 CPU 时间片,直到进程执行完成或发生 I/O 操作。

    • 优点:实现简单,公平性较高。

    • 缺点:可能导致某些长作业等待时间过长,不适合对响应时间要求较高的场景。

  2. 最短作业优先调度(SJF)

    • 特点:优先调度执行时间最短的进程,可以最大程度地减少平均等待时间。

    • 工作原理:根据每个进程的执行时间,选择最短的作业进行调度,直到作业完成或发生 I/O 操作。

    • 优点:平均等待时间较短。

    • 缺点:难以预测每个进程的执行时间,可能导致长作业等待时间过长。

  3. 优先级调度

    • 特点:根据进程的优先级分配 CPU 时间片,优先级高的进程先执行。

    • 工作原理:根据进程的优先级,选择优先级最高的进程进行调度,直到进程完成或发生 I/O 操作。

    • 优点:可以根据需求设置不同的优先级。

    • 缺点:可能导致低优先级进程长时间等待,容易出现优先级反转问题。

  4. 轮转调度(RR)

    • 特点:将 CPU 时间片分配给每个进程,每个进程在一个时间片内执行,然后切换到下一个进程。

    • 工作原理:按照轮流的方式为每个进程分配时间片,直到时间片用完或进程完成。

    • 优点:可以保证每个进程都能得到执行。

    • 缺点:可能导致上下文切换开销增加,对于长时间执行的进程效率较低。

  5. 多级反馈队列调度

    • 特点:将进程按照优先级分成多个队列,每个队列使用不同的调度算法。

    • 工作原理:根据进程的优先级将进程放入相应的队列,然后根据队列的优先级选择进程进行调度。

    • 优点:可以防止某些进程长时间等待或者长时间占用 CPU。

    • 缺点:调度算法复杂,需要根据情况调整参数。

  6. 实时调度

    • 特点:适用于对任务的响应时间有严格要求的实时系统。

    • 工作原理:根据任务的截止时间和优先级进行调度,确保任务在规定的时间内完成。

    • 优点:能够保证任务在规定时间内完成。

    • 缺点:对系统资源要求较高,需要保证实时性。

8、多线程实现的原理?

多线程是指在一个进程中同时运行多个线程。每个线程都拥有自己的执行序列、程序计数器、栈和一些局部变量,但共享进程的地址空间和全局变量。

  1. 操作系统支持:

    多线程的实现离不开操作系统的支持。操作系统提供了线程管理的基本功能,包括创建线程、线程调度、线程同步和线程销毁等。

  2. 线程模型:

    多线程的实现方式有用户级线程和内核级线程两种模型。用户级线程是由用户空间的线程库实现的,对于操作系统来说,只是一个单独的进程;而内核级线程是由操作系统内核管理的,操作系统可以直接调度和管理线程。

  3. 线程调度:

    线程调度是操作系统决定哪个线程获得 CPU 时间片的过程。调度算法决定了线程在多个线程之间的切换顺序和优先级。

  4. 线程同步:

    多线程之间的同步是指线程之间如何协调和共享资源的问题。常见的同步机制包括互斥锁、信号量、条件变量等,用于保护共享资源的访问和控制线程的执行顺序。

  5. 线程安全:

    线程安全是指多个线程访问共享资源时不会产生不确定的结果。在编程中,需要考虑线程安全的问题,使用同步机制来保证共享资源的安全访问。

  6. 编程语言支持:

    不同的编程语言对多线程的支持有所不同。一些语言提供了丰富的多线程库和高级的线程管理接口,使得多线程编程更加容易和灵活。

  7. 硬件支持:

    多线程的实现也与硬件有关。一些多核处理器和超线程技术可以提高多线程程序的性能,使得多个线程可以在同一时刻执行不同的指令。

9、中断了解吗?

中断是计算机系统中用于处理异步事件的一种机制。它允许计算机在正常程序执行过程中,突然转移到另一个任务(通常是处理器需要响应的事件)上,然后在适当的时候返回原来的任务。

中断的基本概念

  • 中断请求(IRQ):外部设备(如硬件设备、时钟等)向处理器发送的请求,要求处理器暂停当前正在执行的任务,转而处理设备请求的事件。
  • 中断向量:每个中断都有一个唯一的标识符,称为中断向量。处理器根据中断向量找到对应的中断服务程序的地址。
  • 中断控制器:负责管理和分发各种中断请求的硬件组件,以确保处理器能够正确响应和处理各种中断。
  • 中断服务程序:处理中断的程序,它负责在中断发生时保存当前进程的状态,执行相应的中断处理程序,然后恢复原来的进程状态。

中断的类型

  • 外部中断:由外部设备发起的中断请求,如硬件设备的输入输出请求。
  • 内部中断:由处理器内部的错误或异常引起的中断,如除零错误、内存访问错误等。
  • 软件中断:由软件或程序主动触发的中断,用于实现系统调用或其他软件功能。

中断处理过程

  1. 中断请求:外部设备向中断控制器发送中断请求。
  2. 中断响应:中断控制器将中断请求发送给处理器,处理器响应中断请求,暂停当前任务。
  3. 中断服务程序:处理器根据中断向量找到对应的中断服务程序的地址,执行相应的中断处理程序。
  4. 中断处理:中断服务程序执行中断处理程序,处理中断请求,并保存当前进程的状态。
  5. 中断返回:处理器执行完中断服务程序后,根据中断处理程序的要求,恢复原来的进程状态,继续执行中断前的任务。

中断的作用

  • 实时响应:允许处理器实时响应外部设备的请求,处理实时性要求高的任务。
  • 提高效率:通过允许处理器在等待外部事件时执行其他任务,提高系统的效率和并发性。
  • 异常处理:用于处理处理器内部和外部设备发生的异常情况,确保系统的稳定性和可靠性。

10、说说线程池?

线程池是一种用于管理和复用线程的机制,它能够有效地管理线程的生命周期,提高线程的利用率,并且可以控制并发线程的数量,防止系统因过多线程而导致的性能下降或资源耗尽。

线程池的基本结构

一个典型的线程池包括以下几个组件:

  1. 任务队列(Task Queue):用于存储需要执行的任务,通常是一个先进先出(FIFO)队列。
  2. 工作线程(Worker Threads):实际执行任务的线程。线程池会预先创建一定数量的工作线程,并且根据需要动态调整工作线程的数量。
  3. 线程管理器(Thread Manager):负责创建、销毁和管理工作线程,以及分配任务给工作线程。

线程池的工作流程

  1. 初始化:线程池在初始化时会创建一定数量的工作线程,并将它们置于等待状态。
  2. 任务提交:当有任务需要执行时,将任务提交到任务队列中。
  3. 任务调度:线程池的线程管理器会从任务队列中取出任务,然后分配给空闲的工作线程执行。
  4. 执行任务:工作线程执行任务,并在任务执行完成后返回到等待状态,继续等待新的任务。
  5. 线程销毁:当线程池不再需要某个工作线程时(可能是因为任务量减少或者系统关闭),线程池会销毁该线程。
  6. 线程池关闭:当不再需要线程池时,需要将所有的任务执行完毕,并且销毁所有的工作线程。

线程池的优点

  1. 减少线程创建和销毁的开销:线程池中的线程可以被复用,减少了线程创建和销毁的开销,提高了系统的性能和响应速度。
  2. 控制并发度:线程池可以限制并发线程的数量,防止系统因过多线程而导致的资源竞争和性能下降。
  3. 管理资源:线程池可以管理系统的资源,包括线程的数量、任务的执行顺序和优先级等,从而更好地利用系统资源。
  4. 提高响应速度:线程池可以提前创建好线程,当有任务到来时可以立即执行,提高了任务的响应速度。

11、死锁如何避免?

死锁是指两个或多个进程(或线程)在执行过程中因争夺资源而造成的一种互相等待的现象,导致它们都无法继续执行。

为了避免死锁,可以采取以下几种方法:

  1. 避免使用多个锁:在设计程序时,尽量避免使用多个锁来管理资源。如果可能的话,尽量将多个锁合并为一个锁,或者使用更高级别的同步机制来管理资源。
  2. 按固定顺序获取锁:如果必须使用多个锁,确保所有线程都按照相同的顺序获取锁。这样可以避免循环等待的情况发生。
  3. 使用超时机制:在获取锁的过程中,可以设置超时时间。如果超过了指定的时间仍然无法获取到锁,就放弃对该资源的获取,避免因等待时间过长而导致系统资源浪费。
  4. 避免嵌套锁:在获取锁的过程中,尽量避免嵌套锁的情况。如果必须使用嵌套锁,确保在获取锁的顺序上没有死锁的可能性。
  5. 使用死锁检测机制:一些操作系统和编程语言提供了死锁检测的机制,可以检测并且解决死锁问题。这种机制可以周期性地检测系统中的死锁情况,并采取相应的措施来解决死锁。
  6. 资源分配策略:合理的资源分配策略可以减少死锁的发生。例如,避免一个进程同时获取多个资源,或者使用资源预分配的方式来避免资源的竞争。
  7. 强制释放资源:当系统检测到潜在的死锁情况时,可以采取强制释放资源的方式来打破死锁。这种方式需要谨慎使用,因为可能会造成数据不一致或者其他问题。

算法

LeetCode92.反转链表②

问题描述:

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

思路:

  1. 首先, 需要定义三个指针:prevcurrnext,它们分别代表反转部分的前一个节点、当前节点和下一个节点。
  2. 需要遍历链表,找到反转部分的起始位置。遍历过程中,需要记录下反转部分的前一个节点 prev 和当前节点 curr
  3. 当找到反转部分的起始位置后, 开始进行反转操作。具体步骤如下:
    • 在反转过程中, 需要不断将当前节点 currnext 指针指向它的前一个节点 prev,以实现反转。
    • 同时, 需要不断更新 prevcurrnext 的位置,使它们指向下一个需要反转的节点。
  4. 当完成反转操作后, 需要处理反转部分的前后连接关系。将反转部分的前一个节点 prevnext 指针指向反转后的头节点,将反转部分的尾节点的 next 指针指向下一个节点。
  5. 最后, 返回链表的头节点作为结果。

参考代码:

c++
#include <iostream>

using namespace std;

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* reverseBetween(ListNode* head, int left, int right) {
        if (!head || left == right) {
            return head;
        }
        
        ListNode* dummy = new ListNode(-1); // 创建一个虚拟头节点
        dummy->next = head;
        ListNode* prev = dummy;
        
        // 将 prev 移动到反转部分的前一个节点
        for (int i = 0; i < left - 1; ++i) {
            prev = prev->next;
        }
        
        ListNode* curr = prev->next;
        ListNode* next = nullptr;
        
        // 开始反转操作
        for (int i = 0; i < right - left; ++i) {
            next = curr->next;
            curr->next = next->next;
            next->next = prev->next;
            prev->next = next;
        }
        
        return dummy->next;
    }
};

// 测试
int main() {
    ListNode* head = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
    int left = 2, right = 4;
    
    Solution solution;
    ListNode* reversed = solution.reverseBetween(head, left, right);
    
    // 输出反转后的链表
    while (reversed) {
        cout << reversed->val << " ";
        reversed = reversed->next;
    }
    cout << endl;
    
    return 0;
}