关于对常见排序算法的分析和总结

关于算法的分类

算法一般分为两种:①比较类排序②非比较类排序

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

常见的比较类排序算法类型如下:

1.选择排序 2.直接插入排序3.希尔排序4.选择排序5.快速排序6.归并排序7.堆排序

我们依次进行介绍

1.选择排序

主要思想:每一次从待排序的数据元素中 选出最小(或最大)的一个元素 存放在序列的起始位置,直到全部待排序的数据元素排完
图示如下:

我们按照这个思路去写一下它的代码

  //1.选择排序
    public static void selectSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; ++i) {
            //数组长度为arr.length,我们只需要比较arr.lengnth-1次,剩下的一定是最后最大或者最小的元素
            //按照升序排序不断去找最小值
            int minIndex = findMinIndex(arr, i);
            //将最小值进行交换
            swap(arr, i, minIndex);
        }

    }

    private static void swap(int[] arr, int i, int minIndex) {
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }

    private static int findMinIndex(int[] arr, int i) {
        //从i开始找最小值
        int minIndex = i;
        for (int j = i + 1; j < arr.length; ++j) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        return minIndex;

    }

 我们不妨去分析一下它的时空复杂度

时间复杂度:第一个数需要同剩下的length-1个数进行比较找最大或者最小值,第二个需要同剩下的length-2个值进行比较,......直到同最后一个数进行比较,需要执行的次数为:1+2+3+...+arr.length-1次,是一个等差数列,那他的时间复杂度则为该等差数列的和S(n)=(1+arr.length-1)(length-1)/2,也就是O(n^2)

空间复杂度如下:通过观察代码我们可以发现,我们在进行遍历时我们只创建了temp,minIndex等变量,那么该算法的空间复杂度为O(1)

稳定性:由于选择排序是在排序前一个数值时,在其余后面数组的数中找最小值并与之进行交换,空间跨度相对较大,所以排序算法并不稳定(ps:我们判断一个算法的稳定性时,主要是通过其在进行数组元素之间的比较时,比较元素的空间跨度的大小(是否大于1))

2.直接插入排序

主要思想:

把待排序的记录按其关键码值的大小 逐个插入到一个已经排好序的有序序列 中,直到所有的记录插入完为止,得到 一个新的有序序列
动图如下:
代码如下:
 //2.插入排序
    //当我们向对一个数组进行升序排列,而如果数组本身已经是升序排列,这时我们不难发现的是,此时所有的数据只需要向前与其前一个数字进行比较,此时
    //时间复杂度为O(n)
    public static void insertSort(int[] arr) {
        //使用插入排序时,本轮数组开始的下标之前数组已经完全有序
        for (int i = 1; i < arr.length; ++i) {
            //将要比较的值暂存起来
            int temp = arr[i];
            //从i的前一个值往前进行比较
            int j = i - 1;
            while (j >= 0) {
                if (arr[j] > temp) {
                    arr[j + 1] = arr[j];
                    j--;
                }
                //如果当前值小于等于arr[i],说明arr[i]已经找到该存放的位置,此时跳出循环

                else {
                    break;
                }
            }
            //将arr[i]的值进行替换
            arr[j + 1] = temp;
        }

    }

时间复杂度:(最坏情况下)从数组第二个元素开始,第二个元素需要向前比较1次,第三个元素需要向前比较两次.....最后一个元素需要向前比较(n-1)次,显而易见的是,其语句运行的次数是一个等差数列,所以其时间复杂度为n(1+n-1)/2=O(n^2)

空间复杂度:我们通过代码发现,在创建时也只是创建了temp和j等变量,所以其空间复杂度为

O(1)

稳定性:由于插入排序的思想为对数组的逐个元素依次进行比较,空间跨度很小,所以其是稳定的

而如果是原数组已经是有序的情况下,那么对于每个元素而言,他只需要与之前的一个元素进行比较即可确定位置,那么其比较次数(时间复杂度)也就变成了O(n),同样,如果插入排序时数组越有序,那么其排序速度也就越快

其实这也向我们提示了它的一种应用场景:如果我们在进行数量相对较大的数组元素之间的排序,当我们确定在某次排序之后(或者某种界限之后)数组元素相对有序,那么我们可以通过插入排序进行对排序方式的优化

3.希尔排序

思想如下:

先选定一个整数,把待排序文件中所有记录分成个组,所 有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达 =1 时,所有记录在统一组内排好序。
换句话说(In other words)希尔排序的思想为:在 进行最后一次排序之前对数组进行分组预排序,利用插入排序数组越有序排序速度越快的特点,从而大大提高最后一次排序的速度
图示如下:

 我们不妨写一下他的代码

 /**
     * 希尔排序是根据插入排序的数组越有序排序速度越快的特点,首先对数组进行预排序,即如果是在升序排序中,使小数尽量排在前面,使大数尽量
     * 排在后面,提高最后一次排序的速度
     *
     * @param arr:要排序的数组
     */
    //3.希尔排序
    public static void shellSort(int[] arr) {
        int gap = arr.length / 2;
        //只有gap等于1进行的排序同于完全的插入排序,其余的均是预排序
        while (gap >= 1) {
            shellCore(arr, gap);
            //更新gap
            gap /= 2;
        }
    }

    private static void shellCore(int[] arr, int gap) {
        //从gap下标开始排序,直到最后一个元素
        for (int i = gap; i < arr.length; ++i) {
            int temp = arr[i];
            int j = i - gap;
            while (j >= 0) {
                if (arr[j] > temp) {
                    arr[j + gap] = arr[j];
                    j = j - gap;
                } else {
                    break;
                }
            }
            arr[j + gap] = temp;
        }

    }

时间复杂度:

希尔排序的时间复杂度不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排 序的时间复杂度都不固定。

因为我们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照 

 空间复杂度:与插入排序类似,同样是只是建立了部分临时变量,空间复杂度为O(1)

稳定性:由于在排序时其数组元素之间的空间跨度相对较大,所以其是不稳定

 4.冒泡排序

思想:比较交换,就是根据序列中两个记录键值的大小比较结果来对换这两个记录在序列中的位置,交换排序的特 点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

假如共n个数据,我们只需要确定其n-1个数据的元素即可,在每次排序时,(升序)将最大的元素放在最后,那么我们可以保证每次排序能确定当前比较元素中的最大元素,也就是说每次比较比较范围都会从后到前减少一个

代码如下:

 //4.冒泡排序
    public static void bubbleSort(int[]arr){
        //共arr.length个元素,需要比较arr.length-1次
        for(int i=0;i< arr.length-1;++i){
            //设置标志值
            boolean flag=false;
            for(int j=0;j< arr.length-1-i;++j){
                if(arr[j]>arr[j+1]){
                    swap(arr,j,j+1);
                    flag=true;
                }
            }
            if(flag==false){
                break;
            }
        }
    }

我们这个代码其实通过标志flag对冒泡排序进行了一定的优化:当某一次排序从头到尾进行比较时,我们发现这一次并没有进行任何一次的交换,那这时数组是有序的,也就没有必要进行下一次的排序 

时间复杂度:第一次需要进行n-1次比较,第二次进行n-2次比较,最后一次进行一次比较,所以其时间复杂度同样为O(n^2)(等差数列)

空间复杂度:中间只创建了i j flag等中间变量,空间复杂度为O(1)

5.堆排序

思路:通过建立堆的数据结构,利用堆数据结构的特点(大跟堆或者小根堆  大跟堆是第一个元素是最大的,小根堆是第一个元素是最小的),将第一个元素和最后一个元素进行交换,同时不断缩小范围(将最后一个元素不断剔除),重复此步骤,当堆中只剩一个元素没有调整时,就已经成为了有序数组

图示如下:

我们按照思路写一下代码:

 //5.堆排序
    public static void heapSort(int[]arr){
        //创建堆
        createHeap(arr);
        //进行堆排序
        heapSortCore(arr);
    }

    private static void heapSortCore(int[] arr) {
        //定义end结点使其不断与根结点进行交换
        int end=arr.length-1;
        while(end>0){//当end==0时,说明其余元素都已经排好,也就不需要在进行最后一次排序
            swap(arr,0,end);
            //这时向下调整时,边界是end为下标的结点,也就是说不需要再考虑end结点的轮换问题(就大跟堆而言,根结点一直是最大的元素)
            shiftDown(arr,0,end);
            end--;
        }


    }

    private static void createHeap(int[] arr) {
        //定义最后一个父节点的坐标
        int parent=(arr.length-1-1)/2;
        //循环实现堆
        while (parent>=0){
            //不断向下遍历
            shiftDown(arr,parent, arr.length);
            parent--;
        }
    }

    private static void shiftDown(int[] arr, int parent,int end) {
        int child=2*parent+1;//默认左子节点为最大子节点
        while (child<end){
            //进行左右节点的比较
            if(child+1< end){//确保右结点存在
                if(arr[child]<arr[child+1]){
                    child+=1;//最大子节点进行轮换
                }
            }
            //将最大子节点与父节点进行比较
            if(arr[parent]<arr[child]){
                swap(arr,child,parent);
            }
            //父子节点不断向下进行遍历
            parent=child;
            child=2*parent+1;
        }
    }
时间复杂度:其时间复杂度主要体现在建堆和不断向下调整的过程:
向下调整:
每个元素进行交换(共进行n-1)次,每个元素均为log n,所以其时间复杂度为O(n*log n)
空间复杂度:O(1)(中间只建立了部分临时变量)

 6.快速排序

基本思路:我们每次选定一个中轴元素,并以此为界限,将比中轴元素小的元素全部放于中轴元素左边,把比中轴元素大的元素全部放于其右边,并不断所有左右界限,直至中轴元素左右界限指向同一个元素

一.hoare法

二.挖坑法

 

 

图示:

代码如下:

1.递归实现 

 /**
     * 快速排序的核心思想是在数组内定义中间值,在中间值的左右范围内使比中间值小的元素放在其左边,比中间值大的元素放在其右边
     * @param arr:进行排序的数组
     * @param left:排序的左边界
     * @param right:排序的右边界
     */
    //6.快速排序
    //6.1快速排序的递归实现
    public static void quickSort(int[]arr,int left,int right){
        //对左右边界的有效性进行校验
        if(left>=right){
            return ;
        }
        int len=quickSortCoreDig(arr,left, right);
        //对左半部分进行递归
        quickSort(arr,left,len-1);
        //对右半部分进行递归
        quickSort(arr,len+1,right);


    }

    /**
     * 快速排序的核心实现方法1:hoare法
     * @param arr
     * @param left
     * @param right
     * @return
     */
    private static int quickSortCore(int[] arr, int left, int right) {
        //定义基准值
        int standardIndex=left;
        int standard=arr[left];
        while (left<right){
            //从右节点向左找比基准值小的值
            while(left<right&&arr[right]>=standard){
                right--;
            }
            while(left<right&&arr[left]<=standard){
                left++;
            }
            //左右节点值的交换
            swap(arr,left,right);
        }
        //当左右节点走到同一个值的时候将standard和这个下标的值进行交换
        swap(arr,standardIndex,left);
        return left;//返回中间结点的值,left或者right都可以
    }
    //快速排序的核心实现方法2:挖坑法
    public static int quickSortCoreDig(int[]arr,int left,int right){
        //首先定义一个基准值
        int standard=arr[left];
        //左右节点向彼此方向寻找
        while(left<right){
            while(left<right&&arr[right]>=standard){
                right--;
            }
            //右下标找到的数字放在左下标
            arr[left]=arr[right];
            //左子节点向右寻找
            while(left<right&&arr[left]<=standard){
                left++;
            }
            //左下标找到的值放到右下标
            arr[right]=arr[left];
        }
        //两个下标交汇时,我们将基准值放入其中
        arr[left]=standard;
        //返回中间结点(左右结点交汇,返回一个即可)、
        return left;

    }

2.迭代实现

 

 //6.2快速排序的迭代实现
    public static void quickSortItr(int[]arr){
        //设计思想与递归完全相同,栈中存放的是左右两半部分或者多部分
        //创建栈空间
        Stack<Integer>stack=new Stack<>();
        int left=0;
        int right=arr.length-1;
        int len=quickSortCore(arr,left, right);
        if(left<len-1){
            stack.push(left);
            stack.push(len-1);
        }
        if(right>len+1){
            stack.push(len+1);
            stack.push(right);
        }
        while (!stack.isEmpty()){
            right=stack.pop();
            left=stack.pop();
             len=quickSortCore(arr,left, right);
            if(left<len-1){
                stack.push(left);
                stack.push(len-1);
            }
            if(right>len+1){
                stack.push(len+1);
                stack.push(right);
            }

        }
    }

通过观察我们可以发现,如果使用迭代方式来完成左右界限的更替,我们一般需要借助某种数据结构进行界限的更替和储存

 

我们去思考以下的情况:如果原数组是一个完全有序的数组,那么对于快速排序而言,其也就退化为一个单链表,其空间复杂度将会大大提高(提高至O(N)),我们这时可以使用三数取中法对数组进行调整

 

 

三数取中:简单来说,三树取中法就是如果原来数组是有序的,那么我们就将其中间大小的数值调整至前面

代码如下:

//利用三树取中法,解决快速排序中的栈溢出问题
    public static int swapMiddle(int []arr,int left,int right){
        //寻找中间下标的值
        int middle=(left+right)/2;
        //对数组的左右大小关系进行判断
        if(arr[left]>arr[right]){
            if(arr[middle]>arr[left]){
                return left;
            }
            else if(arr[middle]<arr[right]){
                return right;
            }
            else{
                return middle;
            }
        }
        else {
            if(arr[middle]<arr[left]){
                return left;
            }
            else if(arr[middle]>arr[right]){
                return right;
            }
            else{
                return middle;
            }
        }
    }

时间复杂度:对于快速排序而言,我们实际上可以理解为是一颗二叉树(左右界限不断更替),那么其时间复杂度也就类似于堆排序:O ( n ∗ l o g n ) 

空间复杂度:递归不断在栈帧中开辟其树高个空间,故其空间复杂度为O ( l o g n ) 

稳定性:比较时元素空间跨度较大,所以并不稳定

7.归并排序

基本思想:我们将原有数组不断进行拆分,直到我们将其拆分为只有两个元素,然后不断将其进行排序合并操作

图示:

 代码实现:

1.递归

 public static void mergeSort(int[]arr,int left,int right){
        //对终止条件进行校验
        if(left>=right){
            return;
        }
        //求中间结点
        int mid=(left+right)/2;
        //对左部分和右部分进行递归分解
        mergeSort(arr,left,mid);
        mergeSort(arr,mid+1,right);
        //对已经分解好的数组进行归并操作
        merge(arr,left,mid,right);
    }
private static void merge(int []arr,int left, int mid, int right) {
        //建立一个额外空间的数组
        int[]tmp=new int[right-left+1];
        //将范围内的数组分解为两部分,依次进行比较并加入数组
        //定义两个下标作为遍历的指标
        int leftIndex=left;
        int rightIndex=mid+1;
        //定义index作为遍历数组的下标
        int index=0;
        while(leftIndex<=mid&&rightIndex<=right){
            if(arr[leftIndex]<arr[rightIndex]){
                tmp[index++]=arr[leftIndex++];
            }
            else {
                tmp[index++]=arr[rightIndex++];
            }
        }
        //对剩余数组进行合并
        while(leftIndex<=mid){
            tmp[index++]=arr[leftIndex++];
        }
        while(rightIndex<=right){
            tmp[index++]=arr[rightIndex++];
        }
        //最后将临时数组合并到原本数组
        for(int i=0;i< tmp.length;++i){
            arr[left+i]=tmp[i];
        }
    }

2.迭代

public static void mergeSortItr(int[]arr){
        //定义步长
        int gap=1;
        //定义迭代的终止条件
        while(gap< arr.length){
            //i是分组后的第一个元素
            for (int i=0;i< arr.length;i+=2*gap){
                int left=i;
                int mid=left+gap-1;
                int right=mid+gap;
                //修正mid和right的值防止越界
                if(mid>= arr.length){
                    mid=arr.length-1;
                }
                if(right>=arr.length){
                    right=arr.length-1;
                }
                //进行归并操作
                merge(arr,left,mid,right);
            }
            //将gap迭代
            gap*=2;
        }

    }
private static void merge(int []arr,int left, int mid, int right) {
        //建立一个额外空间的数组
        int[]tmp=new int[right-left+1];
        //将范围内的数组分解为两部分,依次进行比较并加入数组
        //定义两个下标作为遍历的指标
        int leftIndex=left;
        int rightIndex=mid+1;
        //定义index作为遍历数组的下标
        int index=0;
        while(leftIndex<=mid&&rightIndex<=right){
            if(arr[leftIndex]<arr[rightIndex]){
                tmp[index++]=arr[leftIndex++];
            }
            else {
                tmp[index++]=arr[rightIndex++];
            }
        }
        //对剩余数组进行合并
        while(leftIndex<=mid){
            tmp[index++]=arr[leftIndex++];
        }
        while(rightIndex<=right){
            tmp[index++]=arr[rightIndex++];
        }
        //最后将临时数组合并到原本数组
        for(int i=0;i< tmp.length;++i){
            arr[left+i]=tmp[i];
        }
    }
归并的缺点在于需要 O(N) 的空间复杂度, 归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(N)
4. 稳定性:稳定
应用场景:
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了(由此我们可以看出,归并排序是比较类排序中唯一一个可以应用于外部排序的算法

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

常见的非比较类算法如下:1.计数排序2.基数排序3.桶排序

关于这几种排序,应用场景有限,我把链接放入,大家自主学习

计数排序:计数排序 - 知乎

基数排序:1.10 基数排序 | 菜鸟教程

桶排序:【排序】图解桶排序_str_818的博客-CSDN博客

最后进行奉上排序算法的表格

此篇博客加之代码共耗时12个小时,希望大家可以一键三连喔~~~

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

六子干侧开

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值