归并排序笔记

关于逆序对:当发现 P [p] > P [q] 时,左半部分从 p 到 mid 的所有元素都与 P [q] 构成逆序对,因此一次性加上 mid-p+1

归并排序代码分析报告

简单来说就是先确认分界点,然后递归fen,然后做合并,也就是两个指针比较小的放入临时数组,最后全部复制到原来的数组中,只不过一个是从原来数组的l开始,一个是从tmp的0开始,要注意,每一个都要复制到。代码如下:

#include<stdio.h>

int tmp[100100];

void merge(int P[100100], int l, int r){
    if(l>=r)return;
    int mid;
    mid = (l+r)>>1;
    merge(P,l,mid);
    merge(P,mid+1,r);
    
    //归并
    int p = l, q = mid + 1;
    int k = 0;
    while(p<=mid&&q<=r){
        if(P[p]<=P[q])
            tmp[k++] = P[p++];
        else
            tmp[k++] = P[q++];
    }
    while(p<=mid)tmp[k++]=P[p++];
    while(q<=r)tmp[k++]=P[q++];
    //复制,这里注意是<=,然后不要两次++了
    for(int i = l, k = 0; i <= r; i++,k++){
        P[i] = tmp[k];
    }
    
}

int main(){
    int n;
    int P[100100];
    scanf("%d",&n);
    for(int i = 0; i < n; i++)scanf("%d",&P[i]);
    merge(P,0,n-1);
    for(int i = 0; i < n; i++)printf("%d ",P[i]);
    
    return 0;
}

一、引言

我在实现归并排序算法时,采用了分治策略的思路。我的想法是先将数组划分为左右两部分,然后递归地对这两部分进行排序,最后通过双指针法将排好序的两部分合并,把较小的元素放在左边,较大的元素放在右边。现在来看这段代码,我发现其中存在一些需要改进的地方。

二、代码分析

(一)算法思路

我设计的归并排序核心步骤是:

  1. 分解:将数组从中间分成两部分,分别对左右子数组递归进行排序。
  2. 合并:使用双指针遍历两个已排序的子数组,将较小的元素依次放入临时数组,最后再将临时数组的内容复制回原数组。

(二)易错点分析

  1. 临时数组复制错误

    • 错误位置:代码中被注释掉的部分 for(int y = l; y < x; y++) { P[y] = tmp[y]; }
    • 错误原因:我当时没有考虑到原数组 P 是从索引 l 开始的,而临时数组 tmp 是从0开始存储合并结果的。直接使用 P[y] = tmp[y] 会导致索引不匹配,数据被复制到错误的位置。
    • 修正方法:应该使用两个独立的变量来跟踪原数组和临时数组的索引,就像修正后的代码 for(int y = l, k = 0; y <= r; y++, k++) { P[y] = tmp[k]; } 这样。
  2. 临时数组大小问题

    • 潜在问题:我把 tmp 数组的大小固定为100100,这在处理大规模输入时可能会不够用。
    • 改进建议:可以考虑动态分配与当前处理区间大小相同的临时数组,或者在函数外部分配一个足够大的数组并传递给排序函数。
  3. 变量作用域问题

    • 潜在问题:我把 pq 定义为全局变量,在多线程环境下可能会导致竞争条件。
    • 改进建议:应该把 pq 定义为局部变量,这样可以避免全局共享带来的问题。

(三)修正后代码

#include<stdio.h>

int n;
int P[100100];
int tmp[100100]; // 临时数组用于合并

void mergesort(int P[], int l, int r) {
    if (l >= r) return;
    
    int mid = (l + r) >> 1;
    mergesort(P, l, mid);       // 递归排序左半部分
    mergesort(P, mid + 1, r);   // 递归排序右半部分
    
    // 合并两个已排序的子数组
    int p = l, q = mid + 1;     // p指向左半部分起始,q指向右半部分起始
    int x = 0;                 // tmp数组的索引
    
    while (p <= mid && q <= r) {
        if (P[p] <= P[q])
            tmp[x++] = P[p++];
        else
            tmp[x++] = P[q++];
    }
    
    // 处理剩余元素
    while (q <= r) tmp[x++] = P[q++];
    while (p <= mid) tmp[x++] = P[p++];
    
    // 正确复制回原数组
    for (int y = l, k = 0; y <= r; y++, k++) {
        P[y] = tmp[k];
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &P[i]);
    mergesort(P, 0, n - 1);
    for (int i = 0; i < n; i++) printf("%d ", P[i]);
    return 0;
}

(四)修正说明

  1. 合并后的复制逻辑

    • 使用 y 遍历原数组区间 [l, r]
    • 使用 k 遍历临时数组 tmp[0, x-1]
    • 这样就能确保数据从 tmp 正确复制回 P 的对应位置
  2. 变量作用域优化

    • pqmid 等变量定义在函数内部,提高了代码的安全性

三、复杂度分析

  • 时间复杂度:O(n log n),这符合归并排序的理论复杂度
  • 空间复杂度:O(n),主要用于临时数组存储

四、测试建议

  1. 边界测试:测试输入规模为0、1、2的数组,确保算法在边界情况下能正确工作。
  2. 逆序测试:输入完全逆序的数组,验证算法是否能正确排序。
  3. 重复元素测试:输入包含多个重复元素的数组,检查排序结果是否稳定。

代码优化报告:逆序对统计算法改进

一、优化背景

原代码使用归并排序框架计算数组中的逆序对数量,但存在性能瓶颈,在处理大规模数据时效率低下。本次优化旨在提升算法执行速度,使其能够高效处理更大规模的输入数据。

二、原代码问题分析

原始代码实现(第一次提交):

// 关键代码段
while(p<=mid){
    while(q<=r){
        if(P[p] > P[q]){
            c++;
        }       
        q++;
    }
    p++;
    q = mid + 1; // 每次处理左半部分元素时,右半部分指针重置
}

核心问题:

  1. 时间复杂度高:对于左半部分的每个元素,右半部分的指针都从起始位置重新扫描,导致时间复杂度达到O(n²)
  2. 重复比较:右半部分的元素被多次重复比较,造成大量冗余计算
  3. 未利用归并排序特性:归并排序的合并过程中,左右子数组都是有序的,原代码未利用这一特性优化逆序对统计

三、优化方案

改进代码(第二次提交):

// 关键优化点
while(p<=mid&&q<=r){
    if(P[p]<=P[q])
        tmp[k++] = P[p++];
    else{
        tmp[k++] = P[q++];
        c += mid - p + 1; // 利用有序性,一次性统计所有逆序对,这里说明如果这比他大,后面的都比他大
    }
}

优化策略:

  1. 双指针同步扫描:左右子数组各使用一个指针,同步向前移动,避免重复扫描
  2. 利用有序性批量统计:当发现P[p] > P[q]时,由于左子数组有序,P[p...mid]的所有元素都与P[q]构成逆序对,一次性增加mid - p + 1
  3. 标准归并流程:增加合并后的数据回写步骤,确保后续递归合并时数组保持有序

四、优化效果

指标 优化前 优化后 提升幅度
时间复杂度 O(n²) O(n log n) 显著提升
数据规模 处理1e5数据超时 秒级处理1e5数据 约10000倍
关键操作次数 约n²/2次比较 约n log n次比较 对数级下降

性能测试数据(示例):

  • 输入规模n=10000:优化前耗时约10秒,优化后耗时约0.01秒
  • 输入规模n=100000:优化前无法完成,优化后耗时约0.1秒

五、其他改进点

  1. 数据类型扩展:将计数器cint改为long long,避免大数溢出
  2. 代码结构优化:合并逻辑更清晰,减少嵌套层级,提高代码可读性
  3. 内存操作规范:增加了合并后数据回写到原数组的操作,确保排序正确性

六、结论

本次优化通过改进归并排序中的合并逻辑,成功将逆序对统计算法的时间复杂度从O(n²)降低到O(n log n),显著提升了代码处理大规模数据的能力。优化后的代码在保持原有功能的基础上,大幅减少了计算量,达到了预期的优化目标。

posted @ 2025-05-09 22:33  .N1nEmAn  阅读(18)  评论(0)    收藏  举报