前言
链表排序处理起来比较麻烦,因为它不支持下标操作。这里写一下链表排序的常用方法。
题目
描述
给定一个节点数为n的无序单链表,对其按升序排序。
数据范围:
0
<
n
≤
100000
0<n≤100000
0<n≤100000,保证节点权值在
[
−
1
0
9
,
1
0
9
]
[−10^9,10^9]
[−109,109]之内。
要求:空间复杂度
O
(
n
)
O(n)
O(n),时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
解决方案一
1.1 思路阐述
链表不支持下标运算,那就用数组来做。
先把链表的所有数据存放到一个数组中,对数组进行排序,直接调用C++的库函数sort。
将排序完好数组一个一个给到新的链表即可。
缺点:如果链表数据量太大,那么开辟的空间也会很大。
1.2 源码
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
#include <algorithm>
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类 the head node
* @return ListNode类
*/
ListNode* sortInList(ListNode* head) {
// 所有数据存数组,数组排序,开辟新链表返回
int a[1000000]={0};
int index=0;
ListNode* finalList=new ListNode(-1);
ListNode* cur=finalList;
//存数据
while (head) {
a[index++]=head->val;
head=head->next;
}
//排序
sort(a, a+index);
//排序后的数据存到新链表
int indexB=0;
while (index--) {
ListNode* temp=new ListNode(a[indexB++]);
cur->next=temp;
cur=cur->next;
}
return finalList->next;
}
};
解决方案二
2.1 思路阐述
第二种方式是在链表中使用归并排序。
其实看到题目的要求,空间复杂度
O
(
n
)
O(n)
O(n),时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),最方便当然是用辅助数组排序来做。但是如果空间复杂度改成O(1),那么开辟新数组的方式显然不合适。看到时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),我第一反应就是快排,但是由于链表不支持下标操作,那退而求其次就是归并排序了。
下面粘的是官方的一个解答。
知识点1:分治
分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。
知识点2:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路
:
前面我们做合并两个有序链表不是使用归并思想吗?说明在链表中归并排序也不是不可能使用,合并阶段可以参照前面这道题,两个链表逐渐取最小的元素就可以了,但是划分阶段呢?
常规数组的归并排序是分治思想,即将数组从中间个元素开始划分,然后将划分后的子数组作为一个要排序的数组,再将排好序的两个子数组合并成一个完整的有序数组,因此采用的是递归。而链表中我们也可以用同样的方式,只需要找到中间个元素的前一个节点,将其断开,就可以将链表分成两个子链表,然后继续划分,直到最小,然后往上依次合并。
终止条件: 当子链表划分到为空或者只剩一个节点时,不再继续划分,往上合并。
返回值: 每次返回两个排好序且合并好的子链表。
本级任务: 找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序。
怎么找中间元素呢?我们也可以使用快慢双指针,快指针每次两步,慢指针每次一步,那么快指针到达链表尾的时候,慢指针正好走了快指针距离的一半,为中间元素。
具体做法:
step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,
当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
step 4:将子问题得到的链表合并,参考合并两个有序链表。
2.2 源码
class Solution {
public:
//合并两段有序链表
ListNode* merge(ListNode* pHead1, ListNode* pHead2) {
//一个已经为空了,直接返回另一个
if(pHead1 == NULL)
return pHead2;
if(pHead2 == NULL)
return pHead1;
//加一个表头
ListNode* head = new ListNode(0);
ListNode* cur = head;
//两个链表都要不为空
while(pHead1 && pHead2){
//取较小值的节点
if(pHead1->val <= pHead2->val){
cur->next = pHead1;
//只移动取值的指针
pHead1 = pHead1->next;
}else{
cur->next = pHead2;
//只移动取值的指针
pHead2 = pHead2->next;
}
//指针后移
cur = cur->next;
}
//哪个链表还有剩,直接连在后面
if(pHead1)
cur->next = pHead1;
else
cur->next = pHead2;
//返回值去掉表头
return head->next;
}
ListNode* sortInList(ListNode* head) {
//链表为空或者只有一个元素,直接就是有序的
if(head == NULL || head->next == NULL)
return head;
ListNode* left = head;
ListNode* mid = head->next;
ListNode* right = head->next->next;
//右边的指针到达末尾时,中间的指针指向该段链表的中间
while(right != NULL && right->next != NULL){
left = left->next;
mid = mid->next;
right = right->next->next;
}
//左边指针指向左段的左右一个节点,从这里断开
left->next = NULL;
//分成两段排序,合并排好序的两段
return merge(sortInList(head), sortInList(mid));
}
};
总结
链表排序除了用辅助数组,还可以用归并的思想。关于归并排序在链表中的运用还是有点陌生,需要多实践。