148.排序链表
题目:
给你链表的头结点 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
进阶: 你可以在 O(n log n)
时间复杂度和常数级空间复杂度下,对链表进行排序吗?
解答一:归并排序(递归法)
1.题目要求时间空间复杂度分别为O(nlogn)和O(1),根据时间复杂度我们自然想到二分法,从而联想到归并排序;
2.对数组做归并排序的空间复杂度为 O(n),分别由新开辟数组O(n)和递归函数调用O(logn)组成,而根据链表特性:
a.数组额外空间:链表可以通过修改引用来更改节点顺序,无需像数组一样开辟额外空间;
b.递归额外空间:递归调用函数将带来O(logn)的空间复杂度,因此若希望达到O(1)空间复杂度,则不能使用递归。3.通过递归实现链表归并排序,有以下两个环节:
分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
a.我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。
b.找到中点 slow 后,执行 slow.next = None 将链表切断。
c.递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。
4.合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
a.双指针法合并,建立辅助ListNode h 作为头部。
b.设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
c.返回辅助ListNode h 作为头部的下个节点 h.next。
d.时间复杂度 O(l + r),l, r 分别代表两个链表长度。
5.当题目输入的 head == None 时,直接返回None。
public ListNode sortList(ListNode head) {
// 1、递归结束条件
if (head == null || head.next == null) {
return head;
}
// 2、找到链表中间节点并断开链表 & 递归下探
ListNode midNode = middleNode(head);
ListNode rightHead = midNode.next;
midNode.next = null;
ListNode left = sortList(head);
ListNode right = sortList(rightHead);
// 3、当前层业务操作(合并有序链表)
return mergeTwoLists(left, right);
}
// 找到链表中间节点(876. 链表的中间结点)
private ListNode middleNode(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode slow = head;
ListNode fast = head.next.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 合并两个有序链表(21. 合并两个有序链表)
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode sentry = new ListNode(-1);
ListNode curr = sentry;
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = l1 != null ? l1 : l2;
return sentry.next;
}
解答二:归并排序(从底至顶直接合并)
对于非递归的归并排序,需要使用迭代的方式替换cut环节:
我们知道,cut环节本质上是通过二分法得到链表最小节点单元,再通过多轮合并得到排序结果。
每一轮合并merge操作针对的单元都有固定长度intv,例如:
第一轮合并时intv = 1,即将整个链表切分为多个长度为1的单元,并按顺序两两排序合并,合并完成的已排序单元长度为2。
第二轮合并时intv = 2,即将整个链表切分为多个长度为2的单元,并按顺序两两排序合并,合并完成已排序单元长度为4。
以此类推,直到单元长度intv >= 链表长度,代表已经排序完成。
根据以上推论,我们可以仅根据intv计算每个单元边界,并完成链表的每轮排序合并,例如:
当intv = 1时,将链表第1和第2节点排序合并,第3和第4节点排序合并,……。
当intv = 2时,将链表第1-2和第3-4节点排序合并,第5-6和第7-8节点排序合并,……。
当intv = 4时,将链表第1-4和第5-8节点排序合并,第9-12和第13-16节点排序合并,……。
此方法时间复杂度O(nlogn),空间复杂度O(1)。
class Solution {
public ListNode sortList(ListNode head) {
ListNode h, h1, h2, pre, res;
h = head;
int length = 0, intv = 1;
while (h != null) {
h = h.next;
length++;
}
res = new ListNode(0);
res.next = head;
while (intv < length) {
pre = res;
h = res.next;
while (h != null) {
int i = intv;
h1 = h;
while (i > 0 && h != null) {
h = h.next;
i--;
}
if (i > 0) break;
i = intv;
h2 = h;
while (i > 0 && h != null) {
h = h.next;
i--;
}
int c1 = intv, c2 = intv - i;
while (c1 > 0 && c2 > 0) {
if (h1.val < h2.val) {
pre.next = h1;
h1 = h1.next;
c1--;
} else {
pre.next = h2;
h2 = h2.next;
c2--;
}
pre = pre.next;
}
pre.next = c1 == 0 ? h2 : h1;
while (c1 > 0 || c2 > 0) {
pre = pre.next;
c1--;
c2--;
}
pre.next = h;
}
intv *= 2;
}
return res.next;
}
}
解法三:快排
public static ListNode sortList(ListNode head) {
if(head==null || head.next==null){
return head;
}
ArrayList<Integer> data = new ArrayList();
ListNode pre = head;
int len =0;
while(pre!=null){
data.add(pre.val);
pre=pre.next;
len++;
}
int[] array = data.stream().mapToInt(Integer::intValue).toArray();
quickSort( array,0,len-1);
ListNode first = new ListNode( -1 );
ListNode result = first;
for(int i=0;i<len;i++){
head = head.next;
ListNode temp = new ListNode( array[i]);
first.next = temp;
first = temp;
}
return result.next;
}
public static void quickSort(int[] array, int low, int high) {
/**
* 分析:
* 1.选定一个基准值,array[low]
* 2.右指针从右向左遍历high--,查找比基准值小的数据,左指针从左向右low++,查找比基准值大的数据
* 3.如果指针未相遇,交换左右两值位置,如果指针相遇,则替换基准值的位置
* 4.左递归,右递归
*/
// 方法退出条件,指针相遇或错过
if (low >= high) {
return;
}
// 1. 指定基准值和左右指针记录位置
int pivot = array[low];
int l = low;
int r = high;
int temp = 0;
// 2. 遍历条件,左右指针位置
while (l < r) {
// 2.1 右侧遍历
while (l < r && array[r] >= pivot) {
r--;
}
// 2.2 左侧遍历
while (l < r && array[l] <= pivot) {
l++;
}
// 2.3 l指针还在r指针左侧,尚未相遇
if (l < r) {
temp = array[l];
array[l] = array[r];
array[r] = temp;
}
}
// 3. 当左右指针相遇,交换基准值位置
array[low] = array[l];
array[l] = pivot;
// 4. 根据条件左侧递归遍历
if (low < l) {
quickSort(array, low, l - 1);
}
// 5. 根据条件右侧递归遍历
if (r < high) {
quickSort(array, r + 1, high);
}
}