【Hot 100】 148. 排序链表

目录

  • 引言
  • 十大排序算法
    • [1. 冒泡排序 (Bubble Sort)](#1. 冒泡排序 (Bubble Sort))
    • [2. 选择排序 (Selection Sort)](#2. 选择排序 (Selection Sort))
    • [3. 插入排序 (Insertion Sort)](#3. 插入排序 (Insertion Sort))
    • [4. 希尔排序 (Shell Sort)](#4. 希尔排序 (Shell Sort))
    • [5. 归并排序 (Merge Sort)](#5. 归并排序 (Merge Sort))
    • [6. 快速排序 (Quick Sort)](#6. 快速排序 (Quick Sort))
    • [7. 堆排序 (Heap Sort)](#7. 堆排序 (Heap Sort))
    • [8. 计数排序 (Counting Sort)](#8. 计数排序 (Counting Sort))
    • [9. 桶排序 (Bucket Sort)](#9. 桶排序 (Bucket Sort))
    • [10. 基数排序 (Radix Sort)](#10. 基数排序 (Radix Sort))
    • 总结
  • 排序链表
  • 🙋‍♂️ 作者:海码007
  • 📜 专栏:算法专栏
  • 💥 标题:【Hot 100】 148. 排序链表
  • ❣️ 寄语:书到用时方恨少,事非经过不知难!

引言

今天的题目是对链表进行排序,所以我先简单回顾一下十大排序算法的基本思想以及C++代码实现。

十大排序算法

1. 冒泡排序 (Bubble Sort)

基本思想:重复地遍历要排序的数列,一次比较两个元素,如果顺序错误就交换它们。每次遍历后,最大的元素会"冒泡"到最后。

cpp 复制代码
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr[j], arr[j+1]);
            }
        }
    }
}

2. 选择排序 (Selection Sort)

基本思想:每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾。

cpp 复制代码
void selectionSort(int arr[], int n) {
    for (int i = 0; i < n-1; i++) {
        int min_idx = i;
        for (int j = i+1; j < n; j++) {
            if (arr[j] < arr[min_idx]) {
                min_idx = j;
            }
        }
        swap(arr[i], arr[min_idx]);
    }
}

3. 插入排序 (Insertion Sort)

基本思想:将未排序部分的第一个元素插入到已排序部分的适当位置。

cpp 复制代码
void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = key;
    }
}

4. 希尔排序 (Shell Sort)

讲解视频(这个视频讲的非常好)

希尔排序就是使用分治思想的插入排序,他使用了插入排序的两个优点:

  1. 如果数组基本有序的话,插入排序的时间复杂度为 O(n)
  2. 数据量较小时,排序效率较高

用班级排队来理解希尔排序

想象你是一个体育老师,要帮全班同学按身高从矮到高排队。如果直接用插入排序(让每个同学一个个找到自己的位置),对于乱序的队伍会非常耗时。希尔排序的做法更聪明:

  1. 先把同学分成几组:比如先让每间隔4个同学为一组(第1、5、9...个同学一组,第2、6、10...个同学一组,以此类推)

  2. 在组内排好序:让每个小组内部先按身高排好

  3. 缩小分组范围:然后让每间隔2个同学为一组,再次排序

  4. 最后全体一起排:最后取消分组,全体一起做一次完整的插入排序

这时候队伍已经基本有序了,最后这次完整的排序会非常快!

为什么这样更快?

  • 前期分组排序:就像先把大问题拆成小问题解决
  • 逐步细化:每次排序后队伍都更有序一点
  • 最后一步轻松:当队伍基本有序时,插入排序效率最高

简单代码说明

cpp 复制代码
void shellSort(int arr[], int n) {
    // 初始间隔是数组长度的一半
    for (int gap = n/2; gap > 0; gap /= 2) {
        // 对每个分组进行插入排序
        for (int i = gap; i < n; i++) {
            int temp = arr[i]; // 记住当前这张"牌"
            int j;
            // 把比当前"牌"大的往后移
            for (j = i; j >= gap && arr[j-gap] > temp; j -= gap) {
                arr[j] = arr[j-gap];
            }
            // 把当前"牌"放到正确位置
            arr[j] = temp;
        }
    }
}

关键特点

  • 不是相邻比较:普通插入排序是相邻元素比较,希尔排序是"跳着比较"
  • 越来越精确:比较的间隔从大到小,最后变成1(就是普通插入排序)
  • 前期工作不白费:每次分组排序都为下一次创造了更好的基础

记住这个算法就像"先粗排,再细排,最后精排"的过程,这样就能既快又好地完成排序任务!

5. 归并排序 (Merge Sort)

基本思想:分治法,将列表分成两半,分别排序,然后合并两个有序列表。

cpp 复制代码
void merge(int arr[], int l, int m, int r) {
    int n1 = m - l + 1;
    int n2 = r - m;
    
    int L[n1], R[n2];
    for (int i = 0; i < n1; i++) L[i] = arr[l + i];
    for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];
    
    int i = 0, j = 0, k = l;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) arr[k++] = L[i++];
        else arr[k++] = R[j++];
    }
    while (i < n1) arr[k++] = L[i++];
    while (j < n2) arr[k++] = R[j++];
}

void mergeSort(int arr[], int l, int r) {
    if (l < r) {
        int m = l + (r - l) / 2;
        mergeSort(arr, l, m);
        mergeSort(arr, m + 1, r);
        merge(arr, l, m, r);
    }
}

6. 快速排序 (Quick Sort)

基本思想:分治法,选择一个基准元素,将数组分为两部分,一部分小于基准,一部分大于基准,然后递归排序。

cpp 复制代码
int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] < pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }
    swap(arr[i + 1], arr[high]);
    return i + 1;
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

7. 堆排序 (Heap Sort)

基本思想:利用堆这种数据结构进行排序,首先构建最大堆,然后逐个取出堆顶元素。

cpp 复制代码
void heapify(int arr[], int n, int i) {
    int largest = i;
    int l = 2 * i + 1;
    int r = 2 * i + 2;
    
    if (l < n && arr[l] > arr[largest]) largest = l;
    if (r < n && arr[r] > arr[largest]) largest = r;
    
    if (largest != i) {
        swap(arr[i], arr[largest]);
        heapify(arr, n, largest);
    }
}

void heapSort(int arr[], int n) {
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
    for (int i = n - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        heapify(arr, i, 0);
    }
}

8. 计数排序 (Counting Sort)

基本思想:适用于整数排序,统计每个元素出现的次数,然后计算每个元素在输出数组中的位置。

cpp 复制代码
void countingSort(int arr[], int n) {
    int max_val = *max_element(arr, arr + n);
    int min_val = *min_element(arr, arr + n);
    int range = max_val - min_val + 1;
    
    vector<int> count(range), output(n);
    for (int i = 0; i < n; i++) count[arr[i] - min_val]++;
    for (int i = 1; i < range; i++) count[i] += count[i - 1];
    for (int i = n - 1; i >= 0; i--) {
        output[count[arr[i] - min_val] - 1] = arr[i];
        count[arr[i] - min_val]--;
    }
    for (int i = 0; i < n; i++) arr[i] = output[i];
}

9. 桶排序 (Bucket Sort)

基本思想:将数组分到有限数量的桶里,每个桶再分别排序(可以使用其他排序算法)。

cpp 复制代码
void bucketSort(float arr[], int n) {
    vector<vector<float>> buckets(n);
    
    for (int i = 0; i < n; i++) {
        int bi = n * arr[i];
        buckets[bi].push_back(arr[i]);
    }
    
    for (int i = 0; i < n; i++) {
        sort(buckets[i].begin(), buckets[i].end());
    }
    
    int index = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < buckets[i].size(); j++) {
            arr[index++] = buckets[i][j];
        }
    }
}

10. 基数排序 (Radix Sort)

基本思想:按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。

cpp 复制代码
void countingSortForRadix(int arr[], int n, int exp) {
    int output[n];
    int count[10] = {0};
    
    for (int i = 0; i < n; i++) count[(arr[i] / exp) % 10]++;
    for (int i = 1; i < 10; i++) count[i] += count[i - 1];
    for (int i = n - 1; i >= 0; i--) {
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }
    for (int i = 0; i < n; i++) arr[i] = output[i];
}

void radixSort(int arr[], int n) {
    int max_val = *max_element(arr, arr + n);
    for (int exp = 1; max_val / exp > 0; exp *= 10) {
        countingSortForRadix(arr, n, exp);
    }
}

总结

1. 冒泡排序 (Bubble Sort)

优点

  • 实现简单,代码容易理解
  • 对于小规模数据效率尚可
  • 稳定排序(相同元素相对位置不变)

缺点

  • 效率低下,时间复杂度高
  • 不适用于大规模数据

复杂度

  • 时间复杂度:
    • 最好:O(n)(已排序情况)
    • 平均:O(n²)
    • 最坏:O(n²)
  • 空间复杂度:O(1)(原地排序)

2. 选择排序 (Selection Sort)

优点

  • 实现简单
  • 不占用额外内存空间
  • 交换次数少(最多n-1次交换)

缺点

  • 时间复杂度高
  • 不稳定排序
  • 对已排序数组仍需同样时间

复杂度

  • 时间复杂度:
    • 最好/平均/最坏:O(n²)
  • 空间复杂度:O(1)

3. 插入排序 (Insertion Sort)

优点

  • 对小规模或基本有序数据效率高
  • 稳定排序
  • 实现简单
  • 原地排序

缺点

  • 大规模乱序数据效率低
  • 最坏情况性能差

复杂度

  • 时间复杂度:
    • 最好:O(n)(已排序)
    • 平均:O(n²)
    • 最坏:O(n²)
  • 空间复杂度:O(1)
  1. 希尔排序 (Shell Sort)

优点

  • 插入排序的改进版,效率更高
  • 适用于中等规模数据
  • 原地排序

缺点

  • 复杂度分析复杂
  • 不稳定排序
  • 增量序列选择影响性能

复杂度

  • 时间复杂度:
    • 最好:O(n)
    • 平均:O(n^1.3)(取决于增量序列)
    • 最坏:O(n²)
  • 空间复杂度:O(1)
  1. 归并排序 (Merge Sort)

优点

  • 稳定排序
  • 时间复杂度稳定为O(nlogn)
  • 适合链表排序
  • 适合外部排序

缺点

  • 需要额外O(n)空间
  • 对小规模数据可能不如插入排序

复杂度

  • 时间复杂度:
    • 最好/平均/最坏:O(nlogn)
  • 空间复杂度:O(n)
  1. 快速排序 (Quick Sort)

优点

  • 平均情况下最快的排序算法
  • 原地排序(优化版本)
  • 缓存友好

缺点

  • 最坏情况O(n²)
  • 不稳定排序
  • 递归实现需要栈空间

复杂度

  • 时间复杂度:
    • 最好/平均:O(nlogn)
    • 最坏:O(n²)(已排序或所有元素相同)
  • 空间复杂度:
    • 平均:O(logn)(递归栈)
    • 最坏:O(n)
  1. 堆排序 (Heap Sort)

优点

  • 时间复杂度稳定
  • 原地排序
  • 适合获取前k个元素

缺点

  • 不稳定排序
  • 缓存不友好(跳跃访问)
  • 实际效率常不如快速排序

复杂度

  • 时间复杂度:
    • 最好/平均/最坏:O(nlogn)
  • 空间复杂度:O(1)
  1. 计数排序 (Counting Sort)

优点

  • 线性时间复杂度
  • 稳定排序(实现得当)

缺点

  • 仅适用于整数且范围不大的情况
  • 需要额外空间

复杂度

  • 时间复杂度:O(n+k)(k是数值范围)
  • 空间复杂度:O(n+k)
  1. 桶排序 (Bucket Sort)

优点

  • 当分布均匀时效率高
  • 稳定排序(实现得当)

缺点

  • 需要知道数据分布情况
  • 最坏情况退化为O(n²)
  • 需要额外空间

复杂度

  • 时间复杂度:
    • 最好:O(n)
    • 平均:O(n+k)(k是桶数量)
    • 最坏:O(n²)
  • 空间复杂度:O(n+k)
  1. 基数排序 (Radix Sort)

优点

  • 线性时间复杂度
  • 稳定排序(实现得当)

缺点

  • 仅适用于整数或特定格式数据
  • 需要额外空间
  • 常数因子较大

复杂度

  • 时间复杂度:O(d(n+k))(d是位数,k是基数)
  • 空间复杂度:O(n+k)
排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 适用场景
冒泡排序 O(n²) O(n²) O(1) 稳定 小规模数据教学用途
选择排序 O(n²) O(n²) O(1) 不稳定 交换成本高的情况
插入排序 O(n²) O(n²) O(1) 稳定 小规模或基本有序数据
希尔排序 O(n^1.3) O(n²) O(1) 不稳定 中等规模数据
归并排序 O(nlogn) O(nlogn) O(n) 稳定 大规模数据、外部排序
快速排序 O(nlogn) O(n²) O(logn) 不稳定 通用排序、大规模随机数据
堆排序 O(nlogn) O(nlogn) O(1) 不稳定 需要稳定时间复杂度
计数排序 O(n+k) O(n+k) O(n+k) 稳定 小范围整数
桶排序 O(n+k) O(n²) O(n+k) 稳定 均匀分布的数据
基数排序 O(d(n+k)) O(d(n+k)) O(n+k) 稳定 多位数整数或字符串

实际应用中,快速排序通常是最佳选择,但当需要稳定性或特定数据特性时,其他算法可能更合适。C++中的std::sort通常采用快速排序的优化版本(如内省排序,结合了快速排序、堆排序和插入排序的优点)。

排序链表

  • 🎈 题目链接:
  • 🎈 做题状态:

我的解题

使用了冒泡排序的思想,在排序链表时和排序数组完全是不一样的。因为链表的访问都是从前往后,不能随机访问。所以在实现的时候需要额外处理。

cpp 复制代码
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        // 我感觉这道题用插入排序还是比较难写出代码,相比冒泡排序思路代码就好写一点,因为每次都是比较相邻的节点

        // 边界情况
        if (head == nullptr || head->next == nullptr) return head;

        ListNode* tail = nullptr;
        while (head != tail)
        {
            ListNode* left = head;
            ListNode* right = head->next;
            while(right != tail)
            {
                if (left->val > right->val)
                {
                    swap(left->val, right->val);
                }
                left = left->next;
                right = right->next;
            }
            tail = left;    // 更新结束点位
        }

        return head;
    }
};

代码优化

最适合链表的排序是归并排序,这个时间复杂度还低一点

cpp 复制代码
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        return sortList(head, nullptr);
    }

    ListNode* sortList(ListNode* head, ListNode* tail) {
        if (head == nullptr) {
            return head;
        }
        if (head->next == tail) {
            head->next = nullptr;
            return head;
        }
        ListNode* slow = head, *fast = head;
        while (fast != tail) {
            slow = slow->next;
            fast = fast->next;
            if (fast != tail) {
                fast = fast->next;
            }
        }
        ListNode* mid = slow;
        return merge(sortList(head, mid), sortList(mid, tail));
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;
        }
        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }
        return dummyHead->next;
    }
};

作者:力扣官方题解
链接:https://leetcode.cn/problems/sort-list/solutions/492301/pai-xu-lian-biao-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
相关推荐
Kay_Liang35 分钟前
探究排序算法的奥秘(下):快速排序、归并排序、堆排序
java·数据结构·c++·python·算法·排序算法
鱼嘻2 小时前
数据结构------C语言经典题目(6)
linux·c语言·开发语言·数据结构·算法
彷徨而立2 小时前
【C/C++】字符串拷贝方法
c++
千谦阙听2 小时前
数据结构篇:线性表的另一表达—链表之单链表(下篇)
c语言·数据结构·链表·visual studio
泪光29293 小时前
枚举法——C++算法【泪光2929】
c++
Theodore_10223 小时前
Python3(19)数据结构
大数据·开发语言·数据结构·python·网络爬虫
夏末秋也凉4 小时前
力扣-数组-238 除自身以外数组的乘积
数据结构·算法·leetcode
李匠20244 小时前
C++负载均衡远程调用学习之基础TCP服务
c++·学习
Berserker_D4 小时前
【C/C++】头文件防卫式宏
c语言·开发语言·c++
绒绒毛毛雨4 小时前
将infinigen功能集成到UE5--在ue里面写插件(python和c++)
c++·python·ue5