一次面试中碰到了这个题,面试官要求用快排,我第一时间想到的就是快慢指针,因为这种方法可以让链表的结构不被破坏,但面试官要我用别的思路来解决,在面试官的提示下勉勉强强想到了另一种思路,但那段时间真的很疲惫,每天4个小时实习上下班的地铁+周末去学校给老师打工,还连续两个月,身心俱疲。
表达能力差到极点,说了一句这种思路和归并感觉一样了,但我的本意不是算法上的相同,而是说和归并在链表排序上一样需要大幅度破坏原本链表的结构,这让面试官觉得我分不清快排和归并,面试到这个时候我就已经状态不对了,长期得疲惫加此时的失落感,走到了最后一轮技术三面了,还听说前面的评价还都不错,然后成这样,算时间复杂度的时候就已经心不在焉了,居然说成了log(n),最终理所当然的GG,这篇文章用来总结一下这道题目,同时引入几道他的变式,也提醒大家不要太累了,哪怕还年轻,好在今天把学校的事搞得差不多了,不然真的折磨,好好歇两天,不要下次面试又阿巴阿巴。
题目:链表排序
这是一道经典题目,nlog(n)的时间复杂度下,快排、归并、堆排是都可以完成的。
先来看归并的解法,一共是两种自顶向下的递归和自底向上的迭代
归并的解法
自顶向下————递归
class Solution {
public ListNode sortList(ListNode head) {
return gSort(head);
}
public ListNode gSort(ListNode head) {
if(head == null || head.next == null) {
return head;
}
ListNode fast = head.next;
ListNode slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode temp = slow.next;
slow.next = null;
ListNode left = gSort(head);
ListNode right = gSort(temp);
return merge(left, right);
}
public ListNode merge(ListNode left, ListNode right) {
ListNode temp = new ListNode(0);
ListNode res = temp;
while(left != null && right != null) {
if(left.val <= right.val) {
temp.next = left;
left = left.next;
} else {
temp.next = right;
right = right.next;
}
temp = temp.next;
}
temp.next = left != null ? left : right;
return res.next;
}
}
时间复杂度:O(nlog n),其中 n 是链表的长度。
空间复杂度:O(logn),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间。
自底向上————迭代
class Solution {
public ListNode sortList(ListNode head) {
if (head == null) {
return head;
}
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
ListNode dummyHead = new ListNode(0, head);
for (int subLength = 1; subLength < length; subLength *= 1) {
ListNode prev = dummyHead, curr = dummyHead.next;
while (curr != null) {
ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) {
curr = curr.next;
}
ListNode head2 = curr.next;
curr.next = null;
curr = head2;
for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
curr = curr.next;
}
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null;
}
ListNode merged = merge(head1, head2);
prev.next = merged;
while (prev.next != null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
}
public ListNode merge(ListNode left, ListNode right) {
ListNode temp = new ListNode(0);
ListNode res = temp;
while(left != null && right != null) {
if(left.val <= right.val) {
temp.next = left;
left = left.next;
} else {
temp.next = right;
right = right.next;
}
temp = temp.next;
}
temp.next = left != null ? left : right;
return res.next;
}
}
时间复杂度:O(nlogn),其中 n 是链表的长度。
空间复杂度:O(1)。
快排的解法
这个是重头戏,也是面试里让我和面试官没有达成共识的地方,这里放两种思路,一种是我一开始的思路,一种是面试官引导出的思路,至于面试官的思路,我反问请教的时候他不愿意讲。
快慢指针链表快排
class Solution {
public ListNode sortList(ListNode head) {
//采用快速排序
quickSort(head, null);
return head;
}
public void quickSort(ListNode head, ListNode end) {
if (head != end) {
ListNode node = partion(head, end);
quickSort(head, node);
quickSort(node.next, end);
}
}
public ListNode partion(ListNode head, ListNode end) {
ListNode p1 = head, p2 = head.next;
//走到末尾才停
while (p2 != end) {
//大于key值时,p1向前走一步,交换p1与p2的值
if (p2.val < head.val) {
p1 = p1.next;
int temp = p1.val;
p1.val = p2.val;
p2.val = temp;
}
p2 = p2.next;
}
//当有序时,不交换p1和key值
if (p1 != head) {
int temp = p1.val;
p1.val = head.val;
head.val = temp;
}
return p1;
}
}
荷兰国旗链表快排
class Solution {
public ListNode sortList(ListNode head) {
return quickSort(head);
}
public ListNode quickSort(ListNode head) {
if(head == null || head.next == null) return head;
ListNode slow = head, fast = head;
while(fast.next != null && fast.next.next != null){
fast = fast.next.next;
slow = slow.next;
}
int val = slow.val;
ListNode h1 = new ListNode();
ListNode h2 = new ListNode();
ListNode h3 = new ListNode();
ListNode t1 = h1, t2 = h2, t3 = h3, cur = head;
while(cur != null) {
ListNode next = cur.next;
if(cur.val < val) {
cur.next = t1.next;
t1.next = cur;
t1 = t1.next;
} else if(cur.val > val) {
cur.next = t2.next;
t2.next = cur;
t2 = t2.next;
} else {
cur.next = t3.next;
t3.next = cur;
t3 = t3.next;
}
cur = next;
}
h1 = quickSort(h1.next);
h2 = quickSort(h2.next);
h3 = h3.next;
t3.next = h2;
if(h1 == null) {
return h3;
} else {
t1 = h1;
while(t1.next != null) {
t1 = t1.next;
}
t1.next = h3;
return h1;
}
}
}
变式:链表排序之多个有序链表排序
这个变式是一道天然的归并题,多个有序链表可以看作进行单链表排序时,使用了归并的方法到了一定程度,归并是这道题最自然而然的解法。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0){
return null;
}
return mergeList(lists, 0, lists.length-1);
}
ListNode mergeList(ListNode[] lists, int left, int right){
if(left == right){
return lists[left];
}
int mid = (left + right) / 2;
ListNode leftNode = mergeList(lists, left, mid);
ListNode rightNode = mergeList(lists, mid+1, right);
return merge(leftNode, rightNode);
}
ListNode merge(ListNode leftNode, ListNode rightNode) {
ListNode temp = new ListNode(0);
ListNode cur = temp;
while(leftNode != null && rightNode != null) {
if(leftNode.val <= rightNode.val) {
cur.next = leftNode;
leftNode = leftNode.next;
} else{
cur.next = rightNode;
rightNode = rightNode.next;
}
cur = cur.next;
}
if(leftNode != null) {
cur.next = leftNode;
}
if(rightNode != null) {
cur.next = rightNode;
}
return temp.next;
}
}
时间复杂度:O(kn×logk)
空间复杂度:O(logk)