当前位置: 首页 > article >正文

算法详解——链表的归并排序非递归解法

算法详解——链表的归并排序非递归解法

本文使用倍增法加上归并排序操作实现了对链表的快速排序,比起一般的递归式归并排序要节省空间并且实现要简单的多,比起一般的迭代式归并排序实现也要简单。

1. 题目假设

给定链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

要实现对该链表的快速排序(时间复杂度达到O(nlogn)),比较合适的选择是归并排序(当然快速排序也不是不行)。

image-20241107000431106 ## 2. 回顾一般的解法

一般的解法主要有两种形式,即自顶向下法自底向上法,前者使用递归实现,后者使用迭代实现。

自定向下——递归法

算法思想如下:

  1. 如果当前链表为空或者只有一个结点,直接返回该链表
  2. 找到该链表的中间结点(可以使用快慢指针)
  3. 对左右两条子链执行递归,得到两条排好序的子链
  4. 对两条子链进行归并排序,得到一个有序链表,返回其头指针

自底向上——迭代法

算法思想如下:

  1. 设置步长step为1
  2. 以step个结点为一条子链,从头节点开始遍历,每凑够两条子链,就执行归并操作。如果结点总数小于等于step,直接返回。
  3. 倍增step

但两种方法对于数组来说是比较合适的,因为数组可以随机访问,这样可以很方便的找到中点或者找到下一个长度为step的数组。但是对于链表来说实现起来就比较复杂。

3. 倍增法归并排序

倍增法归并排序的思想是这样的,想象一个64位的整数,那么整数中的第i位能够代表2的i次方的值。

当执行加1操作时,将会从第0位开始如果是0就变为1并返回,如果是1就变为0并继续向后操作。

受此启发,我们可以建立一个64位大小的链表数组,然后遍历待排序链表的每个结点,将其“加入”链表数组。

加入的方法为:从数组的左边开始遍历,如果该位为空就直接加入并返回,如果不为空就执行归并操作并将该位置为空,然后继续向后操作。
在这里插入图片描述

详细操作步骤请看代码和注释。代码如下:

/**
 * Definition for singly-linked list.
 * 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* sortList(ListNode* head) {
        // 创建大小为64的链表指针数组
        ListNode *A[64] = {0};
		
        // 遍历待排序链表的每个结点并将其加入指针数组
        auto it = head;
        while (it != nullptr)
        {
            // 取下当前头节点,并赋值给cur
            auto cur = it;
            it = it->next;
            cur->next = nullptr;
			
    		// 遍历指针数组
            for (int i = 0; i < 64; ++i)
            {
                // 如果当前数组元素为空(对应整数加法中当前位为0),则将其置为当前链表cur
                if (A[i] == nullptr)
                {
                    A[i] = cur;
                    break;
                }
                else	// 否则将cur与当前数组元素所指链表执行归并操作,并将得到的新链表赋给cur,然后将数组元素置空(想想对应加法操作中的什么)
                {
                    cur = merge(cur, A[i]);
                    A[i] = nullptr;
                }
            }
        }
		
        // 最后将指针数组中的所有链表都从左到右进行归并操作,得到一个完整的排好序的链表
        ListNode *res = nullptr;
        for (int i = 0; i < 64; ++i)
        {
            if (A[i] != nullptr)
            {
                res = merge(res, A[i]);
            }
        }
        return res;
    }
	
    // 对left和right两条链表进行归并排序
    ListNode *merge(ListNode *left, ListNode *right)
    {
        ListNode dummy_node;	// 伪头结点
        ListNode *tar = &dummy_node;

        while (left != nullptr & right != nullptr)
        {
            if (left->val < right->val)
            {
                tar->next = left;
                left = left->next;
            }
            else
            {
                tar->next = right;
                right = right->next;
            }
            tar = tar->next;
        }
        if (left) tar->next = left;
        if (right) tar->next = right;
        return dummy_node.next;
    }
};

4. 复杂度分析

空间复杂度

首先分析空间复杂度,可以看到全程只是建立了一个大小为64的指针数组以及归并排序过程中创建了一个伪头节点,因此空间复杂度为O(1)

时间复杂度

时间复杂度的分析就稍有些复杂,不妨从指针数组中每层发生的归并的次数来看:

在这里插入图片描述

在数组第0位上,需要进行n/2次归并得到n/2条的结点数为2的有序链表;在数组第1位上,需要进行n/4次归并,得到n/4条结点数位4的有序链表,依次类推。

在每层的归并操作中,执行比较总次数都是O(n),而总层数可以用O(logn)表示,因此总的时间复杂度就是O(nlogn)。

学习参考

学习更多相关知识请参考零声 github。


http://www.kler.cn/a/384951.html

相关文章:

  • python画图|hist()函数深层体验
  • vue+websocket实现即时聊天平台
  • vue项目使用高德地图
  • 基于SpringBoot的Java教学支持系统开发指南
  • OSS和FastDFS的区别
  • 单链表反转
  • 持续优化,构建更好地 auto git commit 体验
  • JMM(一)[volatilr关键字、乐观锁和悲观锁]
  • 摄像机视频分析软件下载LiteAIServer视频智能分析平台裸土检测
  • 理解Web登录机制:会话管理与跟踪技术解析(一)
  • 【C++】std::cout与std::cin缓冲区
  • 在鱼皮的模拟面试里面学习有感
  • 【Linux基础IO】文件描述符分配规则 重定向
  • 从0开始学习Linux——文件目录
  • docker安装zookeeper,以及zk可视化界面介绍
  • Me-LLaMA——用于医疗领域的新型开源大规模语言模型
  • 如何在 Vue.js 中优化 Element UI 长文本显示
  • 【9695】基于springboot+vue的学生就业管理系统
  • Instagram 青少年账户:安全新升级
  • 反转链表(Leetcode)
  • 与同行争夺白牌商品市场 京东补贴100亿扶持1万家产业带工厂
  • commonJS | module.exports vs exports
  • 推荐FileLink数据跨网摆渡系统 — 安全、高效的数据传输解决方案
  • 说说webpack proxy工作原理?为什么能解决跨域
  • Docker篇(registry私服)
  • 电路设计中的防接反电路