【数据结构】手撕排序NO.1—-排序初识

目录

 一. 前言

二. 排序的概念及运用

        2.1 排序的概念

        2.2 排序的运用

        2.3 常见的排序算法

三. 冒泡and选择排序

        3.1 冒泡排序

        3.2 选择排序

四. 各大排序算法的复杂度和稳定性

 一. 前言

       从本期开始,我们的数据结构将迎来一个新的篇章:排序篇,啪叽啪叽

       排序是数据结构中非常重要的内容,在后续的内容中,我们会对各种各样的排序算法进行剖析和实现,敬请期待哦 

本期要点

  • 对排序进行一个整体的认识
  • 介绍一下两种最简单的排序
  • 笼统地介绍一下各大排序算法的复杂度和稳定性

二. 排序的概念及运用

        2.1 排序的概念

        排序:所谓排序就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。

        稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

        内部排序:数据元素全部放在内存中的排序。

        外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

        2.2 排序的运用

        在日常生活中,我们可以找到许多和排序有关的场景。例如我们进入电脑磁盘右键就可以看到一个排序方式的选项,这是对我们电脑的磁盘文件进行排序,你可以根据需求选择不同排序方式进行排序

         又或者,随便打开一个网购网站/软件,例如京东,我们可以看到左上角就可以对商品的某一维度进行排序

         再或者,世界500强榜单也是经过排序后产生的

总之,无论是在计算机的学习上还是现实生活中,排序都是非常重要的主题,其运用十分广泛,它无处不在

        2.3 常见的排序算法

         排序算法分为比较类排序非比较类排序,如下图所示:

三. 冒泡and选择排序

本期我们先介绍一下我们的两个老朋友:冒泡排序选择排序

        3.1 冒泡排序

  • 思想 : 在一个序列中,每次对序列中的相邻记录的键值大小进行比较,将较大(升序)或较小(降序)的记录向后移动。如此循环,大/小的记录会慢慢“”到序列的后端,整个过程就像是冒泡一样,顾称之为冒泡排序。
  • 冒泡过程:以下是对某个序列进行冒泡排序的过程

可以看出,对于上面具有5个元素的无序数组,我们通过4趟的冒泡后就将其变为有序数组,每一趟冒泡后都可以使最大的数沉底。

  •  动图演示:我们可以通过一下动图感受一下冒泡两两比较的过程:

  •  循环控制:很明显,我们需要两层循环来控制冒泡排序的过程。内层循环控制当前趟的数据交换,外层循环控制冒泡排序的趟数
  • 外层循环结束条件:由于每一趟结束后都有一个数冒到序列后端,因此对于N个数的序列来说,一共需要N-1趟(只剩一个数不需要冒泡)。
    	for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
    	{
            ;  
    	}
  • 内层循环结束条件:内层循环用于数据的比较。已知N个数据共需比较N-1次,由于每一趟结束后就有数据到正确的位置,下一趟需要比较的数据个数就会少1,因此每趟的比较次数随着趟数的增加呈递减趋势,初始为N-1次。
    	for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
    	{
    		for (int j = 0; j < n - 1 - i; j++) //内层循环,次数随趟数增加而递减,初始为N-1
    		{
                ;
    		}
    	}
  • 完整代码:
    void swap(int* p1, int* p2)
    {
    	int tmp = *p1;
    	*p1 = *p2;
    	*p2 = tmp;
    }
    void BubblingSort(int* a, int n)
    {
    
    	for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
    	{
    		for (int j = 0; j < n - 1 - i; j++) //内层循环,次数随趟数增加而递减,初始为N-1
    		{
    			if (a[j] > a[j + 1]) //升序排列,较大的往后移
    			{
    				swap(&a[j], &a[j + 1]); //交换
    			}
    		}
    	}
    }
  • 改进优化:上面的代码还存在着改进空间,我们来看下面两个情景:

对于情境1,我们只需一趟冒泡即可让数组有序,而如果按照上面的代码,我们依旧要进行4趟的冒泡,即有三趟是无效的。

情境1就更夸张了,数组已经有序,我们却傻乎乎的做了4趟无效冒泡。无疑是非常浪费时间的。

考虑到这些情况,我们提出了优化方案在每趟结束后判断一下当前趟是否发生了元素交换,如果没有,则说明序列已经有序了,及时止损,反之继续。优化后的代码如下:

void swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void BubblingSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++) //外层循环,N-1趟
	{
        int flag=0; //标识每一趟是否发生交换 0:没有  1:有
		for (int j = 0; j < n - 1 - i; j++) //内层循环,次数随趟数增加而递减,初始为N-1
		{
			if (a[j] > a[j + 1]) //升序排列,较大的往后移
			{
				swap(&a[j], &a[j + 1]); //交换
                flag = 1;
			}
		}
        //判断是否已经有序
        if(flag == 0)
        {
            break; //有序则退出循环
        }
	}
}
  •  时间/空间复杂度:结合上面的图片和代码我们可以看出,总共N-1趟,每趟N-1,N-2…次比较,共比较 (N-1) + (N-2) + (N-3) + (N-4) + (N-5) + …… + 1次,时间复杂度O(N^{2});而由于没有额外的辅助空间,空间复杂度O(1)
  • 稳定性分析:由于我们是将较大的或较小的进行交换,当两个数相等时并不会进行交换,因而不会改变相同元素的先后次序,所以冒泡排序是稳定的排序。

        3.2 选择排序

  • 思想 : 每一次从待排序的数据元素中选出最小(升序)或最大(降序)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
  • 选择过程:以下是对某个序列进行选择排序的过程: 

  动图演示:我们一样通过动图感受一下选择排序的过程: 

  •  循环控制:类似的,我们需要两层循环来控制选择排序的过程。内层循环遍历序列找出最大/最小值,外层循环控制选择的次数
  • 外层循环结束条件:每一次遍历完都可以选出一个数换到起始位置,一共N个数,故要选N-1次(最后一个数不需要选择)
        for (int i = 0; i < n-1; i++) //外层循环,共要选择n-1次
    	{
            ;
    	}
    
  • 内层循环结束条件:内层循环通过比较进行选数,一开始N个数需要比较N-1次,然后每趟结束后下一次选择的起始位置就往后移动一位,比较次数减1
        for (int i = 0; i < n-1; i++) //外层循环,共选择n-1次
    	{
    		for (int j = i + 1; j < n; j++) //内层循环,起始位置开始向后进行比较,选最小值
    		{
                ;
    		}
    	}
    
  • 完整代码:
    void swap(int* p1, int* p2)
    {
    	int tmp = *p1;
    	*p1 = *p2;
    	*p2 = tmp;
    }
    void SelectSort(int* a, int n)
    {
    	for (int i = 0; i < n-1; i++) //外层循环,共选择n-1次
    	{
    		int mini = i; //记录最小值的下标,初始为第一个数下标
    		for (int j = i + 1; j < n; j++) //内层循环,起始位置开始向后进行比较,选最小值
    		{
    			if (a[mini] > a[j]) //比最小值小,交换下标
    			{
    				mini = j;
    			}
    		}
    		swap(&a[mini], &a[i]); //将最小值与起始位置的数据互换
    	}
    }
  • 时间/空间复杂度:一共选了N-1次,每次选择需要比较N-1,N-2,N-3…次,加起来和冒泡一样时间复杂度O(N);没有用到辅助空间,空间复杂度O(1)
  • 稳定性分析:由于是选数交换,在交换的过程中很可能会打乱相同元素的顺序,例如下面这个例子:

        我们发现,在第一趟交换中,黑5被交换到了红5后面,在整个排序结束后,黑5依然在红5的后方,与最开始的顺序不一致。由此我们可以得出,选择排序是不稳定的排序。

四. 各大排序算法的复杂度和稳定性

废话不多说,直接上表:

排序算法时间复杂度(最好)时间复杂度(平均)时间复杂度(最坏)空间复杂度稳定性

数据敏感度

比较类排序
冒泡排序O(N)O(N^{2})O(N^{2})O(1)稳定
选择排序O(N^{2})O(N^{2})O(N^{2})O(1)不稳定
直接插入排序O(N)O(N^{2})O(N^{2})O(1)稳定
希尔排序O(N^{1.3})

O(NlogN)

\sim O(N^{2})

O(N^{2})O(1)不稳定
堆排序O(NlogN)O(NlogN)O(NlogN)O(1)不稳定
快速排序O(NlogN)O(NlogN)O(N^{2})O(1)不稳定
归并排序O(NlogN)O(NlogN)O(NlogN)O(N)稳定
非比较类排序
基数排序O(N\times K)O(N\times K)O(N\times K)O(N+K)稳定
桶排序O(N)O(N+K)O(N^{2})O(N+K)稳定
计数排序O(N+K)O(N+K)O(N+K)O(K)稳定

以上,就是本期的全部内容啦🌸

制作不易,能否点个赞再走呢🙏

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年12月11日
下一篇 2023年12月11日

相关推荐