排序链表(中等)

文章介绍了如何对给定的链表进行升序排序,提供了暴力排序和归并排序两种方法。暴力排序通过不断插入到有序链表中实现,时间复杂度O(n^2),归并排序则利用分治和快慢指针找到中间节点,时间复杂度为O(nlogn)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

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

示例 1:

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

示例 2:

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

示例 3:

输入:head = []
输出:[]

解题方法

暴力排序法

1、解题思路

        最简单的想法就是,每次从待排序的链表中取下一个节点,插入到之前已经排好序的链表中。实现该方法,需要三个主要的辅助指针,指针q和qre用来遍历已经排好序的链表,寻找节点插入位置,指针p用来指向下一个待排序的节点。

        注意:在排序前以头节点作为单独的链表断开,指针p从第二个节点开始遍历排序

2、代码实现
/**
 * 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) {
        if (head == nullptr) {
            return head;
        }
        struct ListNode* h = new ListNode(); // 辅助头结点
        struct ListNode *p, *q, *qre, *t;
        h->next = head;
        p = head->next;
        h->next->next = nullptr; // 断开待排序的链表
        while (p != nullptr) {
            qre = h;
            q = qre->next;
            while (q != nullptr && q->val < p->val) {
                qre = q;
                q = q->next;
            }
            qre->next = p;
            t = p->next;
            p->next = q;
            p = t;
        }
        return h->next;
    }
};
3、复杂度分析

(1)时间复杂度:在最坏情况下,每次寻找插入位置时,都要遍历一次已排好序的链表,并且遍历链表的长度和当前排序的节点序号有关,第一个节点最多遍历0次,第二个节点最多遍历1次,通过递推可得,时间复杂度T(n)=O(n^2)

(2)空间复杂度:此处只是使用了一些辅助变量,因此空间复杂度S(n)=O(1)

归并排序法

1、解题思路

        类比于数组的归并排序,先使用分治法将链表切分为最小归并单元(只有一个节点),然后再将已排好序的链表两两归并。那么在分治过程中就会出现一个问题,数组可以通过数组下标进行两两分治,但是链表不能按下标访问,只能按顺序访问,因此如何查找一个给定头节点链表的中间节点,就是最先要解决的问题。

        首先想到的是蛮力法先遍历一次链表得到链表的长度再遍历一次链表获取到中间节点,则查找中间节点的时间复杂度就为O(n),但遍历两次链表的操作较为冗杂,因此可以使用快慢指针来替代两次遍历

        快指针一次性遍历两个节点,慢指针一次只遍历一个节点,当快指针不能再继续遍历时,慢指针指向的节点就是中间节点。(其操作就相当于是链表长度除以2取整得到的结果),在实现快慢指针时,有以下注意点:

        (1)快指针必须能够一次性走两步后,慢指针才能走一步。

        如果是快指针走一步,慢指针跟着走一步的话,当链表只有两个节点时,返回的中间节点是第二个节点(快慢指针都从头节点开始),而根据数组分治的策略,每次分治的区间是[head,mid]以及[mid+1,end],那么按照上述情况分治得到的区间将一直会是[head,mid]

        (2)在找到链表的中间节点时,要先记下第mid+1个节点的指针,再断开为两个链表,其实现代码如下所示:

 ListNode* node2 = mid->next;
 mid->next = nullptr;

        在通过中间节点实现分治后,先调用自身递归,将左右链表排序后,再合并两个链表,那么接下来的问题就是如何合并两个已排好序的链表

        在给定两个头节点后,创建一个新的头节点来存储排好序的链表,利用双指针分别遍历两个链表,以双指针均不为空为循环结束条件,一旦有一个为空了,则不需要进行比较排序,直接将剩下的链表接在后面即可。

2、代码实现
/**
 * 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) {
        // 当链表为空或者只有一个节点时,不需要进行排序
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        // 进行分治归并排序
        return merge(head);
    }

    ListNode* merge(ListNode* node) {
        if (node->next == nullptr) {
            return node;// 当只有一个节点时不需要进行分治也不需要进行排序
        }
        // 找中间结点
        ListNode* mid = findMid(node);
        ListNode* node2 = mid->next;
        // 进行分割
        mid->next = nullptr;
        // 分别对子问题进行求解
        ListNode* list1 = merge(node);
        ListNode* list2 = merge(node2);
        // 合并两个链表
        return mergeTwoList(list1, list2);
    }

    ListNode* findMid(ListNode* node) {
        ListNode* slow = node;
        ListNode* fast = node;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }

    ListNode* mergeTwoList(ListNode* node1, ListNode* node2) {
        ListNode *node, *p, *q, *t;
        node = new ListNode();
        node->next = nullptr;
        t = node;
        p = node1;
        q = node2;
        while (p != nullptr && q != nullptr) {
            if (p->val <= q->val) {
                t->next = p;
                t = t->next;
                p = p->next;
            } else {
                t->next = q;
                t = t->next;
                q = q->next;
            }
        }
        if (p) {
            t->next = p;
        } else if (q) {
            t->next = q;
        }
        return node->next;
    }
};
3、复杂度分析

(1)时间复杂度merge函数的递归深度为lognfindMid函数时间复杂度为O(n)mergeTwoList时间复杂度为O(m+n),则merge函数每一层的时间复杂度都为O(n),总的时间复杂度T(n)=O(logn)*O(n)=O(nlogn)

(2)空间复杂度:上述函数只使用了一些辅助变量,则空间复杂度为O(1)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值