排序算法的基础&进阶

369 阅读5分钟
类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定
插入排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
希尔排序O(nlog²n)O(nlog²n)O(nlog²n)O(1)不稳定

关键词含义

n:数据规模

时间复杂度

算法运行过程中所耗费的时间。

空间复杂度

算法运行过程中临时占用存储空间的大小。例如:O(1)表示所需空间大小为常量,与数据量n无关。

稳定性含义

  • 稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序不变。如排序前,a=b,a在b的前面;那么排序后,a依旧在b的前面。
  • 不稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序改变。如排序前,a=b,a在b的前面;那么排序后,a在b的后面。

冒泡排序

原理步骤

  1. 比较相邻的两个数,如果前面的数大于后面的数,就交换这两个数。
  2. 相邻的最前一对数和最后一对数都要进行比较,这样最后一个数就是最大的数。
  3. 每个元素重复以上步骤,除了最后一个数。
  4. 重复1-3的步骤。

代码实现

private static int[] bubbleSort(int array[]) {
    if (array.length == 0) {      
       return array;    
    }
    // 第1个for循环相当于步骤4
    // 数组中的每个数都要进行比较
    for (int i = 0; i < array.length; i++) {
        // 第2个for循环相当于步骤3        
        // array.length -1 是因为后面有j+1,先-1是为了避免数组越界        
        // array.length -1 - i,之所以减i(已经排过1遍,就减1;如果已经排过i遍,就减i),是为了不比较排在最后且已经排好序的数,相当于步骤3的最后一句话        
        for (int j = 0; j < array.length - 1 - i; j++) {
            int temp;            
            // if判断语句相当于步骤1和步骤2            
            if (array[j] > array[j + 1]) {                
                temp = array[j];                
                array[j] = array[j + 1];                
                array[j + 1] = temp;            
            }        
        }    
     }
     return array;
    }

快速排序

原理步骤

  1. 取数组中的一个数作为key。
  2. 从后往前获取数组的数,并将其与key进行对比。
  3. 如果其中一个数小于key,那么就将这个数和key交换位置。
  4. 交换位置之后,从前往后获取数组的数,并将其与key对比。
  5. 如果其中一个数大于key,那么就将这个数和key交换位置。
  6. 重复2-5的过程,直到key前面的数都比key小,key后面的数都比key大,这样就完成一次排序。
  7. 以key为中心,对key前面的数组和后面的数组执行1-6的过程,直到数组完全有序。

代码实现

private static void quickSort(int[] array, int left, int right) {
    if (left >= right) {      
        return;    
    }
    int i, j, x;    
    i = left;    
    j = right; 
    
    // 将key保存起来
    x = array[i];
    while (i < j) {        
        while (i < j && array[j] > x) {            
            j--;        
        }        
        if (i < j) {  
            // 从右往左获取更小的值
            array[i] = array[j];            
            i++;        
        }        
        while (i < j && array[i] < x) {            
            i++;        
        }        
        if (i < j) { 
            // 从左往右比较,如果有更大的数就交换。
            array[j] = array[i];            
            j--;        
        }    
    }    
    // j=i,即把key前面的数和后面的数都比较完了。  
    array[j] = x;    
    quickSort(array, j + 1, right);    
    quickSort(array, left, j - 1);
    }

插入排序

原理

  • 每一步将一个待排序的数插入到已经排好序的序列中,直到插完所有数据。

代码实现

private static int[] insertSort(int array[]) {          
        if (array.length == 0) {                
            return array;          
        }                
        int i, j, temp;          
        // 注释①          
        for (i = 1; i < array.length; i++) {              
            // 注释②                  
            temp = array[i];                  
            // 注释③                  
            for (j = i - 1; j >= 0 && array[j] > temp; j--) {                    
                // 注释④                        
                array[j+1] = array[j];                  
            }                  
            // 注释⑤                  
            array[j+1] = temp;           
        }                  
        return array;
}

注释①

  • 默认数组第一个数(i=0的数)是有序的。

注释②

  • array[i]为待排序的数据。

注释③

  • array[i]前面的数与array[i]进行排序。

注释④

  • arr[j]相当于前数,arr[j+1]相当于后数。
  • 如果前数比后数大,交换位置,前数放到后数的位置。

注释⑤

  • 如果for循环内前数和后数交换了位置(即前数挪到了后数的位置),那么注释⑤处的代码,就是将后数挪到前数的位置,实现交换。
  • 如果缺少注释⑤处的代码,那么前数的位置就会“空缺”,或者说依旧是原来的数,并没有实现交换。
  • 如果for循环内两数并没有交换(即跳出了for循环),此时j=i-1,j+1=i,与tmp=a[i]效果是一样的。

选择排序

原理步骤:

  1. 从未排序的序列中取出最小(最大)的数,放入已排序序列的初始位置;
  2. 继续从未排序序列剩余的数中取最小(最大)的数,放在已排序序列的末尾。
  3. 持续执行②的步骤,直到整个序列有序。

代码实现

private static int[] selectionSort(int[] array) {    
    if (array.length == 0) {        
        return array;    
    }    
    for (int i = 0; i < array.length; i++) {        
        int min = i;        
        for (int j = i; j < array.length; j++) {            
            // 从未排序序列中获取最小值            
            if (array[j] < array[min]) {                
                min = j;            
            }        
        }        
        // 把获取的最小值放入已排序序列的末尾(此时i代表结果集末尾的索引)        
        int temp = array[i];        
        array[i] = array[min];        
        array[min] = temp;    
    }    
    return array;
}

总结1:

  • 插入排序和选择排序可以划为一类排序算法来理解和掌握。
  • 它们都具有相同点——将数组划分为已排序、未排序两个部分,然后将未排序的部分逐个迁移到已排序的部分,最终使整个数组实现完全有序。
  • 而不同点在于从未排序合入到已排序的方式。插入排序会将未排序的数据在已排序的数组中执行直接插入排序;而选择排序会先在未排序的数组中选出最小值,当这个值合入到已排序的数组中时,不需要再进行比较,直接放到已排序数组的末尾就可以了。

希尔排序

原理步骤

  • 把一个数组按增量进行分组。(增量指分组数量)
  • 每个分组采用直接插入排序进行排序。
  • 然后减小增量,每个分组的元素数目增加,直到增量为1,整个文件变为一组,算法结束。

代码实现

private static int[] shellSort(int[] array) {    
    if (array.length == 0) {        
        return array;    
    }
    // gap为分组数目    
    for (int gap = array.length / 2; gap > 0; gap = gap / 2) {    
        // i为索引,对每组进行排序        
        for (int i = gap; i < array.length; i++) {        
        // j为临时变量            
        int j = i;            
        // 分组内元素的个数可能大于2个,因此使用while循环            
        while (j - gap >= 0 && array[j] < array[j - gap]) {            
            // 在同一个分组中,如果后面的数(j)比前面的(j-gap)大,就交换它们的位置                
            int temp = array[j];                
            array[j] = array[j - gap];                
            array[j - gap] = temp;
            j = j - gap;            
        }        
        }    
     }    
     return array;
}

归并排序

原理步骤

  • 将一个数组分为左子数组和右子数组,两个子数组的长度为n/2(n为数组的总长度)。
  • 在两个子数组间进行归并排序(即每个子数组划分为更小的左子数组和右子数组,直到无法再分时,对两个数组进行排序,详情见代码)。
  • 将两个有序的子数组合并为一个最终的有序数组。

代码实现

private static int[] mergeSort(int[] array) {    
    // 数组只有一个元素或没有元素,直接返回。    
    // 脱离递归的条件    
    if (array.length < 2) {        
        return array;    
    }    
    // 将数组分为两半,分别进行排序    
    int[] left = Arrays.copyOfRange(array, 0, array.length / 2);    
    int[] right = Arrays.copyOfRange(array, array.length / 2, array.length);    
    return merge(mergeSort(left),mergeSort(right));
}

    /** 
      * 将左数组与右数组合并为一个有序数组 
      * 注意:此时左数组、右数组已经有序 
      * @param left 
      * @param right 
      * @return 
      */
    private static int[] merge(int[] left, int[] right) {    
        // 合并后的有序数组    
        int[] result = new int[left.length + right.length];    
        for (int index = 0, i = 0, j = 0; index < result.length; index++) {        
        if (i >= left.length) {            
            // 如果左数组已经遍历结束,就插入右数组的值            
            result[index] = right[j];            
            j++;        
        } else if (j >= right.length) {            
            // 如果右数组已经遍历结束,就插入左数组的值            
            result[index] = left[i];            
            i++;        
        } else if (left[i] > right[j]) {            
            // 左数组与右数组的值同时存在时,就对两数进行比较            
            // 如果右数组的值比较小,就插入右数组的值。            
            result[index] = right[j];            
            j++;        
        } else {            
            // 左数组与右数组的值同时存在时,就对两数进行比较            
            // 如果左数组的值比较小,就插入左数组的值。            
            result[index] = left[i];            
            i++;        
        }    
    }    
    return result;
}

总结2:

  • 希尔排序和归并排序可以划为一类排序算法来理解和掌握。
  • 它们都具有相同点——先将整个大的数组分为不同的小组,然后对小组的数据进行排序,最终将所有小组合并为一个有序数组。
  • 而不同点在于分组后的排序方式不同。希尔排序会针对一个小组内的数据执行直接插入排序,而归并排序会直接将两个小组合并为一个有序数组。

排序算法进阶

  • 以上冒泡、快排、插入、选择、希尔、归并这六种排序算法都是基础的排序算法,很多中等、困难难度的算法题一般都是基于上述算法进行解决。(比如《合并两个有序数组》其实就是归并算法的某一部分)
  • 推荐《最小K个数》、《数组中的第K个最大元素》作为进阶学习。(它们都是基于快排实现,类似的变形有最大K个数等)

欢迎关注公众号度熊君,一起分享交流!