【算法训练-链表 一】【反转链表】反转链表、区间反转链表、K个一组反转链表

简介: 【算法训练-链表 一】【反转链表】反转链表、区间反转链表、K个一组反转链表

从今天开始进行高频算法的训练,一方面训练自己的逻辑思维,一方面保持自己的竞争力。训练过程有这么两个基准原则:

  • 首先训练题的来源呢有三个,首选的是三个都出现过的高频题,以:牛客101为基准分类,然后在CodeTop中找近一年内出现频率最多的牛客中该分类的题目,还有就是LeetCode热题100,这个按照最低参考度考虑。
  • 其次同一篇Blog里记录相关性较强的题,可以理解为普通-升级-再升级。例如当前这篇:反转链表-区间反转链表-K个一组反转链表

废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!首先,链表对应的数据结构在这篇Blog中:【基本数据结构 一】线性数据结构:链表,基于对基础知识的理解来进行题目解答。

反转链表【EASY】

先来个easy版本

题干

输入输出示例:

输入:
{1,2,3}
返回值:
{3,2,1}
输入:
{}
复制
{}
说明:空链表则输出空

解题思路

迭代方式,比较简单,就是用双指针一直向后滑动,将引用方向反转,特别需要注意的是,由于下一个节点会因为在反转过程中断掉,所以需要用临时节点记录下一个节点的位置。

代码实现

基本数据结构链表

辅助数据结构

算法迭代

技巧双指针

/*
public class ListNode {
    int val;
    ListNode next = null;
    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {
        // 1 如果为空列表,返回null
        if (head == null) {
            return null;
        }
        // 2 定义双指针
        ListNode cur = head;
        ListNode pre = null;
        while (cur != null) {
            // 存储下一个节点
            ListNode  pNext = cur.next;
            // 当前节点指向上一个[局部反转]
            cur.next = pre;
            // 双指针向后移动
            pre = cur;
            cur= pNext;
        }
        return pre;
    }
}

复杂度分析

时间复杂度O(N):遍历了一遍链表; 空间复杂度O(1):常数级空间

链表内指定区间反转【MID】

升级版,链表内指定区间进行反转

题干

输入:
{1,2,3,4,5},2,4
返回值:
{1,4,3,2,5}
输入:
{5},1,1
返回值:
{5}

解题思路

头插法迭代方式,增加如下几个节点或节点指针:

  • dummy 原始链表头节点的虚假头节点,为了避免当head节点位置改变且无明确位置的指针节点不知道该如何返回的情况
  • pre指向反转区间前一个节点
  • cur指向遍历到的位置(虽然他的位置在移动,但是其实一直指向的是同一个节点,即原始链表m处的节点)
  • pNext 指向cur下一个节点,反转过程就是将这个节点插入到pre的下一个位置

步骤主要分为两大步骤

  1. pre和cur同时向后走m步到达要反转的列表区间,pre指向反转区间前一个节点,cur为当前反转子区间的头节点
  2. 进行n-m次的节点反转,每次反转目标都是将pNext移动到pre的后边,分三步

这三个步骤是:

  1. cur.next = pNext.next;: cur指向下一个待转节点
  2. pNext.next=pre.next: pNext节点指向反转子区间的尾节点,成为反转子区间新的尾节点
  3. pre.next=pNext: pre指向反转子区间新的尾节点pNext

整体示意图如下:

代码实现

基本数据结构链表

辅助数据结构

算法迭代

技巧双指针

import java.util.*;
/*
 * public class ListNode {
 *   int val;
 *   ListNode next = null;
 *   public ListNode(int val) {
 *     this.val = val;
 *   }
 * }
 */
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param head ListNode类
     * @param m int整型
     * @param n int整型
     * @return ListNode类
     */
    public ListNode reverseBetween (ListNode head, int m, int n) {
        if (head == null) {
            return null;
        }
        // 1 设置虚拟头节点[避免当head节点位置改变且无明确位置的指针节点不知道该如何返回的情况]
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        // 2 双指针向后移动m步
        ListNode pre = dummy;
        ListNode cur = head;
        for (int i = 1; i < m; i++) {
            pre = cur;
            cur = cur.next;
        }
        // 3 m到n的区间链表要进行反转[下一个节点和已反转的子区间整体进行反转]
        for (int i = m; i < n; i++) {
            // 1 pNext 指向cur下一个节点,反转过程就是将这个节点插入到pre的下一个位置
            ListNode pNext = cur.next;
            // 2 cur指向下一个待转节点
            cur.next = pNext.next;
            // 3 pNext节点指向反转子区间的尾节点,成为反转子区间新的尾节点
            pNext.next = pre.next;
            // 4 pre指向反转子区间新的尾节点pNext
            pre.next = pNext;
        }
        return dummy.next;
    }
}

复杂度分析

时间复杂度O(N):遍历了一遍链表; 空间复杂度O(1):常数级空间

链表中的节点每k个一组翻转【HARD】

再升级,K个一组反转

题干

输入:
{1,2,3,4,5},2
返回值:
{2,1,4,3,5}
输入:
{},1
返回值:
{}

解题思路

递归方式,我们这时候可以用到自上而下再自下而上的递归或者说栈。如果这个链表有n个分组可以反转,我们首先对第一个分组反转,那么接下来将剩余n-1个分组,n−1个分组反转后的结果接在第一组后面就行了,那这剩余的n−1组就是一个子问题。使用递归的三段式模版:

  • 终止条件: 当进行到最后一个分组,即不足k次遍历到链表尾(0次也算),就将剩余的部分直接返回。
  • 返回值: 每一级要返回的就是翻转后的这一分组的头,以及连接好它后面所有翻转好的分组链表。
  • 本级任务: 对于每个子问题,先遍历k次,找到该组结尾在哪里,然后从这一组开头遍历到结尾,依次翻转,结尾就可以作为下一个分组的开头,而先前指向开头的元素已经跑到了这一分组的最后,可以用它来连接它后面的子问题,即后面分组的头。

具体做法:

  • 步骤一:每次从进入函数的头节点优先遍历链表k次,分出一组,若是后续不足k个节点,不用反转直接返回头。
  • 步骤二:从进入函数的头节点开始,依次反转接下来的一组链表,反转过程直接使用链表反转的算法。
  • 步骤三:这一组经过反转后,原来的头变成了尾,后面接下一组的反转结果,下一组采用上述递归继续。

整个过程如图所示:

代码实现

基本数据结构链表

辅助数据结构

算法递归

技巧双指针

import java.util.*;
/*
 * public class ListNode {
 *   int val;
 *   ListNode next = null;
 *   public ListNode(int val) {
 *     this.val = val;
 *   }
 * }
 */
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param head ListNode类
     * @param k int整型
     * @return ListNode类
     */
    public ListNode reverseKGroup (ListNode head, int k) {
        // 1 定义分组尾节点
        ListNode tail = head;
        // 2 划分本级处理的区间反转子任务
        for (int i = 0; i < k; i++) {
            if (tail == null) {
                // 2-1 任务停止条件是区间不足K个节点,此时直接返回这些节点即可
                return head;
            }
            tail = tail.next;
        }
         // 3 处理本级区间反转任务
        ListNode newHead = reverse(head, tail);
        //  4 剩余区间继续递归反转,直到全部反转
        head.next = reverseKGroup(tail, k);
        return newHead;
    }
    private ListNode reverse(ListNode head, ListNode tail) {
        ListNode pre = null;
        ListNode cur = head;
        while (cur != tail) {
            ListNode pNext = cur.next;
            cur.next = pre;
            pre = cur;
            cur = pNext;
        }
        return pre;
    }
}

复杂度分析

时间复杂度O(N):遍历了一遍链表; 空间复杂度O(N/K):递归最大的栈深度,

拓展:递归和迭代的区别

递归(Recursion)和迭代(Iteration)都是两种解决问题的方法,它们在实现方式和思维方式上有一些区别。

递归

递归是一种通过将问题分解成更小的子问题来解决问题的方法。在递归中,函数会调用自身来解决子问题,直到达到基本情况(递归终止条件),然后逐层返回结果,最终得到整个问题的解。递归通常在问题的结构具有递归性质时使用,例如树形结构、链式结构等。

优点:

  • 可以处理问题的复杂逻辑,将大问题分解为小问题。
  • 在某些情况下,代码相对简洁易懂。

缺点:

  • 递归可能会导致函数调用堆栈的增长,可能导致栈溢出。
  • 递归的性能可能不如迭代。
递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度

为什么要求递归的深度呢?因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。

迭代

迭代是通过循环控制来重复执行一系列操作,从而逐步逼近问题的解。在迭代中,通过在循环体内更新变量的值,逐步推进问题的求解,直到满足特定的条件为止。

优点:

  • 迭代通常比递归更节省内存,因为不需要保存多层的函数调用信息。
  • 性能更高,避免了函数调用的开销。

缺点:

  • 在某些情况下,代码可能相对复杂,特别是涉及到复杂的循环控制逻辑。

在选择使用递归还是迭代时,需要考虑问题的性质、可读性、性能等因素。有些问题更适合用递归解决,而其他问题则更适合用迭代解决。有时候,递归和迭代也可以结合使用,例如一些动态规划问题。

目录
打赏
0
0
0
0
33
分享
相关文章
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
97 29
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
126 25
【狂热算法篇】探秘差分数组:算法星河中闪耀的区间掌控之星
【狂热算法篇】探秘差分数组:算法星河中闪耀的区间掌控之星
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
经典算法之链表篇(三)
经典算法之链表篇(三)
117 4
|
5月前
|
经典算法之链表篇(二)
经典算法之链表篇(二)
基于LSB最低有效位的音频水印嵌入提取算法FPGA实现,包含testbench和MATLAB对比
本项目展示了一种基于FPGA的音频水印算法,采用LSB(最低有效位)技术实现版权保护与数据追踪功能。使用Vivado2019.2和Matlab2022a开发,完整代码含中文注释及操作视频。算法通过修改音频采样点的最低有效位嵌入水印,人耳难以察觉变化。然而,面对滤波或压缩等攻击时,水印提取可能受影响。该项目运行效果无水印干扰,适合实时应用场景,核心逻辑简单高效,时间复杂度低。
基于GA遗传算法的拱桥静载试验车辆最优布载matlab仿真
本程序基于遗传算法(GA)实现拱桥静载试验车辆最优布载的MATLAB仿真,旨在自动化确定车辆位置以满足加载效率要求(0.95≤ηq≤1.05),目标是使ηq尽量接近1,同时减少车辆数量和布载耗时。程序在MATLAB 2022A版本下运行,展示了工况1至工况3的测试结果。通过优化模型,综合考虑车辆重量、位置、类型及车道占用等因素,确保桥梁关键部位承受最大荷载,从而有效评估桥梁性能。核心代码实现了迭代优化过程,并输出最优布载方案及相关参数。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等