数组(二)排序(冒泡插入选择快速)

排序介绍

排序对任何程序员来说都不模式,在很多编程语音中提供了很多编程函数,如java中Arrays.sort()等。排序算法有很多,本章节着重讲解集中常见的排序算法,如冒泡排序、插入排序、选择排序、快速排序。其他还有很多排序算法,有兴趣也可以自行了解。

分析排序算法

排序算法有很多,那么哪种算法比较好,什么情况下该用哪种算法,该怎么界定呢?我们可以从算法的执行效率、内存消耗两个维度来分析。由于现在计算机内存都很大,不会很在意算法执行的额外使用的内存空间,更多在意算法的时间复杂度。

执行效率

执行效率一般从时间复杂度和算法比较交换移动的次数来观察。
一般分析算法的复杂度,需要计算算法平均的时间复杂度,即最理想到最不理想情况下时间复杂度的平均。
上诉常见的集中算法,一般都是通过比较后交换或移动元素起到排序的作用,所以交换移动的次数也是洞察算法执行效率必不可少的。

内存消耗

算法的内存消耗可以通过空间复杂度来衡量,即算法排序中所需要额外使用的内存空间。

常见算法排序

冒泡排序

原理介绍

说到数组的排序,很多程序员想起的第一种一定是冒泡排序。做为最基础的排序算法,特点是简单易懂。
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
假设需排序的数组为{4,5,3,2,1},第一次比较相邻的两个元素如下图:
在这里插入图片描述
从上图看过,一次冒泡操作后,5这个元素已经存储在正确的位置上。要想完成所有数据的排序,我们只要进行5次这样的冒泡操作就行了。在这里插入图片描述

冒泡代码

附上冒泡排序的代码:

 public static void bubblingSort(int[] array) {
        for(int i = 0;i<array.length;i++){
            for(int j = 0;j<array.length-1-i;j++){
                if(array[j] > array[j+1]){
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
        }
    }

那么上面代码能否有优化空间呢?答案是有的,但内部的循环没有元素移动时,表示当前数组已完成排序,则不需要继续后续操作了。
改进后的代码:

 public static void bubblingSort(int[] array) {
        for(int i = 0;i<array.length;i++){
        	//可提前推出标志
        	int exitFlag = false;
            for(int j = 0;j<array.length-1-i;j++){
                if(array[j] > array[j+1]){
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                   exitFlag = true; 
                }
            }
            if(!exitFlag){
            	break;
            }
        }
    }

复杂度

冒泡排序的时间复杂度比较简单,n*n,为O(n2)。
空间复杂度因为不使用额外的空间,为O(1)。

插入排序

原理介绍

顾名思义,插入排序的核心是插入,即每个位置的元素插入到合适的位置中。
我们将数组中的数据分为两个区间,已排序区间未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
如下图,左侧是已排序区间,右侧为未排序区间。
在这里插入图片描述
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据插入到已排序区间时,需要拿此数据与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素插入。

插入代码

 public static void insetSort(int[] array) {
        if(null == array || array.length < 2){
            return;
        }
        for(int i = 1 ; i < array.length ; i++){
            int j = i - 1;
            int val = array[i];
            //寻找插入位置&移动元素
            for(;j >=0 ;j--){
                if(array[j] > val){
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j+1] = val;
        }

复杂度

插入排序的时间复杂度,为O(n2)。
空间复杂度因为不使用额外的空间,为O(1)。

选择排序

原理介绍

选择排序和插入排序原理类似,都分为已排序区间未排序区间。每次从未排序区间内找最小值,与已排序区间的后一位交换位置,循环至数组最后。
在这里插入图片描述

选择排序代码

public static void selectionSort(int[] array) {
        for(int i = 0;i < array.length-1;i++){
            //找出未排序中的最小值
            int minIndex = i;
            int tempValue = array[i];
            for(int j = i+1; j < array.length;j++){
                if(array[j] < tempValue){
                    tempValue = array[j];
                    minIndex = j;
                }
            }
            //当前值与最小值替换
            if(i != minIndex){
                int temp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = temp;
            }
        }
    }

复杂度

选择排序的时间复杂度,为O(n2)。
空间复杂度因为不使用额外的空间,为O(1)。

快速排序

原理介绍

快速排序也叫快排,常年混迹于面试算法环节中,非常考验面试者的基础,是我们必须掌握的一张算法。其核心思想是分而治之

如果要排序数组中下标从left到right之间的一组数据,我们选择left到right之间的任意一个数据作为pivot(分区点),一般是left或者right位。
遍历left到right之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组left到right之间的数据就被分成了三个部分,前面left到pivot-1之间都是小于pivot的,中间是pivot,后面的pivot+1到right之间是大于pivot的。
根据分治、递归的处理思想,我们可以用递归排序下标从left到pivot-1之间的数据和下标从pivot+1到right之间的数据,直到区间缩小为1,就说明所有的数据都有序了。
在这里插入图片描述
上诉思想可以用代码下面表示:

 private void quickSort(int[] arr, int left, int right) {
    if(left < right){
    	//寻找分区点,递归调用
        int parIndex = partition(arr,left,right);
        quickSort(arr,left,parIndex-1);
        quickSort(arr,parIndex+1,right);
    }
 }

通过上面的代码,可以看出还缺少个重要的partition方法。partition()就是随机选择一个元素作为pivot,然后对A[left…right]分区,函数返回pivot的下标。
如果不考虑额外使用空间,可以选择left位置做为pivot,申明两个临时数组A、B,循环数组,将小于pivot的元素放至A,大于pivot的元素放至B,最后将A、B数组拷贝至原数组。

那么有没有不需要使用额外内存空间的方式呢?答案是有的。
我们可以通过对比、交换的方式,来达到这种效果。假设pivot为left的值,这边定义两个游标,i表示循环下表,swap表示交换位,初始时swap = i = left+1。通过i把A[left+1…r]分成两部分。A[left+1…i-1]的元素都是小于pivot的,叫它“已处理区间”,A[i…right]是“未处理区间”。每次都从未处理的区间A[i…right]中取一个元素A[j],与pivot对比,如果小于pivot,则与swap位置的元素交换位置,随后swap+1。最后只需要将A[swap-1]与A[left]交换,则达到partition()想要的效果。
在这里插入图片描述

快排代码

 private static void quickSort(int[] arr, int left, int right) {
    if(left < right){
        int parIndex = partitionRight(arr,left,right);
        quickSort(arr,left,parIndex-1);
        quickSort(arr,parIndex+1,right);
    }
 }
 //以lfet位置为基准分区
 private int partitionLeft(int[] array,int left,int right){
        int referVal = array[left];
        int swapIndex = left+1;
        for(int i = left+1;i <= right;i++){
            if(array[i] < referVal){
                swap(array,i,swapIndex);
                swapIndex++;
            }
        }
        swap(array,left,swapIndex-1);
        return swapIndex-1;
    }
 //交换位置
 private void swap(int[] array,int i,int j){
     int temp = array[i];
     array[i] = array[j];
     array[j] = temp;
 }

复杂度

假设每次分区操作,正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并是相同的。则快排的时间复杂度也是O(nlogn)。但也存在比较极端的例子。如果数组中的数据原来已经是有序的了,比如2,4,6,8。如果我们每次选择最后一个元素作为pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约n次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约n/2个元素,这种情况下,快排的时间复杂度就从O(nlogn)退化成了O(n2)。
针对上诉极端情况,选择pivot可以选择left right和两者中间位3个数进行对比,选择大小中间的元素做为pivot,可避免这种极端情况。

最终平均下来,快排的时间复杂度还是O(nlogn)。
空间复杂度因为不使用额外的空间,为O(1)。

总结

冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是O(n2),比较高,适合小规模数据的排序。

快速排序是稍微复杂的排序算法,用的是分治的思想,代码都通过递归来实现。快速排序算法虽然最坏情况下的时间复杂度是O(n2),但是平均情况下时间复杂度都是O(nlogn)。不仅如此,快速排序算法时间复杂度退化到O(n2)的概率非常小,我们可以通过合理地选择pivot来避免这种情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值