每日一题——排序链表(递归 + 迭代)

news2024/11/23 4:06:20

排序链表(递归 + 迭代)

题目链接

注:本体的解法建立在归并排序的基础之上,如果对这一排序还不太了解,建议看看:

👉归并排序

👉八大排序算法详解

👉合并两个有序链表


既然采用递归排序来解决这道题,那么我们就要采用分治的思想

分治法(Divide and Conquer)是一种解决问题的算法设计策略,它将一个大的问题分解为若干个小的子问题,逐步解决这些子问题,并将它们的解合并成一个大问题的解

同理,我们将排序一整串长链表的问题分解为若干个简单的子问题,而这些简单的子问题就是对链表的单个元素进行成对处理:两个长度为1的链表可以合成一个长度为2的有序链表,两个长度为2的链表可以合成一个长度为4的链表,以此类推,直到得到完整的有序链表。

而递归排序有递归和迭代两种实现方式,接下来我们分别进行分析:

递归(自顶向下)

自顶向下归并排序: 自顶向下归并排序是一种递归的实现方式。它的基本思想是将原始数组递归地分成两半,然后分别对这两半进行排序,最后将排好序的子数组合并成一个有序数组。整个过程可以看作是自顶向下地将问题逐步分解并解决的过程。

步骤:

  1. 将原始链表分成两半。
  2. 递归地对这两半链表进行排序(继续分割并排序)。
  3. 合并两个排好序的子链表,得到一个完整的有序链表。

如图所示:

实现代码

//合并两个有序链表
struct ListNode* merge(struct ListNode* headA, struct ListNode* headB)
{
    if (headA == NULL || headB == NULL)
        return headA == NULL ? headB : headA;

    struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* cur = newHead;
    struct ListNode* cur1 = headA;
    struct ListNode* cur2 = headB;

    while (cur1 && cur2)
    {
        if (cur1->val < cur2->val)
        {
            cur->next = cur1;
            cur1 = cur1->next;
        }
        else
        {
            cur->next = cur2;
            cur2 = cur2->next;
        }
        cur = cur->next;
    }

    cur->next = cur1 == NULL ? cur2 : cur1;

    struct ListNode* ret = newHead->next;
    free(newHead);
    return ret;
}

struct ListNode* sortList(struct ListNode* head){
    if (head == NULL || head->next == NULL)
        return head;

    //利用双指针找到链表的中间节点
    struct ListNode* left = NULL;
    struct ListNode* mid = head;
    struct ListNode* right = head;
    while (right && right->next)
    {
         left = mid;
         mid = mid->next;
         right = right->next->next;
    }

    left->next = NULL;	//从中间节点出断开,得到两个子链表
	
    //对子链表不断地进行递归分割,直到分割为不可分割的子链表(一个元素),在进行合并
    return merge(sortList(head), sortList(mid));
}

为加深理解,我们以上个例子为例,分析整个递归过程(序号为执行顺序):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YV28dJZT-1692709107509)(C:/Users/HUASHUO/AppData/Roaming/Typora/typora-user-images/image-20230822175144390.png)]

迭代(自底向上)

自底向上归并排序是一种迭代的实现方式。它的基本思想是先将原始数组中的每个元素看作是一个独立的有序数组,然后从底层开始两两合并这些小的有序数组,直到合并成一个完整的有序数组。

步骤:

  1. 将原始链表中的每个元素看作是一个有序链表。
  2. 从底层开始,将相邻的两个小的有序链表合并成更大的有序链表。
  3. 逐步合并相邻的有序链表,直到合并成一个完整的有序链表。

如图:

虽然迭代发较递归法容易理解许多,但是代码却复杂了不止一点点,下面我们来仔细分析:

  • 由于链表的排序可能会改变原链表的头部,因此我们要定义一个哨兵位newHead来确定链表的头部

  • 第一次要合并两个长度为1的子链表,第二次要合并两个长度为2的子链表…………,因此我们要用一个计数器subLength来记录每次循环合并的子链表长度,而为了确定循环终止条件,我们要求出整个链表的长度length

int length = 0;
struct ListNode* cur = head;
while (cur)	//求出链表长度
{
    length++;
    cur = cur->next;
}

//定义哨兵位头节点
struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
newHead->next = head;
//开始合并
for (int subLength = 1; subLength < length; subLength *= 2)
{………………}
  • 对于每一个subLength,我们都需要一个cur,这个cur用来:遍历整个链表,同时确保其移动的步数为sublength - 1,不断找到需要合并的两个子链表,同时还要一个指针prev用来链接合并后返回的指针。

    for (int subLength = 1; subLength < length; subLength *= 2)
        {
        	//对于每一个subLength,cur都从第一个有效节点开始确定要合并的子链表
            cur = newHead->next;	
        
            struct ListNode* prev = newHead;
        
            while (cur)
            {
                struct ListNode* head1 = cur;	//第一个子链表的头即为cur的起点
                for (int i = 1; i < subLength && cur->next != NULL; i++)
                    cur = cur->next;
    			//确定第一个子链表的范围后断开和之后的节点的链接,cur向后移
                struct ListNode* temp = cur->next;
                cur->next = NULL;
                cur = temp;
    	
                //如果cur后移后不为空,说明存在第二条子链表,可以合并
                if (cur)
                {
                    struct ListNode* head2 = cur;	//第二个子链表的头即为cur的起点
                    for (int i = 1; i < subLength && cur->next != NULL; i++)
                        cur = cur->next;
    				//确定第二条子链表的范围后断开和之后的节点的链接,同时cur后移
                    temp = cur->next;
                    cur->next = NULL;
                    cur = temp;
                    
                    //找到两条子链表的头后开始合并
                    struct ListNode* merged = merge(head1, head2);
                    prev->next = merged;	//用prev进行连接
    				
                    //prev移动到部分有序链表的末尾,用来之后的链接
                    while (prev->next)
                        prev = prev->next;
                    prev->next = temp;	//为防止访问不到剩余节点,prev要链接到还未排序的节点
                }
            }
        }
    

实现代码

//合并两个有序链表
struct ListNode* merge(struct ListNode* headA, struct ListNode* headB)
{
    if (headA == NULL || headB == NULL)
        return headA == NULL ? headB : headA;

    struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* cur = newHead;
    struct ListNode* cur1 = headA;
    struct ListNode* cur2 = headB;

    while (cur1 && cur2)
    {
        if (cur1->val < cur2->val)
        {
            cur->next = cur1;
            cur1 = cur1->next;
        }
        else
        {
            cur->next = cur2;
            cur2 = cur2->next;
        }
        cur = cur->next;
    }

    cur->next = cur1 == NULL ? cur2 : cur1;

    struct ListNode* ret = newHead->next;
    free(newHead);
    return ret;
}

struct ListNode* sortList(struct ListNode* head){
    if (head == NULL || head->next == NULL)
        return head;
	
    //求链表长度
    int length = 0;
    struct ListNode* cur = head;
    while (cur)
    {
        length++;
        cur = cur->next;
    }
	
    //开始排序
    struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    newHead->next = head;
    for (int subLength = 1; subLength < length; subLength *= 2)
    {
        cur = newHead->next;
        struct ListNode* prev = newHead;
        while (cur)
        {
            struct ListNode* head1 = cur;
            for (int i = 1; i < subLength && cur->next != NULL; i++)
                cur = cur->next;

            struct ListNode* temp = cur->next;
            cur->next = NULL;
            cur = temp;

            if (cur)
            {
                struct ListNode* head2 = cur;
                for (int i = 1; i < subLength && cur->next != NULL; i++)
                    cur = cur->next;

                temp = cur->next;
                cur->next = NULL;
                cur = temp;

                struct ListNode* merged = merge(head1, head2);
                prev->next = merged;

                while (prev->next)
                    prev = prev->next;
                prev->next = temp;
            }
        }
    }
	
    //释放哨兵位,并返回结果
    struct ListNode* ret = newHead->next;
    free(newHead);
    return ret;
}

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

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

相关文章

初学Zephyr系统,相关文档参考

https://docs.zephyrproject.org/3.0.0/reference/kconfig/index-all.html Zephyr下所有配置项Configuration Options 链接如上&#xff0c;写个博客防止自己找不到 我在调试NCS中的例程的时候会需要对prj.conf进行配置从而对Kconfig进行配置 BLE的相关API可参考https://do…

[oneAPI] 基于BERT预训练模型的英文文本蕴含任务

[oneAPI] 基于BERT预训练模型的英文文本蕴含任务 Intel DevCloud for oneAPI 和 Intel Optimization for PyTorch基于BERT预训练模型的英文文本蕴含任务语料介绍数据集构建 模型训练 结果参考资料 比赛&#xff1a;https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0…

Spring@Scheduled定时任务接入XXL-JOB的一种方案(基于SC Gateway)

背景 目前在职的公司&#xff0c;维护着Spring Cloud分布式微服务项目有25个。其中有10个左右微服务都写有定时任务逻辑&#xff0c;采用Spring Scheduled这种方式。 Spring Scheduled定时任务的缺点&#xff1a; 不支持集群&#xff1a;为避免重复执行&#xff0c;需引入分…

基于jeecg-boot的flowable流程加签功能实现

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 今天我…

【C++】C++ 引用详解 ① ( 变量的本质 - 引入 “ 引用 “ 概念 | 引用语法简介 | 引用做函数参数 | 复杂类型引用做函数参数 )

文章目录 一、变量的本质 - 引入 " 引用 " 概念1、变量的本质 - 内存别名2、引入 " 引用 " 概念 - 已定义变量的内存别名3、" 引用 " 的优点 二、引用语法简介1、语法说明2、代码示例 - 引用的定义和使用 三、引用做函数参数1、普通引用必须初始…

minikube安装

minikube也是需要docker环境的&#xff0c;首先看一下docker 下载docker.repo源到本地 通过repo里面查找最新的docker 开始安装docker 修改docker 下载加速地址&#xff0c; systemctl deamon-reload 下载minikube minikube start | minikube curl -LO https://storage.goog…

Mybatis(二)映射文件配置与动态SQL

Mybatis&#xff08;二&#xff09;映射文件配置 1.Mybatis映射文件配置 1.入参 1.1.parameterType(了解) CRUD标签都有一个属性parameterType&#xff0c;底层的statement通过它指定接收的参数类型。入参数据有以下几种类型&#xff1a;HashMap&#xff0c;基本数据类型&a…

会计资料基础

会计资料 1.会计要素及确认与计量 1.1 会计基础 1.2 六项会计要素小结 1.3 利润的确认条件 1.3.1 利润的定义和确认条件 1.4 会计要素及确认条件 2.六项会计要素 2.1 资产的特征及其确认条件 这部分资产可以给企业带来经济收益&#xff0c;但是如果不能带来经济利益&#xff…

提升团队合作效率:企业网盘的文件管理和协作利用方法

随着信息技术的飞速发展&#xff0c;企业越来越依赖于网络和云服务来提高工作效率。在这样的背景下&#xff0c;企业网盘作为一种重要的在线存储和协作工具&#xff0c;正在被越来越多的企业所采用。本文将探讨如何利用企业网盘进行文件管理和协作&#xff0c;从而构建高效的团…

Windows快捷键常用介绍,提高工作(摸鱼)效率

一&#xff1a;背景 本文主要是讲解Windows电脑常见的快捷键&#xff0c;包括ctrl快捷键&#xff0c;win快捷键&#xff0c;不管是开发人员还是普通办公人员&#xff0c;都是很方便的。我们平时没事操作都是用鼠标去选择对应的功能&#xff0c;或者在我的电脑--控制面板寻找&a…

把matlab的m文件打包成单独的可执行文件

安装Matlab Compiler Adds-on在app里找到Application Compiler 选择要打包的文件matlab单独的运行程序的话需要把依赖的库做成runtime. 这里有两个选项. 上面那个是需要对方在联网的情况下安装, 安装包较小.下面那个是直接把runtime打包成安装程序, 大概由你的程序依赖的库的多…

谷粒商城环境搭建一:Docker容器部署

Docker容器部署 VMware虚拟机安装 参考&#xff1a;VMware虚拟机安装Linux教程 Docker安装 Linux安装Docker # 1.更新apt包索引 sudo apt-get update# 2.安装以下包以使apt可以通过HTTPS使用存储库&#xff08;repository&#xff09; sudo apt-get install -y apt-transpor…

滑动窗口介绍

1.基本概念 利用单调性&#xff0c;使用同向双指针&#xff0c;两个指针之间形成一个窗口 子串与子数组都是连续的一段子序列时不连续的 2.为什么可以用滑动窗口&#xff1f; 暴力解决时发现两个指针不需要回退&#xff08;没必要回退&#xff0c;一定不会符合结果&#xf…

什么是有效的预测性维护 ?

在现代制造业的背景下&#xff0c;设备的可靠性和生产效率成为了企业追求的关键目标。而预测性维护&#xff08;Predictive Maintenance&#xff0c;简称PdM&#xff09;作为一种先进的维护策略&#xff0c;逐渐成为了实现这些目标的重要工具。然而&#xff0c;什么是有效的预测…

2023年国赛数学建模思路 - 案例:随机森林

文章目录 1 什么是随机森林&#xff1f;2 随机深林构造流程3 随机森林的优缺点3.1 优点3.2 缺点 4 随机深林算法实现 建模资料 ## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 什么是随机森林&#xff…

AP9235 dc-dc升压恒流电源驱动IC 2.8-30V 输出电流2A SOT23-6

概述 AP9235B 系列是一款固定振荡频率、恒流输出的升压型DC/DC转换器&#xff0c;非常适合于移动电话、PDA、数码相机等电子产品的背光驱动。输出电压可达30V &#xff0c;3.2V输入电压可以驱动六个串联LED&#xff0c; 2.5V输入电压可以驱动两路并联LED&#xff08;每路串联…

你不知道的 malloc 内幕

你不知道的 malloc 内幕 1. 引言&#xff1a;一个例子例1例2 2. 基础概念2.1 内存管理发展过程2.2 虚拟存储器2.3 内存分配机制2.4 VMA2.4.1 进程的 VMA2.4.2 vma 分析 3. 实例分析3.1 malloc 到底干了啥3.2 memset 的偷天换日3.2.1 虚拟地址转物理地址3.2.2 page fault 3.3 fr…

线程池UncaughtExceptionHandler无效?可能是使用方式不对

背景 在业务处理中&#xff0c;使用了线程池来提交任务执行&#xff0c;但是今天修改了一小段代码&#xff0c;发现任务未正确执行。而且看了相关日志&#xff0c;也并未打印结果。 源码简化版如下&#xff1a; 首先&#xff0c;自定义了一个线程池 public class NamedThrea…

iMX6ULL QT环境配置 | CMake在Linux下的交叉编译环境搭建及使用

习惯了使用cmake&#xff0c;再也不想回到手写makefile的年代了。相比手写makefile&#xff0c;使用cmake则像是实现了机动化&#xff0c;管理项目工程的编译变得很简单了。况且cmake很流行&#xff0c;linux下的很多软件源码包&#xff0c;很多也都使用了cmake的方式编译。因此…

大数据课程K4——Spark的DAGRDD依赖关系

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的DAG; ⚪ 掌握Spark的RDD的依赖关系; ⚪ 了解Spark对于DAG的Stage的划分; 一、DAG概念 1. 概述 Spark会根据用户提交的计算逻辑中的RDD的转换和动作来生成RDD之间的依赖关…