解析七大排序

目录

1. 排序

2. 常见的排序算法

2.1 直接插入排序 (减治)

2.1.1 思路

2.1.2 具体实现

2.1.3 时间复杂度

2.1.4 空间复杂度

2.1.5 稳定性

2.1.6 代码

2.2 希尔排序 

2.2.1 思路

2.2.2 具体实现

2.2.3 时间复杂度

2.2.4 空间复杂度

2.2.5 稳定性

2.2.6 代码

2.3 选择排序 (减治)

2.3.1 思路

2.3.2 具体实现

2.3.3 时间复杂度

2.3.4 空间复杂度

2.3.5 稳定性

2.3.6 代码

2.4 堆排序 (减治)

2.4.1 思路

2.4.2 具体实现

2.4.3 时间复杂度

2.4.4 空间复杂度

2.4.5 稳定性

2.4.6 代码

2.5 冒泡排序 (减治)

2.5.1 思路

2.5.2 优化

2.5.3 时间复杂度

2.5.4 空间复杂度

2.5.5 稳定性

2.5.6 代码

2.6 快速排序 (分治)

2.6.1 思路

2.6.2 具体实现

2.6.3 时间复杂度

2.6.4 优化

2.6.5 空间复杂度

2.6.6 稳定性

2.6.7 代码

2.7 归并排序 (分治)

2.7.1 思路

2.7.2 具体实现

2.7.3 时间复杂度

2.7.4 空间复杂度

2.7.5 稳定性

2.7.6 代码

3. 总结


1. 排序

是的一串记录按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

一般情况下,采用原地排序,默认为升序。

  • 稳定性
    • 稳定
      • 经过排序后,原来相同关键字的相对顺序不变。
    • 不稳定
      • 经过排序后,原来相同关键字的相对顺序可能发生改变。
  • 内部排序
    • 数据全部在内存中处理的排序
  • 外部排序
    • 对于数据元素较多的情况,必须在内外存之间移动数据的排序。
  • 减治算法
    • 每次减少一个数,剩下的数用同样的方法进行处理。
  • 分治算法
    • 每次将数列分区,每个区用同样的方法进行处理。

2. 常见的排序算法

  • 2.1 直接插入排序 (减治)

    • 把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
    • 适用于当数组接近有序(大概率有序),数组的数量较小时(参考值20)
    • 2.1.1 思路

      • 设有序部分的最后一个下标为 j ,所要插入的数的下标为 i 。
        • 有序部分[0,i)
        • 无序部分[i,array.length)
      • 1. 在有序部分,查找合适的位置
        • 合适的位置存在3种情况
          • a[ i ] > a[ j ]        找到合适的位置
          • a[ i ] == a[ j ]      为了保证稳定性,认为找到合适的位置
          • a[ i ] < a[ j ]        j--
      • 2. 将指定的数插入到合适的下标处
    • 2.1.2 具体实现

      • 1.  遍历查找
        • 从前往后 / 从后往前
        • 插入的位置:j+1
        • 插入的过程相当于 给定 pos 的顺序表做插入
      • 2.  二分查找
        • 左闭右开查找,只需要对有序部分进行二分。且left==right时,区间中没有值。
        • left=0 
        • right=i 
        • mid=left+(right-left)/2
        • 插入位置:left
        • 插入数字a[i]与a[mid]的关系
          • a[mid]==a[i] / a[mid]<a[i]       left=mid+1
          • a[mid]>a[i]                             right=mid
    • 2.1.3 时间复杂度

      • 最坏  O(n²)  -> 逆序
      • 平均  O(n²)
      • 最好  O(n)   -> 有序
    • 2.1.4 空间复杂度

      • 常数 O(1)
    • 2.1.5 稳定性

      • 稳定
    • 2.1.6 代码

/**
 * 插入排序
 * Author:qqy
 */
public class InsertSort {
    /**
     * 遍历查找
     * 先找位置后插入
     * @param array
     */
    public static void insertSort(int[] array){
        for(int i=0;i<array.length;i++){
            int val=array[i];
            //在有序位置从后向前遍历查找
            int j;
            for(j=i-1;j>=0 && array[i]<array[j];j--){
            }

            //在j+1处插入
            for(int k=i;k>j+1;k--){
                array[k]=array[k-1];
            }
            array[j+1]=val;
        }
    }

    /**
     * 遍历查找  必会
     * 边找位置边插入
     * @param array
     */
    public static void insertSort2(int[] array){
        for(int i=0;i<array.length;i++) {
            int val = array[i];
            int j;
            for (j = i - 1; j >= 0 && array[j] > val; j--) {
                array[j + 1] = array[j];
            }
            array[j + 1] = val;
        }
    }

    /**
     * 二分查找
     * @param array
     */
    public static void insertSort3(int[] array){
        for(int i=0;i<array.length;i++){
            int val=array[i];
            int left=0;
            int right=i;
            while(left<right){
                int mid=left+(right-left)/2;
                if(array[mid]>val){
                    right=mid;
                }else {
                    left=mid+1;
                }
            }
            for(int k=i;k>left;k--){
                array[k]=array[k-1];
            }
            array[left]=val;
        }
    }
}
  • 2.2 希尔排序 

    • 把待排序的记录进行多次分组插排,直到每组的个数为1。
    • 分组插排
      • 分组越多,最大数的步伐越大
      • 分组越少,越接近有序
    • gap:每组个数
      • gap 从大到小,当gap==1,相当于直接插入排序。
      • gap = length
        • gap = (gap/3)+1
        • gap=gap/2
    • 2.2.1 思路

      • 1. 将一组记录按照gap分组,将每组中的数据进行直接插入排序
      • 2. 缩小gap -> 1
      • 3. 直到gap==1
    • 2.2.2 具体实现

      • 1.  在直接插入排序的基础上进行修改,只比较组内数据
      • 2.  利用循环进行分组比较,每次比较完,更改gap值
      • 3.  当gap==1,跳出循环
    • 2.2.3 时间复杂度

      • 本质上还是直接插入排序
        • 最坏  O(n²)  -> 逆序  但是减少了最坏情况出现的概率
        • 平均  O(n^1.2~1.3)
        • 最好  O(n)   -> 有序
    • 2.2.4 空间复杂度

      • 常数 O(1)
    • 2.2.5 稳定性

      • 不稳定,因为不能保证同样的数字放在同一个分组中。
    • 2.2.6 代码

/**
 * 希尔排序
 * Author:qqy
 */
public class ShellSort {
    private static void insertSortWithGap(int[] array, int gap) {
        for(int i=0;i<array.length;i++){
            int val=array[i];
            int j;
            for(j=i-gap;j>=0 && array[j]>val;j-=gap){
                array[j+gap]=array[j];
            }
            array[j+gap]=val;
        }
    }

    public static void shellSort(int[] array){
        int gap=array.length;
        while(true){
            gap=(gap/3)+1;
            insertSortWithGap(array,gap);
            if(gap==1){
                break;
            }
        }
    }
}
  • 2.3 选择排序 (减治)

    • 每次选出最大的一个数,置于最后。

    • 2.3.1 思路

      • 1.  找到最大数,将最大数与无序部分的最后一个数进行交换
      • 2.  找到最小数,将最小数与无序部分的第一个数进行交换
    • 2.3.2 具体实现

      • 每次选择之后
        • 最大数
          • 无序部分  [0,array.length - i)
          • 有序部分  [array.length - i,array.length)
        • 最小数
          • 无序部分  [i,array.length)
          • 有序部分  [0,i)
      • 1. n个数需要选n次,最外层循环n次
      • 2.  内层遍历找出无序部分最大数 / 最小数的下标
      • 3.  将最大数 / 最小数与无序部分的最后一个 / 第一个数交换
    • 2.3.3 时间复杂度

      • 无论是否有序,都需要先遍历找到最大值,然后进行交换
        • 最好  O(n²)  
        • 平均  O(n²)
        • 最坏  O(n²)  
    • 2.3.4 空间复杂度

      • 常数 O(1)
    • 2.3.5 稳定性

      • 不稳定
    • 2.3.6 代码

/**
 * 选择排序
 * Author:qqy
 */
public class SelectSort {
    //选择排序
    public static void selectSort(int[] array) {
        for(int i=0;i<array.length;i++){
            int max=0;
            //找出最大值
            for(int j=1;j<array.length-i;j++){
                if(array[max]<array[j]){
                    //不需要交换值,只需要记录最大值的位置就好
                    max=j;
                }
            }
            int t=array[max];
            array[max]=array[array.length-i-1];
            array[array.length-i-1]=t;
        }
    }
}
  • 2.4 堆排序 (减治)

    • 每次选出最大的一个数,置于最后。

    • 2.4.1 思路

      • 1.  建最大堆
      • 2.  将无序部分的最后一个数与根节点交换
      • 3.  无序部分堆化 -> 2
    • 2.4.2 具体实现

      • 1.  利用向下调整建立最大堆
      • 2.  利用循环不停交换无序部分的最后一个数和最大值(根结点)
      • 3.  直至无序部分的最后一个数为根结点
    • 2.4.3 时间复杂度

      • 最好  O(n*log(n))
      • 平均  O(n*log(n))
      • 最坏  O(n*log(n))
    • 2.4.4 空间复杂度

      • 常数 O(1)
    • 2.4.5 稳定性

      • 不稳定,因为数字交换后的顺序无法保证。
    • 2.4.6 代码

/**
 * 堆排序
 * Author:qqy
 */
public class HeapSort {
    //堆排序
    public static void heapSort(int[] array) {
        //向下调整建大堆
        createHeap(array);
        for(int i=0;i<array.length;i++) {
            int t = array[0];
            array[0] = array[array.length - i - 1];
            array[array.length-i-1]=t;
            heapify(array,array.length-i-1,0);
        }
    }

    public static void heapSort1(int[] array) {
        createHeap(array);
        for(int i=array.length-1;i>0;i--){
            int t=array[i];
            array[i]=array[0];
            array[0]=t;
            heapify(array,i,0);
        }
    }

    public static void heapify(int[] array, int size, int index) {
        while(2*index+1<size){
            int max=2*index+1;
            if(max+1<size && array[max+1]>array[max]){
                max+=1;
            }
            if(array[max]<=array[index]){
                break;
            }
            int t=array[max];
            array[max]=array[index];
            array[index]=t;

            index=max;
        }
    }

    private static void createHeap(int[] array) {
        for (int i = (array.length - 2) / 2; i >= 0; i--) {
            heapify(array, array.length, i);
        }
    }
}
  • 2.5 冒泡排序 (减治)

    • 从第一个数开始,两个相邻的数字依次比较,将大数向后推。

    • 也可以从最后一个数开始,两个两个相邻的数字依次比较,将小数向前推。

    • 2.5.1 思路

      • 外部循环
        • 经过一次冒泡过程,该数组中最大的数字一定放在了最后。
        • 我们只需要将剩下的n-1个数字再次进行冒泡排序。
        • 一共需要n次冒泡过程,但由于最后只剩一个数字的时候不需要进行比较,可以优化为实际只需要n-1次冒泡过程。
      • 内部循环
        • 数字两两比较,大的在后
        • 第一次冒泡排序需要比较n个数字,也就是比较n-1次
        • 每一次外部循环会使得需要比较的数字 -1
        • 第二次冒泡排序需要比较n-1个数字,也就是比较n-2次
        • ...
        • 第 i 次冒泡排序需要比较n-1-i个数字,也就是比较n-i-2次
    • 2.5.2 优化

      • 假设所给的数字已经有序,我们就不需要进行排序。因此,我们需要添加判断数字是否有序的部分。
        • 如何判断是否有序:
          • 当经过一次冒泡排序后,没有任何数字进行交换,则证明有序。
          • 我们可以设置一个标记位来标记是否有数字进行交换。

    • 2.5.3 时间复杂度

      • 最好  O(n)   -> 有序
      • 平均  O(n²) 
      • 最坏  O(n²)  -> 逆序
    • 2.5.4 空间复杂度

      • 常数 O(1)
    • 2.5.5 稳定性

      • 稳定,因为array[j] > array[j + 1] 
    • 2.5.6 代码

/**
 * 冒泡排序
 * Author:qqy
 */
public class BubbleSort {
    public static void bubbleSort(int[] array) {
        //假设有序
        boolean flag = true;
        //外部循环一共需要n-1次冒泡排序
        for (int i = 0; i < array.length - 1; i++) {
            //外部循环第i次需要比较n-2-i次
            for (int j = 0; j < array.length - 1 - i; j++) {
                //将两个数字中较大的置后
                if (array[j] > array[j + 1]) {
                    int t = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = t;

                    //进入到if中,则表明有数字进行交换,无序
                    flag = false;
                }
            }
            //有序,退出循环
            if (flag == true) {
                break;
            }
        }
    }
}
  • 2.6 快速排序 (分治)

    • 任取待排序元素序列中的某元素作为基准值,根据基准值将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值。左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

    • 2.6.1 思路

      • 1.  取得基准值
      • 2.  将区内所有大于基准值的数置于右侧,小于基准值的数置于左侧。
      • 3.  更改基准值 -> 2
    • 2.6.2 具体实现

      •  排序区间 array[ left , right ],排序一次后 基准值的下标为 pivotIndex
      • 1.  将每个区中最右边的数作为基准值array[right]
      • 2.  遍历整个区间
        • 将区间分为三部分
          •  <  基准值   [ left , pivotIndex-1]
          • == 基准值   privotIndex
          •  >  基准值   [pivotIndex+1 , right ]
        • 如何分区
          • a. Hover  (左右遍历)
            • 处理基准值以外的数据,设置begin(左边)和end(右边)标记。
            • 比较
              • array[begin] <= 基准值 ,begin++;
              • array[end] >= 基准值 ,end--;
              • 否则交换 array[begin] 和 array[end] 
              • 直至begin和end相遇
            • 将基准值与相遇位置处的值进行交换
              • 因为
                • 先比较左区,再比较右区(否则第一个数有可能没有参与比较)
              • 所以
                • ① array[begin] 一定大于基准值 
                • ② begin与end相遇处(array[end] 是与begin交换后的数字)的数字一定大于基准值
          • b. 挖坑  (左右遍历)
            • 处理基准值以外的数据,设置begin(左边)和end(右边)标记。
            • 比较
              • 将基准值取出,则其位置为“坑”
              • 移动begin
                • array[begin]  <= 基准值 ,begin++;
                • 否则,将值放于end处(坑) -> 移动end
              • 移动end
                • array[end]  >= 基准值 ,end--;
                • 否则,将值放于begin处(坑) -> 移动begin
              • 直至begin和end相遇
            • 将基准值放入begin处
          • c. 前后下标  (单向遍历)
            • 处理基准值以外的数据,设置small(左边)和big(左边)标记。
            • 比较
              • 保证小于基准值的数在[0 , small),大于基准值的数在[small , big]
              • 移动下标
                • array[big] < 基准值
                  • 交换array[big] 和 array[small] 
                  • small++
                  • big++ 
                • 否则,big++ 
                • 直到big == right
              • 交换array[small] 和基准值 
            • 左右分区继续进行数据处理
    • 2.6.3 时间复杂度

      • 每次分区的时间复杂度为O(n),而需要进行的分区次数为log(n) ~ n
        • log(n)   ->   看做二叉树,n个数一共有log(n)层
        • n   ->   当二叉树为单支树时,n个树就有n层
      • 最好  O(n*log(n))
      • 平均  O(n*log(n))
      • 最坏  O(n²)   ->  当数组已经有序或者数组逆序时
    • 2.6.4 优化

      • 为了避免单支树的出现,可以改变基准值的取法
        • 随机法   ->   radom.nextInt(4)
        • 三数取中法   ->   选择左边、中间以及右边的数,比较大小,取中间大小的数字
      • 将基准值交换至最右边,即可使用之前的方法进行快速排序。
    • 2.6.5 空间复杂度

      • 二叉树的高度
        • 最好   O(log(n)) 
        • 平均   O(log(n)) 
        • 最坏   O(n)
    • 2.6.6 稳定性

      • 不稳定
    • 2.6.7 代码

/**
 * 快速排序
 * Author:qqy
 */
public class QuickSort {
    public static void quickSort(int[] array,int left,int right){
        if(left>=right){
            return;
        }
        
        int pivot=theMiddleOfThreeNumbers(array,left,right);
        swap(array,right,pivot);

        //获取每次排序后,基准值的位置
        int pivotIndex=partitionIndex(array,left,right);
//        int pivotIndex=partitionHover(array,left,right);
//        int pivotIndex=partitionPit(array,left,right);
        
        //小于基准值区间[left,pivotIndex-1]
        quickSort(array,left,pivotIndex-1);
        //大于基准值区间[pivotIndex+1,right]
        quickSort(array,pivotIndex+1,right);
    }

    //分区方法 -> hover
    public static int partitionHover(int[] array,int left,int right){
        int begin=left;
        int end=right;
        int pivot=array[right];
        //直到两者相遇
        while(begin<end) {
            while (begin<end && array[begin] <= pivot) {
                begin++;
            }
            while (begin<end && array[end] >= pivot) {
                end--;
            }
            swap(array,begin,end);
        }
        swap(array,begin,right);
        return begin;
    }
    public static void swap(int[] array,int a,int b){
        int t=array[a];
        array[a]=array[b];
        array[b]=t;
    }

    //分区方法 -> 挖坑
    public static int partitionPit(int[] array,int left,int right){
        int begin=left;
        int end=right;
        int pivot=array[right];
        while(begin<end){
            while(begin<end && array[begin]<=pivot){
                begin++;
            }
            array[end]=array[begin];
            while(begin<end && array[end]>=pivot){
                end--;
            }
            array[begin]=array[end];
        }
        array[begin]=pivot;
        return begin;
    }

    //分区方法 -> 前后下标
    public static int partitionIndex(int[] array,int left,int right){
        int small=left;
        int big=left;
        int pivot=array[right];
        while(big<right) {
            if (array[big] < pivot) {
                swap(array, small, big);
                small++;
            }
            big++;
        }
        swap(array,small,right);
        return small;
    }

    //三数取中法
    public static int theMiddleOfThreeNumbers(int[] array,int left,int right){
        int mid=left+(right-left)/2;
        if(array[left]<array[right]){
            if(array[left]>array[mid]){
                return left;
            }else if(array[right]<array[mid]){
                return right;
            }
        }else{
            if(array[left]<array[mid]){
                return left;
            }else if(array[right]>array[mid]){
                return right;
            }
        }
        return mid;
    }
}
  • 2.7 归并排序 (分治)

    • 先使每个子序列有序,再将两个有序表合并成一个有序表,称为二路归并。
    • 外部排序最好的算法
      • 需要使用外部排序的情况
        • 内存放不下
      • 步骤
        • 先把数据切割成内存放的下的大小(n份)
        • 对每一份进行排序
        • 合并n个有序数组(归并)
    • 2.7.1 思路

      • 1.  将排序区间平均切分为两个部分
      • 2.  对两个区间进行排序(递归)
      • 3.  合并两个有序区间成为一个有序区间(需要额外空间)
    • 2.7.2 具体实现

      • 1.  将排序区间平均分为两个区间 [low,mid)  、[mid,high)
      • 2.  两个区间递归调用mergeSort  /  非递归:1个数merge()、2个数merge()、4个数merge()、8个数merge()... ...
      • 3.  直至区间内只有1个数或没有数
      • 4.  创建一个额外空间,存放合并后的区间
        • 如何合并
          • 设置两个变量 left,right分别遍历两个区间
          • 比较array[left] 和 array[right]
            • 将较小的放入额外空间,下标向后走
            • 继续比较
            • 直到任一区间为空,将另一个区间的剩余值直接放入额外空间
          • 将额外空间的值返还给原空间
    • 2.7.3 时间复杂度

      • 最好  O(n*log(n))
      • 平均  O(n*log(n))
      • 最坏  O(n*log(n))
    • 2.7.4 空间复杂度

      •  O(n) + O(log(n)) -> O(n)
    • 2.7.5 稳定性

      • 稳定
    • 2.7.6 代码

/**
 * 归并排序
 * Author:qqy
 */
public class MergeSort {
    //非递归
    public static void mergeSortNorR(int[] array) {
        int[] extra = new int[array.length];
        for(int i=1;i<array.length;i*=2){
            for(int j=0;j<array.length;j+=2*i){
                int low=j;

                int mid=j+i;
                //若没有右区间
                if(mid>array.length){
                    mid= array.length;
                }

                int high=mid+i;
                //若右区间越界
                if(high>array.length){
                    high= array.length;
                }
                merge(array,low,mid,high,extra);
            }
        }
    }

    //递归
    public static void mergeSort(int[] array) {
        int[] extra = new int[array.length];
        //左闭右开
        mergeSortInner(array, 0, array.length, extra);
    }

    public static void mergeSortInner(int[] array, int low, int high, int[] extra) {
        if (low >= high - 1) {
            return;
        }
        int mid = low + (high - low) / 2;

        //左区间 [low,mid)   有区间 [mid,high)
        mergeSortInner(array, low, mid, extra);
        mergeSortInner(array, mid, high, extra);

        //合并两个有序区间
        merge(array, low, mid, high, extra);
    }

    public static void merge(int[] array, int low, int mid, int high, int[] extra) {
        int left = low;
        int right = mid;
        //x标记额外空间的下标
        int x = 0;
        while (left < mid && right < high) {
            if (array[left] <= array[right]) {
                extra[x++] = array[left++];
            } else {
                extra[x++] = array[right++];
            }
        }

        //若第一个区间有剩余,直接加入
        while (left < mid) {
            extra[x++] = array[left++];
        }
        //第二个队列有剩余
        while (right < high) {
            extra[x++] = array[right++];
        }

        //返还给原数组
        for (int k = low; k < high; k++) {
            array[k] = extra[k - low];
        }
    }
}

3. 总结

排序方法概念算法时间复杂度空间复杂度稳定性数据敏感性
最好平均最坏最好 / 平均最坏
直接插入排序

获取无序部分任一数,

插入有序部分合适位置

减治O(n)O(n²)O(n²)O(1)稳定敏感
希尔排序

分组插入排序,

直至组内只有一个数

特殊O(n)O(n^1.2)O(n²)O(1)不稳定敏感
选择排序

获取无序中的最大数,

放入无序部分的最后

减治O(n²)O(1)不稳定不敏感
堆排序

建立最大堆,

交换根结点与无序部分的最后一个结点的值,

再堆化

减治O(n*log(n))O(1)不稳定不敏感
冒泡排序

狗熊掰玉米 ->

当遇到较大的玉米,

扔掉旧的,

抱着新的继续掰

减治O(n)O(n²)O(n²)O(1)稳定敏感
快速排序

利用三种方式获取基准值,

将基准值与最后一个数交换。

根据基准值分区,

小于基准值置左;

大于基准值置右

分治O(n*log(n))O(n*log(n))O(n²)O(log(n)) O(n)不稳定敏感
归并排序

均分数据,

两个区间分别排序,

合并两个有序区间

分治O(n*log(n))O(n)稳定不敏感
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值