排序进阶(C语言)

目录

1.快速排序

1.1前后指针法

1.2非递归实现快速排序

2.归并排序

2.1.递归

2.2非递归

3.计数排序


你好

1.快速排序

1.1前后指针法

设置两个指针,prev与cur

cur找小:curleft + 1 遍历到 right。如果 a[cur] 小于a[keyi],并且 prevcur 不相等(即 prev 还未更新到 cur 的位置),就将 a[prev]a[cur] 交换,将所有小于keyi的值交换到数组前面部分

while (cur <= right)
{
	if (a[cur] < a[keyi] && ++prev!=cur)//cur找小
	{
		Swap(&a[prev], &a[cur]);
		
	}
	cur++;
}

a[keyi]a[prev] 交换,使基准值位于其最终的位置。最终返回 prev,即基准值的索引,用于在快速排序中进一步递归排序左右子数组。

Swap(&a[prev], &a[keyi]);
return prev;

完整代码:

//快速排序 前后指针
int PartSort2(int* a, int left, int right)
{
	//三数取中
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);
	//取keyi,左边取keyi,右边先走;右边取keyi,左边先走,保证begin与end相遇位置比keyi小
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev!=cur)//cur找小
		{
			Swap(&a[prev], &a[cur]);
			
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	
	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

1.2非递归实现快速排序

使用栈模拟递归的过程,保持每个子区间的处理状态。

核心操作是分区,将数组分为左右两个子区间,并处理这些子区间。

非递归实现避免了递归深度过大可能导致的问题,同时保持了快速排序的基本效率。
调用 PartSort2 函数对数组 a 中的区间 [begin, end] 进行分区操作,并返回基准值的位置 keyi。基准值左边的元素都小于它,右边的元素都大于它。

如果 keyi + 1 小于 end,说明右子区间 [keyi + 1, end] 需要处理。将右子区间的 beginend 入栈

如果 begin 小于 keyi - 1,说明左子区间 [begin, keyi - 1] 需要处理。将左子区间的 beginend 入栈

int keyi = PartSort2(a, begin, end);
//分为左右两个区间
//[begin,key-1] keyi [key+1,end]
if (keyi + 1 < end)//右区间入栈,两个数据以上入栈
{
	STpush(&st, end);
	STpush(&st, keyi + 1);
}
if (begin < keyi - 1)//左区间入栈
{
	STpush(&st,keyi - 1);
	STpush(&st, begin);
}

利用栈保存待处理的子区间,依次处理这些子区间,确保每个子区间都得到排序,能避免深递归导致的栈溢出问题

完整代码:

//非递归
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	//头与尾入栈
	STpush(&st, right);
	STpush(&st, left);

	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		int keyi = PartSort2(a, begin, end);
		//分为左右两个区间
		//[begin,key-1] keyi [key+1,end]
		if (keyi + 1 < end)//右区间入栈,两个数据以上入栈
		{
			STpush(&st, end);
			STpush(&st, keyi + 1);
		}
		if (begin < keyi - 1)//左区间入栈
		{
			STpush(&st,keyi - 1);
			STpush(&st, begin);
		}
	}
	STDestroy(&st);
}

2.归并排序

2.1.递归

时间复杂度是 O(n log n),空间复杂度是 O(n)

归并排序是一种分治法(Divide and Conquer)的排序算法,其主要步骤是将数组分成两半,递归地对每一半进行排序,然后将两个有序的半数组合并成一个有序数组

计算中间位置 mid,将当前子数组分成两个子数组 [begin, mid][mid + 1, end]

int mid = (begin + end) / 2;

分别递归地对两个子数组进行排序。

_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);

归并

  • 使用两个指针 begin1begin2 分别遍历左右子数组。
  • 比较两个子数组中的元素,将较小的元素放入临时数组 tmp 中。
  • 处理完两个子数组中较小的一部分后,将剩余的部分(如果有)直接拷贝到 tmp 中。
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
    if (a[begin1] < a[begin2])
    {
        tmp[i++] = a[begin1++];
    }
    else
    {
        tmp[i++] = a[begin2++];
    }
}
while (begin1 <= end1)
{
    tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
    tmp[i++] = a[begin2++];
}

最后把tmp数组拷贝回原数组a中相应位置即可

memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));

完整·代码

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	//归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid+1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);

	free(tmp);
	tmp = NULL;
}

2.2非递归

定义一个gap 控制每组子数组的大小,开始时为1,随后每次归并后翻倍。

for 循环每次归并相邻的两个子数组,大小为 gap

void MergeSortNonR(int* a, int n)
{
    // 分配一个临时数组,用于存储归并过程中间结果
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        return;
    }
    
    // gap表示每组归并数据的大小,初始值为1
    int gap = 1;
    while (gap < n)
    {
        // 遍历数组,按每个gap大小进行归并
        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;

            // 打印当前正在归并的两个子数组的索引范围
            printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);

            // 如果第二个子数组的起始索引超出数组范围,跳出循环
            if (begin2 >= n)
                break;

            // 如果第二个子数组的结束索引超出数组范围,修正结束索引
            if (end2 >= n)
                end2 = n - 1;

            // 用于临时存储合并后的数据的索引
            int j = i;
            // 合并两个子数组
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }

            // 如果第一个子数组还有剩余,将其复制到临时数组
            while (begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }

            // 如果第二个子数组还有剩余,将其复制到临时数组
            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }

            // 将临时数组中的合并结果复制回原数组的对应位置
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }

        // 打印一行,以便观察归并过程
        printf("\n");

        // 每次归并后,gap翻倍,处理更大的块
        gap *= 2;
    }

    // 释放临时数组的内存
    free(tmp);
    tmp = NULL;
}

3.计数排序

计数排序是一种非比较排序算法,适用于范围较小的整数数据

// 确定数组中的最小值和最大值
    int min = a[0], 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;

循环遍历数组,找到最小值 min 和最大值 max,以确定排序范围

计算值的范围range 是最大值和最小值之间的范围加1,用于分配计数数组的大小。

// 统计每个值的出现次数
    for (int i = 0; i < n; i++)
    {
        count[a[i] - min]++;
    }

统计每个值的出现次数,遍历数组 a,根据每个元素值更新 count 数组中对应索引的位置,出现一次count++
注:

数组 a 中的值可能是任意整数,并且可能不是从0开始。例如,如果 a 的最小值是 3,最大值是 7,那么 a 中的值可能是 34567

我们希望将这些值映射到 count 数组的索引中。为了使 count 数组能够覆盖所有可能的值,我们需要将 a 中的每个值映射到从0开始的索引

  • 由于 count 数组的索引从0开始,我们需要将 a 中的每个值 a[i] 映射到一个非负的索引。通过减去最小值 min,我们将 a 中的值调整到从0开始的范围。
  • 例如,如果 min3,则 count[a[i] - 3] 会将值 3 映射到 count[0],值 4 映射到 count[1],以此类推。这确保了 count 数组的索引是从0开始的,并且能够准确记录每个值的出现次数。
// 将排序后的值放回原数组
    int j = 0;
    for (int i = 0; i < range; i++)
    {
        // 将计数数组中每个值放入原数组
        while (count[i]--)
        {
            a[j++] = i + min;
        }
    }

最后排序好放回a数组

完整代码:

#include <stdio.h>
#include <stdlib.h>

void CountSort(int* a, int n)
{
    // 确定数组中的最小值和最大值
    int min = a[0], 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* count = (int*)calloc(range, sizeof(int));
    if (count == NULL)
    {
        // 分配失败时打印错误并退出函数
        perror("calloc fail");
        return;
    }

    // 统计每个值的出现次数
    for (int i = 0; i < n; i++)
    {
        count[a[i] - min]++;
    }

    // 将排序后的值放回原数组
    int j = 0;
    for (int i = 0; i < range; i++)
    {
        // 将计数数组中每个值放入原数组
        while (count[i]--)
        {
            a[j++] = i + min;
        }
    }

    // 释放计数数组的内存
    free(count);
}

感谢,再见

相关推荐
一只自律的鸡1 分钟前
C语言项目 天天酷跑(上篇)
c语言·开发语言
带多刺的玫瑰13 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
qystca1 小时前
洛谷 P11242 碧树 C语言
数据结构·算法
冠位观测者1 小时前
【Leetcode 热题 100】124. 二叉树中的最大路径和
数据结构·算法·leetcode
XWXnb61 小时前
数据结构:链表
数据结构·链表
悲伤小伞1 小时前
C++_数据结构_详解二叉搜索树
c语言·数据结构·c++·笔记·算法
hnjzsyjyj3 小时前
“高精度算法”思想 → 大数阶乘
数据结构·高精度算法·大数阶乘
佳心饼干-4 小时前
C语言-09内存管理
c语言·算法
物联网牛七七4 小时前
4、数据结构与算法解析(C语言版)--栈
c语言·栈操作
F-2H7 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++