排序算法实现

目录

一、引言

二、代码整体结构

三、宏定义与头文件

​四、插入排序函数(Insertsort)

函数作用

代码要点分析

五、希尔排序函数(ShellSort)

函数作用

代码要点分析

六、选择排序

(一)算法原理

(二)代码实现

(三)要点与注意点

七、堆排序

(一)算法原理

(二)代码实现

(三)要点与注意点

八、冒泡排序

(一)算法原理

(二)代码实现

(三)要点与注意点

九、快速排序

 (一)、快速排序的基本原理

(二)、代码中的关键模块与实现细节

1. Swap 函数:

2. QuickSort 1函数:

3.QuickSort2函数剖析

4.QuickSort3函数剖析

优化

5. 递归调用:划分完成后,对基准值左右两部分分别递归调用 QuickSort3 函数。

6.非递归快速排序 

(三)、快速排序的要点与注意点

十、 归并排序 

递归实现 

非递归实现 

十一、计数排序 

十二、打印数组函数(PrintSort)

 函数作用

代码要点分析

十三、测试函数(TestSort)与主函数(main)

 函数作用

代码要点分析

十四、总结​


 

一、引言
 


在计算机科学领域,排序算法是非常基础且重要的内容。不同的排序算法有着各自的特点和适用场景。本文将基于给定的C语言代码,深入剖析排序的实现过程,帮助读者更好地理解这两种排序算法的原理与应用。

 

 

作者主页:共享家9527-CSDN博客

 


二、代码整体结构
 


代码主要包含了冒泡排序,选择排序,插入排序,快速排序,归并排序(还未添加), 希尔排序 ,堆排序、打印数组函数以及测试函数和主函数,整体结构清晰,便于理解和维护。下面我们对每个函数进行详细分析。


三、宏定义与头文件
 


c
  
#define _CRT_SECURE_NO_WARNINGS
 //插排
void Insertsort(int* a, int n);
//希尔
void ShellSort(int* a, int n);
void PrintSort(int* a, int n);
//选排
void SelectSort(int* a, int n);
//堆排
void HeapSort(int* a, int n);
//冒泡
void BubbleSort(int* a, int n);
//快排
void QuickSort1(int* a, int left, int right);
void QuickSort2(int* a, int left, int right);
void QuickSort3(int* a, int left, int right);


 
 #define _CRT_SECURE_NO_WARNINGS  这一宏定义的作用是在使用一些被认为可能存在安全风险的C标准库函数(如 scanf 、 strcpy 等)时,避免编译器产生警告信息。 #include"Sort.h"  表示包含自定义的头文件 Sort.h ,虽然在给出的代码中未看到该头文件的具体内容,但通常它会包含一些函数声明、类型定义等内容,方便代码的模块化管理。


四、插入排序函数(Insertsort)

 


c
  
//插排
void Insertsort(int* a, int n){
    for (int i = 1;i < n;i++)
    {
        int end = i - 1;
        int temp = a[i];
        while (end >= 0)
        {
            if (temp < a[end])
            {
                a[end + 1] = a[end];
                end--;
            }
            else
            {
                break;
            }
        }
        a[end + 1] = temp;
    }
}
 
 


函数作用
 


插入排序函数的功能是对给定的整数数组进行排序,排序的基本思想是将一个数据插入到已经排好序的数组中的适当位置。
 


代码要点分析
 


1. 外层循环: for (int i = 1;i < n;i++)  从数组的第二个元素开始(因为第一个元素可以看作是已经排好序的子数组),依次将每个元素插入到前面已排序的子数组中的合适位置。
 
2. 初始化变量: int end = i - 1;  定义 end 变量表示已排序子数组的最后一个元素的下标。 int temp = a[i];  将当前待插入的元素暂存到 temp 变量中。
 
3. 内层循环: while (end >= 0)  当 end 大于等于0时,继续循环,即只要还在已排序的子数组范围内,就进行比较和移动操作。 if (temp < a[end])  如果待插入元素小于当前已排序子数组的最后一个元素,则将该元素向后移动一位,同时 end 减1,继续向前比较。当遇到不满足该条件的元素时,说明找到了待插入元素的合适位置,通过 break 跳出循环。
 
4. 插入操作: a[end + 1] = temp;  将暂存的待插入元素插入到合适的位置。
 


五、希尔排序函数(ShellSort)


c
  
//希尔
void ShellSort(int* a, int n)
{
    int gap = 9;
    while (gap > 0)
    {
        gap /= 2;
        for (int j = 0;j < gap;j++)
        {
            for (int i = j;i < n - gap;i += gap)
            {
                int end = i;
                int temp = a[i + gap];
                while (end >= 0)
                {
                    if (temp < a[end])
                    {
                        a[end + gap] = a[end];
                        end -= gap;
                    }
                    else
                    {
                        break;
                    }
                }
                a[end + gap] = temp;
            }
        }
    }
}
 


 


函数作用
 


希尔排序是插入排序的一种改进版本,通过将数组按照一定的间隔( gap )进行分组,对每组分别进行插入排序,随着间隔逐渐减小,最终完成整个数组的排序。
 


代码要点分析
 


1. 初始间隔设置: int gap = 9;  定义初始的分组间隔,这里设置为9,不同的初始间隔可能会影响排序的效率。
 
2. 间隔调整循环: while (gap > 0)  当间隔大于0时,继续进行排序操作。每次循环中, gap /= 2;  将间隔逐渐缩小,直到间隔为1时,相当于进行一次普通的插入排序。
 
3. 分组循环: for (int j = 0;j < gap;j++)  对每个分组进行遍历,确保每个分组都能进行插入排序操作。
 
4. 组内插入排序循环: for (int i = j;i < n - gap;i += gap)  对每个分组内的元素进行插入排序,类似于插入排序的过程,但这里元素之间的比较和移动是按照间隔 gap 进行的。
 
5. 插入操作:与插入排序类似,通过比较和移动元素,将当前元素插入到分组内合适的位置。
 

六、选择排序
 


(一)算法原理


选择排序的工作原理就像是在一群小朋友中挑选个子最矮和最高的小朋友,然后分别让他们站到队伍的两端。它通过不断地从未排序序列中选择最小(或最大)的元素,将其放到已排序序列的末尾(或开头)。
 
在代码实现中,我们使用了双端选择的策略,即同时寻找最小和最大的元素。设置两个指针 left 和 right ,分别指向数组的开头和结尾。在每一轮遍历中,从 left 到 right 的范围内寻找最小元素的下标 mini 和最大元素的下标 maxi 。然后将最小元素与 left 位置的元素交换,在交换之后,如果原来的最大元素下标 maxi 恰好是 left ,说明最大元素被交换走了,需要更新 maxi 为 mini ,最后将最大元素与 right 位置的元素交换。每完成一轮, left 指针右移一位, right 指针左移一位,直到 left 大于等于 right 。
 


(二)代码实现
 


c
  
// 交换两个整数的值
void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 选择排序
void SelectSort(int* a, int n)
{
    int left = 0, right = n - 1;
    while (left < right)
    {
        int mini = left, maxi = left;
        for (int i = left + 1; i <= right; i++)
        {
            if (a[i] < a[mini])
            {
                mini = i;
            }
            if (a[i] > a[maxi])
            {
                maxi = i;
            }
        }

        Swap(&a[left], &a[mini]);
        if (maxi == left)
        {
            maxi = mini;
        }
        Swap(&a[right], &a[maxi]);
        ++left;
        --right;
    }
}
 
 


(三)要点与注意点
 


1. 双端选择:同时选择最小和最大元素,减少排序的轮数,提高效率。
 
2. 边界条件:在交换最大元素时,需要注意 maxi 是否等于 left ,如果相等,需要更新 maxi 。
 
3. 时间复杂度:选择排序的时间复杂度为O(n²),因为无论数据初始状态如何,都需要进行n(n - 1)/2次比较。
 


七、堆排序
 


(一)算法原理
 


堆排序可以想象成是在一个大堆里挑选水果,我们把最大的水果先挑出来,然后再调整堆,继续挑下一个最大的水果。它利用了二叉堆的数据结构,分为大顶堆和小顶堆,这里我们使用大顶堆来实现降序排序。
 
首先,通过 AdjustDown 函数将数组构建成一个大顶堆,大顶堆的特点是每个父节点的值都大于或等于其左右子节点的值。然后,将堆顶元素(即最大元素)与堆的最后一个元素交换,此时最大元素就被放到了数组的末尾。接着,对剩下的元素重新调整为大顶堆,重复这个过程,直到整个数组有序。


(二)代码实现
 


c
  
// 向下调整函数,将以parent为根的子树调整为大顶堆
void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;

    while (child < n)
    {
        if (child + 1 < n && a[child + 1] > a[child])
        {
            child++;
        }

        if (a[parent] < a[child])
        {
            Swap(&a[parent], &a[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

// 堆排序
void HeapSort(int* a, int n)
{
    // 构建大顶堆
    for (int i = (n - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(a, n, i);
    }

    int end = n - 1;
    while (end)
    {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        end--;
    }
}
 
 


(三)要点与注意点
 


1. 堆的构建:从最后一个非叶子节点开始,依次调用 AdjustDown 函数,将数组构建成大顶堆。
 
2. 调整堆:在交换堆顶元素和堆的最后一个元素后,需要对堆顶元素进行调整,使其重新满足大顶堆的性质。
 
3. 时间复杂度:堆排序的时间复杂度为O(nlogn),因为构建堆的时间复杂度为O(n),每次调整堆的时间复杂度为O(logn),一共需要调整n - 1次。
 


八、冒泡排序
 


(一)算法原理
 


冒泡排序就像是水中的气泡,小的气泡会慢慢浮到水面,大的气泡则会沉到水底。它通过相邻元素的比较和交换,将最大(或最小)的元素逐步“冒泡”到数组的末尾(或开头)。
 
在每一轮遍历中,从数组的第一个元素开始,比较相邻的两个元素,如果前一个元素大于后一个元素,则交换它们的位置。这样,每一轮遍历都会将当前未排序部分的最大元素移动到末尾,经过n - 1轮遍历后,数组就会变得有序。
 


(二)代码实现
 


c
  
// 冒泡排序
void BubbleSort(int* a, int n)
{
    for (int i = 0; i < n; i++)
    {
        for (int j = 1; j < n - i; j++)
        {
            if (a[j - 1] > a[j])
            {
                Swap(&a[j - 1], &a[j]);
            }
        }
    }
}
 
 


(三)要点与注意点
 


1. 双层循环:外层循环控制遍历的轮数,内层循环控制每一轮比较和交换的次数。
 
2. 优化:可以设置一个标志位,如果在某一轮遍历中没有发生交换,说明数组已经有序,可以提前结束排序。
 
3. 时间复杂度:冒泡排序的时间复杂度为O(n²),在最好情况下,即数组已经有序时,时间复杂度为O(n)。

九、快速排序

 


 (一)、快速排序的基本原理
 


快速排序是一种分治算法,基本思想就是选一个基准值,把数组分成两部分,比基准值小的放一边,大的放另一边,然后递归地对这两部分再进行排序。就好像老师把同学们按身高排队,先找一个同学当“标准身高”,比他矮的站一边,比他高的站另一边,然后再分别在矮的和高的队伍里继续找“标准身高”来细分,直到所有人都排好队。
 


(二)、代码中的关键模块与实现细节
 


上述代码中,快速排序功能被封装在多个函数中,每个函数各司其职,协同完成排序任务。
 


1. Swap 函数:
 

c
  
void Swap(int* arr1, int* arr2)
{
    int tmp = *arr1;
    *arr1 = *arr2;
    *arr2 = tmp;
}
 


 
这个函数就像是一个神奇的交换器,负责交换两个整数的值。在快速排序中,我们经常需要交换数组中的元素,这个函数就派上大用场啦。
 


2. QuickSort 1函数:

 

 
c
  
void QuickSort(int* a, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    int randi = left + rand() % (right - left + 1);
    Swap(&a[left], &a[randi]);

    int begin = left, end = right;
    int keyi = left;

    while (left < right)
    {
        while (left < right && a[right] >= a[keyi])
        {
            right--;
        }
        while (left < right && a[left] <= a[keyi])
        {
            left++;
        }
        Swap(&a[left], &a[right]);
    }
    Swap(&a[keyi], &a[left]);
    keyi = left;
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
}
 

3.QuickSort2函数剖析
 


代码框架

 
c
  
void QuickSort2(int* a, int left, int right) {
    if (a == NULL) {
        return;
    }
    if (left >= right) {
        return;
    }
    // 三数取中
    int randi = medianOfThree(a, left, right);
    Swap(&a[left], &a[randi]);
    int begin = left, end = right;
    int key = a[left];
    int hold = left;

    while (left < right) 
    {
        // 右找小
        while (left < right && a[right] >= key)
            right--;
        a[hold] = a[right];
        hold = right;

        // 左找大
        while (left < right && a[left] <= key)
            left++;
        a[hold] = a[left];
        hold = left;
    }
    a[hold] = key;
    QuickSort2(a, begin, hold - 1);
    QuickSort2(a, hold + 1, end);
}


要点
 
1. 参数检查:一开始检查数组指针 a 是否为 NULL ,如果是则直接返回,这就像进入一个房间前先看看门是否能进,避免空指针错误。
 
2. 递归终止条件: if (left >= right)  这一条件判断是递归的终止点,当排序区间的左边界大于等于右边界时,说明该区间已经不需要再排序,就像切蛋糕切到不能再切了。
 
3. 基准值选择:这里使用了三数取中方法,通过 medianOfThree 函数找到合适的基准值,并将其与数组最左边的元素交换。三数取中能让我们在选取基准值时更“聪明”,减少极端情况的发生,就像在一群人中选一个中等身高的人作为代表。
 
4. 划分过程:通过两个 while 循环,一个从右向左找小于基准值的元素,一个从左向右找大于基准值的元素,然后交换这两个元素的位置,直到 left 和 right 相遇。这个过程就像两个相向而行的人,一个找小的,一个找大的,然后交换他们手里的“宝贝”。
 
5. 递归调用:将数组划分为两部分后,分别对左右两部分递归调用 QuickSort2 函数继续排序,直到整个数组有序。
 
注意点
 
1. 边界条件:在划分过程中,两个 while 循环里的 left < right 条件不能少,否则会导致数组越界访问,就像你在一个有限的跑道上跑步,不能跑出去了还不收脚。
 
2. 基准值的更新:在划分完成后,别忘了将基准值放到正确的位置,即 a[hold] = key ,否则基准值就“迷路”了,整个排序也就乱套了。
 


4.QuickSort3函数剖析
 


代码框架 


c
  
void QuickSort3(int* a, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    // 三数取中
    int randi = medianOfThree(a, left, right);
    Swap(&a[left], &a[randi]);

    int pre = left, cur = left + 1;
    int keyi = left;
    while (cur <= right)
    {
        if (a[cur] < a[keyi])
        {
            pre++;
            Swap(&a[cur], &a[pre]);
        }
        cur++;
    }
    Swap(&a[keyi], &a[pre]);
    keyi = pre;
    QuickSort3(a, left, keyi - 1);
    QuickSort3(a, keyi + 1, right);
}

 
要点
 
1. 同样的递归终止条件:和 QuickSort2 一样, if (left >= right)  用于判断是否需要继续递归,这是递归函数的“刹车”机制。
 
2. 基准值选择:也是采用三数取中选择基准值并交换到最左边。
 
3. 划分逻辑:使用两个指针 pre 和 cur , cur 从 left + 1 开始遍历数组,当 a[cur] 小于基准值 a[keyi] 时, pre 指针向前移动并交换 a[cur] 和 a[pre] 。这个过程就像一个班级里,小个子的同学都往前排站。
 

优化

int PartSort3(int* a, int left, int right)
{
    // 三数取中
    int randi = medianOfThree(a, left, right);
    Swap(&a[left], &a[randi]);
 
    int pre = left, cur = left + 1;
    int keyi = left;
    while (cur <= right)
    {
        if (a[cur] < a[keyi])
        {
            pre++;
            Swap(&a[cur], &a[pre]);
        }
        cur++;
    }
    Swap(&a[keyi], &a[pre]);
    keyi = pre;
    return keyi;
    

}
void QuickSort3(int* a, int left, int right)
{
    if (left >= right)
    {
        return;
    }
    //区间优化
    if((right-left+1)>10)
    {
    	int keyi=PartSort3(a, left,right);
        QuickSort3(a, left, keyi - 1);
        QuickSort3(a, keyi + 1, right);
	}
    else
    {
    	ShellSort(a+left, right-left+1);
	}
}


5. 递归调用:划分完成后,对基准值左右两部分分别递归调用 QuickSort3 函数。
 


注意点
 
1. 指针移动:
在遍历过程中, cur 指针的移动逻辑要正确,每次循环都要确保 cur 指针向前移动,否则会陷入死循环,就像一个人一直在原地踏步走。
 
2. 交换逻辑:交换条件要准确,只有当 a[cur] < a[keyi] 时才进行交换,否则排序结果会出错,就像分水果时,把大的小的分错了篮子。

 
这是快速排序的核心函数。
 
- 递归终止条件:当 left >= right 时,说明当前要处理的数组片段只有一个元素或者没有元素,不需要再排序,直接返回。
 
- 随机选择基准值: int randi = left + rand() % (right - left + 1); 这行代码从当前数组片段中随机选择一个元素作为基准值,然后通过 Swap 函数将它和数组最左边的元素交换。为什么要随机选呢?这样能避免在某些特殊数据情况下(比如数组已经有序)快速排序退化成O(n²)的时间复杂度,就像给排序算法穿上了“防滑鞋”,在各种数据地形上都能稳定发挥。
 
- 划分过程:通过两个 while 循环,从数组的两端向中间扫描。右边的 while 循环找到小于基准值的元素,左边的 while 循环找到大于基准值的元素,然后交换这两个元素。当左右指针相遇时,说明已经完成了一次划分,把数组分成了两部分。接着,将基准值放到正确的位置(通过 Swap(&a[keyi], &a[left]); )。
 
- 递归调用:最后,对基准值左边和右边的子数组分别递归调用 QuickSort 函数,继续进行排序,就像把大问题不断分解成小问题,直到所有小问题都解决,整个数组也就有序啦。
 

 

 

6.非递归快速排序
 


快速排序以其高效性闻名,基于分治策略,将一个数组通过选择基准元素划分为两个子数组,使得左边子数组的元素都小于基准,右边子数组的元素都大于基准,然后递归地对两个子数组进行排序。非递归实现则借助栈来模拟递归调用。
 
代码实现
 

c
  
//非递归快排
void QuickSortNonR(int* a, int left, int right)
{
    ST st;
    STInit(&st);
    StackPush(&st,right);
    StackPush(&st, left);
    while (!STEmpty(&st))
    {
        int begin = STTop(&st);
        STPop(&st);
        int end = STTop(&st);
        STPop(&st);
        int keyi= Partsort(a, left, right);
        if (keyi + 1 < end)
        {
            StackPush(&st, end);
            StackPush(&st, keyi+1);
        }
        if (begin < keyi - 1)
        {
            StackPush(&st,keyi-1);
            StackPush(&st, begin);
        }
    }
    STDestory(&st);
}
 


 
实现要点
 
1. 栈的运用:创建一个栈结构 ST ,用于存储待排序的数组区间。首先将初始的左右边界 left 和 right 压入栈中。
 
2. 区间处理:每次从栈中取出一对边界 begin 和 end ,通过 Partsort 函数进行划分操作。 Partsort 函数会选择一个基准元素,将数组在 begin 到 end 区间内的元素进行重排,使得左边部分小于基准,右边部分大于基准,并返回基准元素的最终位置 keyi 。
 
3. 子区间入栈:根据划分结果,将基准元素左右两侧的子区间(如果长度大于1)压入栈中,以便后续继续处理。例如,如果 keyi + 1 < end ,说明基准右侧还有足够元素需要排序,将 end 和 keyi + 1 压入栈;同理,如果 begin < keyi - 1 ,将 keyi - 1 和 begin 压入栈。
 
注意事项
 
1. 栈操作的完整性:在使用栈之前,必须正确初始化( STInit ),确保栈的内部状态正确。排序完成后,要及时销毁栈( STDestory ),释放相关资源,避免内存泄漏。
 
2. 边界条件的判断:在判断子区间是否需要继续排序时,务必仔细检查边界条件。例如,在判断 keyi + 1 < end 和 begin < keyi - 1 时,任何错误都可能导致子区间遗漏或重复处理,影响排序结果的正确性。
 



(三)、快速排序的要点与注意点
 


1. 基准值的选择:随机选择基准值是个提升算法性能的好办法,但在实际应用中,还有其他选择基准值的策略,比如三数取中(选取数组开头、中间和结尾的三个数,取中间大小的数作为基准值)。不同策略在不同数据分布下各有优劣,就像不同的交通工具在不同路况下有不同的表现。
 
2. 边界条件处理:在递归调用 QuickSort 时,一定要注意边界条件。像前面代码中,如果递归调用写成 QuickSort(a, keyi + 1, end - 1); 就错啦,这样会导致右边的子数组少处理一个元素,一定要写成 QuickSort(a, keyi + 1, end); ,边界条件就像是走迷宫时的边界墙,一不小心撞上去就迷路啦。
 
3. 时间复杂度:平均情况下,快速排序的时间复杂度是O(n log n),这也是它速度快的原因。但在最坏情况下(比如每次选择的基准值都是数组中的最大或最小元素),时间复杂度会退化为O(n²)。所以在实际使用中,要尽量避免这种最坏情况的发生,就像开车时要尽量避开拥堵路段。
 
4. 空间复杂度:快速排序是一种原地排序算法,空间复杂度为O(log n),因为递归调用需要用到栈空间。这就好比整理书架,不需要额外的大空间,在书架上直接整理就能把书放整齐。
 


 
十、归并排序
 


归并排序也是基于分治思想的排序算法。它将一个数组不断地分成两半,对每一半进行排序,然后将排序好的两半合并成一个有序数组。
 


递归实现
 


代码实现
 

c
  
void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc error");
        return;
    }

    _MergeSort(a, 0, n - 1, tmp);
}

void _MergeSort(int* a, int left, int right, int* tmp)
{
    if (left >= right)
        return;

    int mid = left + (right - left) / 2;

    _MergeSort(a, left, mid, tmp);
    _MergeSort(a, mid + 1, right, tmp);

    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int i = left;
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] > a[begin2])
        {
            tmp[i++] = a[begin2++];
        }
        else
        {
            tmp[i++] = a[begin1++];
        }
    }

    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }

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

    for (int j = left; j <= right; j++)
    {
        a[j] = tmp[j];
    }
}
 


 
实现要点
 
1. 递归分解:通过递归不断地将数组划分为左右两个子数组,直到子数组的长度为1或0(即 left >= right ),此时子数组已经有序。
 
2. 合并操作:当左右两个子数组都递归排序完成后,进行合并操作。使用两个指针 begin1 和 begin2 分别指向左右子数组的起始位置,通过比较 a[begin1] 和 a[begin2] 的大小,将较小的元素依次放入临时数组 tmp 中。如果其中一个子数组的元素全部放入 tmp 后,另一个子数组还有剩余元素,则直接将剩余元素依次放入 tmp 。
 
3. 结果回写:合并完成后,将临时数组 tmp 中 left 到 right 范围内的元素复制回原数组 a ,完成该区间的排序。
 
注意事项
 
1. 递归终止条件:递归函数必须有明确的终止条件,即 left >= right ,否则会导致无限递归,最终耗尽栈空间,引发栈溢出错误。
 
2. 临时数组管理:在 MergeSort 函数中分配临时数组 tmp 时,要检查内存分配是否成功。如果分配失败, malloc 会返回 NULL ,此时应进行适当的错误处理,如打印错误信息并返回。排序完成后,虽然这里没有显示释放 tmp 的代码,但在实际应用中,需要确保在合适的地方释放该内存,避免内存泄漏。
 


非递归实现
 


代码实现

 
c
  
void MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc error");
        return;
    }

    int gap = 1;
    while (gap < n)
    {
        for (int i = 0; i < n; i += 2 * gap)
        {
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;
            //整体拷贝tmp数组,修正边界
            if (end1 >= n)
            {
                end1 = n - 1;
                begin2 = n;
                end2 = n - 1;
            }
            else if (begin2 >= n)
            {
                begin2 = n;
                end2 = n - 1;
            }
            else if (end2 >= n)
            {
                end2 = n - 1;
            }

            printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);

            int j = i;
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] > a[begin2])
                {
                    tmp[j++] = a[begin2++];
                }
                else
                {
                    tmp[j++] = a[begin1++];
                }
            }

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

            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
        }
        printf("\n");
        memcpy(a, tmp, sizeof(int) * n);
        gap *= 2;
    }
}
 


 
实现要点
 
1. 步长控制:通过一个变量 gap 来控制每次合并的子数组大小。初始时 gap 为1,然后每次循环翻倍,直到 gap 大于或等于数组长度 n 。
 
2. 子数组划分与合并:在每一轮循环中,从数组的起始位置开始,以 2 * gap 为间隔划分出多个子数组对。对于每对子数组,分别确定其起始和结束位置 begin1 、 end1 、 begin2 、 end2 。然后进行合并操作,与递归实现中的合并操作类似,将较小的元素依次放入临时数组 tmp 。
 
3. 边界处理:由于子数组的长度可能不是 gap 的整数倍,需要处理边界情况。例如,当 end1 超出数组范围时,将其修正为 n - 1 ,并相应调整 begin2 和 end2 ;当 begin2 超出数组范围时,直接将其设为 n , end2 设为 n - 1 ;当 end2 超出数组范围时,将其修正为 n - 1 。
 
4. 结果更新:每一轮合并完成后,将临时数组 tmp 中的内容复制回原数组 a ,然后增大 gap ,进行下一轮合并。
 
注意事项
 
1. 边界条件处理:边界条件的处理是归并排序非递归实现的关键。任何一处边界处理不当,都可能导致数组越界访问或排序结果错误。在修正边界时,要充分考虑各种可能的情况,确保每个子数组都能正确合并。
 
2. 内存拷贝操作:在每一轮合并完成后,使用 memcpy 函数将临时数组 tmp 的内容复制回原数组 a 。需要注意 memcpy 函数的参数,确保复制的字节数正确,避免数据错误。同时,也要注意内存分配和释放,确保临时数组 tmp 在不再使用时被正确释放。
 


十一、计数排序
 


计数排序是一种线性时间复杂度的排序算法,适用于数据范围相对较小的场景。它通过统计每个元素出现的次数,然后根据统计结果将元素按顺序放回数组,从而实现排序。
 
代码实现

 
c
  
void CountSort(int* a, int n)
{
    int min = a[0];
    int max = a[0];
    for (int i = 1; i < n; i++)
    {
        if (a[i] < min)
        {
            min = a[i];
        }

        if (a[i] > max)
        {
            max = a[i];
        }
    }

    int range = max - min + 1;
    int* tmp = (int*)calloc(sizeof(int), range);
    if (tmp == NULL)
    {
        perror("malloc error");
        return;
    }

    for (int j = 0; j < n; j++)
    {
        tmp[a[j] - min]++;
    }

    int q = 0;
    for (int p = 0; p < range; p++)
    {
        while (tmp[p]--)
        {
            a[q++] = p + min;
        }
    }
}
 


 
实现要点
 
1. 确定数据范围:遍历数组,找出数组中的最小值 min 和最大值 max ,计算数据范围 range = max - min + 1 。这个范围决定了计数数组的大小。
 
2. 计数数组创建与统计:创建一个大小为 range 的计数数组 tmp ,使用 calloc 函数进行内存分配,确保数组元素初始化为0。然后再次遍历原数组,对于每个元素 a[j] ,在计数数组 tmp 中对应的位置 a[j] - min 上加1,表示该元素出现的次数。
 
3. 排序重构:遍历计数数组 tmp ,对于每个非零的元素 tmp[p] ,将 p + min 依次放回原数组 a 中,重复 tmp[p] 次。这样,原数组 a 就被排序成了有序数组。
 
注意事项
 
1. 数据范围限制:计数排序的效率依赖于数据范围。如果数据范围过大,计数数组会占用大量内存,导致空间复杂度急剧上升,甚至可能出现内存不足的情况。因此,计数排序适用于数据范围相对较小且已知的场景。
 
2. 内存分配与释放:在使用 calloc 分配计数数组 tmp 的内存时,要检查分配是否成功。如果分配失败, calloc 会返回 NULL ,此时应进行错误处理,如打印错误信息并返回。排序完成后,要记得释放 tmp 所占用的内存,避免内存泄漏。
 
 


十二、打印数组函数(PrintSort)
 


c
  
void PrintSort(int* a, int n)
{
    for (int i = 0;i < n;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
}
 


 
函数作用
 


该函数的功能是将给定的整数数组按照顺序打印输出,方便查看数组在排序前后的状态。
 


代码要点分析
 


通过一个简单的 for 循环遍历数组,使用 printf 函数逐个输出数组元素,并在最后换行,使输出结果更加清晰易读。
 


十三、测试函数(TestSort)与主函数(main)
 


c
  
void TestSort()
{
    int arr[] = { 9,5,1,3,7,8,4,2,6,0 };
    int n = sizeof(arr) / sizeof(arr[0]);
    PrintSort(arr, n);
    Insertsort(arr, n);
    //ShellSort(arr, n);
    PrintSort(arr, n);
}

int main()
{
    TestSort();
    return 0;
}
 


 
函数作用
 


 TestSort 函数用于测试排序算法的正确性,在函数内部创建一个测试数组,先打印数组的初始状态,然后调用插入排序函数对数组进行排序,再次打印排序后的数组状态。 main 函数则是程序的入口,调用 TestSort 函数来执行测试。
 


代码要点分析
 


1. 在 TestSort 函数中, int arr[] = { 9,5,1,3,7,8,4,2,6,0 };  创建一个包含10个整数的测试数组。 int n = sizeof(arr) / sizeof(arr[0]);  计算数组的元素个数。
 
2. 调用 PrintSort 函数打印数组初始状态,然后调用 Insertsort 函数进行排序,注释掉的 ShellSort(arr, n);  表示如果需要测试希尔排序,可取消注释。最后再次调用 PrintSort 函数打印排序后的数组。
 
3.  main 函数中仅调用 TestSort 函数,程序从这里开始执行。
 


十四、总结


通过对上述代码的详细分析,我们深入了解排序的实现过程。插入排序简单直观,适用于小规模数据的排序;希尔排序作为插入排序的改进算法,通过分组和逐渐缩小间隔的方式,提高了排序效率,尤其在处理大规模数据时表现更为出色。选择排序简单直观,双端选择策略在一定程度上提高了效率;堆排序利用二叉堆的数据结构,时间复杂度较低,适用于处理大规模数据;冒泡排序虽然简单,但在数据量较大时效率较低,不过它的思想容易理解,常用于教学和简单场景。快速排序以其高效的性能在排序算法中占据重要地位,理解它的原理和实现细节,以及注意要点,能帮助我们在编程中更好地运用它来解决实际问题。希望大家都能掌握这个算法界的“闪电侠”,让数据处理变得又快又好!在实际应用中,我们可以根据数据的特点和规模,选择合适的排序算法来满足需求。

 

 

评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值