C语言:冒泡法排序及其通用实现

目录

引言:

一、冒泡排序的基本原理

算法步骤

二、C语言实现

代码解释:

三、优化冒泡排序的效率

优化1:标志位

例如:对初始数组:[4, 2, 5, 3, 7, 8]

优化2:鸡尾酒排序

例如:对初始数组:[5, 1, 4, 2, 8, 0, 3]

四、冒泡排序的通用实现

qsort 函数概述

代码解释:

五、总结


引言:

        冒泡排序(Bubble Sort)是计算机科学领域中最简单的排序算法之一,广泛应用于学术教学和初学者编程实践。它之所以得名“冒泡”,是因为数据交换的过程类似于水中气泡的上升,较小或较大的元素经过一系列交换后“浮”到数组的顶端。尽管在实际应用中由于其较低的效率并不常用,冒泡排序仍是理解和学习其他复杂排序算法的基础。本文将详细介绍C语言实现的冒泡排序算法及其扩展。

一、冒泡排序的基本原理

        冒泡排序的核心思想是通过重复遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行的,直到没有再需要交换的元素,这表示数列已经排序完成。

算法步骤

  1. 比较相邻的两个元素,如果前者比后者大,则交换它们的位置。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有元素重复以上的步骤,除了最后已经排序好的元素。
  4. 重复步骤1~3,直到排序完成。

二、C语言实现

根据其原理我们很快能写出一个可以对整形数列排序的程序,如下:

#include <stdio.h>

void bubbleSort(int arr[], int sz) 
{
    int i, j, temp;
    for (i = 0; i < sz-1; i++) 
    {     
        // 最后 i 个已经排序好了
        for (j = 0; j < sz-i-1; j++) 
        {
            if (arr[j] > arr[j+1]) 
            {
                // 交换两个元素
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

int main() 
{
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int sz = sizeof(arr)/sizeof(arr[0]);
    bubbleSort(arr, sz);
    printf("排序后的数组: \n");
    for (int i = 0; i < sz; i++)
        printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

代码解释:

  • bubbleSort 函数接受一个整数数组 arr 和该数组的长度 n 作为参数。
  • 外层循环负责遍历整个数组,内层循环负责进行实际的比较和交换操作。
  • 如果一对相邻的元素顺序错误(即,arr[j] > arr[j+1]),我们就交换它们的位置。
  • 每完成一次外层循环,就有一个元素被放置在其最终位置,这就是为什么内层循环可以逐渐减少遍历的元素数量。

三、优化冒泡排序的效率

        虽然冒泡排序不是很高效,但我们仍然可以对其进行一些优化,以减少排序过程中的比较和交换次数。

优化1:标志位

void BubbleSort-2(int arr[], int sz) {
    int i, j, temp;
    int swapped;
    for (i = 0; i < sz - 1; i++) {
        swapped = 0;
        for (j = 0; j < sz - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = 1;
            }
        }
        // 如果在这一轮排序中没有发生交换,则说明数组已经有序,可以结束排序
        if (swapped == 0)
            break;
    }

        通过设置一个标志位swapped,如果在某一轮排序结束后没有任何元素交换位置,那么可以提前结束排序,因为剩余的元素已经是有序的了。

例如:对初始数组:[4, 2, 5, 3, 7, 8]

第一趟排序:

  • 比较 4 和 2,发现 4 > 2,交换它们。数组变为 [2, 4, 5, 3, 7, 8],swapped 设置为 1。
  • 比较 4 和 5,无需交换,因为 4 < 5。
  • 比较 5 和 3,发现 5 > 3,交换它们。数组变为 [2, 4, 3, 5, 7, 8]。
  • 比较 5 和 7,无需交换,因为 5 < 7。
  • 比较 7 和 8,无需交换,因为 7 < 8。

第一趟排序结束后数组是 [2, 4, 3, 5, 7, 8],发生了交换,因此需要继续排序。

第二趟排序:

  • 比较 2 和 4,无需交换,因为 2 < 4。
  • 比较 4 和 3,发现 4 > 3,交换它们。数组变为 [2, 3, 4, 5, 7, 8],swapped 设置为 1。
  • 比较 4 和 5,无需交换,因为 4 < 5。
  • 比较 5 和 7,无需交换,因为 5 < 7。

由于最后两个元素已经是排序好的,所以这趟排序只进行了三次比较。

第二趟排序结束后数组是 [2, 3, 4, 5, 7, 8],发生了交换,继续排序。

第三趟排序:

  • 比较 2 和 3,无需交换,因为 2 < 3。
  • 比较 3 和 4,无需交换,因为 3 < 4。
  • 比较 4 和 5,无需交换,因为 4 < 5。

第三趟排序没有发生任何交换,swapped 仍然是 0,这意味着数组已经是有序的。根据优化冒泡排序的逻辑,排序操作在这里结束。

最终排序后的数组是 [2, 3, 4, 5, 7, 8],此时排序已完成。

        通过这个例子,你可以看到,尽管初始数组部分无序,但是在第三趟排序之后,我们检测到没有发生交换,从而得知数组已经是有序的,无需进行额外的排序,这就是 optimizedBubbleSort 函数中引入 swapped 变量的优势所在。

        优化后的冒泡排序可以在最好的情况(数组已经有序)下达到 O(n) 的时间复杂度,而平均和最坏的情况时间复杂度仍然是 O(n^2)。这种优化虽然在最坏情况下并不改善时间复杂度,但在最好情况下或者数组已经接近排序完成的情况下,可以大大减少排序所需的时间。

优化2:鸡尾酒排序

        鸡尾酒排序(Cocktail Sort),也被称为定向冒泡排序。这个算法是冒泡排序的一个变体,它在每轮迭代中会在数组的两个方向上进行排序 

void cocktailSort(int arr[], int sz) 
{
    int i, left = 0, right = sz - 1, temp;
    int swapped = 1;
    while (swapped) 
    {
        swapped = 0;
        for (i = left; i < right; ++i) 
        {
            if (arr[i] > arr[i + 1]) 
            {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = 1;
            }
        }
        if (!swapped) 
        {
            break;
        }
        swapped = 0;
        --right;
        for (i = right - 1; i >= left; --i) 
        {
            if (arr[i] > arr[i + 1]) 
            {
                temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = 1;
            }
        }
        ++left;
    }
}
例如:对初始数组:[5, 1, 4, 2, 8, 0, 3]

我们按步骤使用鸡尾酒排序来排序这个数组

正向遍历(从左到右):

  • 比较5和1,交换:[1, 5, 4, 2, 8, 0, 3]
  • 比较5和4,交换:[1, 4, 5, 2, 8, 0, 3]
  • 比较5和2,交换:[1, 4, 2, 5, 8, 0, 3]
  • 比较5和8,不交换:[1, 4, 2, 5, 8, 0, 3]
  • 比较8和0,交换:[1, 4, 2, 5, 0, 8, 3]
  • 比较8和3,交换:[1, 4, 2, 5, 0, 3, 8]

        此时,最大的元素8已到达它正确的位置。

反向遍历(从右到左):

end 现在指向数组的倒数第二个元素,因为最后一个元素8已经在正确的位置。

  • 比较3和0,交换:[1, 4, 2, 5, 3, 0, 8]
  • 比较5和3,不交换:[1, 4, 2, 5, 3, 0, 8]
  • 比较5和2,交换:[1, 4, 5, 2, 3, 0, 8]
  • 比较4和5,不交换:[1, 4, 5, 2, 3, 0, 8]
  • 比较4和1,不交换:[1, 4, 5, 2, 3, 0, 8]
  • 此时,最小的元素0已到达它正确的位置。
经过第一轮鸡尾酒排序:[1, 4, 5, 2, 3, 0, 8] 

重复这个过程:接下来我们继续从左到右,然后是从右到左,交替进行,每次遍历结束,start 会增加1,end 会减少1,缩小未排序的范围。

在几轮迭代之后,数组将完全有序:

完全排序后的数组:[0, 1, 2, 3, 4, 5, 8] 

        在这个排序过程中,你可以看到鸡尾酒排序是如何在数组的两端迭代地进行排序,每次正向遍历将最大的元素置于其最终位置,反向遍历则是将最小的元素置于其最终位置。这种排序方式适合于大部分元素已经接近于其最终位置的情况,在这种场景下,鸡尾酒排序的效率通常会比传统的冒泡排序要好。

四、冒泡排序的通用实现

        在探讨排序算法时,我们不得不提及C语言标准库中的qsort函数,这是一个灵活且强大的快速排序实现。qsort可以对数组进行排序,无论其元素是什么类型,只要提供了一个比较函数。

qsort 函数概述

在C语言中,qsort函数是快速排序算法的实现,定义在stdlib.h头文件中。其函数原型如下:

void qsort(void *base, size_t nmemb, size_t size,
                    int (*compar)(const void *, const void *));

        这里,base是指向待排序数组的第一个对象的指针,nmemb是数组中的元素数量,size是每个元素的大小,而compar是一个函数指针,指向一个比较两个元素的函数。这个比较函数必须返回一个整数,表示两个元素的相对顺序。

        由于qsort是基于快速排序算法,它在效率上通常优于简单排序算法,如冒泡排序。其平均时间复杂度为O(n log n),是处理大型数据集的理想选择。

        而对于冒泡排序,我门或许可以借鉴函数qsort函数对上面的程序进行优化,使冒泡排序可以同样对不同的数据进行排序。

下面是实现的代码

int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
void Swap(char* buf1, char*buf2, size_t width)
{
	int i = 0;
	for (i = 0; i < width; i++) 
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
//接收一个指向数据首地址的void*类型指针、数组中元素的数量、每个元素的大小以及一个比较函数指针。
void bubble_sort2(void* base, size_t sz, size_t width, int (*cmp)(const void*p1, const void*p2))
{
	for (int i = 0; i < sz - 1; i++)
	{
		//每一趟冒泡排序的过程
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if(cmp((char*)base + j * width, (char*)base + (j + 1) * width)>0)
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

int main()
{
	//设计和实现bubble_sort2(),这个函数能够排序任意类型的数据
	int arr[] = { 3,1,5,7,9,2,4,0,8,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort2(arr, sz, sizeof(arr[0]), cmp_int);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}
代码解释:
  • cmp_int 函数       

         cmp_int 函数是一个比较函数,用于冒泡排序中比较两个整数的大小。它接受两个const void*类型的指针参数,强转为int*类型后进行比较,返回它们之间的差值。如果第一个数较大,返回正值;如果第二个数较大,返回负值;如果相等,返回零。

  • Swap 函数        

   Swap 函数用于交换两个数据项。由于冒泡排序需要交换元素位置,Swap 通过操作字节(char 类型数据),而不是具体类型的数据,这个函数能够交换任何大小和类型的数据,只要传递正确的字节宽度。在这个函数内,使用了一个循环来交换每一个字节,从而实现任意类型数据的交换。

        通过这种方式,我们将冒泡排序实现通用化了。这个函数接受任意类型的数组和元素大小,使之类似于qsort

五、总结

  •         冒泡排序,尽管在效率方面不具备与高级排序算法相抗衡的性能,仍然拥有其在教学和理解排序算法基本概念中的独特地位。本文介绍了冒泡排序的基本原理,并提供了一个简单的C语言实现。随后,我们探讨了如何通过引入标志位和鸡尾酒排序技术来优化冒泡排序的效率。
  •         更进一步,我们借鉴了C标准库中`qsort`函数的设计,将冒泡排序的实现泛化,使其能够像`qsort`一样对任意类型的数据进行排序。我们的泛型`bubble_sort2`函数,通过接收比较函数参数,可以灵活适应不同的数据类型,突破了原始冒泡排序只能应用于整型数组的限制。
  •         在更全局的视野中,学习冒泡排序不仅仅是为了使用它,更重要的是通过它来理解算法的基础——比较和交换,以及如何分析和优化算法的性能。这为学习更复杂的数据结构和算法打下坚实的基础。尽管在实际应用中我们更倾向于使用更高效的排序算法,冒泡排序在教育和基础研究中仍占有一席之地。
  •         总之,冒泡排序是每个学习编程和算法的人必经的里程碑。通过对其进行实现与优化,不仅能够加深对排序算法的理解,还能够提升解决问题的能力和编程技巧。这种基本的算法理论和实践的结合,是成为一个优秀的软件工程师不可或缺的一部分。

以上如有写错的地方请多多指教。

  • 11
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

现实的龍

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值