初探算法的魅力——手把手带你实现最经典的冒泡排序

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

之前我们系统学习了指针,这一章我们挑战一下指针编程项目中的————冒泡排序
并尝试利用冒泡排序实现qsort函数



冒泡排序是一种简单的排序算法。它重复地“遍历”要排序的数列,一次比较两个相邻的元素,如果它们的顺序错误(例如,从小到大排序时,前一个比后一个大),就把它们交换过来。
请添加图片描述



野蛮生长版:基础版(理解概念)

#include <stdio.h>

// 冒泡排序函数 (原始版本)
void bubbleSort(int arr[], int n) {
    // 外层循环:控制排序的轮数,需要进行 n-1 轮
    for (int i = 0; i < n - 1; i++) {
        // 内层循环:负责每一轮中的相邻元素比较和交换
        // 随着轮数 i 的增加,最后 i 个元素已经有序,所以 j 的范围逐渐减小
        for (int j = 0; j < n - 1 - i; j++) {
            // 如果前面的元素比后面的大,就交换它们
            if (arr[j] > arr[j + 1]) {
                // 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
	}
    printf("\n");
}


        void printArray(int arr[], int size) {
            for (int i = 0; i < size; i++)
                printf("%d ", arr[i]);
        }

// 主函数来测试
int main() {
    int arr[] = { 64, 34, 25, 12, 22, 11, 90 };
    int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度

    printf("Original array: \n");
    printArray(arr, n);

    bubbleSort(arr, n); // 调用冒泡排序函数

    printf("Sorted array: \n");
    printArray(arr, n);

    return 0;
}

上面代码中,我们以数组arr为研究对象


我们观察外层循环:控制排序的轮数
在这里插入图片描述
这个数组内一共有七个元素,每每两个元素进行比较,总共有六轮排序


我们观察内层循环:负责每一轮中的相邻元素比较和交换


这样第一个大循环都会把最大的那个数推到最右边
第二次大循环,数组末尾的 i 个元素都已经是排好序的最大值了,内层循环无需再处理它们,所以比较的范围每次减少 1

这避免了已经就位的元素被再次进行无意义的比较,是原始版本中唯一的“优化”



效率性能优:优化版

1)标志位 + 动态边界

首先我们先了解什么叫动态边界

向我们之前的野蛮生长版,它是固定边界并且没办法检测是否发生交换


如果我们碰到1000个元素,只有一对无序
如果我们使用原始版本,必须完成999轮循环,每轮比较次数递减,非常繁琐!!!

动态边界可以彻底解决这两个问题

  • 1. 标志位优化 (宏观感知)
    检测整个数组是否已经有序
    如果某一轮没有发生任何交换,说明数组已经完全有序,立即终止排序

  • 2. 动态边界优化 (微观优化)
    智能调整每轮的比较范围,跳过已经有序的尾部元素
    不再使用固定的 n-1-i 边界

代码可视化执行

以数组 [5, 3, 8, 6, 2] 为例:
第1轮执行

初始: [5, 3, 8, 6, 2], lastSwapIndex = 4
比较范围: j=0到3
交换发生: 位置0(5↔3), 位置2(8↔6), 位置3(8↔2)
currentSwapIndex = 3 (最后一次交换位置)
更新: lastSwapIndex = 3
结果: [3, 5, 6, 2, 8]

第2轮执行

比较范围: j=0到2 (因为lastSwapIndex=3)
交换发生: 位置2(6↔2)
currentSwapIndex = 2
更新: lastSwapIndex = 2
结果: [3, 5, 2, 6, 8]

第3轮执行

比较范围: j=0到1 (lastSwapIndex=2)
交换发生: 位置1(5↔2)
currentSwapIndex = 1
更新: lastSwapIndex = 1
结果: [3, 2, 5, 6, 8]

第4轮执行

比较范围: j=0到0 (lastSwapIndex=1)
交换发生: 位置0(3↔2)
currentSwapIndex = 0
更新: lastSwapIndex = 0
结果: [2, 3, 5, 6, 8]

第5轮执行

比较范围: j=0到-1 → 循环不执行
swapped = false → 提前终止!

代码思路解析

  • 首先我们需要判断数组是否经过交换,这里我们用到布尔类型
//定义头文件
#include <stdio.h>
#include <stdbool.h>
  • 第一轮需要比较所有元素,所以边界设为数组最后一个索引
int lastSwapIndex = n - 1; // 初始化最后交换位置为数组末尾
  • 我们定义一个布尔标志位,检测是否发生交换
bool hasSwapped = false;
  • 我们观察基础版的冒泡排序函数
void bubbleSort(int arr[], int n) {
    // 外层循环:控制排序的轮数,需要进行 n-1 轮
    for (int i = 0; i < n - 1; i++) {
        // 内层循环:负责每一轮中的相邻元素比较和交换
        // 随着轮数 i 的增加,最后 i 个元素已经有序,所以 j 的范围逐渐减小
        for (int j = 0; j < n - 1 - i; j++) {
            // 如果前面的元素比后面的大,就交换它们
            if (arr[j] > arr[j + 1]) {
                // 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
	}
    printf("\n");
}


  • 我们需要在bubbleSort函数里面创建一个临时变量,并修改一下循环条件
void bubbleSort(int arr[], int n) {
    int lastSwapIndex = n - 1;
        for(int i = 0; i < n - 1; i++) {

            for (int j = 0; j < lastSwapIndex; j++) {
                // 如果前面的元素比后面的大,就交换它们
                if (arr[j] > arr[j + 1]) {
                    // 交换 arr[j] 和 arr[j+1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    printf("\n");
}
  • 冒泡排序发生在循环里面,在外层循环创建布尔类型
void bubbleSort(int arr[], int n) {
    int lastSwapIndex = n - 1;
        for(int i = 0; i < n - 1; i++) {
			bool hasSwapped = false;

            for (int j = 0; j < lastSwapIndex; j++) {
                // 如果前面的元素比后面的大,就交换它们
                if (arr[j] > arr[j + 1]) {
                    // 交换 arr[j] 和 arr[j+1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    printf("\n");
}
  • 内层循环在if条件判断里面写上:
hasSwapped = true;


在上面函数经过一次大循环后,数组里面最大的数已经被退到最右边,所以我们就把边界定义为倒数第二位


  • 更新边界信息:

我们通过创建临时变量来存储边界,避免和lastSwapIndex产生冲突

void smartBubbleSort(int arr[], int n) {
    int lastSwapIndex = n - 1;  // 动态边界:记录有效比较范围
    
    for (int round = 0; round < n - 1; round++) {
        bool hasSwapped = false;          // 标志位:检测是否发生交换
        int currentSwapBoundary = -1;     
        
        // 智能比较:只遍历到动态边界
        for (int j = 0; j < lastSwapIndex; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                
                hasSwapped = true;           // 标记发生交换
                currentSwapBoundary = j;     // 更新本轮交换边界
            }
        }


为何currentSwapBoundary初始化为-1

这里讲解一下为什么将currentSwapBoundary初始化为-1

  • 尚未记录有效位置

有效的数组索引范围是:0 到 n-1
-1 明显在这个范围之外,不会与任何有效索引混淆

  • 只有真正交换过才能才更新
if (currentSwapBoundary != -1) {  // 只有真正交换过才更新
    lastSwapIndex = currentSwapBoundary;
}
  • 如果没有这个检查或者为其它值:
// 假设本轮没有发生任何交换
// currentSwapBoundary 保持初始值

// 如果是随机初始值:
lastSwapIndex = 随机值;  // 可能导致数组越界!

// 如果是0:
lastSwapIndex = 0;  // 错误地将边界设为0,下轮无法正确比较
// 其他可能但不好的选择:
int currentSwapBoundary = 0;     // 可能被误认为是有效位置
int currentSwapBoundary = n;     // 可能越界,语义不清晰
int currentSwapBoundary = -999;  // 魔数,不够直观



最后的代码为

void smartBubbleSort(int arr[], int n) {
    int lastSwapIndex = n - 1;  // 动态边界:记录有效比较范围

    for (int i = 0; i < n - 1; i++) {
        bool hasSwapped = false;          // 标志位:检测是否发生交换
        int currentSwapBoundary = -1;     // 本轮实际交换边界

        // 智能比较:只遍历到动态边界
        for (int j = 0; j < lastSwapIndex; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;

                hasSwapped = true;           // 标记发生交换
                currentSwapBoundary = j;     // 更新本轮交换边界
            }
        }

        // 更新下一轮的边界
        if (currentSwapBoundary != -1) {
            lastSwapIndex = currentSwapBoundary;
        }

        // 如果没有发生交换,提前结束
        if (!hasSwapped) {
            printf("🚀 性能碾压:第%d轮提前终止!\n", i + 1);
            break;
        }

        printf("第%d轮后边界更新为:%d\n", i + 1, lastSwapIndex);
    }
}


完整代码展示

#include <stdio.h>
#include <stdbool.h>

// 冒泡排序性能优化版(标志位 + 动态边界)
void smartBubbleSort(int arr[], int n) {
    int lastSwapIndex = n - 1;  // 动态边界:记录有效比较范围

    for (int i = 0; i < n - 1; i++) {
        bool hasSwapped = false;          // 标志位:检测是否发生交换
        int currentSwapBoundary = -1;     // 本轮实际交换边界

        // 智能比较:只遍历到动态边界
        for (int j = 0; j < lastSwapIndex; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;

                hasSwapped = true;           // 标记发生交换
                currentSwapBoundary = j;     // 更新本轮交换边界
            }
        }

        // 更新下一轮的边界
        if (currentSwapBoundary != -1) {
            lastSwapIndex = currentSwapBoundary;
        }

        // 如果没有发生交换,提前结束
        if (!hasSwapped) {
            printf("🚀 性能碾压:第%d轮提前终止!\n", i + 1);
            break;
        }

        printf("第%d轮后边界更新为:%d\n", i + 1, lastSwapIndex);
    }
}

// 打印数组函数
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 复制数组函数(用于测试多个用例)
void copyArray(int source[], int dest[], int n) {
    for (int i = 0; i < n; i++) {
        dest[i] = source[i];
    }
}

int main() {
    printf("=== 动态边界冒泡排序测试 ===\n\n");

    // 测试用例1:普通乱序数组
    printf("测试1: 普通乱序数组\n");
    int arr1[] = {64, 34, 25, 12, 22, 11, 90};
    int n1 = sizeof(arr1) / sizeof(arr1[0]);
    
    printf("排序前: ");
    printArray(arr1, n1);
    
    smartBubbleSort(arr1, n1);
    
    printf("排序后: ");
    printArray(arr1, n1);
    printf("\n");

    // 测试用例2:完全有序数组(展示提前终止)
    printf("测试2: 完全有序数组\n");
    int arr2[] = {1, 2, 3, 4, 5, 6};
    int n2 = sizeof(arr2) / sizeof(arr2[0]);
    
    printf("排序前: ");
    printArray(arr2, n2);
    
    smartBubbleSort(arr2, n2);
    
    printf("排序后: ");
    printArray(arr2, n2);
    printf("\n");

    // 测试用例3:几乎有序数组
    printf("测试3: 几乎有序数组\n");
    int arr3[] = {1, 2, 3, 5, 4, 6, 7, 8};
    int n3 = sizeof(arr3) / sizeof(arr3[0]);
    
    printf("排序前: ");
    printArray(arr3, n3);
    
    smartBubbleSort(arr3, n3);
    
    printf("排序后: ");
    printArray(arr3, n3);
    printf("\n");

    // 测试用例4:完全逆序数组
    printf("测试4: 完全逆序数组\n");
    int arr4[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
    int n4 = sizeof(arr4) / sizeof(arr4[0]);
    
    printf("排序前: ");
    printArray(arr4, n4);
    
    smartBubbleSort(arr4, n4);
    
    printf("排序后: ");
    printArray(arr4, n4);
    printf("\n");

    // 测试用例5:包含重复元素的数组
    printf("测试5: 包含重复元素的数组\n");
    int arr5[] = {5, 2, 8, 2, 5, 1, 8, 1};
    int n5 = sizeof(arr5) / sizeof(arr5[0]);
    
    printf("排序前: ");
    printArray(arr5, n5);
    
    smartBubbleSort(arr5, n5);
    
    printf("排序后: ");
    printArray(arr5, n5);
    printf("\n");

    // 性能对比演示
    printf("=== 性能优化演示 ===\n");
    
    int test1[] = {1, 2, 3, 4, 5, 6};  // 完全有序
    int test2[] = {6, 5, 4, 3, 2, 1};  // 完全逆序
    int n = sizeof(test1) / sizeof(test1[0]);
    
    printf("完全有序数组: ");
    printArray(test1, n);
    smartBubbleSort(test1, n);
    
    printf("完全逆序数组: ");
    printArray(test2, n);
    smartBubbleSort(test2, n);

    return 0;
}


2)鸡尾酒排序

调酒师调制鸡尾酒时,会左右摇晃调酒器
鸡尾酒排序也是从左到右、从右到左交替进行的排序过程

从左到右:→ → → → → 
从右到左:← ← ← ← ← 
从左到右:→ → → → → 
这种双向交替的运动方式很像调鸡尾酒时的摇晃动作
#include <stdio.h>

// 鸡尾酒排序函数
void cocktailSort(int arr[], int n) {
    int swapped = 1;  // 标记是否发生交换
    int start = 0;    // 左边界
    int end = n - 1;  // 右边界
    
    while (swapped) {
        swapped = 0;
        
        // 从左到右的冒泡排序(将最大元素移到右边)
        for (int i = start; i < end; i++) {
            if (arr[i] > arr[i + 1]) {
                // 交换元素
                int temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = 1;
            }
        }
        
        // 如果没有发生交换,说明数组已经有序
        if (!swapped) {
            break;
        }
        
        swapped = 0;
        end--;  // 右边界减1,因为最后一个元素已经有序
        
        // 从右到左的冒泡排序(将最小元素移到左边)
        for (int i = end - 1; i >= start; i--) {
            if (arr[i] > arr[i + 1]) {
                // 交换元素
                int temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = 1;
            }
        }
        
        start++;  // 左边界加1,因为第一个元素已经有序
    }
}

// 打印数组函数
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 测试代码
int main() {
    int arr[] = {5, 1, 4, 2, 8, 0, 2};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("原始数组: ");
    printArray(arr, n);
    
    cocktailSort(arr, n);
    
    printf("排序后数组: ");
    printArray(arr, n);
    
    return 0;
}


3)利用冒泡排序实现qsort函数

qsort函数复习

qsort 是 C 语言标准库中的通用快速排序函数,可以对任意类型的数组进行排序,只要你提供比较函数。

1)函数原型
void qsort(void *base, size_t nitems, size_t size,
           int (*compar)(const void *, const void *));
参数名类型说明
basevoid *指向数组起始位置(任意类型)
nitemssize_t数组元素的个数
sizesize_t每个元素的字节大小(通常使用 sizeof(…))
compar函数指针比较函数,返回值与 strcmp 类似
比较函数 compar
int compare_ints(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);  // 升序
}


完整代码

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

int compare_ints(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);  // 升序
}

int main() {
    int arr[] = {5, 2, 9, 1, 3};
    size_t len = sizeof(arr) / sizeof(arr[0]);

    qsort(arr, len, sizeof(int), compare_ints);

    for (size_t i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}


我们已经将冒泡排序算法讲解透彻,重点掌握其循环与数组比较的知识点

如果你觉得对你有帮助,请给文章来波三连哦

在这里插入图片描述

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值