排序算法再梳理

277 阅读7分钟

排序算法再梳理

1、算法概述

1.1 算法分类

十种常见的算法一般可以分为两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

1.2 算法复杂度分析

算法复杂度在这里不做具体分析,待以后再仔细说明

1.3 排序算法中的相关概念

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律的增长。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

2、算法分析及实现

2.1 Bubble Sort(冒泡排序)

  • 基本思想

    冒泡排序是直观上理解最简单的一种排序算法,该算法重复访问要排序的数列,一次比较两个元素,顺序不符就进行交换,直到没有在交换的数据,排序完成。每趟都有一个元素在其最终位置上,这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,像水中的气泡从水底浮到水面。

  • 算法描述

    冒泡排序的算法过程如下:

    1. 比较相邻的两个元素,如果前者比后者大,则进行交换
    2. 每趟对相邻元素最同样的操作,每一趟比较结束都会有一个元素在其最终位置上,参与下次比较的元素 个数减一
    3. 重复以上步骤直到没有元素发生位置交换,排序完成
  • 代码实现

 package com.cchux.Sort;
 
 public class BubbleSort {
     public static void sort(int[] array) {
         if(array == null || array.length==0){
             return;
         }
         int length = array.length;
         //双重for循环,外层进行length-1循环
         for (int i = 0; i < length - 1; i++) {
             //内层循环需要对两两元素进行比较
             for (int j = 0; j < length - 1 - i; j++) {
                 if(array[j] > array[j+1]){
                     //交换操作使用位运算
                     array[j] = array[j]^array[j+1];
                     array[j+1] = array[j]^array[j+1];
                     array[j] = array[j]^array[j+1];
                 }
             }
         }
     }
 }
  • 复杂度分析

    冒泡排序是稳定的排序算法,最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1)。

平均时间复杂度 最好情况 最坏情况 空间复杂度
O(n²) O(n) O(n²) O(1)

2.2 QuickSort(快速排序)

  • 基本思想

    QuickSort(快速排序)是对冒泡排序的一种改进,借用了分治的思想,由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据与选取的基准值进行比较分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  • 算法描述

  • 快速排序使用分治策略将一个序列划分为两个子序列:

    1. 从数列中挑选一个元素,作为“基准”
    2. 重新对数列进行排序,比基准值小的元素放到基准左边,比基准值大的元素放到基准值右边(相同的数可以放到任一边)。在这个分区结束之后,该基准值就位于数列的中间位置。这个成为分区的操作(partition)
    3. 递归的把小于基准值元素的子序列和大雨基准值元素的子序列进行排序
    4. 重复以上步骤,递归到最底部,当数列的大小是零或者一时,排序完成。每次迭代过程都会确定一个元素的最终位置
  • 代码实现

    package com.cchux.Sort;
    
    public class QuickSort {
        
        public static void sort(int[] arr, int low, int high) {
            if(arr == null || arr.length==0){
                return;
            }
    
            if (low >= high) return;
    
            int left = low;
            int right = high;
            //一般选取第一个元素为基准值
            int temp = arr[left];
    
            while (left<right){
                //从后往前找小于基准值的
                while (left<right && arr[right]>=temp){
                    right--;
                }
                arr[left] = arr[right];
                //从前往后找大于基准值的
                while (left<right && arr[left]<=temp){
                    left++;
                }
                arr[right] = arr[left];
            }
            
            arr[left] = temp;
            //递归调用对子序列进行排序
            sort(arr,low,left-1);
            sort(arr,left+1, high);
        }
    }
    
  • 复杂度分析

    快速排序并不稳定,快速排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序。

    平均时间复杂度 最好情况 最坏情况(全部有序) 空间复杂度
    O(nlogn) O(nlogn) O(n²) O(1)

2.3 直接插入排序

  • 基本思想

    直接插入排序的基本思想是:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过为止。

  • 算法描述

    一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

    ①. 从第一个元素开始,该元素可以认为已经被排序

    ②. 取出下一个元素,在已经排序的元素序列中从后向前扫描

    ③. 如果该元素(已排序)大于新元素,将该元素移到下一位置

    ④. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

    ⑤. 将新元素插入到该位置后

    ⑥. 重复步骤②~⑤

  • 代码实现

    public static void insertSort(int[] arr){
            if(arr == null || arr.length == 0){
                return;
            }
    
            for (int i = 1; i < arr.length; i++) {
                int j = i-1;
                int temp = arr[i];
                while (j>=0 && arr[j]>temp){
                    arr[j+1] = arr[j];
                    j--;
                }
                arr[j+1] = temp;
            }
    }
    
  • 复杂度分析

    直接插入排序是不稳定的排序算法

    平均时间复杂度 最好情况 最坏情况 空间复杂度
    O(n²) O(n) O(n²) O(1)

2.3 Shell Sort (希尔排序)

  • 基本思想

    是第一个突破O(n2)的排序算法,是直接插入排序的改进版本,不同之处在于它会优先比较距离较远的元素,又称缩小增量排序。

  • 算法描述

    先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

    • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
    • 按增量序列个数k,对序列进行k 趟排序;
    • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • 代码实现

    public class ShellSort {
    
       public static void sort(int[] arr) {
           int gap = arr.length / 2;
           for (;gap > 0; gap = gap/2) {
               for (int j = 0; (j + gap) < arr.length; j++) { //缩小gap,直到1为止
                   for (int k = 0; (k + gap) < arr.length; k+=gap) { //使用当前gap进行组内插入排序
                       if (arr[k] > arr[k+gap]) { //交换操作
                           arr[k] = arr[k] + arr[k+gap];
                           arr[k+gap] = arr[k] - arr[k+gap];
                           arr[k] = arr[k] - arr[k+gap];
                       }
                   }
               }
           }
       }
    }
    
  • 复杂度分析

    不稳定排序算法,希尔排序第一个突破O(n2)的排序算法;是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素,直接插入排序是稳定的;而希尔排序是不稳定的,希尔排序的时间复杂度和步长的选择有关,常用的是Shell增量排序,也就是N/2的序列,Shell增量序列不是最好的增量序列,其他还有Hibbard增量序列、Sedgewick 增量序列等,具体可以参考,希尔排序增量序列简介。