鏈表排序
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 算法步驟
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合並后的序列;
- 設定兩個指針,最初位置分別為兩個已經排序序列的起始位置;
- 比較兩個指針所指向的元素,選擇相對小的元素放入到合並空間,並移動指針到下一位置;
- 重復步驟 3 直到某一指針達到序列尾;
- 將另一序列剩下的所有元素直接復制到合並序列尾。
3.2.3 動態演示
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. 還有歸並排序在指針上面使用的優點,不用在申請空間了,沒有數組那么浪費空間,簡直就是給鏈表量身定做的排序算法。