LootCode-鏈表排序-Java


鏈表排序

0.來源

來源:力扣(LeetCode)
題目鏈接:https://leetcode-cn.com/problems/sort-list

1.題目描述

在 O(n log n) 時間復雜度和常數級空間復雜度下,對鏈表進行排序。

2.測試用例

示例 1:

輸入: 4->2->1->3

輸出: 1->2->3->4
示例 2:

輸入: -1->5->3->4->0

輸出: -1->0->3->4->5

3.解題思路

3.1 總體思路

​ 看到鏈表排序,給我的第一個反應就是應該是能實現,主要是我對這題有解題的思路,先不說時間復雜度和空間復雜度什么的,我感覺選擇排序或者是插入排序應該都能實現對鏈表的排序

​ Talk is cheap show me the code..

好吧,上偽代碼.(由於我主要用的是Java編程,所以就用Java 來實現了)

while(沒有到最后一個節點){
  Node cursorNode = currentNode.next;
  
  while( cursorNode != null){
    
      把找到比第一層循環節點的小的節點與它進行交換
        
      cursorNode = cursorNode.next;
  }
  
}

大概就是這樣 ,和 選擇排序實現差不多。

但是看題目: 需要時間復雜度 O(n log n) 還有 常數級別的空間復雜度,這個需要的時間復雜度,讓我想起了歸並排序,一看是也是沒有想通,但是看了遍數組的歸並排序和LeetCode上大佬們的題解就清晰思路了,下面是歸並排序的基本思路

3.2歸並排序思路說明

3.2.1 基本思想

總體概括就是從上到下遞歸拆分,然后從下到上逐步合並。

  • 遞歸拆分

先把待排序數組分為左右兩個子序列,再分別將左右兩個子序列拆分為四個子子序列,以此類推直到最小的子序列元素的個數為兩個或者一個為止。

  • 逐步合並

將最底層的最左邊的一個子序列排序,然后將從左到右第二個子序列進行排序,再將這兩個排好序的子序列合並並排序,然后將最底層從左到右第三個子序列進行排序..... 合並完成之后記憶完成了對數組的排序操作(一定要注意是從下到上層級合並,可以理解為遞歸的層級返回)

3.2.2 算法步驟

  1. 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並后的序列;
  2. 設定兩個指針,最初位置分別為兩個已經排序序列的起始位置;
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置;
  4. 重復步驟 3 直到某一指針達到序列尾;
  5. 將另一序列剩下的所有元素直接復制到合並序列尾。

3.2.3 動態演示

img

3.2.4 算法特性

和選擇排序一樣,歸並排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因為始終都是 O(nlogn) 的時間復雜度。代價是需要額外的內存空間。

3.2.5 代碼展示

/**
 * 遞歸拆分
 * @param arr   待拆分數組
 * @param left  待拆分數組最小下標
 * @param right 待拆分數組最大下標
 */
public static void mergeSort(int[] arr, int left, int right) {
    int mid = (left + right) / 2;  // 中間下標
    if (left < right) {
        mergeSort(arr, left, mid); // 遞歸拆分左邊
        mergeSort(arr, mid + 1, right); // 遞歸拆分右邊
        sort(arr, left, mid, right); // 合並左右
    }
}

/**
 * 合並兩個有序子序列
 * @param arr   待合並數組
 * @param left  待合並數組最小下標
 * @param mid   待合並數組中間下標
 * @param right 待合並數組最大下標
 */
public static void sort(int[] arr, int left, int mid, int right) {
    int[] temp = new int[right - left + 1]; // 臨時數組,用來保存每次合並年之后的結果
    int i = left;
    int j = mid + 1;
    int k = 0; // 臨時數組的初始下標
    // 這個while循環能夠初步篩選出待合並的了兩個子序列中的較小數
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }
    // 將左邊序列中剩余的數放入臨時數組
    while (i <= mid) {
        temp[k++] = arr[i++];
    }
    // 將右邊序列中剩余的數放入臨時數組
    while (j <= right) {
        temp[k++] = arr[j++];
    }
    // 將臨時數組中的元素位置對應到真真實的數組中
    for (int m = 0; m < temp.length; m++) {
        arr[m + left] = temp[m];
    }
}

3.3鏈表使用歸並排序注意點

1.找到中間節點

​ 解: 這個方法是使用 【slow fast 快慢雙指針】 來完成的,聽起來是挺高大上的,其實原理特別簡單,就是一個每次向后挪動一個、另一個向后挪動兩個,肯定是快指針的先到最后,而且是慢指針的二倍。 這就和跑步一樣,如果一個人的速度是你的二倍,在相同時間內,他的路程肯定是你的二倍。

​ 中間節點也根據節點個數來分開,如果是奇數個,中間節點就是中間,如果是偶數個中間節點就是中間位置的前一個節點 ,其實 把慢指針當作中間節點就可以了。

2.從中間節點斷開,然后分別用這兩個鏈表進行排序

​ 如何斷開: 就是將 slow指針的next節點用一個節點給保存下來當作右邊鏈表的開始節點,並將slow指針的next設置成 null

4.代碼實現

class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
    }
}

public class LinkListSort {

    public static ListNode sortList(ListNode head) {

        // 設置遞歸終止條件:如果是一個節點,或者是 null 就可以返回
        if ( head == null || head.next == null)
        {
            return head;
        }

        // 通過 快慢雙指針 來尋找鏈表分割的點
        ListNode slowNode = head;
        ListNode fastNode = head.next;

        while (fastNode!=null && fastNode.next!=null)
        {
            slowNode = slowNode.next;
            fastNode = fastNode.next.next;
        }

        // 設置右部分鏈表的開始部分
        ListNode temp = slowNode.next;

        // 從中間斷開鏈表
        slowNode.next = null;

        ListNode leftNode = sortList((ListNode) head);

        ListNode rightNode = sortList((ListNode) temp);

        //設置一個新的頭節點來保存排序后的效果
        ListNode cursorNode = new ListNode(0);
        ListNode resNode = cursorNode;

        // 對兩個鏈表進行排序

        while ( leftNode!=null && rightNode!=null)
        {
            if(leftNode.val < rightNode.val)
            {
                cursorNode.next= leftNode;
                leftNode = leftNode.next;
            }else{
                cursorNode.next = rightNode;
                rightNode = rightNode.next;
            }

            // 將指針節點向后移動
            cursorNode = cursorNode.next;
        }

        // 判斷兩條鏈表是否循環到結尾,如果沒循環到結尾將未循環完的掛在上面
        cursorNode.next = leftNode == null ? rightNode : leftNode;


        return resNode.next;
    }


    public static void main(String[] args) {
        ListNode head = new ListNode(4);
        ListNode a = new ListNode(2);
        ListNode b = new ListNode(1);
        ListNode c = new ListNode(3);

        head.next =a;
        head.next.next = b;
        head.next.next.next=c;

        ListNode listNode = sortList2( head);

        while ( listNode!=null )
        {
            System.out.print(listNode.val+" ");

            listNode = listNode.next;
        }
    }
  
}

5.總結

​ 1. 學習到了slow 和 fast 雙指針,
2. 還有歸並排序在指針上面使用的優點,不用在申請空間了,沒有數組那么浪費空間,簡直就是給鏈表量身定做的排序算法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM