算法(三):排序算法(上)

目录

前言

基本排序算法(Basic Sorting Algorithm)

冒泡排序(Bubble Sort)

直接选择排序(Selection Sort)

直接插入排序(Insertion Sort)


前言

上一章,我们认识了算法的概念,分类,这一次我们真正进入算法海洋之一的排序算法。

友情提示:本文中的代码由Java实现

先了解一下专业术语:

空间复杂度和时间复杂度:见文章算法(一):算法复杂度之时间复杂度和空间复杂度

稳定和不稳定:假如一组数据中存在两个相同的元素A=B,A在前B在后,排序后AB的前后顺序保持不变称之为稳定,反之则称之为不稳定,换句话说就是能避免不必要的位置交换

内排序和外排序:所有的排序操作都在内存中执行叫内排序,因数据较大把数据放在磁盘中进行排序的叫外排序

再来看一眼结构图:

最后看一下表格:

排序方式时间复杂度(最佳)时间复杂度(最坏)时间复杂度(平均)空间复杂度稳定性
冒泡排序O(n)O(n²)O(n²)O(1)稳定
直接选择排序O(n²)O(n²)O(n²)O(1)不稳定
直接插入排序O(n)O(n²)O(n²)O(1)稳定
希尔排序O(nlogn)O(n²)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定

快速排序

O(nlogn)O(n²)O(nlogn)O(nlogn)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定

 

 

 

 

 

 

 

 

 

基本排序算法(Basic Sorting Algorithm)

冒泡排序(Bubble Sort)

描述:从第一个元素开始和下一个元素比较,如果第一个比第二个大,那么交换两个元素的位置,继续和下一个元素比较;如果第一个比第二个小,那就拿第二个元素和下一个元素比较,直到最大的元素就会出现在最后一个位置,没有元素可以继续比较,算是完成一轮比较,然后继续按照上述规则,继续进行比较,除了最后面已经找出的最大元素,以此类推。

动态图展示:

代码:

/**
  * 冒泡排序
  * @param n
  * @return
  */
public static void sort(int[] n) {
    // 每一轮冒泡出开一个元素 外层循环次数就表示数组总共需要冒泡几轮 
    for(int i=0;i<n.length;i++) {

        // 临时变量 用于冒泡时数据交换的备份
        int temp;

        // 内层循环表示该轮进行两两比较的次数 
	for(int j = 0;j<n.length-1-i;j++) {

            //判断两个元素 如果前一个大于后一个 那么准备进行位置交换 反之不做任何操作
	    if(n[j]>n[j+1]) {

                //把大的元素赋值到临时变量上
		temp = n[j];

                //把小的元素赋值到大的元素所在的位置
		n[j]=n[j+1];

                //把临时变量中的大元素值赋值到小元素所在的位置
		n[j+1]=temp;

                // 完成交换 继续循环

	    }

	}

    }
	
}

看完之后是不是内心想着原来如此,酱紫啊什么的。别高兴太早,其实冒泡排序还是可以进行优化的,接下往下看:

冒泡排序过程中最后面的都是有序的,每次排序都是只对前面的元素进行排序,大家想过没,有这么一种情况,假如前面的一部分元素已经是有序的或者说整体已经是有序的,但是由于不清楚是否有序,而进行冒泡排序。实际上只是做了无用功,没有发生任何交换,岂不是白白的浪费了时间,那该怎么进行改进呢?

其实很简单,我们知道外层循环是是冒泡的轮数,内层循环是进行比较找出最大的元素,只要后面的元素比前面的元素大,就会发生位置交换,换言之,如果没有发生交换,则表示一组元素中每两个相邻的元素都是前面的小于后面的,也就是说本身已经是有序的,既然数据有序就可以终止排序了,总结一句话:如果没有发生交换,则表示元素已经有序。

优化:

/**
  * 冒泡排序优化版
  * @param n
  * @return
  */
  public static void sort(int[] n) {
    // 每一轮冒泡出开一个元素 外层循环次数就表示数组总共需要冒泡几轮 
    for(int i=0;i<n.length;i++) {

        // 临时变量 用于冒泡时数据交换的备份
        int temp;
        
        // 布尔值 纪录是否经过交换 每一轮开始前重置状态为false 表示为交换
        boolean flag=false;

        // 内层循环表示该轮进行两两比较的次数 
	for(int j = 0;j<n.length-1-i;j++) {

            // 判断两个元素 如果前一个大于后一个 那么准备进行位置交换 反之不做任何操作
	    if(n[j]>n[j+1]) {

                // 把大的元素赋值到临时变量上
		temp = n[j];

                // 把小的元素赋值到大的元素所在的位置
		n[j]=n[j+1];

                // 把临时变量中的大元素值赋值到小元素所在的位置
		n[j+1]=temp;

                // 发生了交换 纪录状态
                flag=true;

	    }

	}
        
        // 内层循环完毕后 查看flag状态是否发生了交换
        if(!flag){

            // 如果flag没有变化,则表示这一轮没有发生交换 当前就是有序的排列 终止程序
            return;

        }

        //如果执行到这里,证明发生了交换 继续下一轮的比较

    }
	
}

复杂度分析:

时间复杂度:冒泡排序的程序总运行次数为n(n+1)/2,即n²/2+n/2;只保留最高次幂,去除系数,得到时间复杂度为n²;即T(n)=O(n²);这是最差情况下的,同时也是平均情况下的时间复杂度;而最佳的情况下则是已经有序的一组数据,使用优化后的冒泡排序,只需要进行一轮比较就可以了,总次数为n,时间复杂度T(n)=O(n)。

空间复杂度:除去程序本身的大小,额外定义了几个辅助变量,与n的大小无关,所以空间复杂度S(n)=O(1)。

稳定性分析:

冒泡排序两个相同的元素经过两两互换称为相邻的元素时,只有判断为前方元素大于后方元素才进行互换,所以相等的时候不会互换,相对位置关系在排序后不会变好。根据稳定性原则,冒泡排序时稳定的。

直接选择排序(Selection Sort)

选择排序(Selection-sort) 是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,然后和首位元素进行位置互换,然后再从剩余未排序元素中继续寻找最小(大)元素,然后和未排序元素的首位进行位置互换。以此类推,直到所有元素均排序完毕。

动态图如下:

代码如下:

   /**
     * 直接选择排序
     * @param array
     * @return
     */
    public static int[] selectionSort(int[] array) {
        // 外层循环表示选择次数 每次目标是选择出一个最小的元素
        for (int i = 0; i < array.length; i++) {

            // 局部变量 纪录最小元素的索引 默认为未排序元素的首位元素索引
            int minIndex = i;

            // 内层循环排序 遍历未排序元素
            for (int j = i; j < array.length; j++) {

                // 如果未排序元素小于最小元素 
                if (array[j] < array[minIndex]){
                
                    // 以该元素的的索引覆盖默认索引
                    minIndex = j; 

                }
                             
            }

            // 进行位置互换
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }

        return array;
    }

 最初始的选择排序其实还是有优化的余地的,比如:最后进行位置互换的时候,如果未排序的首位元素本身就是最小的,一轮比较下来没有比它更小的,那最后的互换操作就有点多余了。另外如果每次只找出最小的元素排在前面,那么完全可以做到同时找到最大的元素放在后面,最终形成前端有序,后端有序,中间部分无序的情况,每次排序都排出一个最大和最小,加快一半的速度完成

优化后的代码(程序为了方便理解,多写了几行if else语句,可以合并):

   /**
     * 直接选择排序优化版
     * @param array
     * @return
     */
    public static int[] selectionSort(int[] array) {
        // 数组长度
        int len = array.size();
        
        // 外层循环表示选择次数 进行选择的数组范围是left~right 没次选出一个最大值和最小值
        for (int left = 0, right = len - 1; left < right; left++, right--) {

            // 纪录最小和最大元素的索引 默认为待排序元素的首位和末位元素的索引
            int min = left;     
            int max = right; 

            // 内层循环排序 遍历待排序元素
            for (int i = left; i <= right; i++) {

                // 如果待排序中的某元素小于指定的最小元素 
                if (array[i] < array[min]){
                
                    // 则该元素的的索引就是最小值索引
                    min = i; 

                }

    
                // 如果待排序中的某元素大于指定的最大元素 
                if (array[i] > array[max]){
                
                    // 则该元素的的索引就是最大值索引
                    max = i; 

                }                           
            }

            // 如果待排序元素首位索引left和最小元素的索引min相等
            if(left == min){

                //表示待排序元素首位就是最小值,不需要进行交换

            }else{
                // 首位不是最小值,交换首位和最小值的位置
                swap(array, left, min);
            }

            // 如果待排序元素首位的索引left和最大元素的索引max相等
            if(left == max){

                // 如果首位是最大值,上面一步已经最大值和最小值进行了交换,首位索引left对应的是最小值,最大值对应的索引是min,所以此时最大值的的索引max应该改为min
                max = min;
            }
            
           
            // 如果待排序元素末位索引right和最大元素的索引max相等
            if(right == max){

                //表示待排序元素末位就是最大值,不需要进行交换

            }else{

                //末位不是最大值,交换末位和最大值的位置
                swap(array,right,max);
            }
            
        }

        return array;
    }

    // 位置互换
    public static void swap(int[] array,int a,int b) {

        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;

    }

复杂度分析:

时间复杂度:直接选择排序的程序总运行次数为n(n+1)/2,即n²/2+n/2;只保留最高次幂,去除系数,得到时间复杂度为n²;即T(n)=O(n²);

空间复杂度:除去程序本身的大小,额外定义了几个辅助变量,与n的大小无关,所以空间复杂度S(n)=O(1)。

稳定性分析:

直接选择排序是不稳定的。举个例子{2²,2³,1}(数字2右上角只是做标记区分排序前后的两个相同元素2的位置关系,不要看成平方和立方),第一次排序把1排到前面,其他位置不变,一次排列就有序了,实际上原来处于第二和第三位置的两个元素2,均向后移动了一位,变成了{1,2²,2³},根据稳定性规则,此排序方法不稳定。

直接插入排序(Insertion Sort)

直接插入排序(Insertion Sort),就是把一个个元素插入到有序元素中对应的位置上。举个例子:一套书,共有十部,被随意的排在一起,使用插入排序,把后面的书序号拿出来,往前放找对位置插进去,最终把书本排列成有序从第一部到第十部的样子。

 动态图:

 

静态图(temp临时储存的元素值   ):

 

代码:

   /**
     * 直接插入排序
     * @param array
     * @return
     */
    public static int[] insertionSort(int[] array) {

        // 外层循环为插入的次数 从第二位元素开始算次数 
        for (int i = 0; i < array.length - 1; i++) {

            // 把准备插入的元素记为临时变量 array[0]不参与排序,从第二位array[1]开始算 i+1就是要插入的元素的索引
            int temp = array[i + 1];

            // 首次比较的是要插入元素和该元素的前一位,i就是前一位元素的的索引
            int preIndex = i;

            // 比较要插入的元素和前一位元素,如果准备插入的元素小于前一位的元素
            while (preIndex >= 0 && temp < array[preIndex]) {

                // 前一位元素向后移动一位 索引变成要插入元素原来的位置
                array[preIndex + 1] = array[preIndex];

                // 索引自减1,继续循环向更前一位比较
                preIndex--;

            }

            // 一直比较到要插入元素大于某一个元素或者索引preIndex为负数,没有元素可以比较,把要插入元素放在要比较元素索引后面,要比较的元素索引为preIndex,所以插入元素最终的位置就是preIndex+1
            array[preIndex + 1] = temp;

        }

        return array;

    }

复杂度分析:

时间复杂度:直接插入排序的程序总运行次数最差为n(n+1)/2,即n²/2+n/2(数据本身从大到小的顺序,需要经历比较和位置变化);最佳为n次(数据本身已经是从小到大的顺序,只需要比较不需要更换元素位置),只保留最高次幂,去除系数,得到时间复杂度为即最坏和平均:T(n)=O(n²),最佳:T(n)=O(n);

空间复杂度:除去程序本身的大小,额外定义了几个辅助变量,与n的大小无关,所以空间复杂度S(n)=O(1)。

稳定性分析:

直接插入排序是稳定的。和冒泡排序类似,每次都是两两比较,并且只有小于前一位元素才会继续比较,大于等于前一位元素都会放在该元素后面,所以远离相同的两个元素在排序后相对位置关系不会变化,根据稳定性规则,此排序方法稳定。

 

好了,暂时告一段落了,排序算法的队伍比较大,本章就只介绍基本排序算法,后续文章将会介绍几个高效排序算法。

古德拜!😁😁😁😁😁😁

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值