经典排序算法原理与优劣对比

17 篇文章 1 订阅
10 篇文章 1 订阅
本文介绍了四种常见的排序算法:冒泡排序、插入排序、归并排序和快速排序。冒泡排序和插入排序时间复杂度为O(n*n),归并排序和快速排序平均时间复杂度为O(nlogn)。归并排序是稳定的,而快速排序不稳定。在实际应用中,需要根据数据规模和稳定性需求选择合适的排序算法。
摘要由CSDN通过智能技术生成

      二分查找要求原数组必须有序。其实,由有序到无序,这是算法领域最常见的一类问题。本课时主要学习4种常见的排序算法,包括冒泡排序、插入排序、归并排序以及快速排序。

衡量一个排序算法的优劣,我们主要从以下3个角度进行分析:

  1. 时间复杂度,具体包括,最好时间复杂度、最坏时间复杂度以及平均时间复杂度。
  2. 空间复杂度,如果空间复杂度为1,也叫作原地排序。
  3. 稳定性,排序的稳定性是指相等的数据对象,在排序之后,顺序是否能保证不变。

常见的排序算法及其思想

1.冒泡排序

原理:从第一个数据开始,依次比较相邻元素的大小。如果前者大于后者,则进行交换,把大的元素往后交换。通过多轮迭代,直到没有交换操作为止。冒泡排序就像在一个水池中处理数据一样,每次会把最大的那个数据传递到最后。

性能:冒泡排序平均时间复杂度为O(n*n),空间复杂度为O(1),排序过程中,相同的元素不做交换,所以冒泡排序是稳定的排序算法。

代码如下:

public static void main(String[] args){
    int[] arr = {1,0,3,4,5,-6,7,8,9,10};
    //外循环控制趟数,每一趟都会把最大值推到最后,10个数据的话9趟就能排序成功 
    //又因为内循环比较的时候是和后一位比较的,所以内循环下标需要小于arr.length-i
    for(int i=1; i < arr.length ; i++){
        for(int j=0 ; j < arr.length-i ; j++){
            if(arr[j] > arr[j+1]){
                int temp;
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

2.插入排序

原理:选择未排序的元素,插入到已排序区间的合适位置,直到未排序区间为空。插入排序顾名思义,就是从左到右维护一个已经排好序的序列,直到所有的待排数据全都完成插入的动作。

性能:插入排序的时间复杂度为O(n*n),空间复杂度为O(1),插入过程中,相同元素顺序不会发生改变,所以插入排序是稳定的排序算法。

代码如下:

public static void main(String[] args){
    int[] arr = {2,3,5,1,23,6,78,34};
    //外层循环对除了第一个元素以外的所有元素
    //内存循环对当前元素前面的有序表进行插入位置查找,并移动
    for(int i=1;i<arr.length;i++){
        int temp = arr[i];
        int j = i-1;
        for(;j>=0;j--){
            if(arr[j] > temp){
                //如果比待排的数字大,就将其后移
                arr[j+1] = arr[j];
            }else{
                //如果发现比待排数字小,则说明可插入位置就是j+1,内循环可结束
                break;
            }
        }
        //将待排数字赋值到可插入的位置,循环继续
        arr[j+1] = temp;
    }
}

小结:插入排序和冒泡排序算法的异同点

相同点:冒泡排序和插入排序的平均时间复杂度都是O(n*n),空间复杂度都是O(1),且都是稳定的排序算法,都属于原地排序。

不同点:冒泡排序每轮的交换操作时动态的,所以需要三个赋值操作才能完成;而插入排序每轮的交换动作会固定待插入的数据,因此只需要一步赋值操作。

以上两种排序算法比较简单,可以帮助我们对排序的思想建立基本了解,接下里再学习两种时间复杂度更低的排序算法,它们的时间复杂度都可以达到O(nlogn)。

3.归并排序

归并排序的原理:实际上就是上一节课我们讲的分治法。它首先将数组不断地二分,直到最后每个部分只包含1个数据。然后再对每个部分分别进行排序,最后将排好序的相邻的两部分合并在一起,这样整个数组就有序了。

public static void main(String[] args) {
        int[] nums = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 10 };
        int[] newNums = mergeSort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(newNums));
}

public static int[] mergeSort(int[] nums, int left, int right){
    //递归终止条件
    if(left == right){
        return new int[]{nums[left]};
    }

    int middle = left + (right-left)/2;
    //利用递归不断的将数组两分,直到达到递归终止条件,也即数组只有1个元素
    int[] leftArr = mergeSort(nums, left, middle);
    int[] rightArr = mergeSort(nums, middle+1, right);
    
    //然后开始将二分后的两部分进行排序、合并
    int[] newNums = new int[leftArr.length+rightArr.length];
    //m表示newNums的下标,i和j分别指向二分后数组的开始位置,从头比较两个有序数组
      如果左侧的数小,则将数添加到新数组,左侧下标加1,继续左右比较,直到左右两数 
      组,有一个到数组尾部。最后将剩余一侧的数组全部添加到新数组中即可。
    int m=0,i=0,j=0;
    while( i < leftArr.length && j < rightArr.length){
        newNums[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : 
           rightArr[j++];
    }
    while(i < leftArr.length){
        newNums[m++] = leftArr[i++];
    }
    while(j < right.length){
        newNums[m++] = rightArr[j++];
    }
}

归并操作的工作原理如下:

第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置;

第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾。

性能:归并排序采用了二分的迭代方式,复杂度是log n。每次的迭代,需要对两个有序数组进行合并,这样的动作在O(n)的时间复杂度下就可以完成。因此归并排序的复杂度就是二者的乘积O(n log n)。并且它的执行频次与输入序列无关,因此,归并排序最好、最坏、平均复杂度都是O(nlogn)。空间复杂度方面,由于每次合并的操作都需要开辟基于数组的临时内存空间,所以空间复杂度为O(n)。归并排序的时候,相同元素的前后顺序不变,所以归并排序是稳定的排序算法。

归并排序的速度仅次于快速排序,一般应用于总体无序,但各子项相对有序的序列。归并排序的比较次数小于快速排序,移动次数一般多于快速排序的移动次数。 

4.快速排序

原理:快速排序的原理也是分治法,它是对冒泡排序的一种改进。通过一趟排序将要排序的数列分为两部分,其中一部分的所有数据比另外一部分的所有都要小,然后再分别对这两部分进行快速排序,整个排序过程可以递归进行,以达到整个数列变成有序数列。

性能:在快排的最好时间的复杂度下,如果每次选择分区点时,都能选中中位数,把数组等分成两个,那么此时的时间复杂度和归并一样,都是O(nlogn)。而最坏的时间复杂度下,也就是每次分区都选中了最小值或最大值,得到不均等的两组。那么就要n次的分区操作,每次分区平均扫描n/2个元素,此时时间复杂度就是O(n*n)。因此快排的平均复杂度是O(nlogn)。快排使用了交换法,因此空间复杂度为O(1),分区过程涉及交换操作,所以快排是不稳定的排序算法。

一趟快速排序的算法是: 

1)设置两个变量i、j,排序开始的时候:i=0,j=N-1; 

2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];

3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换; 

4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换; 

5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

排序演示

假设一开始序列{xi}是:5,3,7,6,4,1,0,2,9,10,8。

此时,ref=5,i=1,j=11,从后往前找,第一个比5小的数是x8=2,因此序列为:2,3,7,6,4,1,0,5,9,10,8。

此时i=1,j=8,从前往后找,第一个比5大的数是x3=7,因此序列为:2,3,5,6,4,1,0,7,9,10,8。

此时,i=3,j=8,从第8位往前找,第一个比5小的数是x7=0,因此:2,3,0,6,4,1,5,7,9,10,8。

此时,i=3,j=7,从第3位往后找,第一个比5大的数是x4=6,因此:2,3,0,5,4,1,6,7,9,10,8。

此时,i=4,j=7,从第7位往前找,第一个比5小的数是x6=1,因此:2,3,0,1,4,5,6,7,9,10,8。

此时,i=4,j=6,从第4位往后找,直到第6位才有比5大的数,这时,i=j=6,ref成为一条分界线,它之前的数都比它小,之后的数都比它大,对于前后两部分数,可以采用同样的方法来排序。

public static int[] qsort(int arr[],int start,int end) {        
    int pivot = arr[start];        
    int i = start;        
    int j = end;        
    while (i<j) {            
        while ((i<j)&&(arr[j]>pivot)) {                
            j--;            
        }            
        while ((i<j)&&(arr[i]<pivot)) {                
            i++;            
        }            
        if ((arr[i]==arr[j])&&(i<j)) {                
            i++;            
        } else {                
            int temp = arr[i];                
            arr[i] = arr[j];                
            arr[j] = temp;            
        }        
    }        
    if (i-1>start) arr=qsort(arr,start,i-1);        
    if (j+1<end) arr=qsort(arr,j+1,end);        
    return (arr);    
}    
 
public static void main(String[] args) {        
    int arr[] = new int[]{3,3,3,7,9,122344,4656,34,34,4656,5,6,7,8,9,343,57765,23,12321};        
    int len = arr.length-1;        
    arr=qsort(arr,0,len);        
    for (int i:arr) {            
        System.out.print(i+"\t");        
    }    
}

 

总结:

如果对数据规模较小的数据进行排序,可以选择时间复杂度为O(n*n)的排序算法,因为当数据规模小的时候O(n*n)和O(nlogn)仅仅相差几十毫秒,对实际影响不大。但对数据规模较大的数据进行排序,就需要选择时间复杂度为O(nlogn)的排序算法了。归并排序空间复杂度为O(n),也就意味着当排序100M的数据,就需要200M的空间,所以对空间资源消耗会很多,快排的平均时间复杂度为O(nlogn),但如果分区点选择不好的话,可能逼近O(n*n),空间复杂度为O(1),而且快排不具备稳定性,这也要看你所面对的问题是否有稳定性的需求。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值