链表结构
单链表或者双链表
单链表的实现可以移植到双链表上,就是指针的修改需要注意一下
一般给我们的都是单链表
题目要求
是否可以交换结点的值?
还是只能交换结点,值无法改变?
最简单的要求是我们可以交换结点的值,如果要求只能交换结点的位置,那么代码会变得稍微复杂一些
一般做法
任何时候,我们都可以遍历一次链表,把链表的值拷贝下来存放到数组中,对数组进行排序,然后将排序后的结果依次赋值给链表结点,此时的开销相当于我们对数组排序的基础上增加了遍历的开销
如果不是特别要求的话,直接使用一般做法即可完成排序
说明
并不是所有算法都适用于链表排序,例如堆排序,希尔排序等,这些排序需要我们利用数组下标来调整元素位置,放在链表上实现效果不是很好,我们只讨论几个比较好实现的例子
为了简单起见,我们只实现升序排列的代码,并且默认可以交换结点的值,链表结构我们使用leetcode上的链表结构,代码用java描述
所有code可以在这里验证正确性:
https://leetcode-cn.com/problems/sort-list/
选择排序——交换值
/**
* 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) {
ListNode cur = head;
ListNode t;
int temp = 0;
while(cur!=null){
t = cur;
ListNode min_node = cur;
int min_val = cur.val;
// select minimal val
while(t!=null){
if(t.val<min_val){
min_val = t.val;
min_node = t;
}
t=t.next;
}
// change val
temp = cur.val;
cur.val = min_val;
min_node.val = temp;
// cur go to next position
cur = cur.next;
}
return head;
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度
O
(
1
)
O(1)
O(1)
注:上面的代码,change val
部分如果封装成一个函数,那么就会超时
冒泡排序——交换值
我们对两两结点交换较大值到后面
/**
* 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 void swap(ListNode a,ListNode b){
int temp = a.val;
a.val = b.val;
b.val = temp;
}
public ListNode sortList(ListNode head) {
ListNode cur = head;
ListNode t = null;
int len = 0;
int ind = 1;
// get length
while(cur != null){
len += 1;
cur = cur.next;
}
while(ind < len){
cur = head;
int i = 0;
while(i < len - ind){
t = cur.next;
if(t.val < cur.val){
// change val
swap(cur, t);
}
// cur go to next position
cur = cur.next;
i += 1;
}
ind += 1;
}
return head;
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度
O
(
1
)
O(1)
O(1)
O ( n 2 ) O(n^2) O(n2) 的时间复杂度通过不了这道题,但是基本的大部分测试样例都通过了,代码还是正确的
插入排序——交换结点
插入排序我们这里构造一个dummy head,方便我们插入排序
/**
* 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) {
ListNode dummyhead = new ListNode();
dummyhead.next = null;
ListNode cur = head;
ListNode t;
ListNode pre;
// 依次摘下结点
while(cur != null){
pre = dummyhead;
t = cur.next;
// 寻找合适的位置插入
while(pre.next != null && pre.next.val < cur.val){
pre=pre.next;
}
cur.next = pre.next;
pre.next = cur;
cur = t;
}
return dummyhead.next;
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度
O
(
1
)
O(1)
O(1)
相比之前的代码,插入排序更加精简,因为它不是在原链表上操作
快速排序——交换值
单链表上不能像数组一样,用两个首尾指针逼向中间来确认枢纽元素的位置,这里我们用两个i、j指针,同时往next方向移动来确定枢纽元素的位置
注意我们用[low,high)
来表示链表的区间,每次我们都选择low
结点作为枢纽元素,尝试将它安排到最终位置上
初始化
// [low,high) 为链表边界,初始时 low = head, high = null
ListNode i = low;
ListNode j = low.next;
int key = low.val;
对于i、j指针指向的结点采取这样的操作
if(i.val < key){
j = j.next;
swap(i,j);
}
i、j不断往后遍历,直到p指针达到边界,此时做如下操作
swap(i,low);
如此我们就能确认一个元素的最终位置了,然后能够划分出两个待排序的子区间,重复上述步骤即可
为了加深印象,解释一下操作的意义:
我们每次遍历将low
作为枢纽元素,目的是将它放置到最终的位置上
然后我们使用i、j两个指针用来遍历链表
注意我们暂时的排除了low
这个结点,考虑的是(low,high)
这个结点区间,我们对low结点之后的结点进行遍历
j指针负责遍历链表,然后每次遇到一个比key小的元素的时候,我们让i的位置右移1位,表示有1个元素比key小,然后我们交换i、j结点的值,将小的元素交换到左边,最后我们对于i的位置,交换low和i结点即可,此时low结点将会交换到最后的位置上
/**
* 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 void swap(ListNode a,ListNode b){
int temp = a.val;
a.val = b.val;
b.val = temp;
}
public ListNode sortList(ListNode head) {
if(head == null || head.next == null)
return head;
qsort(head,null);
return head;
}
public void qsort(ListNode low, ListNode high){
if(low == high || low.next == high)
return ;
ListNode middle = partition(low,high);
qsort(low,middle);
qsort(middle.next,high);
}
public ListNode partition(ListNode low, ListNode high){
if(low.next == high)
return null;
int key = low.val;
ListNode i = low;
ListNode j = low.next;
while(j!=high){
if(j.val<key){
i=i.next;
swap(i,j);
}
j=j.next;
}
swap(i,low);
return i;
}
}
时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度
O
(
l
o
g
n
)
O(logn)
O(logn)
归并排序
归并排序被认为是最好的链表排序方法 —— 《算法》
《算法》书上的 ex 2.2.17要求我们实现对链表的自然排序,这是我之前实现的参考代码:https://github.com/hhmy27/Alg4_Code/blob/master/src/ch02/part2/ex_2_2_17.java
这是归并排序的一个版本
递归
一样按照数组的递归方式来实现,分为两个部分
- divide
我们使用快慢指针来找到链表的中间结点,找完之后有如下情况
- head 头指针
- slow 链表的中间结点
- fast 链表尾
我们取mid = slow.next
,作为第二个部分的起点,那么我们现在有[head,slow]
,[mid,tail]
两个结点(tail指的是链表的最后一个节点)
为了区分出两个结点,我们还需要设置slow.next = null
那么我们递归调用 mergeSort(head)
,meageSort(mid)
,即可递归排序两个链表部分
- merge
merge的话,我们使用 dummy head 来构造一条新的链表,这个技巧和插入排序的一样,下面是java代码
/**
* 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) {
if(head == null || head.next == null)
return head;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
ListNode mid = slow.next;
slow.next = null;
// sort two part
ListNode a = sortList(head);
ListNode b = sortList(mid);
// merge
ListNode dummyhead = new ListNode();
ListNode cur = dummyhead;
while(a != null && b != null){
if(a.val<=b.val){
cur.next = a;
a=a.next;
}else{
cur.next = b;
b=b.next;
}
cur = cur.next;
}
if(a!=null){
cur.next = a;
}
if(b!=null){
cur.next = b;
}
return dummyhead.next;
}
}
时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度
O
(
n
)
O(n)
O(n) —— 大小为n的辅助数组
归并递归实现写起来的感觉就是非常的对称(🤣🤣
迭代
数组的归并排序里面有迭代实现,链表上我们也可以用递归的方法来实现
具体步骤就是按照步长pace=1,2,4...
来合并相邻结点,前提条件是我们需要知道链表的长度len
,当pace >= len
的时候,我们就不需要继续迭代了
考虑实现我们这里有两种方法
-
按照步长摘下h1,h2两条链表,其中h1长度一定等于pace,如果找不到h2时,我们结束一趟的迭代,让pace*2继续迭代
对于摘下的链表,我们合并它们,然后将合并完的链表放回原来的位置即可 -
按照步长,我们记录h1,h2的长度
c1,c2
,在合并的判断条件将h1!=null
改为c1!=0
,然后再尝试两条链表
方法2的代码量会比方法1少很多,因为方法1要考虑错综复杂的指针关系
整体代码不难,但是放在链表上实现要考虑很多的细节,需要比较强的代码能力
/**
* 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 static ListNode sortList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode dummyhead = new ListNode();
dummyhead.next = head;
ListNode pre;
// 统计长度
int len = 0;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
len += 1;
}
int pace = 1;
// 合并的两个子链表
ListNode h1;
ListNode h2;
int c1 = 0;
int c2 = 0;
while (pace < len) {
// 按照pace归并一趟
pre = dummyhead;
cur = dummyhead.next;
while (cur != null) {
h1 = cur;
int i = pace;
// 获得第一部分的链表
while (cur != null && i > 0) {
i -= 1;
cur = cur.next;
}
// 找不到完整的h1链表
if (i > 0)
break;
h2 = cur;
i = pace;
while (cur != null && i > 0) {
i -= 1;
cur = cur.next;
}
c1 = pace;
c2 = pace - i;
while (c1 > 0 && c2 > 0) {
if (h1.val <= h2.val) {
c1 -= 1;
pre.next = h1;
h1 = h1.next;
} else {
c2 -= 1;
pre.next = h2;
h2 = h2.next;
}
pre = pre.next;
}
// 续上结点
pre.next = (c1 == 0 ? h2 : h1);
// pre走到最后一个节点,然后指向cur,此时cur停留在下一个h1处
while (c1 != 0) {
c1 -= 1;
pre = pre.next;
}
while (c2 != 0) {
c2 -= 1;
pre = pre.next;
}
pre.next = cur;
}
pace *= 2;
}
return dummyhead.next;
}
}
这代码写起来真的累。。
ref
链表排序(冒泡、选择、插入、快排、归并、希尔、堆排序)
单链表排序----快排 & 归并排序
Sort + 快速 + 归并 + 迭代 + 插入 + 堆排序(10行代码,6解法)
Sort List (归并排序链表)