【LeetCode】排序链表 [M](排序)

166 篇文章 0 订阅
78 篇文章 37 订阅

148. 排序链表 - 力扣(LeetCode)

一、题目

给你链表的头结点 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 = []
输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 104] 内
  • -105 <= Node.val <= 105

二、代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        // 记录链表有多长
        int n = 0;
        ListNode cur = head;
        while (cur != null) {
            cur = cur.next;
            n++;
        }
        
        // 要返回的排序完的链表头部
        ListNode h = head;
        // 按照步长分组,每次将两个分组两两进行合并操作,leftTeamFirst为当前正在操作的两个组的左边的那一组的头节点
        ListNode leftTeamFirst = h;
        // 每次都是操作两组,将两组合并完之后,就再去操作后面的两组。这个pre记录的就是上一次操作两组合并完之后的链表的尾节点
        ListNode pre = null;
        // 从步长为1开始,每次步长乘2,步长要小于n。通过迭代模拟递归归并过程。
        for (int step = 1; step < n; step <<= 1) {
            // 开始从整个链表的头节点开始,进行以step为步长的合并操作。
            // 整个过程就是将列表以长度step分成一组,然后从左往右再两两一组,从左往右将两两一组的按从小到大进行合并
            // 在合并过程中保持整个链表还是连通的,合并后的前一组链表要和下一组合并后的链表连接好,为了方便后续再以step*2的步长进行合并操作
            // 开始两两一组进行合并,将两个组按照从小到大合并成一个
            while (leftTeamFirst != null) {
                // 从leftTeamFirst开始,向后推step*2个长度,划分出两个组,每一个组的长度就是step。hthtn数组存储两个组的头节点和尾节点,以及两个组后面的下一组的头节点
                ListNode[] hthtn = hthtn(leftTeamFirst, step);
                // 将划分出来的两个组进行合并,合并为一组,组内从小到达排序,返回合并后的链表头尾节点
                ListNode[] mhmt = merge(hthtn[0], hthtn[1], hthtn[2], hthtn[3]);

                // 如果此时h为leftTeamFirst,说明这是第一次合并后的操作,需要设置合并后的头节点
                if (h == leftTeamFirst) {
                    // 设置合并后链表的头节点
                    h = mhmt[0];
                    // 设置合并后链表的尾节点,在下一次合并时,下一次合并后链表的头节点应该挂在pre的后面
                    pre = mhmt[1];
                // 非首次的合并操作
                } else {
                    // 将合并后的链表挂在pre后边
                    pre.next = mhmt[0];
                    // 更新pre,下一次合并后链表的头节点就可以挂在pre的后面
                    pre = mhmt[1];
                }
                // 合并完当前两组之后,将leftTeamFirst设置为hthtn[4],即设置为下一组要合并的两个链表的左组头节点,后续进行相同的操作
                leftTeamFirst = hthtn[4];
            }

            // 将leftTeamFirst设置为h,从整个链表的头节点开始,继续以step*2的步长进行合并操作,模拟归并排序
            leftTeamFirst = h;
            // pre设置为空
            pre = null;
        }

        // 返回头节点
        return h;
    }

    // 从leftTeamFirst开始,向后推step*2个长度,划分出两个组,每一个组的长度就是step
    // 返回划分出来的两个组的头尾节点,以及后面的下一组的头节点
    public ListNode[] hthtn(ListNode leftTeamFirst, int step) {
        // 记录划分出来的链表长度
        int cnt = 0;
        // 左组的头尾节点,初始化赋值为leftTeamFirst
        // 左组的头一定是leftTeamFirst
        ListNode lh = leftTeamFirst;
        ListNode lt = leftTeamFirst;
        // 右组的头尾节点,这里先赋值为null,有可能此时节点数量已经不够分给第二组了
        ListNode rh = null;
        ListNode rt = null;
        // 两个组的下一组的头节点,这个是用于在对整个链表进行分组合并的过程中,保证合并后的链表前后还是连通的,不能中断。
        ListNode next = null;
        
        // 从leftTeamFirst开始向后进行分组
        while (leftTeamFirst != null) {
            cnt++;
            // 当cnt小于等于step时,说明左组还没有划分出来,要继续划分左组,更新lt
            if (cnt <= step) {
                lt = leftTeamFirst;
            }

            // 当nt == step + 1,说明左组刚刚划分完成,需要开始划分右组了,这里初始化右组的头节点
            if (cnt == step + 1) {
                rh = leftTeamFirst;
            }

            // 如果cnt大于step了,说明此时整改划分右组,持续更新rt
            if (cnt > step) {
                rt = leftTeamFirst;
            }

            // 当cnt等于step*2,说明左右组划分完了,左右组的长度都是step,直接跳出循环
            if (cnt == (step << 1)) {
                break;
            }
            // 向后遍历链表
            leftTeamFirst = leftTeamFirst.next;
        }

        // 将划分出来的两组断开,为了方便后续的合并操作
        // 左组最后一个节点next设置为null
        lt.next = null;
        // 如果右组的rt不为空,则将rt.next设置为空,和其他链表断开
        if (rt != null) {
            // 要先记录一下rt.next,记录上两个组的下一组的同节点,用于后续将合并后的链表进行连接操作
            next = rt.next;
            rt.next = null;
        }

        // 返回结果
        return new ListNode[] {lh, lt, rh, rt, next};
    }

    // 将划分出来的两个组进行合并,合并为一组,组内从小到达排序,返回合并后的链表头尾节点
    // 此时lh~lt这个组内是有序的,rh~rt这个组内是有序的
    public ListNode[] merge(ListNode lh, ListNode lt, ListNode rh, ListNode rt) {
        // 如果rh为空,说明没有右组,直接返回左组。一般就是最后两组的划分,剩余的节点已经不够分给右组了
        if (rh == null) {
            return new ListNode[] {lh, lt};
        }

        // 先初始化选择好合并后的头节点,两个链表哪个头小就设置为哪个
        ListNode head = lh.val < rh.val ? lh : rh;
        // 当前合并到的节点
        ListNode cur = head;
        // 用来遍历左组链表的指针
        ListNode li = lh;
        // 用来遍历右组链表的指针
        ListNode ri = rh;
        // 根据head设置的情况,来对li和ri进行向后移动,只有已经选择的节点所在的链表在需要向后移动一个位置
        if (head == lh) {
            li = li.next;
        } else {
            ri = ri.next;
        }

        // 合并两个链表,按照从小到大排列
        while (li != null && ri != null) {
            // 将较小的节点挂在cur节点的后面
            if (li.val < ri.val) {
                cur.next = li;
                cur = li;
                li = li.next;
            } else {
                cur.next = ri;
                cur = ri;
                ri = ri.next;
            }
        }

        while (li != null) {
            cur.next = li;
            cur = li;
            li = li.next;
        }

        while (ri != null) {
            cur.next = ri;
            cur = ri;
            ri = ri.next;
        }
        // 执行到这里,cur指向的就是合并后链表的尾节点
        return new ListNode[] {head, cur};
    }
}

三、解题思路 

如果是数组,可以用堆排序,可以实现时间复杂度O(N*logN),空间复杂度O(1)。但链表情况不可以,因为不能方便定位父跟左右孩子。

通过链表进行归并排序是不需要额外空间复杂度的,并且还不能用递归实现,必须用迭代实现才行。所以整个代码就是用迭代实现链表的归并排序。

这种题其实涉及到的算法不多或者没那么难。主要就是考验coding能力和细心。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值