9.排序(上)

 

最经典、常用的排序:
提问:插入排序和冒泡排序的时间复杂度相同,都是O(n2),在实际开发中,为什么更倾向于使用插入排序?
 
如何分析一个“排序算法”?
    排序算法的执行效率
        1.最好、最坏、平均情况时间复杂度
          这样区分的原因:
                    1)有些排序算法会区分,为了好对比。
                    2)不同有序度的数据对排序执行有影响,我们需要知道排序算法在不同数据下的性能表现。
        2.时间复杂度的系数、常数、低阶
            时间复杂度是反映数据规模n很大时的增长趋势,所以我们会忽略系数、常数、低阶等,但在实际开发中,排序常常可能是10,100,1000这样的规模很小的数据,因此我们也需要把系数、常数、低阶考虑进来。
        3.比较次数和交换(或移动)次数
            基于比较的排序算法的执行过程,会涉及元素比较大小和元素移动或交换,我们所以也需要考虑。
    
    排序算法的内存消耗
        算法内存消耗可以用空间复杂度衡量,此外,针对排序算法的空间复杂度,用原地排序(Sorted in place)特指空间复杂度是O(1)的排序算法。
        
    排序算法的稳定性
        稳定性:指待排序的序列中有值相等的元素,在排序后,那些值相等的元素之间的先后顺序不变。
        例如,2,5,2,7,排序后就是,2,2,5,7,如果,两个2排序后先后顺序不变就是稳定的排序算法。
        稳定性看起来没用,其实在应用场景中,很多都是根据key进行排序,但相同的key对应的value是不同的。
        例如希望给一系列订单先按订单金额排序,金额相同的再按订单时间排序。最容易想到的思路是先将订单按金额排序,再遍历订单,将金额相同的再按时间排序。思路简单,但实现困难。
        而借助稳定性算法,先按订单时间排序,再用稳定排序算法按金额排序就好了。
        
        
        
冒泡排序(Bubble Sort)
 
经过一次冒泡操作后,6这个元素已经在正确的位置上了,要完成所有的数据排序,只要进行6次冒泡操作。
 
    冒泡优化:
        当某次冒泡操作没有数据交换时,说明已经达到完全有序,不再需要执行后面的操作,如
        
    冒泡代码:
      
 1   // 冒泡排序,a 表示数组,n 表示数组大小
 2         public void bubbleSort(int[] a, int n) {
 3           if (n <= 1) return;
 4          
 5          for (int i = 0; i < n; ++i) {
 6             // 提前退出冒泡循环的标志位
 7             boolean flag = false;
 8             for (int j = 0; j < n - i - 1; ++j) {
 9               if (a[j] > a[j+1]) { // 交换
10                 int tmp = a[j];
11                 a[j] = a[j+1];
12                 a[j+1] = tmp;
13                 flag = true;  // 表示有数据交换      
14               }
15             }
16             if (!flag) break;  // 没有数据交换,提前退出
17           }
18         }    
        1)冒泡排序是原地排序算法吗?
            是,冒泡过程只涉及了相邻数据的操作,只需要常量级的临时空间,空间复杂度为O(1)。
        2)冒泡排序是稳定的排序算法吗?
            是,冒泡中,两个相等值不交换位置,所以是稳定的。
        3)冒泡排序时间复杂度是多少?
            最好情况下,要排序的数据已经有序,只要进行一次冒泡操作,时间复杂度为O(n)。最坏情况是刚好,倒序排列,需要进行n次冒泡,时间复杂度是O(n2)。
            
            那么平均情况下时间复杂度是多少呢?
                n个数据的数组,有n!种排列方式,如果用概率论定量分析平均时间复杂度,会很复杂。通过“有序度”和“逆序度”来分析。
                有序度就是数组中有序的元素对的个数:
                 有序元素对:a[i] <= a[j], 如果 i < j
                完全倒序排列的数组,如6,5,4,3,2,1,有序度是0,完全有序数组,如1,2,3,4,5,6,有序度是n*(n-1)/2,即15,这种完全有序的数组的有序度叫做满有序度
                逆序度,可类比
                    逆序元素对:a[i] > a[j], 如果 i < j。
                    
                公式:逆序度 = 满有序度 - 有序度
                排序过程就是增加有序度,减少逆序度的过程,直至满有序度。
                
                举例:
                数组4,5,6,3,2,1,其中,有序元素对有(4,5),(4,6)(5,6),有序度是3,n = 6,满有序度是15。
                冒泡操作包含两个子操作比较和交换,每交换一次有序度加1,不管算法怎么改进,交换次数总是确定的,即为逆序度,为n*(n-1)/2 - 初始有序度,此例中为15-3=12。
                因此最坏的交换次数为n*(n-1)/2 ,最好为0,取中间值表示情况n*(n-1)/4。
                平均情况下要交换n*(n-1)/4次,比较次数肯定比交换多,因此,平均情况下时间复杂度为O(n2)。这个推导不严谨,但很多时候很实用。
                
 
插入排序(Insertion Sort)
 
    有序数组中插入数据,保持有序:
                
    插入排序借助上面的思想实现:
        先将数据分为两个区间,已排序区间和未排序区间。插入排序算法的核心就是取未排序区间的元素插入到有序区间中,直到未排序区间为空。
        如下图,左侧是已排序区间,右侧是未排序区间
    
        
        
        插入排序也包含两种操作,一种是比较,一种是移动。而移动数等于逆序度,如下图
    4,5,6,1,3,2的有序度为5,满有序度为n * (n - 1) / 2 = 15,逆有序度为10。
            代码实现:
             
 1    // 插入排序,a 表示数组,n 表示数组大小
 2                 public void insertionSort(int[] a, int n) {
 3                   if (n <= 1) return;
 4                 
 5                   for (int i = 1; i < n; ++i) {
 6                     int value = a[i];
 7                     int j = i - 1;
 8                     // 查找插入的位置
 9                     for (; j >= 0; --j) {
10                       if (a[j] > value) {
11                         a[j+1] = a[j];  // 数据移动
12                       } else {
13                         break;
14                       }
15                     }
16                     a[j+1] = value; // 插入数据
17                   }
18                 }         
        1)插入算法是原地排序算法吗?
            是,插入排序算法运行并不需要额外的存储空间,空间复杂度为O(1)。
        2)插入排序算法是稳定的排序算法吗?
            是,插入排序中,值相同的元素,可以将后面出现的元素插入到前面出现的元素后面。
        3)插入排序算法那的时间复杂度是多少?
            O(n2),最好是完全有序时,只需要遍历一次,每次比较一次就能确定插入位置,时间复杂度为O(n)。注意,这里是从尾到头遍历有序的数据。
            如果数组逆序的,每次插入都相当于在数组第一个位置插入新的数据,所以需要移动大量数据,最坏情况时间复杂度为O(n2)。
            数组中插入一个数据的平均时间复杂度是O(n),对于插入排序,每次插入操作都相当于在数组中插入一个数据,循环执行n次插入操作,所以平均时间复杂度为O(n2)。
            
            
            
选择排序(Selection Sort)
    也分排序区间和位排序区间,但是每次选择排序会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
                
        1)选择算法是原地排序算法吗?
            是,选择排序算法运行并不需要额外的存储空间,空间复杂度为O(1)。
        2)选择排序算法是稳定的排序算法吗?
            不是,选择排序每次都要找剩余未排序元素中最小值,并和前面元素交换位置,破坏了稳定性。
        3)选择排序算法那的时间复杂度是多少?
        最好、最坏、平均时间复杂度都是O(n2)。
        
开篇解答
    冒泡和插入排序不管如何优化,元素交换次数都等于原始数据的逆序度,时间复杂度为O(n2)。
    但冒泡排序的数据交换比插入排序的数据交换要更复杂,冒泡需要3个复制操作,而插入只需要1个。
    冒泡排序中数据的交换操作:
   
 if (a[j] > a[j+1]) { // 交换
       int tmp = a[j];
       a[j] = a[j+1];
       a[j+1] = tmp;
       flag = true;
    }
    
    插入排序中数据的移动操作:
    if (a[j] > value) {
      a[j+1] = a[j];  // 数据移动
    } else {
      break;
    }

  

小结
 
    
 
posted @ 2019-04-15 09:50  夏天换上冬装  阅读(232)  评论(0编辑  收藏  举报