Leetcode 剑指 Offer II 077.排序链表

2024-05-12 10:36
文章标签 leetcode 链表 排序 ii offer 077

本文主要是介绍Leetcode 剑指 Offer II 077.排序链表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

题目难度: 中等

原题链接

今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~

题目描述

给定链表的头结点 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 * 10^4] 内
  • -10^5 <= Node.val <= 10^5

进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

题目思考

  1. 如何做到 O(n log n) 时间复杂度和常数级空间复杂度?

解决方案

思路
  • 分析题目, 一个很容易想到的思路就是将链表每个节点存到一个数组中, 然后使用自定义排序, 对数组按照节点的值从小到大排序, 再依次连起来即可
  • 不过这样虽说时间复杂度可以做到 O(NlogN), 但使用了额外空间, 不满足进阶要求, 如何优化呢?
  • 要想做到常数级空间复杂度排序, 首先就不能采用递归, 因为递归有额外递归栈的空间消耗
  • 另外题目给的是单向链表, 不好从后往前遍历, 所以经典的快速排序在这里也不适用, 如果额外增加向前的连接, 又需要额外空间
  • 常见的 O(NlogN)排序还有归并排序, 虽然递归归并的实现方式比较经典, 但我们也可以利用迭代的方式实现, 这样就可以做到常数级空间复杂度
  • 具体思路就是先将相邻的单个节点进行归并, 然后再将 2 个节点作为整体, 将相邻部分归并, 然后是 4 个, 8 个, 以此类推, 最终实现全部节点的排序
  • 举个例子, 对于链表1->5->4->3->0->2
    1. 第一步, 对相邻节点从小到大归并排序, 即 1 和 5 归并, 4 和 3 归并, 0 和 2 归并, 得到1->5->3->4->0->2
    2. 第二步, 对相邻两个节点从小到大归并排序, 即1->53->4归并, 0->2不变, 得到1->3->4->5->0->2
    3. 第二步, 对相邻四个节点从小到大归并排序, 即1->3->4->50->2归并, 得到0->1->2->3->4->5
  • 这样只需要 logN 次归并, 然后每次归并只需要遍历每个节点常数次, 总体时间复杂度是 O(NlogN), 且没有使用额外空间
  • 具体实现方面, 我们需要记录链表总长度和当前归并部分的长度
  • 然后针对每个归并长度遍历整个链表, 分别得到当前待归并的左右部分, 归并后再继续下个左右部分的归并, 直到达到末尾
  • 最终当归并长度大于等于总长度时, 说明整个链表已经排序好了, 返回链表头即可
  • 另外注意在遍历过程中需要处理好节点的连接关系, 避免导致无限循环
  • 下面代码给出了每一步详细的注释, 大家可以结合代码和注释来理解
复杂度
  • 时间复杂度 O(NlogN): 每次归并排序的长度都是上次长度乘以 2, 所以一共只需要归并 logN 次, 然后每次归并时需要遍历所有节点两遍 (一遍记录左右部分的头和尾, 一遍实际归并), 这部分时间复杂度是 O(N), 所以整体时间复杂度就是 O(NlogN)
  • 空间复杂度 O(1): 只使用了几个常数空间的变量
代码
class Solution:def sortList(self, head: ListNode) -> ListNode:def merge(head1, head2):# 将链表head1和head2进行归并排序# 使用哨兵节点简化处理dummy = ListNode(0)cur = dummywhile head1 or head2:if not head2 or head1 and head1.val <= head2.val:# head2为空, 或者head1的值更小, 追加head1cur.next = head1# head1向后移动head1 = head1.nextelse:# 此时只能追加head2cur.next = head2# head2向后移动head2 = head2.next# 当前节点向后移动cur = cur.next# 返回归并排序后的链表头和尾return (dummy.next, cur)# 统计原始链表的节点数n = 0cur = headwhile cur:cur = cur.nextn += 1# 从长度为1的两个链表开始归并排序cnt = 1# 使用哨兵节点简化处理dummy = ListNode(0, head)# 只要长度不超过n, 就说明有至少两个链表需要归并while cnt < n:# ptail存储前一个归并好的链表的末尾, 初始化为哨兵ptail = dummy# cur是当前处理的节点cur = dummy.nextwhile cur:# lhead和ltail是当前待归并的左侧链表头和尾lhead = curltail = cur# 遍历lcnt个节点, 并更新ltail和curlcnt = cntwhile lcnt > 0 and cur:ltail = curcur = cur.nextlcnt -= 1if ltail:# 断开ltail和右侧的连接ltail.next = None# rhead和rtail是当前待归并的右侧链表头和尾rhead = currtail = cur# 遍历rcnt个节点, 并更新rtail和currcnt = cntwhile rcnt > 0 and cur:rtail = curcur = cur.nextrcnt -= 1if rtail:# 断开rtail和右侧的连接rtail.next = None# 归并左右链表, 得到归并后的新链表的头和尾nhead, ntail = merge(lhead, rhead)# 将前一个归并部分的尾指向当前归并部分的头ptail.next = nhead# 然后更新ptail尾当前归并部分的尾ptail = ntail# 归并长度乘以2, 不断循环归并排序更大长度的两部分, 直到整个链表有序cnt *= 2# 最终哨兵节点的下一个节点就是全部排序后的头return dummy.next

大家可以在下面这些地方找到我~😊

我的 GitHub

我的 Leetcode

我的 CSDN

我的知乎专栏

我的头条号

我的牛客网博客

我的公众号: 算法精选, 欢迎大家扫码关注~😊

算法精选 - 微信扫一扫关注我

这篇关于Leetcode 剑指 Offer II 077.排序链表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/982390

相关文章

哈希leetcode-1

目录 1前言 2.例题  2.1两数之和 2.2判断是否互为字符重排 2.3存在重复元素1 2.4存在重复元素2 2.5字母异位词分组 1前言 哈希表主要是适合于快速查找某个元素(O(1)) 当我们要频繁的查找某个元素,第一哈希表O(1),第二,二分O(log n) 一般可以分为语言自带的容器哈希和用数组模拟的简易哈希。 最简单的比如数组模拟字符存储,只要开26个c

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

csu1329(双向链表)

题意:给n个盒子,编号为1到n,四个操作:1、将x盒子移到y的左边;2、将x盒子移到y的右边;3、交换x和y盒子的位置;4、将所有的盒子反过来放。 思路分析:用双向链表解决。每个操作的时间复杂度为O(1),用数组来模拟链表,下面的代码是参考刘老师的标程写的。 代码如下: #include<iostream>#include<algorithm>#include<stdio.h>#

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

hdu 1285(拓扑排序)

题意: 给各个队间的胜负关系,让排名次,名词相同按从小到大排。 解析: 拓扑排序是应用于有向无回路图(Direct Acyclic Graph,简称DAG)上的一种排序方式,对一个有向无回路图进行拓扑排序后,所有的顶点形成一个序列,对所有边(u,v),满足u 在v 的前面。该序列说明了顶点表示的事件或状态发生的整体顺序。比较经典的是在工程活动上,某些工程完成后,另一些工程才能继续,此时

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

leetcode-24Swap Nodes in Pairs

带头结点。 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) { val = x; }* }*/public class Solution {public ListNode swapPairs(L

leetcode-23Merge k Sorted Lists

带头结点。 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) { val = x; }* }*/public class Solution {public ListNode mergeKLists

建立升序链表

题目1181:遍历链表 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:2744 解决:1186 题目描述: 建立一个升序链表并遍历输出。 输入: 输入的每个案例中第一行包括1个整数:n(1<=n<=1000),接下来的一行包括n个整数。 输出: 可能有多组测试数据,对于每组数据, 将n个整数建立升序链表,之后遍历链表并输出。 样例输