链表排序相关

链表结构

单链表或者双链表
单链表的实现可以移植到双链表上,就是指针的修改需要注意一下
一般给我们的都是单链表

题目要求

是否可以交换结点的值?
还是只能交换结点,值无法改变?

最简单的要求是我们可以交换结点的值,如果要求只能交换结点的位置,那么代码会变得稍微复杂一些

一般做法

任何时候,我们都可以遍历一次链表,把链表的值拷贝下来存放到数组中,对数组进行排序,然后将排序后的结果依次赋值给链表结点,此时的开销相当于我们对数组排序的基础上增加了遍历的开销

如果不是特别要求的话,直接使用一般做法即可完成排序

说明

并不是所有算法都适用于链表排序,例如堆排序,希尔排序等,这些排序需要我们利用数组下标来调整元素位置,放在链表上实现效果不是很好,我们只讨论几个比较好实现的例子

为了简单起见,我们只实现升序排列的代码,并且默认可以交换结点的值,链表结构我们使用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
    我们使用快慢指针来找到链表的中间结点,找完之后有如下情况
  1. head 头指针
  2. slow 链表的中间结点
  3. 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的时候,我们就不需要继续迭代了

考虑实现我们这里有两种方法

  1. 按照步长摘下h1,h2两条链表,其中h1长度一定等于pace,如果找不到h2时,我们结束一趟的迭代,让pace*2继续迭代
    对于摘下的链表,我们合并它们,然后将合并完的链表放回原来的位置即可

  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 (归并排序链表)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值