8.5.归并排序和基数排序

1. 归并排序

核心思想

  1. 分解,将待排序序列递归地分成两个子序列,直到每个子序列仅含有一个元素(此时天然有序)。

  2. 将两个有序子序列合并成为一个更大的有序序列,最终得到完整的有序序列。

实现代码(C语言)

  1. 递归实现归并排序的代码
void Merge(int arr[], int low, int mid, int high) {
    int i = low, j = mid + 1, k = 0;
    int *temp = (int *)malloc((high - low + 1) * sizeof(int)); // 辅助数组

    while (i <= mid && j <= high) {
        if (arr[i] <= arr[j]) temp[k++] = arr[i++]; // 保证稳定性
        else temp[k++] = arr[j++];
    }

    // 处理剩余元素
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= high) temp[k++] = arr[j++];

    // 将temp数组拷贝回原数组
    for (i = low, k = 0; i <= high; i++, k++) {
        arr[i] = temp[k];
    }
    free(temp);
}

void MSort(int arr[], int low, int high) {
    if (low < high) {
        int mid = (low + high) / 2;
        MSort(arr, low, mid);      // 递归左半部分
        MSort(arr, mid + 1, high); // 递归右半部分
        Merge(arr, low, mid, high); // 合并
    }
}

void MergeSort(int arr[], int n) {
    MSort(arr, 0, n - 1);
}

  1. 非递归实现的归并排序
// 合并两个有序子数组
void Merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
    int *L = (int *)malloc(n1 * sizeof(int));
    int *R = (int *)malloc(n2 * sizeof(int));

    // 拷贝数据到临时数组
    for (int i = 0; i < n1; i++) L[i] = arr[left + i];
    for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j];

    // 合并
    int i = 0, j = 0, k = left;
    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++];

    free(L);
    free(R);
}

// 非递归归并排序
void MergeSort(int arr[], int n) {
    for (int step = 1; step < n; step *= 2) {          // 子数组长度从1开始翻倍
        for (int left = 0; left < n - 1; left += 2 * step) {
            int mid = left + step - 1;
            int right = (left + 2 * step - 1) < (n - 1) ? (left + 2 * step - 1) : (n - 1);
            Merge(arr, left, mid, right);             // 合并相邻子数组
        }
    }
}

算法特性

  1. 时间复杂度为\(O(n log_{2}n)\),空间复杂度为\(O(n)\)

  2. 归并排序具有稳定性,可以用于需要保持关键字原始顺序的场景。

  3. 可以用于大数据排序(外部排序,具体见\(\rightarrow\)8.7.外部排序),链表排序(无需随机访问,合并操作非常适合链表结构)。

  4. 不适用于有内存限制的问题(需要\(O(n)\))的辅助空间,也不适用于实时系统(递归调用和空间开销较大)。

  5. 虽然归并排序和快速排序的时间复杂度相同,但是归并排序的常数因子较大,实际运行可能慢于快速排序。

  6. 归并排序适用于顺序存储和链式存储的线性表。



2. 基数排序(链式)

核心思想

  1. 按位排序,从最低位(LSD)或者最高位(MSD)开始,根据当前位的值将数据分配到对应的链式桶中。

  2. 链式桶,每个桶是一个链表,存储相同位值的元素,避免数据移动开销。

  3. 顺序收集,按桶的顺序(如\(0\rightarrow9\))将链表元素合并,形成新一轮待排序序列。

代码实现(C语言)

typedef struct Node {
    int data;
    struct Node *next;
} Node;

// 获取数字的第d位(从0开始,个位为0)
int GetDigit(int num, int d) {
    for (int i = 0; i < d; i++) num /= 10;
    return num % 10;
}

// 链式基数排序
void RadixSort(int arr[], int n) {
    // 1. 初始化桶(10个链表)
    Node *buckets[10] = {NULL};
    Node *tails[10] = {NULL};  // 记录每个链表的尾部

    // 2. 计算最大位数d
    int max_val = arr[0];
    for (int i = 1; i < n; i++) 
        if (arr[i] > max_val) max_val = arr[i];
    int d = 0;
    while (max_val > 0) { max_val /= 10; d++; }

    // 3. 按位分配与收集
    for (int exp = 0; exp < d; exp++) {
        // 分配阶段:将数据插入对应桶的链表尾部
        for (int i = 0; i < n; i++) {
            int digit = get_digit(arr[i], exp);
            Node *new_node = (Node *)malloc(sizeof(Node));
            new_node->data = arr[i];
            new_node->next = NULL;

            if (buckets[digit] == NULL) {
                buckets[digit] = new_node;
                tails[digit] = new_node;
            } else {
                tails[digit]->next = new_node;
                tails[digit] = new_node;
            }
        }

        // 收集阶段:按桶顺序合并链表
        int idx = 0;
        for (int i = 0; i < 10; i++) {
            Node *p = buckets[i];
            while (p != NULL) {
                arr[idx++] = p->data;
                Node *temp = p;
                p = p->next;
                free(temp);  // 释放节点
            }
            buckets[i] = NULL;  // 清空桶
        }
    }
}

算法特性

  1. 时间复杂度为\(O(d * (n + k))\),空间复杂度为O(n + k)。

  2. 时间复杂度较低,稳定排序,非比较型算法,不是原地排序。

  3. 适用于整数排序,字符串排序,多关键字排序等。

  4. 基数排序适用于顺序存储和链式存储的线性表。

常见问题

  1. 链式基数排序为何适合外部排序?

    • 链表结构支持分块存储,适合磁盘顺序读写(减少随机访问)。
  2. 如何处理负数?

    • 所有数字减去最小值,排序后还原。
  3. 如何优化空间?

    • 使用静态链表,避免频繁动态内存分配

posted @ 2025-03-25 19:38  薛定谔的AC  阅读(45)  评论(0)    收藏  举报