【C语言】归并排序和计次排序

一、归并排序

        归并排序要额外多开一个数组用于存放归并后的数据,然后再把排序好的数据放回原数组。

        归并排序使用递归的写法,大致分位两个步骤,一个是分解,一个是归并。而归并的数据必须是有序的才行,那么如何保证其最开始是有序的呢?

        其实这里进入递归到了最底层,其数据被分为单一个数为一个区间的时候,这里就认为其是有序的。

        分解会把数据分解为两个区间,然后做比较,如果是升序的话,一个个比较,把较小的放到临时数组。

        此时此段区间就变为有序了,返回上一层这段区间就又可以用来被归并。

简单讲解归并流程,假设这里数组已经被分解为了两个有序的区间:

        这里就要依次进行比较然后把较小的数据放到临时数组。把数据放下去以后,i 往前走一步,然后再进行比较。

         

        这里2<4,把2放到下面的数组,然后 j 往前走一位。

        依次走到最后数组数据就排序好了,但是这里要注意一个情况,这种比较的方法固定要有一个区间先走完,这里左区间先走完了,但是右区间还剩一个数,也没办法比较了。

        所以为了防止这个情况,在归并结束后,如果哪个区间还有数据,就直接加到数组后面就可以,因为排序的数组是有序的,所以直接加到数组后面不会出错。

 

         

 代码:

void _MergeSort(int* a, int begin, int end, int* temp)
{

    if (begin >= end) //至少要有两个区间
        return;

    int mid = (begin + end) / 2;

    //分治  [beging,mid]  [mid+1,end]
    _MergeSort(a ,begin,mid,temp);
    _MergeSort(a, mid+1, end, temp);

    int begin1 = begin,end1 = mid;
    int begin2 = mid + 1,end2 = end;

    int i = begin1;    //这里注意是beging1

    //归并 把小的放到temp数组中去
    while (begin1 <= end1 && begin2 <= end2) {

        if (a[begin1]<a[begin2]) {

            temp[i++] = a[begin1++];
        }
        else {
            temp[i++] = a[begin2++];
        }

    }

    //如果一个数组走完了,还剩有数据,全部放到数组后面去
    while (begin1<=end1) {
        temp[i++] = a[begin1++];
    }

    while (begin2<=end2) {
        temp[i++] = a[begin2++];
    }

    //把数据拷贝回原数组 
    memcpy(a + begin, temp + begin,(end-begin+1)*sizeof(int));
} 


//递归实现
void MergeSort(int *a,int n)
{
    int* temp = (int*)calloc(n, sizeof(int));
    
    if (temp==NULL){
        perror("tmp初始化失败\n");
        return;
    }

    _MergeSort(a, 0, n-1, temp);
    free(temp);
}

测试:

        归并排序的时间复杂度是典型的:O(N*logN );

        

二、归并排序非递归实现

        归并的非递归直接可以在数组中处理。

        其思路是把数组中的数进行分组,设gap个数为一组,每组数又有两个区间,这两个区间做比较,归并成一个有序的区间。有序的区间又可以分组,然后归并。

       

        gap=1,每单个一个数分为一组,归并后刚好变为gap=2,两个数的组。依次归并,最后整个数组就有序了。

//归并排序的非递归     
void MergeSortNonR(int* a, int n)
{

    int* temp = (int*)calloc(n, sizeof(int));
    if (temp == NULL) {
        perror("tmp初始化失败\n");
        return;
    }

    int gap = 1;

    while (gap < n) {

        for (int i = 0; i < n; i += 2*gap) { //注意这里是一次跳两组

            //[i][i+gap-1] [i+gap][i+2*gap-1]
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;


            int j = begin1;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2]) {

                    temp[j++] = a[begin1++];
                }
                else {
                    temp[j++] = a[begin2++];
                }
            }

            while (begin1 <= end1)
            {
                temp[j++] = a[begin1++];
            }

            while (begin2 <= end2) {

                temp[j++] = a[begin2++];

            }

        }

        //这里每个数据都归并了所以可以全部拷贝回去。
        memcpy(a, temp, sizeof(int) * n);

        gap *= 2;

    }
    
    free(temp);
}


        思路比较好理解,但是要注意的是边界的控制。如果数组的长度不为2的次方倍数,那么不控制边界会造成越界。

        比如一个数组长度等于9,调试的时候这里打印一下区间。

         这里就会发现,有点地方的区间大于了数组的长度(数组下标是0-8,9也越界了)。除了begin1不会越界以外,其他的边界都会越界。

         所以这里为了防止边界越界,要控制一下边界。


//归并排序的非递归     
void MergeSortNonR(int* a, int n)
{

    int* temp = (int*)calloc(n, sizeof(int));
    if (temp == NULL) {
        perror("tmp初始化失败\n");
        return;
    }

    int gap = 1;

    while (gap < n) {

        printf("gap=%d-->",gap);
        for (int i = 0; i < n; i += 2*gap) { //注意这里是一次跳两组

            //[i][i+gap-1] [i+gap][i+2*gap-1]
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;

            //修正边界
            if (end1 >= n) { //end1越界
                
                end1 = n - 1;
                begin2 = n;
                end2 = n - 1;
            }
            else if (begin2 >= n) { //begin2越界
                
                begin2 = n;
                end2 = n - 1;
            }
            else if (end2 >= n) { //end2越界
                
                end2 = n - 1;
            }

            //打印边界:
            printf("--[%d][%d],[%d][%d]",begin1,end1,begin2,end2);
            int j = begin1;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2]) {

                    temp[j++] = a[begin1++];
                }
                else {
                    temp[j++] = a[begin2++];
                }
            }

            while (begin1 <= end1)
            {
                temp[j++] = a[begin1++];
            }

            while (begin2 <= end2) {

                temp[j++] = a[begin2++];

            }

        }
        //这里每个数据都归并了所以可以全部拷贝回去。
        memcpy(a, temp, sizeof(int) * n);
        printf("\n");
        gap *= 2;

    }
    
    free(temp);
}

PS:越界后设 begin2 < end2 ,这样越界的右区间就不会进入归并的步骤了。

测试: 

        

        

        还有一种思路,如果区间发生数组越界,那么就不进行归并了。


//归并排序的非递归    第二个控制边界的办法   
void MergeSortNonR_Test(int* a, int n)
{

    int* temp = (int*)calloc(n, sizeof(int));
    if (temp == NULL) {
        perror("tmp初始化失败\n");
        return;
    }

    int gap = 1;

    while (gap < n) {


        printf("gap=%d-->", gap);
        for (int i = 0; i < n; i += 2 * gap) { //注意这里是一次跳两组

            //[i][i+gap-1] [i+gap][i+2*gap-1]
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;


            //如果end1、begin2越界就不归了
            if (end1 >= n || begin2 >= n) {
                
                break;
            }
            else if (end2 >= n) { //end2越界还是要归并
                
                end2 = n - 1;
            }
           

            int m = end2 - begin1 + 1;

            //打印边界:
            printf("--[%d][%d],[%d][%d]", begin1, end1, begin2, end2);


            int j = begin1;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2]) {

                    temp[j++] = a[begin1++];
                }
                else {
                    temp[j++] = a[begin2++];
                }
            }

            while (begin1 <= end1)
            {
                temp[j++] = a[begin1++];
            }

            while (begin2 <= end2) {

                temp[j++] = a[begin2++];

            }
            //注意如果不归并了,这里不能把数据拷贝回去,不然会把未初始化的值拷贝回去,所以只能拷贝已经归并了的数据。
            memcpy(a + i, temp + i, sizeof(int) * m);
        }


        printf("\n");
        gap *= 2;

    }

    free(temp);
}

PS:如果是区间越界就跳过归并的话,这里要注意就不能把全部的数据都拷贝回原来的数组,因为没发生归并,这里临时数组里面是未初始化的值,拷贝回去会造成覆盖。所以这里要注意,这里拷贝的话只能拷贝已经归并了的数据。

三、计次排序

        计次排序也要额外申请一个数组,其数组大小是原数组最大元素和最小元素的差值。其存的数据是原数组里面数据出现的次数。

        然后按照出现的次数把数据写回原来的数组。此排序是个非比较排序。

        

        

  代码:

//计次循环
void CountSort(int *a,int n)
{
    //选出最大最小的数,计算差值
    int max=a[0], min=a[0];

    for (int i = 0; i < n;i++) {
        if (a[i]>max) {
            max = a[i];
        }
        if (a[i]<min) {
            min = a[i];
        }
    }

    int rang = max - min+1;

    int* count = (int*)calloc(rang,sizeof(int));
    if (count==NULL) {
        perror("count为NULL");
        exit(-1);
    }

    for (int i = 0; i < n;i++) {
           
        //记录出现数据的次数
        count[a[i] - min]++;
    }

    //放回原数组
    int k = 0;
    for (int j = 0; j < rang;j++) {

        while (count[j]--) {
            a[k++] = j + min;
        }

    }


    free(count);
}

测试:

       

局限性:

        1.浮点数、字符串不能排序。

        2.数据范围大,空间复杂度会很高。

        此排序的性质就决定了它只能排序范围比较小,重复度较高的数据。如果数据范围交到,其时间和空间的消耗都比较多。

时间复杂度: O( max(rang,N) )        

空间复杂度: O(rang)        //最小值和最大值的差值越大约消耗空间


拓展:

排序稳定性:

一个数组 6 3 5 4 5 9 1

两个5,排序完成后,红色的还是在黄色的前面,保证相对顺序不变,那么排序就是稳定的。

对于单个数据,稳定性毫无意义。

但是多个数据,部分数据相同,有意义。

排序 时间最坏 时间最好 空间复杂度 稳定性
插入排序 O(N^2) O(N) O(1)
希尔排序 O(N^1.3) O(N^1.3) O(1) ×
选择排序 O(N^2) O(N^2) O(1) ×
堆排序 O(N*logN) O(N*logN) O(1) ×
冒泡排序 O(N^2) O(N) O(N)
快速排序 O(N^2) O(N*logN) O(logN) ×
归并排序 O(N*logN) O(N*logN) O(N)

​​​​​​​

猜你喜欢

转载自blog.csdn.net/weixin_45423515/article/details/125627853
今日推荐