数据结构与算法分析----八大排序(暂时缺少堆排序)

概述

排序分为内部排序和外部排序:

在这里插入图片描述
来自百度,侵删

八大排序都属于内部排序
时间复杂度:
在这里插入图片描述
来自百度,侵删

稳定性指的是在排序两个同长度,但内容不同的数列下,速度是否基本不变,若基本不变,则稳定,反之,则不稳定

我觉得吧,这些排序中,速度的快慢,基本都取决于数据交换的次数,一般交换的次数越少,则速度越快。单单仅比较或者赋值的话,次数多一点一般影响不大。所以很多的速度快的排序都是用空间换时间。下面的排序为啥有的速度快,有的速度慢,就是这个原因。

排序开始之前

下面的排序例子我们都按照从小到大进行排序
先讲一个下面所有排序都需要用到的测速度的方式:
在这里插入图片描述
for循环用于生成很多个随机数,下面红线标注的是计时

下面请多结合前面思路分析和讲解来看代码。

冒泡+选择

最简单且最普及的两个
首先是冒泡

冒泡

冒泡可以说是八大排序中最简单的了,他的原理就是相邻两个数之间相互比较,然后得到一个最值,然后对剩下的数再进行两两比较得最值,每一轮的比较都能得到一个最值,当得到的最值数量达到所有数的个数少一位的时候,即表示排序完成
更详细的思路:
首先,一个数列,我们让这个数列从头到尾,依次两两比较,每次两个数的比较若前面的数更大,则交换两个数的位置,这样,当比较进行到数列尾部,我们便能得到此数列中的一个最大值,并且此时已经把它放在了数列的尾部,然后我们不动这个最值,对数列剩下的数据再进行从头到尾的两两比较,然后重复上面的过程,直到排序结束。
排序结束的标志:
当未排序的数列中仅剩一个数值,或者某次从头到尾的比较,未发生数值交换。

代码实现

下面是冒泡的代码
在这里插入图片描述这个比较简单,就不再写注释

排序80000个数据的速度测试

在这里插入图片描述
这个效率还是比较低的,80000用了10秒

全部代码

如下:

public class BubbleSort {
    
    

    public static void main(String[] args) {
    
    
        BubbleDemo bubble=new BubbleDemo();

        int[] array=new int[80000];
        for (int i = 0; i < 80000; i++) {
    
    
            array[i]= (int) (Math.random()*80000);  //Math.random可以获得一个0到1之间的随机浮点数
        }
        Long time01=System.currentTimeMillis();
//        该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。

        bubble.Bubble(array);
        Long time02=System.currentTimeMillis();
        System.out.println("排序80000个数据共消耗了(毫秒)"+(time02-time01)+"毫秒");
    }
}
class BubbleDemo{
    
    
    public int[] Bubble(int[] array){
    
    
        int temp=0;
        boolean flag=false;
        for (int i = 0; i < array.length - 1; i++) {
    
    
            for (int j = 0; j < array.length - 1 - i; j++) {
    
    
                if (array[j]>array[j+1]){
    
    
                    flag=true;
                    temp=array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                }
            }
            if (!flag){
    
    
                break;
            }else {
    
    
                flag=false;
            }
        }

        return array;
    }
}

选择排序

选择排序比冒泡排序难那么一点
选择排序的原理:
原理:和冒泡也有点像,也是每次从乱序的数列中寻找最值,然后把最值放在乱序数列的头部或尾部,将这个最值从乱序数列中剔除出去,再对剩下的乱序数列再找再剔除
不过选择排序数字交换的次数更少,他只变更乱序数列中的最值和头部或者尾部的数据的位置
如下:
选择排序:时间复杂度:O(n^2)
原理:每次找到数列中的最值,并将其放在数列头部
大致原理:每次找到最值并将其放在数列头部后,就固定此值的位置,后面数列中的最值放在这些固定值的后面,当数列中仅剩一个值未固定,则表示排序结束
进一步的原理:假设首先找到了数列中最小的值,则将此最小值和数列头的数据进行交换,此时则表示完成第一次排序,然后进入下一次排序,在下一次找最小值的数列中,去除掉数列头的位置(即上次寻找到的最小值), 在剩余的数列中寻找最小值,找到后重复上面的操作

代码实现

实现:
在这里插入图片描述

80000个数据排序速度测试

在这里插入图片描述
比冒泡是快的多了

上面也有一个小的知识点

即JVM维护的纳秒,看上面注释

全部代码

如下:

import java.util.Arrays;

public class SelectSort {
    
    
    public static void main(String[] args) {
    
    
        SelectSortDemo SortDemo=new SelectSortDemo();
        int array[]=new int[80000];
        for (int i = 0; i < 80000; i++) {
    
    
            array[i]=(int)Math.random()*80000;
        }
        Long time1=System.currentTimeMillis();
        SortDemo.Sort(array);
        Long time2=System.currentTimeMillis();
        System.out.println("排序80000个数据耗时(毫秒)"+(time2-time1));

//        System.nanoTime()返回的是纳秒,nanoTime而返回的可能是任意时间,甚至可能是负数。
//        每个JVM维护一份,和系统时间无关,可用于计算时间间隔,比System.currentTimeMillis的精度要高。
//        修改了系统时间会对System.currentTimeMillis造成影响,而对System.nanoTime没有影响
//        该函数只能用于计算时间差,不能用于计算距离现在的时间。
    }
}
//        选择排序:时间复杂度:O(n^2)
//        原理:每次找到数列中的最值,并将其放在数列头部
//        大致原理:每次找到最值并将其放在数列头部后,就固定此值的位置,后面数列中的最值放在这些固定值的后面,当数列中仅剩一个值未固定,则表示排序结束
//        进一步的原理:假设首先找到了数列中最小的值,则将此最小值和数列头的数据进行交换,此时则表示完成第一次排序,
//        然后进入下一次排序,在下一次找最小值的数列中,去除掉数列头的位置(即上次寻找到的最小值),
//        在剩余的数列中寻找最小值,找到后重复上面的操作
class SelectSortDemo{
    
    
    public int[] Sort(int[] array){
    
    
//        首先肯定是需要两层for循环嵌套。外层的for循环用于控制次数,即需要寻找几次最值。
//        当所有的数列中的值只剩下一个没被固定,则表示排序完成,所以此处是0~array.length-1。即数列中数值的数量-1
        for (int i = 0; i <array.length-1 ; i++) {
    
    

//            每次的寻找最小值都需要先拥有一个值,我们此处得到第一个值当做初始的值,
//            简单来说,就是在还未找到最小值的那部分数列中,首先取其第一个值,用于二层循环中的第一次比较
            int n=array[i];   //得到第一个值
            int m=i;    //得到第一个值对应的索引,用于后续的交换

//            二层for循环,这个for循环用于控制比较和交换,j同时用于作为数组的索引
//            首先,第一个值我们已经有了,索引我们要跳过第一个值,所以j从i+1开始,
//            这里是需要比较的,即我们需要验证数列中的所有数值,所以此处索引应该到达array.length-1,所以此处是< array.length
            for (int j = 1+i; j < array.length; j++) {
    
    

//                进行比较,如果第一个值不是最值
                if (n>array[j]){
    
    
//                    我们则抛弃第一个值,来使用此时找到的最值。(后续的for循环中会持续找最值并更新最值,知道for结束)
                    n=array[j];
//                    并更新对应的索引
                    m=j;
                }
            }

//            上面而层for循环结束,则表示找到了一个最值,若此最值不在数列头部,则将其和数列头部数据进行交换,将其放在当前所属数列的头部,
            if (m!=i) {
    
    
                array[m] = array[i];
                array[i] = n;
            }
        }
        return array;
    }
}

归并排序

归并排序使用了分治算法:
对于分治算法百度百科这样说:分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
在这里的使用:
我们把数列用递归进行分割,每次取当前数列的中间值,从中间值把数列分为左右两部分,用递归,再对左右数列再分,直到数列中仅剩一个数值,就停止分割,上述过程即的过程
的过程即递归的回溯过程:对两个数列进行排序和整合,每处理两个相邻的数列就将他们合并,然后再与其他由两个数列合并成的数列进行整合处理
下面是更详细的思路:

归并排序
总的来看,感觉归并排序好像要简单一些

  1. 思路:采用分治算法,把一个数列分而治之。就是把一个数列先进行分开,再依次对分开的数列进行处理
  2. 大致思路:采用递归,对一个数列进行分,每次把数列分为两部分,一直递归,一直到每个数列仅剩一个数值,然后此时开始回溯。回溯过程中对两个数列进行排序和整合,每处理两个相邻的数列就将他们合并,然后再与其他由两个数列合并成的数列进行整合处理
  3. 进一步思路和原理:(下面以从小到大为例)
    首先我们要知道如何处理两个相邻的数列,这里我们这样做,写一个方法,接收几个参数,包括:此数列最左侧、最右侧、中间值的索引和原数组和一个临时数组。这些索引用于从原数组中得到要处理的数列,并可根据中间值,把两个数列分为左右两部分。临时数组用于对两个的合并排序,同时把合并后的数列放在原数组的位置上
    合并和处理的过程:
    首先按照中间值的索引得到左右两个数列,设左右数列分别为甲乙,甲数列从最左侧到中间值,乙从中间值+1到最右侧, 每次从一个数列中取出一个数据,和另一个数列中的数据依次比较。
    首先记录下最左侧和最右侧的索引(用于后续把临时数组中的数据放回到数组中)。
    假设每次从甲中取出一个和乙中数据依次比较,则把小的数据放在临时数组中,并同时更改索引,假设甲中的值小,则把甲中的这个数据放在临时数组中,并把甲当前索引向后移动一位,同时把临时数组当前索引也向后移动一位(为了下次的存放数据)
    然后再用甲的当前索引所指的数据和乙数列比较,再重复上述操作,直到某个数列空了,则把另一个数列中的剩余数据按照顺序加载临时数组后面
    这时便得到一个有序数列,再把这个放在临时数组中的数列更换到原数列中的位置处,此时这里便有了一个有序数列,此数列后续会和其他数列再进行比较,再重复上述操作
    直到整个数据排序完成。

注意一点:因为我们对于两个数列的合并处理是从两个仅有一个数值的数列开始的,所以每次参加排序的数列都是有序的,所以其实每次的对两数列的融合排序都是对两个有序数列的联合排序

要想非常巧妙的完成对整个数列实现上述的操作,这里便需要一个非常巧妙的递归来对数列进行分组和处理

  1. 分组和递归回溯:
    重写一个方法,接收四个参数,分别是元素组,临时数组,最左侧索引,最右侧索引
    方法体中为接下来的递归的展开和回溯做好准备。首先,每次递归的展开,我们需要得到当前递归所使用的的数列的中间值,我们需要此值对数列进行"中分",得到此值后,我们便利用此值对数列进行分割。这里的分割是借由两次对自身的调用来实现的,我们是要每次递归把数列分割为左右两部分,所以每次递归需要调用两次本身(等于一次分割,即分为左右两部分),一次属于左侧,一次属于右侧
    具体原理
    第一次调用自身,我们把该传入最右侧索引的位置传入中间值,其他的参数照旧。此次即为数列分出来的左侧部分
    第二次调用自身,我们把该传入最左侧索引的位置传入中间值+1,其他的参数照旧。此次即为数列分出来的右侧部分。
    整个递归再分割过程中所形成的的各个数列间的关系就像完全二叉树,根是原数组,然后根据原数组然后分出下一层,再根据这层再生成下一层,直到每层中的数列都仅有一个元素,形成左侧部分的递归用于生成每层中左边的数列,形成右侧部分的递归用于生成每层中右侧的数列。
    在两次调用自身的代码下面进行调用对两个数列的处理,即调用我们前面写的方法,传入左右中的索引和原数组和临时数组

注意一点,这里并不是把数组全部分好才会执行排序和整合部分的代码,他是分(即递归的展开)和治(即回溯和排序组合)有些地方交叉着进行的。可以借由二叉树来理解,还是那个数列的二叉树,很简单,这里不知道怎么描述,不描述了,反正就是分的过程中进行二叉树的创建,碰到底了,便进行一次回溯,累计两次便执行一次对俩数列的合并和排序,而且此操作是从树的左侧开始,并不是说数整个构建好后才开始执行数列的合并
可以结合代码理解,就是一个树枝叶的创建和枝叶的合并的过程

代码实现

这个归并排序的代码稍微多一点,直接粘贴代码:

class MergeSortDemo{
    
    
    public void SortDemo(int[] array,int left,int right,int[] temp){
    
    
        if (left<right){
    
      //
            int mid=(left+right)/2;  //获得数列的中间索引值
//            下面俩分别是把数列分为左和右两部分
            SortDemo(array,left,mid,temp); //递归,调用自己,此递归主要处理每段数列左侧的那部分数列。传入中间的索引作为下一次数列的最右侧的值
            SortDemo(array,mid+1,right,temp);  //递归,此递归主要处理每段数列右侧的那部分数列。传入中间的索引作为下一次数列的最左侧的值
            merge(array,left,mid,right,temp);  //此过程是主要在回溯的过程发生作用,每次回溯进入到这里,便进行排序和交换
//            System.out.println("temp+"+Arrays.toString(temp));
        }
    }
    public void merge(int[] array,int left,int mid,int right,int[] temp){
    
    
//        记录索引值
        int i=left;
        int j=mid+1;
        int t=0;

        while(i<=mid&&j<=right){
    
      //比对左右两个有序数列
//            每次先取左侧数列的一个值和右侧数列的值比较,把小的值放在临时数组中,并把索引向后移动
            if (array[i]<=array[j]){
    
    
                temp[t]=array[i];
                t++;
                i++;
            }else {
    
    
                temp[t]=array[j];
                t++;
                j++;
            }
        }

        while (i<=mid){
    
      //如果左侧还剩余有数值,则全部填充到临时数组中
            temp[t]=array[i];
            t++;
            i++;
        }

        while (j<=right){
    
      //若右侧还剩余有数值,则填充到临时数组中
            temp[t]=array[j];
            t++;
            j++;
        }

        t=0;
        int start=left;  //此值,用于记录一个原数列的起始位置,用于用临时数组把原数组中数列的更改
        while (start<=right){
    
      //对原数列进行更改
            array[start]=temp[t];
            t++;
            start++;
        }
    }
}

结合上面的讲解看代码

八百万个数据排序速度测试

在这里插入图片描述
8百万个仅1秒,已经很快了

全部代码

如下:

import java.util.Arrays;

public class MergeSort {
    
    
    public static void main(String[] args) {
    
    
        MergeSortDemo sortDemo=new MergeSortDemo();
        int[] array={
    
    5,12,1,6,78,-5,4};
        int[] temp=new int[array.length];
        sortDemo.SortDemo(array,0,array.length-1,temp);
        System.out.println(Arrays.toString(array));  //这是是测试排序是否正确

        //下面才是八百万个数据测速
        int Array[]=new int[8000000];
        int[] Temp=new int[Array.length];
        for (int i = 0; i < 8000000; i++) {
    
    
            Array[i]= (int) (Math.random()*8000000);
        }
        Long time1=System.currentTimeMillis();
        sortDemo.SortDemo(Array,0,Array.length-1,Temp);
        System.out.println("耗时(毫秒)"+(System.currentTimeMillis()-time1));
    }
}

//归并排序
//总的来看,感觉归并排序好像要简单一些
//思路:采用分治算法,把一个数列分而治之。就是把一个数列先进行分开,再依次对分开的数列进行处理
//大致思路:采用递归,对一个数列进行分,每次把数列分为两部分,一直递归,一直到每个数列仅剩一个数值,然后此时开始回溯
// 回溯过程中对两个数列进行排序和整合,每处理两个相邻的数列就将他们合并,然后再与其他由两个数列合并成的数列进行整合处理
//进一步思路和原理:(下面以从小到大为例)
//  首先我们要知道如何处理两个相邻的数列,这里我们这样做,写一个方法,接收几个参数,包括:此数列最左侧、最右侧、中间值的索引和原数组和一个临时数组
//  这些索引用于从原数组中得到要处理的数列,并可根据中间值,把两个数列分为左右两部分。临时数组用于对两个的合并排序,同时把合并后的数列放在原数组的位置上
//  合并和处理的过程:
//      首先按照中间值的索引得到左右两个数列,设左右数列分别为甲乙,甲数列从最左侧到中间值,乙从中间值+1到最右侧,
//      每次从一个数列中取出一个数据,和另一个数列中的数据依次比较。
//      首先记录下最左侧和最右侧的索引(用于后续把临时数组中的数据放回到数组中)。
//      假设每次从甲中取出一个和乙中数据依次比较,则把小的数据放在临时数组中,并同时更改索引,
//      假设甲中的值小,则把甲中的这个数据放在临时数组中,并把甲当前索引向后移动一位,同时把临时数组当前索引也向后移动一位(为了下次的存放数据)
//      然后再用甲的当前索引所指的数据和乙数列比较,再重复上述操作,直到某个数列空了,则把另一个数列中的剩余数据按照顺序加载临时数组后面
//      这时便得到一个有序数列,再把这个放在临时数组中的数列更换到原数列中的位置处,此时这里便有了一个有序数列,此数列后续会和其他数列再进行比较,再重复上述操作
//      直到整个数据排序完成。
//      注意一点,因为我们对于两个数列的合并处理是从两个仅有一个数值的数列开始的,所以每次参加排序的数列都是有序的,所以其实每次的对两数列的融合排序都是对两个有序数列的联合排序
//  要想非常巧妙的完成对整个数列实现上述的操作,这里便需要一个非常巧妙的递归来对数列进行分组和处理
//  分组和递归回溯:
//      重写一个方法,接收四个参数,分别是元素组,临时数组,最左侧索引,最右侧索引
//      方法体中为接下来的递归的展开和回溯做好准备。首先,每次递归的展开,我们需要得到当前递归所使用的的数列的中间值,我们需要此值对数列进行"中分"
//      得到此值后,我们便利用此值对数列进行分割。
//      这里的分割是借由两次对自身的调用来实现的
//      我们是要每次递归把数列分割为左右两部分,所以每次递归需要调用两次本身(等于一次分割,即分为左右两部分),一次属于左侧,一次属于右侧
//      具体原理:第一次调用自身,我们把该传入最右侧索引的位置传入中间值,其他的参数照旧。此次即为数列分出来的左侧部分
//      第二次调用自身,我们把该传入最左侧索引的位置传入中间值+1,其他的参数照旧。此次即为数列分出来的右侧部分
//      整个递归再分割过程中所形成的的各个数列间的关系就像完全二叉树,根是原数组,然后根据原数组然后分出下一层,再根据这层再生成下一层,直到每层中的数列都仅有一个元素
//      形成左侧部分的递归用于生成每层中左边的数列,形成右侧部分的递归用于生成每层中右侧的数列。
//  在两次调用自身的代码下面进行调用对两个数列的处理,即调用我们前面写的方法,传入左右中的索引和原数组和临时数组
//  注意一点,这里并不是把数组全部分好才会执行排序和整合部分的代码,他是分(即递归的展开)和治(即回溯和排序组合)有些地方交叉着进行的
//  可以借由二叉树来理解,还是那个数列的二叉树,很简单,这里不知道怎么描述,不描述了,
//  反正就是分的过程中进行二叉树的创建,碰到底了,便进行一次回溯,累计两次便执行一次对俩数列的合并和排序,而且此操作是从树的左侧开始,并不是说数整个构建好后才开始执行数列的合并
//  可以结合代码理解,就是一个树枝叶的创建和枝叶的合并的过程


class MergeSortDemo{
    
    
    public void SortDemo(int[] array,int left,int right,int[] temp){
    
    
        if (left<right){
    
      //
            int mid=(left+right)/2;  //获得数列的中间索引值
//            下面俩分别是把数列分为左和右两部分
            SortDemo(array,left,mid,temp); //递归,调用自己,此递归主要处理每段数列左侧的那部分数列。传入中间的索引作为下一次数列的最右侧的值
            SortDemo(array,mid+1,right,temp);  //递归,此递归主要处理每段数列右侧的那部分数列。传入中间的索引作为下一次数列的最左侧的值
            merge(array,left,mid,right,temp);  //此过程是主要在回溯的过程发生作用,每次回溯进入到这里,便进行排序和交换
//            System.out.println("temp+"+Arrays.toString(temp));
        }
    }
    public void merge(int[] array,int left,int mid,int right,int[] temp){
    
    
//        记录索引值
        int i=left;
        int j=mid+1;
        int t=0;

        while(i<=mid&&j<=right){
    
      //比对左右两个有序数列
//            每次先取左侧数列的一个值和右侧数列的值比较,把小的值放在临时数组中,并把索引向后移动
            if (array[i]<=array[j]){
    
    
                temp[t]=array[i];
                t++;
                i++;
            }else {
    
    
                temp[t]=array[j];
                t++;
                j++;
            }
        }

        while (i<=mid){
    
      //如果左侧还剩余有数值,则全部填充到临时数组中
            temp[t]=array[i];
            t++;
            i++;
        }

        while (j<=right){
    
      //若右侧还剩余有数值,则填充到临时数组中
            temp[t]=array[j];
            t++;
            j++;
        }

        t=0;
        int start=left;  //此值,用于记录一个原数列的起始位置,用于用临时数组把原数组中数列的更改
        while (start<=right){
    
      //对原数列进行更改
            array[start]=temp[t];
            t++;
            start++;
        }
    }
}

直接插入排序

思路
感觉像是冒泡和选择的结合体,比较大小的方式像冒泡,排序的方式像选择。
依次取出数组(无序列表)中的元素,按照顺序放在另外一个数组(有序列表)中,最后得到的那个数组就是有序的
进一步的思路
首先把第一个元素分出去,此时有序列表中只有一个数字,此时也可以称其为有序列表
然后循环无序列表,依次按大小顺序加入到有序列表中
大致思路
我们可以不用写两个数组,可以直接在当前的数组中进行操作,此时的操作就开始像选择和冒泡了
首先我们把当前数组看作两部分,一开始,数组头元素单独作为一个列表,此时他即为有序列表,然后数组后面的元素作为一个列表,即为无序列表
我们开始循环后面的无序列表,从数组下标为1的元素开始。
每从无序列表中取出一个元素(甲),我们先用一个变量来保存一下他的值,然后将其与有序列表中的元素进行比较,比较顺序是从有序列表的尾部向有序的头部,
假设我们是想按照从小到大的顺序排,
当遇到比甲大的元素的时候,则让甲在数组中所在的位置变成这个比甲大的元素的值
此时数组中便有了两个相同的元素(在前面的称为乙,在后面的称为丙,这俩都是那个比甲大的值。丙所在的位置就是原甲所在的位置,此时他与有序列表相邻),
然后此时判断乙前面是否还有元素,若没有,则把乙所在的位置换成甲,此次排序结束,即最小值放在了数组的最前面
若有,则判断这个元素和甲的大小,若大于甲,则把乙所在的位置换成这个元素,即重复上面的操作,若小于,则把乙所在的位置换成甲,结束此次排序
当遇到的元素比甲小,则停止此次排序。进入下一次排序,选择无序列表中下一个元素,进行比较。
此时甲所在的位置便成为了有序列表的尾部,此时从无序列表中选择的这个数据就成了此无序列表的头

直接插入排序的实现

在这里插入图片描述
代码量不是很多

八万个数据排序速度测试

在这里插入图片描述
说实话,由于交换的次数依然不少,排序的速度不尽人意,仅比选择排序快一点,这里仅是排序八万个数据,需要的速度已经快到达一秒

全部代码

如下

import java.util.Arrays;

public class InsertSort {
    
    
    public static void main(String[] args) {
    
    
        InsertSortDemo sortDemo=new InsertSortDemo();
        int[] array=new int[80000];
        for (int i = 0; i < 80000; i++) {
    
    
            array[i]= (int) (Math.random()*80000);
        }
        int[] array1={
    
    1,23,45,2,-2,48};
        System.out.println(Arrays.toString(sortDemo.SortDemo(array1)));  //这个仅是为了证明排序的正确性

        Long time1=System.currentTimeMillis();
        sortDemo.SortDemo(array); //这里是排序八十万个数据
        Long time2=System.currentTimeMillis();
        System.out.println("排序80000个数据耗时"+(time2-time1));
    }
}
//        思路:
//        感觉像是冒泡和选择的结合体,比较大小的方式像冒泡,排序的方式像选择。
//        依次取出数组(无序列表)中的元素,按照顺序放在另外一个数组(有序列表)中,最后得到的那个数组就是有序的
//        进一步的思路:
//        首先把第一个元素分出去,此时有序列表中只有一个数字,此时也可以称其为有序列表
//        然后循环无序列表,依次按大小顺序加入到有序列表中
//        大致思路:
//        我们可以不用写两个数组,可以直接在当前的数组中进行操作,此时的操作就开始像选择和冒泡了
//        首先我们把当前数组看作两部分,一开始,数组头元素单独作为一个列表,此时他即为有序列表,然后数组后面的元素作为一个列表,即为无序列表
//        我们开始循环后面的无序列表,从数组下标为1的元素开始。
//        每从无序列表中取出一个元素(甲),我们先用一个变量来保存一下他的值,然后将其与有序列表中的元素进行比较,比较顺序是从有序列表的尾部向有序的头部,
//        假设我们是想按照从小到大的顺序排,
//        当遇到比甲大的元素的时候,则让甲在数组中所在的位置变成这个比甲大的元素的值
//        此时数组中便有了两个相同的元素(在前面的称为乙,在后面的称为丙,这俩都是那个比甲大的值。丙所在的位置就是原甲所在的位置,此时他与有序列表相邻),
//        然后此时判断乙前面是否还有元素,若没有,则把乙所在的位置换成甲,此次排序结束,即最小值放在了数组的最前面
//        若有,则判断这个元素和甲的大小,若大于甲,则把乙所在的位置换成这个元素,即重复上面的操作,若小于,则把乙所在的位置换成甲,结束此次排序
//        当遇到的元素比甲小,则停止此次排序。进入下一次排序,选择无序列表中下一个元素,进行比较。
//        此时甲所在的位置便成为了有序列表的尾部,此时从无序列表中选择的这个数据就成了此无序列表的头
class InsertSortDemo{
    
    
    public int[] SortDemo(int[] array){
    
    
        for (int i = 1; i < array.length; i++) {
    
       //此for循环用于依次获得无序列表中的元素
            int n=array[i];   //定义一个变量保存每次从无序列表中得到的数值(即待插入的数值)
            int m=i-1;  //保存待插入数值的前一个数值的索引,用于后面的while循环对有序列表的遍历
            while (m>=0&&n<array[m]){
    
      //array[m]表示有序列表中的值,m的值后面会更新。m作为有序列表中的索引,此处也能用它来判断有序列表是否遍历结束
//                循环的结束条件:当有序列表中的某值比取出的值小,或有序列表遍历完成
                array[m+1]=array[m];  //更新有序列表的值,此处即得到两个相同的值
                m--;  //我们对有序列表的遍历是从尾到头的,所以此处用自减。当索引变成-1则表示有序列表遍历结束
            }
            if (m!=i-1) {
    
      //这里可以做一个判断,如果未进入到while循环中,即直接取出的元素大于有序列表尾的元素,则不用进行下面的赋值操作。他的标志是m是否变化
                array[++m] = n;  //将相同的两个值靠前面的那个值给换成我们从无序列表中取出的元素。因为上面的m是先自减,再判断循环条件是否成立,所以此处的m需要做一次自增
            }
        }
        return array;
    }
}

希尔排序

希尔排序就是对插入排序的一种优化的排序
在原插入排序的基础上加入了步长不为1的概念,减少了数据交换的次数,确实加快了排序速度
什么是步长?步长在排序中,就是每次在数列中选择的数据两者相隔的索引之差。
希尔排序:希尔排序就是在插入排序的基础上,对插入排序进行优化,使其排序过程中的交换次数减少, 是对插入排序的一种优化,只要懂得插入排序,这个就并不难勒。

原插入排序的一些缺点和希尔排序的改进:若数组1,2,3,4,5,6,0排序,则最后一个0需要移动很多次,浪费时间,希尔排序可以减少这个过程, 所以这里希尔这个人不愧是大佬,引入了步长不为1的情况,简单来说,就是让每次的i递增更多一点
思路:对插入排序引入步长不为1的情况,再用从无序列表中选出的数值与有序列表比对的过程中,对有序列表的遍历做出一些改变,让遍历时候的索引每次递增更改一下,加快对比过程,减少移动次数
大致思路:在原插入排序外套一个for循环,这个循环用于设置步长,for中i初始值是原数列长度除以二,随后i的更新是i=i/2,然后在这个for循环中就是一个插入排序,不同的是,插入排序中每次序列的递增的步长是外面每次得到的步长
原理:一开始,将插入排序的步长设置高一点,就等于在原数列中,每次均匀的抽出几个位置,对在这些位置上的数据进行插入排序。每次抽的位置数量在以二倍于上一次抽的位置数量的速度递增,直到最后,每个位置都需要抽出进行比较,即步长变为一的时候,进行最后一个插入排序,然后排序完成

代码实现

在这里插入图片描述因为和插入排序相比,这里就仅多了一个for循环和一些小细节,这里就没再注释

8百万个数据排序速度测试

在这里插入图片描述
速度还是很快的,比原插入排序是快了很多

总代码

如下:

import java.util.Arrays;

public class ShellSort {
    
    
    public static void main(String[] args) {
    
    
        int[] array={
    
    1,5,2,8};
        ShellSortDemo sortDemo=new ShellSortDemo();
        System.out.println(Arrays.toString(sortDemo.SortDemo(array)));
        int[] Array=new int[8000000];
        for (int i = 0; i < 8000000; i++) {
    
    
            Array[i]= (int) (Math.random()*8000000);
        }
        Long time1=System.currentTimeMillis();
        sortDemo.SortDemo(Array);
        Long time2=System.currentTimeMillis();
        System.out.println("排序8000000个数据耗时(毫秒)"+(time2-time1));
    }
}
//希尔排序:希尔排序就是在插入排序的基础上,对插入排序进行优化,使其排序过程中的交换次数减少
//  是对插入排序的一种优化,只要懂得插入排序,这个就并不难勒
//  原插入排序的一些缺点:若数组1,2,3,4,5,6,0排序,则最后一个0需要移动很多次,浪费时间,希尔排序可以减少这个过程
//  所以这里希尔这个人不愧是大佬,引入了步长不为1的情况,简单来说,就是让每次的i递增更多一点
//  思路:对插入排序引入步长不为1的情况,再用从无序列表中选出的数值与有序列表比对的过程中,对有序列表的遍历做出一些改变,
//  让遍历时候的索引每次递增更改一下,加快对比过程,减少移动次数
//  大致思路:在原插入排序外套一个for循环,这个循环用于设置步长,for中i初始值是原数列长度除以二,随后i的更新是i=i/2
//          然后在这个for循环中就是一个插入排序,不同的是,插入排序中每次序列的递增的步长是外面每次得到的步长
//  原理:一开始,将插入排序的步长设置高一点,就等于在原数列中,每次均匀的抽出几个位置,对在这些位置上的数据进行插入排序。
//    每次抽的位置数量在以二倍于上一次抽的位置数量的速度递增,直到最后,每个位置都需要抽出进行比较,即步长变为一的时候,进行最后一个插入排序,然后排序完成
class ShellSortDemo{
    
    
    public int[] SortDemo(int[] array){
    
    
        int num,k;
        for (int i = array.length/2; i >0; i/=2) {
    
    
            for (int j =  i; j < array.length; j++) {
    
    
                num=array[j];
                for (k = j; k >=i&&num<array[k-i]; k-=i) {
    
    
                    array[k]=array[k-i];
                }
                if (k!=j){
    
    
                    array[k]=num;
                }
            }
        }
        return array;
    }
}

基数排序

基数排序又称“桶子法”。
顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

基数排序,桶排序的进阶版(下面按照从小到大排序来进行示例)
思路:先定义十个桶,分别表示0-9,然后对数列进行遍历,每次遍历对每个数取其最后面位数的数值,按照此值对每个数放在不同容器中,每轮遍历结束便按照桶从0-9的顺序取出数据替换原数列中的数据,遍历的轮数是数列中最大值的位数

进一步的思路:定义十个容器,称为桶,从0到9标记每个桶。然后从数列中找到最大值,得到其的位数,记为n。然后开始对数列进行遍历。

  1. 首先,取每个数值的个位数,看是0到9中的哪个值,然后把这个数值放在定义好的桶中,后续放入桶的数值需要在前面数值的后面,不能放在之前数值的前面。一轮遍历结束后,按照从每个桶的标记,从0到9,依次取出每个桶中的数据(从每个桶的头部开始取)放在原数列中(替换掉原数列中的所有数据)
  2. 然后进入之后的遍历,此时便遍历上次遍历得到的数列,之后的遍历是取出数据的更高位数对应的值,然后再更新这个数列的数据,一直循环,遍历n次,即遍历最大值的位数的次数。然后此时数列便为有序数列。当某些数值他的位数不够,则用0来凑,用0来替代他的高位。比如1,可以当作00001

原理:排序中,若遇到同位数的值,则可以通过不同标记的桶,把更大的值放在更后面的桶中,保证同位数的值之间的有序性。对有不同位数的值,低位数的值,他通过0来补充位数,然后他的高位则变成了0,和高位数的值比较,便能保证低位数的值能处在前面的桶,保证了不同位数值之间的有序性

代码实现

class RadixSortDemo{
    
    
    public void SortDemo(int[] array){
    
    
        int[][] bucket = new int[10][array.length];  //定义10个桶,防止数据溢出,每个桶的深度都需要定义为原数列的长度
        int[] bucketLength=new int[10];  //存储每个桶中有效数据的数量,用于后续对每个桶取数据和放数据的时候使用,
        // 防止在取数据的时候对桶进行过多的遍历和取出错误数据和浪费时间

//        下面代码用于得到数列中最大值的位数
        int maxLength=array[0];
        for (int i = 1; i < array.length; i++) {
    
    
            if (array[i]>maxLength){
    
    
                maxLength=array[i];
            }
        }
        maxLength=(maxLength+"").length();



//        核心部分
        for (int i=0,n=1;i<maxLength;i++,n=n*10){
    
      //i用于控制对数列的遍历次数,n用于后续获得每个数值对应位数的值

//          将元素放入对应的桶中
            for (int j = 0; j < array.length; j++) {
    
      //遍历数列
                int DigitElement=array[j]/n%10;  //取出对应位数的值
//                bucketLength[DigitElement]表示的是DigitElement桶中的元素个数,在此处可以作为每个桶中应该添加元素的位置
//                DigitElement的作用就是表示每个元素对用的桶
                bucket[DigitElement][bucketLength[DigitElement]]=array[j];  //根据对应的值,把数值放到对应的桶中
                bucketLength[DigitElement]++;   //更新记录每个桶有效长度的数组中的数据
            }


//            把桶中的数据放回到原数列中
            int index=0;  //用于替换原数列,充当索引
            for (int j = 0; j < bucketLength.length; j++) {
    
      //控制遍历每个桶
                if (bucketLength[j]!=0){
    
    
                    for (int k = 0; k < bucketLength[j]; k++) {
    
      //取出桶中的数据
                        array[index++]=bucket[j][k];
                    }
                    bucketLength[j]=0;  //此时,桶中的数据取出完毕,把桶的有效数据的数量设置为0
                }
            }
        }
    }
}

8百万个数据排序速度测试

在这里插入图片描述
半秒,超级快了

全部代码

如下:

import java.util.Arrays;

public class RadixSort {
    
    
    public static void main(String[] args) {
    
    
        int[] array={
    
    1,45,4,2,98,6,42,0};
        RadixSortDemo sortDemo=new RadixSortDemo();
        sortDemo.SortDemo(array);
        System.out.println(Arrays.toString(array));

        int Array[]=new int[8000000];
        for (int i = 0; i < 8000000; i++) {
    
    
            Array[i]= (int) (Math.random()*8000000);
        }
        Long time1=System.currentTimeMillis();
        sortDemo.SortDemo(Array);
        System.out.println("耗时(毫秒)"+(System.currentTimeMillis()-time1));
    }
}
//基数排序,桶排序的进阶版(下面按照从小到大排序来进行示例)
//思路:先定义十个桶,分别表示0-9,然后对数列进行遍历,每次遍历对每个数取其最后面位数的数值,按照此值对每个数放在不同容器中,
//      每轮遍历结束便按照桶从0-9的顺序取出数据替换原数列中的数据,遍历的轮数是数列中最大值的位数
//进一步的思路:定义十个容器,称为桶,从0到9标记每个桶。然后从数列中找到最大值,得到其的位数,记为n。然后开始对数列进行遍历。
//      首先,取每个数值的个位数,看是0到9中的哪个值,然后把这个数值放在定义好的桶中,后续放入桶的数值需要在前面数值的后面,不能放在之前数值的前面
//      一轮遍历结束后,按照从每个桶的标记,从0到9,依次取出每个桶中的数据(从每个桶的头部开始取)放在原数列中(替换掉原数列中的所有数据)
//      然后进入之后的遍历,此时便遍历上次遍历得到的数列,之后的遍历是取出数据的更高位数对应的值,然后再更新这个数列的数据,一直循环,
//      遍历n次,即遍历最大值的位数的次数。然后此时数列便为有序数列。当某些数值他的位数不够,则用0来凑,用0来替代他的高位。比如1,可以当作00001
//原理:排序中,若遇到同位数的值,则可以通过不同标记的桶,把更大的值放在更后面的桶中,保证同位数的值之间的有序性
//    对有不同位数的值,低位数的值,他通过0来补充位数,然后他的高位则变成了0,和高位数的值比较,便能保证低位数的值能处在前面的桶,保证了不同位数值之间的有序性
class RadixSortDemo{
    
    
    public void SortDemo(int[] array){
    
    
        int[][] bucket = new int[10][array.length];  //定义10个桶,防止数据溢出,每个桶的深度都需要定义为原数列的长度
        int[] bucketLength=new int[10];  //存储每个桶中有效数据的数量,用于后续对每个桶取数据和放数据的时候使用,
        // 防止在取数据的时候对桶进行过多的遍历和取出错误数据和浪费时间

//        下面代码用于得到数列中最大值的位数
        int maxLength=array[0];
        for (int i = 1; i < array.length; i++) {
    
    
            if (array[i]>maxLength){
    
    
                maxLength=array[i];
            }
        }
        maxLength=(maxLength+"").length();



//        核心部分
        for (int i=0,n=1;i<maxLength;i++,n=n*10){
    
      //i用于控制对数列的遍历次数,n用于后续获得每个数值对应位数的值

//          将元素放入对应的桶中
            for (int j = 0; j < array.length; j++) {
    
      //遍历数列
                int DigitElement=array[j]/n%10;  //取出对应位数的值
//                bucketLength[DigitElement]表示的是DigitElement桶中的元素个数,在此处可以作为每个桶中应该添加元素的位置
//                DigitElement的作用就是表示每个元素对用的桶
                bucket[DigitElement][bucketLength[DigitElement]]=array[j];  //根据对应的值,把数值放到对应的桶中
                bucketLength[DigitElement]++;   //更新记录每个桶有效长度的数组中的数据
            }


//            把桶中的数据放回到原数列中
            int index=0;  //用于替换原数列,充当索引
            for (int j = 0; j < bucketLength.length; j++) {
    
      //控制遍历每个桶
                if (bucketLength[j]!=0){
    
    
                    for (int k = 0; k < bucketLength[j]; k++) {
    
      //取出桶中的数据
                        array[index++]=bucket[j][k];
                    }
                    bucketLength[j]=0;  //此时,桶中的数据取出完毕,把桶的有效数据的数量设置为0
                }
            }
        }
    }
}

快速排序

我感觉快速排序整体原理不是很复杂,但是实现起来有点麻烦

思路
有点像是优化版的冒泡排序,也是数值比较,然后交换,这里用到了递归。
整体的思路:对于一个数列,从中找一个数值作为标准值,然后用数列中的其他数据与标准值作比较,比标准值大的放到标准值右边,比标准值小的放在标准值的左边。(放到哪边取决于想要的排序的顺序)。然后分好后,用递归对标准值左右两边的数列持续上述操作,直到数列中仅剩一个元素,此时即排序完成。
大致思路:每次从数列中选取一个标准数,然后根据标准数把数列分为左右两部分,然后再分别对左右两部分取标准数,再分,再对分出来的数列重复这样的操作,就这样递归,直到得到的数列仅剩一个元素,此时即排序完成。
进一步思路:(以从小到大排序作为例子)
快速排序的比较和交换过程:为了方便,下面把比较的过程和交换的过程分开讲。实际中,比较一般是和交换是交叉的

  1. 比较过程:每次需要排序的数列有一个起始位置和终止位置,我们这里需要分别从起始位置和结束位置开始,向数列中心依次取出数据和标准数作比较。
    这里的比较过程是交叉进行的,先从终止位置依次取数据与标准数比较,若比标准数大或相等,则让表示终止位置的索引向前移动一位,此处即自减1,若比标准数小,则终止这个从终止位置开始的比较,进入一次数据的交换,进入从起始位置开始的比较,同样是依次取出数据,与标准数进行比较,当比标准数小或相等,则此索引向后移动一位,即自增1,若比标准数大,则终止这个从起始位置开始的比较,进入一次交换,然后,再从上次终止位置的索引最后经过变化后的值开始,进入下一轮的比较,重复上面的比较过程,
    比较的终止条件:由于起始索引和终止索引一个一直自增一个一直自减,总会有一个相遇的过程,无论在哪层循环中,当两者相遇(即两个值相等的时候),则表示以此数列和以此标准值的此排序结束
  2. 交换过程:(此处的交换并不是说是把两个值互换,而是把一个值赋给另一个值,然后会得到两个相同的值)
    由于这个比较过程是由两侧向中心收束的所以为了提高效率,减少交换的次数,这里的交换过程也是由两侧向中心收束的。
    首先,我们每次递归都是选取数列的最左侧的第一个数据作为标准数的,一开始,我们先保存这个数据。我们第一次的比较是从终止索引开始的(终止索引会在比较过程中发生变化),当终止索引代表的值比标准数小,则将起始索引的值换成终止索引的值(终止索引的值不变),这个交换便结束。
    然后进入从起始索引的比较,当起始索引代表的值比标准数大,则将终止索引代表的值换成起始索引代表的值(起始索引的值不变)。这样其实也是一种交换的方法,和冒泡类似。能保证每次得到一个把比标准数小的数值和一个比标准数大的数值,然后两者交换。非常巧妙的一种设计,每次交换完成,数列中都会有两个相同的值,且左侧的由起始索引指向,右侧的由终止索引指向。当起始索引碰到终止索引,则表示这一列数组比对完成,即,循环结束,此时,当前数列还是会有两个相同的值,我们只需要把起始和终止所指向的那个数换成标准数,则表示此次递归结束,即此数列处理结束,然后从标准数的位置分开,依次处理两边的数列,用递归来进行递进

这里有一个小细节
如果仅看此排序的原理,会觉得在交换的过程有一点问题,
因为,当在起始索引等于终止索引的时候,此数列中会有两个相同的值,此时这俩索引指向哪个值便十分关键,假设这俩值都大于标准值,若这起始和终止都指向靠后面的那个值,则若把标准值赋给他们指向的值,则此时标准值的左边有了大于标准值的值,这便出现了问题。
同理,若俩值都小于标准值,且俩索引都指向前面的,则最后赋完值后,就有了问题,但是这个算法非常巧妙,使若俩值都小于标准值,俩索引都指向后面的,若俩值都大于标准值,俩索引都指向前面的。后面到对应的位置进行分析解释

代码实现

class QuickSortDemo{
    
    

    public int[] SortDemo(int[] array,int start,int end){
    
    
        //每次的递归,就是对一段数列的排序,每次所求的数列即建立在原数组上的,所以这里我们需要设置两个参数,用来设置此数列在数组中的起始索引和结束索引

        if (start>=end) {
    
    
            //这里是设置递归的结束条件,当起始索引大于结束索引,则表示此数列中没有数据(出现这样情况的原因是标准数的一侧没有数据),
//            当结束时索引等于起始索引,则表示此数列中只有一个数据,则不需要排序。
            return array;
        }

            int num=array[start]; //假设我们每次取数列的第一个值为标准数(对于快速排序来说取哪个都行,但是取不同的数值,程序的写法可能需要出现一些更改)

//        记录起始和结束位置
            int right=end;
            int left=start;


//            这里我们下面的解释排除掉数列中除标准数外的所有值都大于标准数的情况,
//            因为当这种情况发生,就不会发生交换,仅仅是遍历一遍,更不会发生数列中出现两个相同值的情况,那就不会出现上面小细节中所说的问题

            while (left!=right){
    
      //这里对数列进行循环,所有循环的结束条件是left==right
                while (left!=right&&num<=array[right]){
    
      //此循环用于从终止索引开始,寻找到一个比标准数小的值
//            下面是对对应小细节问题的解释

                    right--;//当因为right--导致的left==right,则在两个相同的值之间,则两者便都指向靠左侧的那个值
//                    因为left==right而结束的循环,则必定是因为right自减到left,此时则必定是同时指向左侧的那个值

//                    且此时两个相同的值则必定是大于标准值的,为什么?
//                    因为到这里之前,必定会出现赋值操作,一定有一次赋值是把左边大的值赋值到右边小的值上面,所以个相同的值必定是大于标准值的
//                    为什么必定会出现赋值操作,且一定有一次赋值是下面的while后的交换?
//                    因为能到这里,则说明left!=right,若下面的while循环的结束不是因为left==right,则只能是因为,前面有个值大于标准值,此时就会发生交换
                }

                if (left!=right){
    
       //若上面循环的结束不是因为触碰到了所有循环的结束条件(left==right),则进行交换操作(即把一个值赋给另一个值)
//                    当left==right了,则这里的交换便没有意义了
                    array[left]=array[right];
                    //在这之前,两个相同的值是产生于上次下面的交换,则这俩相同的值必定大于标准值
                    // 所以咱们把找到的这个小于标准值的值赋给两个相同的值中左边的那个
                }

                while (left!=right&&num>=array[left]){
    
      //此循环用于从起始索引开始,寻找一个比标准数大的值
//            下面是对对应小细节问题的解释
//                    若能进入到这里,则表示之前必定进行过一次赋值,即两个相同值都小于标准值

//                    当在这里运行的时候left==right,则在两个相同的值之间,则两者便都指向靠右侧的那个值
//                    上面for循环的结束若不是因为left==right,则必定会有赋值,
//                    赋值完成之后,数列中会出现两个相同且比标准值小的数值,在赋值完成后,left都指向左侧那个,right都指向右侧那个,

                    left++;//当因为left++导致的left==right,则在两个相同的值之间,两者便都指向靠右侧的那个值
//                    因为left==right而结束这个while循环,则必定是因为left自增到right,此时则必定是同时指向右侧的那个值
                }

                if (left!=right) {
    
      //若上面循环的结束不是因为触碰到了所有循环的结束条件(left==right),则进行交换操作(即把一个值赋给另一个值)
//                    当left==right了,则这里的交换便没有意义了
                    array[right] = array[left];//把左边大的值赋值到右边小的值上面
                    //在这之前,两个相同的值是产生于上面的交换,则这俩相同的值必定小于标准值
                    // 所以咱们把找到的这个大于标准值的值赋给两个相同的值中右边的那个
                }

            }
            if (array[left] != num) {
    
    
//                若数列中除标准数外的所有值都大于标准数,则上面就不会发生交换,则这里也就没必要进行值的更改
                array[left] = num;
            }
//            把数列分为两部分,进行递归
            SortDemo(array,start,right);  //左侧数列,从数组头开始,到标准值位置
            SortDemo(array,right+1,end);//右侧数列,从标准值位置的下一个数值开始,到数组尾
        return array;
    }
}

8百万个数据排序速度测试

在这里插入图片描述
接近一秒,也是挺快的

全部代码

如下:

import java.util.Arrays;

public class QuickSort {
    
    
    public static void main(String[] args) {
    
    
        int[] array={
    
    56,1,-5,4,3,14,5,0,1,3,12,13,14};
        QuickSortDemo sortDemo=new QuickSortDemo();
        System.out.println(Arrays.toString(sortDemo.SortDemo(array,0,array.length-1)));
        int Array[]=new int[8000000];
        for (int i = 0; i < 8000000; i++) {
    
    
            Array[i]= (int) (Math.random()*8000000);
        }
        Long time1=System.currentTimeMillis();
        sortDemo.SortDemo(Array,0,Array.length-1);
        System.out.println("耗时(毫秒)"+(System.currentTimeMillis()-time1));
    }
}
//    快速排序:
//    思路:
//    有点像是优化版的冒泡排序,也是数值比较,然后交换,这里用到了递归。
//    整体的思路:对于一个数列,从中找一个数值作为标准值,然后用数列中的其他数据与标准值作比较,比标准值大的放到标准值右边,比标准值小的放在标准值的左边。(放到哪边取决于想要的排序的顺序)
//    然后分好后,用递归对标准值左右两边的数列持续上述操作,直到数列中仅剩一个元素,此时即排序完成。
//    大致思路:
//    每次从数列中选取一个标准数,然后根据标准数把数列分为左右两部分,然后再分别对左右两部分取标准数,再分,再对分出来的数列重复这样的操作,就这样递归
//    直到得到的数列仅剩一个元素,此时即排序完成。
//    进一步思路:(以从小到大排序作为例子)
//    快速排序的比较和交换过程:为了方便,下面把比较的过程和交换的过程分开讲。实际中,比较一般是和交换是交叉的
//      比较过程:每次需要排序的数列有一个起始位置和终止位置,我们这里需要分别从起始位置和结束位置开始,向数列中心依次取出数据和标准数作比较。
//          这里的比较过程是交叉进行的,先从终止位置依次取数据与标准数比较,若比标准数大或相等,则让表示终止位置的索引向前移动一位,此处即自减1,
//          若比标准数小,则终止这个从终止位置开始的比较,进入一次数据的交换,进入从起始位置开始的比较,同样是依次取出数据,与标准数进行比较
//          当比标准数小或相等,则此索引向后移动一位,即自增1,若比标准数大,则终止这个从起始位置开始的比较,进入一次交换,
//          然后,再从上次终止位置的索引最后经过变化后的值开始,进入下一轮的比较,重复上面的比较过程,
//          比较的终止条件:由于起始索引和终止索引一个一直自增一个一直自减,总会有一个相遇的过程,
//          无论在哪层循环中,当两者相遇(即两个值相等的时候),则表示以此数列和以此标准值的此排序结束
//      交换过程:(此处的交换并不是说是把两个值互换,而是把一个值赋给另一个值,然后会得到两个相同的值)
//          由于这个比较过程是由两侧向中心收束的所以为了提高效率,减少交换的次数,这里的交换过程也是由两侧向中心收束的
//          首先,我们每次递归都是选取数列的最左侧的第一个数据作为标准数的,一开始,我们先保存这个数据。
//          我们第一次的比较是从终止索引开始的(终止索引会在比较过程中发生变化),当终止索引代表的值比标准数小
//          则将起始索引的值换成终止索引的值(终止索引的值不变),这个交换便结束。
//          然后进入从起始索引的比较,当起始索引代表的值比标准数大,则将终止索引代表的值换成起始索引代表的值(起始索引的值不变)
//          这样起始也是一种交换的方法,和冒泡类似。能保证每次得到一个把比标准数小的数值和一个比标准数大的数值,然后两者交换
//          非常巧妙的一种设计,每次交换完成,数列中都会有两个相同的值,且左侧的由起始索引指向,右侧的由终止索引指向。当起始索引碰到终止索引,则表示这一列数组比对完成
//          即,循环结束,此时,当前数列还是会有两个相同的值,我们只需要把起始和终止所指向的那个数换成标准数,则表示此次递归结束,即此数列处理结束
//    然后从标准数的位置分开,依次处理两边的数列,用递归来进行递进

//    这里有一个小细节:
//    如果仅看此排序的原理,会觉得在交换的过程有一点问题,
//    因为,当在起始索引等于终止索引的时候,此数列中会有两个相同的值,此时这俩索引指向哪个值便十分关键,
//    假设这俩值都大于标准值,若这起始和终止都指向靠后面的那个值,则若把标准值赋给他们指向的值,则此时标准值的左边有了大于标准值的值,这便出现了问题
//    同理,若俩值都小于标准值,且俩索引都指向前面的,则最后赋完值后,就有了问题
//    但是这个算法非常巧妙,使若俩值都小于标准值,俩索引都指向后面的,若俩值都大于标准值,俩索引都指向前面的。后面到对应的位置进行分析解释
class QuickSortDemo{
    
    

    public int[] SortDemo(int[] array,int start,int end){
    
    
        //每次的递归,就是对一段数列的排序,每次所求的数列即建立在原数组上的,所以这里我们需要设置两个参数,用来设置此数列在数组中的起始索引和结束索引

        if (start>=end) {
    
    
            //这里是设置递归的结束条件,当起始索引大于结束索引,则表示此数列中没有数据(出现这样情况的原因是标准数的一侧没有数据),
//            当结束时索引等于起始索引,则表示此数列中只有一个数据,则不需要排序。
            return array;
        }

            int num=array[start]; //假设我们每次取数列的第一个值为标准数(对于快速排序来说取哪个都行,但是取不同的数值,程序的写法可能需要出现一些更改)

//        记录起始和结束位置
            int right=end;
            int left=start;


//            这里我们下面的解释排除掉数列中除标准数外的所有值都大于标准数的情况,
//            因为当这种情况发生,就不会发生交换,仅仅是遍历一遍,更不会发生数列中出现两个相同值的情况,那就不会出现上面小细节中所说的问题

            while (left!=right){
    
      //这里对数列进行循环,所有循环的结束条件是left==right
                while (left!=right&&num<=array[right]){
    
      //此循环用于从终止索引开始,寻找到一个比标准数小的值
//            下面是对对应小细节问题的解释

                    right--;//当因为right--导致的left==right,则在两个相同的值之间,则两者便都指向靠左侧的那个值
//                    因为left==right而结束的循环,则必定是因为right自减到left,此时则必定是同时指向左侧的那个值

//                    且此时两个相同的值则必定是大于标准值的,为什么?
//                    因为到这里之前,必定会出现赋值操作,一定有一次赋值是把左边大的值赋值到右边小的值上面,所以个相同的值必定是大于标准值的
//                    为什么必定会出现赋值操作,且一定有一次赋值是下面的while后的交换?
//                    因为能到这里,则说明left!=right,若下面的while循环的结束不是因为left==right,则只能是因为,前面有个值大于标准值,此时就会发生交换
                }

                if (left!=right){
    
       //若上面循环的结束不是因为触碰到了所有循环的结束条件(left==right),则进行交换操作(即把一个值赋给另一个值)
//                    当left==right了,则这里的交换便没有意义了
                    array[left]=array[right];
                    //在这之前,两个相同的值是产生于上次下面的交换,则这俩相同的值必定大于标准值
                    // 所以咱们把找到的这个小于标准值的值赋给两个相同的值中左边的那个
                }

                while (left!=right&&num>=array[left]){
    
      //此循环用于从起始索引开始,寻找一个比标准数大的值
//            下面是对对应小细节问题的解释
//                    若能进入到这里,则表示之前必定进行过一次赋值,即两个相同值都小于标准值

//                    当在这里运行的时候left==right,则在两个相同的值之间,则两者便都指向靠右侧的那个值
//                    上面for循环的结束若不是因为left==right,则必定会有赋值,
//                    赋值完成之后,数列中会出现两个相同且比标准值小的数值,在赋值完成后,left都指向左侧那个,right都指向右侧那个,

                    left++;//当因为left++导致的left==right,则在两个相同的值之间,两者便都指向靠右侧的那个值
//                    因为left==right而结束这个while循环,则必定是因为left自增到right,此时则必定是同时指向右侧的那个值
                }

                if (left!=right) {
    
      //若上面循环的结束不是因为触碰到了所有循环的结束条件(left==right),则进行交换操作(即把一个值赋给另一个值)
//                    当left==right了,则这里的交换便没有意义了
                    array[right] = array[left];//把左边大的值赋值到右边小的值上面
                    //在这之前,两个相同的值是产生于上面的交换,则这俩相同的值必定小于标准值
                    // 所以咱们把找到的这个大于标准值的值赋给两个相同的值中右边的那个
                }

            }
            if (array[left] != num) {
    
    
//                若数列中除标准数外的所有值都大于标准数,则上面就不会发生交换,则这里也就没必要进行值的更改
                array[left] = num;
            }
//            把数列分为两部分,进行递归
            SortDemo(array,start,right);  //左侧数列,从数组头开始,到标准值位置
            SortDemo(array,right+1,end);//右侧数列,从标准值位置的下一个数值开始,到数组尾
        return array;
    }
}

堆排序后续补充

猜你喜欢

转载自blog.csdn.net/qq_45821251/article/details/120999707