【排序算法】图解冒泡排序(多图+解决两种无效比较问题)

简介: 本文主要介绍冒泡排序算法,通过图片一步步解释每一趟每一次的交换。并且,本文在给出冒泡排序基础版代码后,分析了基础版存在的两个问题,解决该问题可以避免出现不必要的比较。代码通过C#实现,并输出每一次交换的情况和比较次数,方便各位小伙伴比较算法的优缺点。
🐋作者简介:博主是一位.Net开发者,同时还是RPA和低代码平台的践行者。
🐶座右铭:总有一天你所坚持的会反过来拥抱你。
🌈写在前面:

本文主要介绍冒泡排序算法,通过图片一步步解释每一趟每一次的交换。并且,本文在给出冒泡排序基础版代码后,分析了基础版存在的两个问题,解决该问题可以避免出现不必要的比较。代码通过C#实现,并输出每一次交换的情况和比较次数,方便各位小伙伴比较算法的优缺点。


👉本文关键字:经典算法、排序算法、交换排序、冒泡排序、图解、C#

一、排序算法分类

  • 内部排序

    指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。

  • 外部排序

    数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。

  • 常见的分类方法

    排序.png

二、算法效率

1. 时间复杂度

度量一个程序(算法)执行时间的两种方法。

  • 事后统计的方法

    这种方法可行,但有两个问题:一是要想对设计的算法的运行性能进行评测,需要事件运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素。

  • 事前估算的方法

    通过分析某个算法的时间复杂度来判断哪个算法更优。

  • 时间频度

    一个算法花费的时间与算法中语句的执行次数成正比。一个算法中的语句执行次数称为语句频度或者时间频度。记为T(n)

此处引用清华大学《数据结构》课程的一段话,一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。

2. 空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元。

三、交换排序

1. 冒泡排序

冒泡排序的英文Bubble Sort,是一种最基础的交换排序。之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动。

2. 快速排序

快速排序Quick Sort是对冒泡排序的一种改进。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

四、算法实践

1. 图解算法原理

冒泡排序的升序排列基本思想就是通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。

下面通过一个动图来看一看冒泡排序到底是怎么样移动的。

冒泡排序.gif

那么,具体是如何移动的,我们以上面动图的数组[36, 26, 27, 2, 4, 19, 50, 48]为例。

假设我们有数组[36, 26, 27, 2, 4, 19, 50, 48],要求按升序排列。

  • 第1趟排序

    • 第1次比较

      第一趟第一次比较.png

36和26比较,36较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续比较下面两个相邻的两个数大小关系。
  • 第2次比较

    第一趟第二次比较.png

36和27比较,36较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第3轮比较。
  • 第3次比较

    第一趟第三次比较.png

36和2比较,36较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第4轮比较。
  • 第4次比较

    第一趟第四次比较.png

36和4比较,36较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第5轮比较。
  • 第5次比较

    第一趟第五次比较.png

36和19比较,36较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第6轮比较。
  • 第6次比较

第一趟第六次比较.png

​ 36和50比较,50较大,不交换位置。比较完毕之后,红色移动窗向后挪一位继续第7轮比较。

  • 第7次比较

第一趟第七次比较.png

​ 50和48比较,50较大,交换位置。第一趟排序结束。

  • 第一趟比较结果(比较次数是 8 - 1 - 0)

第一趟结果.png

  • 第2趟排序

    • 第1次比较

      第二趟第一次比较.png

27和26比较,27较大,**不交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续比较下面两个相邻的两个数大小关系。
  • 第2次比较

    第二趟第二次比较.png

 2和27比较,27较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第3轮比较。
  • 第3次比较

    第二趟第三次比较.png

    27和4比较,27较大,交换位置。比较完毕之后,红色移动窗向后挪一位继续第4轮比较。

  • 第4次比较

    第二趟第四次比较.png

27和19比较,27较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第5轮比较。
  • 第5次比较

    第二趟第五次比较.png

27和36比较,36较大,**不交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第6轮比较。
  • 第6次比较

第二趟第六次比较.png

​ 36和48比较,48较大,不交换位置。第二趟排序结束。

  • 第二趟比较结果(比较次数是 8 - 1 - 1)

第二趟结果.png

  • 第3趟排序

    • 第1次比较

      第三趟第一次比较.png

2和26比较,26较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续比较下面两个相邻的两个数大小关系。
  • 第2次比较

    第三趟第二次比较.png

26和4比较,26较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第3轮比较。
  • 第3次比较

    第三趟第三次比较.png

19和2比较,26较大,**交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第4轮比较。
  • 第4次比较

    第三趟第四次比较.png

26和27比较,27较大,**不交换位置**。比较完毕之后,**红色移动窗向后挪一位**继续第5轮比较。
  • 第5次比较

第三趟第五次比较.png

36和27比较,36较大,**不交换位置**。第三趟排序结束。
  • 第三趟比较结果(比较次数是 8 - 1 - 2)

第三趟结果.png

之后的第4、5、6趟排序同理。

  • 第7趟排序 ( 7 = 8 - 1 )
  • 第七趟比较结果(比较次数是 8 - 1 - 6)

    第七趟结果.png

从上述图解我们可以看出:

如果数组要求升序排列,那么每一趟排序就可以确定一个数,即第一趟能确定将末位上的数归位,第二趟能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。

进一步地,每一趟排序都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后红色窗体向后挪一位继续比较下面两个相邻的两个数大小关系,重复此步骤,直到最后一个还没归位的数。这里需要强调的是,并不是每一趟排序都需要比较同样的次数。从图中我们可以看出,第1趟比较了7次,第2趟比较6次。以此类推,有 n 个数进行排序,第 i + 1 趟(循环中 i 是从 0 开始的),我们只需要比较 n-1-i 次。

2. 算法实现

    /// <summary>
    /// 冒泡排序静态类
    /// </summary>
    public static class BubbleSort
    {
        // 将前面额冒泡排序算法,封装成一个方法
        public static void BubbleSortMethod(int[] arr)
        {
            // 冒泡排序 的时间复杂度 O(n^2)
            int temp = 0; // 临时变量
            int count = 0; // 计数比较次数
            for (int i = 0; i < arr.Length - 1; i++)
            {
                Console.WriteLine($"==============第{i + 1}趟排序==============");
                for (int j = 0; j < arr.Length - 1 - i; j++)
                {
                    // 如果前面的数比后面的数大,则交换
                    if (arr[j] > arr[j + 1])
                    {
                        temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                    }
                    Console.WriteLine($"第{i + 1}趟第{j + 1}次交换后的数组");
                    Console.WriteLine(string.Join(" ", arr));
                }
                Console.WriteLine($"第{i + 1}趟排序结果");
                Console.WriteLine(string.Join(" ",arr));
            }
            Console.WriteLine($"==============一共比较{count}次==============");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            int[] intArray = new int[] { 36, 26, 27, 2, 4, 19, 50, 48 };

            //输出最大最小值
            //Console.WriteLine($"最大值{MaxMin.Max(intArray)}");
            //Console.WriteLine($"最小值{MaxMin.Min(intArray)}");

            //测试冒泡排序
            BubbleSort.BubbleSortMethod(intArray);

        }
    }

==============运行结果===============
==============第1趟排序==============
第1趟第1次交换后的数组
26 36 27 2 4 19 50 48
第1趟第2次交换后的数组
26 27 36 2 4 19 50 48
第1趟第3次交换后的数组
26 27 2 36 4 19 50 48
第1趟第4次交换后的数组
26 27 2 4 36 19 50 48
第1趟第5次交换后的数组
26 27 2 4 19 36 50 48
第1趟第6次交换后的数组
26 27 2 4 19 36 50 48
第1趟第7次交换后的数组
26 27 2 4 19 36 48 50
第1趟排序结果
26 27 2 4 19 36 48 50
==============第2趟排序==============
第2趟第1次交换后的数组
26 27 2 4 19 36 48 50
第2趟第2次交换后的数组
26 2 27 4 19 36 48 50
第2趟第3次交换后的数组
26 2 4 27 19 36 48 50
第2趟第4次交换后的数组
26 2 4 19 27 36 48 50
第2趟第5次交换后的数组
26 2 4 19 27 36 48 50
第2趟第6次交换后的数组
26 2 4 19 27 36 48 50
第2趟排序结果
26 2 4 19 27 36 48 50
==============第3趟排序==============
第3趟第1次交换后的数组
2 26 4 19 27 36 48 50
第3趟第2次交换后的数组
2 4 26 19 27 36 48 50
第3趟第3次交换后的数组
2 4 19 26 27 36 48 50
第3趟第4次交换后的数组
2 4 19 26 27 36 48 50
第3趟第5次交换后的数组
2 4 19 26 27 36 48 50
第3趟排序结果
2 4 19 26 27 36 48 50
==============第4趟排序==============
第4趟第1次交换后的数组
2 4 19 26 27 36 48 50
第4趟第2次交换后的数组
2 4 19 26 27 36 48 50
第4趟第3次交换后的数组
2 4 19 26 27 36 48 50
第4趟第4次交换后的数组
2 4 19 26 27 36 48 50
第4趟排序结果
2 4 19 26 27 36 48 50
==============第5趟排序==============
第5趟第1次交换后的数组
2 4 19 26 27 36 48 50
第5趟第2次交换后的数组
2 4 19 26 27 36 48 50
第5趟第3次交换后的数组
2 4 19 26 27 36 48 50
第5趟排序结果
2 4 19 26 27 36 48 50
==============第6趟排序==============
第6趟第1次交换后的数组
2 4 19 26 27 36 48 50
第6趟第2次交换后的数组
2 4 19 26 27 36 48 50
第6趟排序结果
2 4 19 26 27 36 48 50
==============第7趟排序==============
第7趟第1次交换后的数组
2 4 19 26 27 36 48 50
第7趟排序结果
2 4 19 26 27 36 48 50
==============一共比较28次==============
第一层循环是用来控制趟数,也就是 n 个数就要比较 n-1 趟;第二层是控制第 i +1 趟的比较次数, i+1 趟就是比较了 n-1-i 次。

我们通过运行结果可以看出,输出的结果与我们图解分析的内容是一致

3. 优化版(解决两种无效比较的情况)

a. 减少趟数

细心的朋友会发现,在上述例子中,我们在第4、5、6趟的排序都是多余,因为我们在第三趟的时候就已经得到了一个升序排列的数组,也就是说第4、5、6趟的排序没有进行交换

因此在排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序。此时我们要在排序过程中设置一个标志flag判断元素是否进行过交换,从而减少不必要的比较。
    /// <summary>
    /// 冒泡排序静态类
    /// </summary>
    public static class BubbleSort
    {
        // 将前面额冒泡排序算法,封装成一个方法
        public static void BubbleSortMethod(int[] arr)
        {
            // 冒泡排序 的时间复杂度 O(n^2)
            int temp = 0; // 临时变量
            int count = 0; // 计数比较次数
            bool flag = false; // 标识变量,表示是否进行过交换
            for (int i = 0; i < arr.Length - 1; i++)
            {
                Console.WriteLine($"==============第{i + 1}趟排序==============");
                for (int j = 0; j < arr.Length - 1 - i; j++)
                {
                    // 如果前面的数比后面的数大,则交换
                    if (arr[j] > arr[j + 1])
                    {
                        flag = true;
                        temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                    }
                    count++;
                    Console.WriteLine($"第{i + 1}趟第{j + 1}次交换后的数组");
                    Console.WriteLine(string.Join(" ", arr));
                }
                Console.WriteLine($"第{i + 1}趟排序结果");
                Console.WriteLine(string.Join(" ",arr));

                if (!flag)
                { 
                    break; // 在一趟排序中,一次交换都没有发生过
                }
                else
                {
                    flag = false; // 重置flag, 进行下次判断
                }
            }
            Console.WriteLine($"==============一共比较{count}次==============");
        }
    }
==============运行结果===============
==============第1趟排序==============
第1趟第1次交换后的数组
26 36 27 2 4 19 50 48
第1趟第2次交换后的数组
26 27 36 2 4 19 50 48
第1趟第3次交换后的数组
26 27 2 36 4 19 50 48
第1趟第4次交换后的数组
26 27 2 4 36 19 50 48
第1趟第5次交换后的数组
26 27 2 4 19 36 50 48
第1趟第6次交换后的数组
26 27 2 4 19 36 50 48
第1趟第7次交换后的数组
26 27 2 4 19 36 48 50
第1趟排序结果
26 27 2 4 19 36 48 50
==============第2趟排序==============
第2趟第1次交换后的数组
26 27 2 4 19 36 48 50
第2趟第2次交换后的数组
26 2 27 4 19 36 48 50
第2趟第3次交换后的数组
26 2 4 27 19 36 48 50
第2趟第4次交换后的数组
26 2 4 19 27 36 48 50
第2趟第5次交换后的数组
26 2 4 19 27 36 48 50
第2趟第6次交换后的数组
26 2 4 19 27 36 48 50
第2趟排序结果
26 2 4 19 27 36 48 50
==============第3趟排序==============
第3趟第1次交换后的数组
2 26 4 19 27 36 48 50
第3趟第2次交换后的数组
2 4 26 19 27 36 48 50
第3趟第3次交换后的数组
2 4 19 26 27 36 48 50
第3趟第4次交换后的数组
2 4 19 26 27 36 48 50
第3趟第5次交换后的数组
2 4 19 26 27 36 48 50
第3趟排序结果
2 4 19 26 27 36 48 50
==============第4趟排序==============
第4趟第1次交换后的数组
2 4 19 26 27 36 48 50
第4趟第2次交换后的数组
2 4 19 26 27 36 48 50
第4趟第3次交换后的数组
2 4 19 26 27 36 48 50
第4趟第4次交换后的数组
2 4 19 26 27 36 48 50
第4趟排序结果
2 4 19 26 27 36 48 50
==============一共比较22次==============

我们通过运行结果可以看出,比较次数减少到了22次,并且只进行了4趟排序,避免了一部分不必要的比较

b. 减少每趟比较次数

另外还有一种情况,如果数组中前半部分是无序的,后半部分的数值都大于前半部分并且是有序的,比如[19,26,2,4,27,36,48,50]这个数组,可以看出数组后半部分[27,36,48,50]的数值都大于前半部分并且是有序的,但是每一趟还是会去比较多次。

因此我们在每一轮排序的最后,记录一下最后一次元素交换的位置,那个位置也就是无序数列的边界,再往后就是有序区了。
    /// <summary>
    /// 冒泡排序静态类
    /// </summary>
    public static class BubbleSort
    {
        // 将前面额冒泡排序算法,封装成一个方法
        public static void BubbleSortMethod(int[] arr)
        {
            // 冒泡排序 的时间复杂度 O(n^2)
            int temp = 0; // 临时变量
            int count = 0; // 计数比较次数
            bool flag = false; // 标识变量,表示是否进行过交换               
            int lastChangeIndex = 0; //记录最后一次交换的位置                      
            int border = arr.Length - 1; //记录边界,每次比较只需要比到这里为止
            for (int i = 0; i < arr.Length - 1; i++)
            {
                Console.WriteLine($"==============第{i + 1}趟排序==============");
                for (int j = 0; j < border; j++)
                {
                    // 如果前面的数比后面的数大,则交换
                    if (arr[j] > arr[j + 1])
                    {
                        flag = true;
                        temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                        lastChangeIndex = j;
                    }
                    count++;
                    Console.WriteLine($"第{i + 1}趟第{j + 1}次交换后的数组");
                    Console.WriteLine(string.Join(" ", arr));
                }
                Console.WriteLine($"第{i + 1}趟排序结果");
                Console.WriteLine(string.Join(" ",arr));
                border = lastChangeIndex;
                if (!flag)
                { 
                    break; // 在一趟排序中,一次交换都没有发生过
                }
                else
                {
                    flag = false; // 重置flag, 进行下次判断
                }
            }
            Console.WriteLine($"==============一共比较{count}次==============");
        }
    }
==============运行结果===============
==============第1趟排序==============
第1趟第1次交换后的数组
26 36 27 2 4 19 50 48
第1趟第2次交换后的数组
26 27 36 2 4 19 50 48
第1趟第3次交换后的数组
26 27 2 36 4 19 50 48
第1趟第4次交换后的数组
26 27 2 4 36 19 50 48
第1趟第5次交换后的数组
26 27 2 4 19 36 50 48
第1趟第6次交换后的数组
26 27 2 4 19 36 50 48
第1趟第7次交换后的数组
26 27 2 4 19 36 48 50
第1趟排序结果
26 27 2 4 19 36 48 50
==============第2趟排序==============
第2趟第1次交换后的数组
26 27 2 4 19 36 48 50
第2趟第2次交换后的数组
26 2 27 4 19 36 48 50
第2趟第3次交换后的数组
26 2 4 27 19 36 48 50
第2趟第4次交换后的数组
26 2 4 19 27 36 48 50
第2趟第5次交换后的数组
26 2 4 19 27 36 48 50
第2趟第6次交换后的数组
26 2 4 19 27 36 48 50
第2趟排序结果
26 2 4 19 27 36 48 50
==============第3趟排序==============
第3趟第1次交换后的数组
2 26 4 19 27 36 48 50
第3趟第2次交换后的数组
2 4 26 19 27 36 48 50
第3趟第3次交换后的数组
2 4 19 26 27 36 48 50
第3趟排序结果
2 4 19 26 27 36 48 50
==============第4趟排序==============
第4趟第1次交换后的数组
2 4 19 26 27 36 48 50
第4趟第2次交换后的数组
2 4 19 26 27 36 48 50
第4趟排序结果
2 4 19 26 27 36 48 50
==============一共比较18次==============

我们通过运行结果可以看出,比较次数减少到了18次,并且只进行了4趟排序,第3趟仅进行3次比较,第4趟仅进行2次比较,避免了一部分不必要的比较

4.时间复杂度

若数组是正序的,一趟即可完成排序。冒泡排序最好的时间复杂度为 O(n)

从图中我们可以看出,8个数字需要7趟,第1趟比较了7次,第2趟比较6次。最后比较次数是7+6+ ... + 1 = 28次,与上述代码输出一致。如果是 n 个数字,那么就是

$$ (n-1)+(n-2)+...+2+1 = n(n-1)/2 = n^2/2 - n/2 $$

根据复杂度的规则,去掉低阶项和常数系数,那复杂度就是 O(n^2)了。

5.空间复杂度

算法执行过程中,只需要一个临时变量temp来进行交换,所以空间复杂度是O(1)


⭐写在结尾:

文章中出现的任何错误请大家批评指出,一定及时修改。

希望写在这里的小伙伴能给个三连支持

相关文章
|
24天前
|
机器学习/深度学习 搜索推荐 算法
【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题
【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题
|
6天前
|
机器学习/深度学习 算法 搜索推荐
数据结构算法--2 冒泡排序,选择排序,插入排序
**基础排序算法包括冒泡排序、选择排序和插入排序。冒泡排序通过相邻元素比较交换,逐步将最大值“冒”到末尾,平均时间复杂度为O(n^2)。选择排序每次找到剩余部分的最小值与未排序部分的第一个元素交换,同样具有O(n^2)的时间复杂度。插入排序则类似玩牌,将新元素插入到已排序部分的正确位置,也是O(n^2)复杂度。这些算法适用于小规模或部分有序的数据。**
|
16天前
|
算法 搜索推荐
数据结构与算法-冒泡排序
数据结构与算法-冒泡排序
10 2
|
21天前
|
算法
数据结构和算法学习记录——时间复杂度的计算(嵌套循环、大O的渐进表示法、双重循环、常数循环、strchr、冒泡排序、二分查找、斐波那契数列递归)
数据结构和算法学习记录——时间复杂度的计算(嵌套循环、大O的渐进表示法、双重循环、常数循环、strchr、冒泡排序、二分查找、斐波那契数列递归)
28 1
|
2天前
|
搜索推荐 算法 大数据
​【数据结构与算法】冒泡排序:简单易懂的排序算法解析
​【数据结构与算法】冒泡排序:简单易懂的排序算法解析
|
3天前
|
机器学习/深度学习 搜索推荐 算法
【C/排序算法】:快速排序和冒泡排序
【C/排序算法】:快速排序和冒泡排序
8 0
|
7天前
|
搜索推荐 算法
排序算法之冒泡排序
排序算法之冒泡排序
7 0
|
10天前
|
搜索推荐
排序算法---冒泡排序----详解&&代码
排序算法---冒泡排序----详解&&代码
|
1月前
|
算法
循环嵌套思路详解 | 一个“在盒子里过家家”的算法 -- 以冒泡排序与打印菱形为例(一)
本文介绍了编程中的一种思想,通过菱形打印问题来阐述如何理解和使用循环嵌套。文章提到,初学者在面对循环结构时,可以通过先识别代码块的结束括号来理解整体结构,提高阅读效率。作者提出了“在盒子里过家家”的理解思路,将外层循环看作一个个盒子,内层循环视为盒子里的操作,弱化循环嵌套的概念,强调以盒子为单位思考问题。此外,文章还通过示例解释了内外循环的关系,帮助读者更好地理解循环控制和执行过程。
51 3
|
1月前
|
算法
循环嵌套思路详解 | 一个“在盒子里过家家”的算法 -- 以冒泡排序与打印菱形为例(二)
本文介绍了如何运用特定思路解析和解决编程问题,特别是涉及双层循环的情况。首先,通过一个打印特定图案的例子,解释了将“盒子”作为思考单位的概念,即分析问题中每个循环的作用和内容。接着,以冒泡排序为例,展示了如何将问题分解为外层循环(趟数)和内层循环(每趟的比较与交换),并通过盒子思路简化理解和实现代码。最后,提到了菱形打印的代码优化,鼓励读者思考并应用这种思维方式。总的来说,文章强调了自然地理解和运用循环结构,而不是机械地记忆。
43 2