排序算法

排序算法介绍

什么是排序?

  • 排序也叫排序算法(Sort Algorithm),排序是将一组数据按照指定的方式进行排列的过程。

  • 排序算法的分类:内部排序和外部排序

    • 内部排序:将指定需要处理的数据加载到内存在内存中进行排序的方法(面试最常见)。

    • 外部排序:当数据量过大,无法全部加入到内存中的时候,需要借助外部存储器进行排序

  • 内部排序还可以分为比较排序和非比较排序

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

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

常见的排序算法(内部排序)

在这里插入图片描述

排序算法的时间,空间复杂度比较

在这里插入图片描述

  • 桶排序,计数排序,,基数排序,都不是基于比较的排序,它们是典型的使用空间换时间
  • 其中:插入排序,堆排序,归并排序,快速排序最好记住。

相关概念

  • 稳定:如果a原本在b前面且a=b,排序之后a仍然在b的前面。

  • 不稳定:如果a原本在b的前面且a=b,排序之后 a 可能会出现在 b 的后面。

  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数

验证排序算法代码

public static void check(){
        //生成随机数的类
        Random r=new Random();
        //创建一个大小为1000的数组,先不赋值
        int[] arrayCheck=new int[1000];
        //给数组赋值
        for(int i=0;i<arrayCheck.length;i++){
            //这个随机生成的随机数范围[0,1000)
            arrayCheck[i]=r.nextInt(1000);
        }
        //赋值一份数组
        int[] arrayCopy=new int[arrayCheck.length];
        System.arraycopy(arrayCheck,0,arrayCopy,0,arrayCheck.length);

        //使用自己的排序算法对arrayCheck数组排序
        Bubble(arrayCheck);

        //使用系统自带的排序算法对arrayCopy数组进行排序
        Arrays.sort(arrayCopy);
        //设置一个标志位
        boolean flag=true;
        //比较排序之后两个数组看看是不是一样
        for (int i=0;i<arrayCheck.length;i++){
            //如果有一个不一样,就设置为false
            if(arrayCheck[i]!=arrayCopy[i]){
                flag=false;
            }
        }
        //打印结果
        System.out.println(flag==true?"true":"false");
    }

1、冒泡排序

算法描述

  • 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

两两比较,然后交换,找出大的往后放,比较次数和交换次数最多,这种排序方式基本不用

示意动图
在这里插入图片描述
Java代码实现

public static int[] Bubble(int[] array) {
        //一共需要遍历的次数
        for (int i = 1; i < array.length; i++) {
            for (int j = 0; j < array.length - i; j++) {
                //两两比较,前面大就交换
                if (array[j] > array[j+1]) {
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                }
            }
        }
        return array;
    }

2,选择排序

算法描述

  • 在未排序序列中找到最小元素,存放到待排序序列的起始位置
  • 从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾
  • 以此类推,直到所有元素均排序完毕

先选出来最小的,然后在去排放,这个不仅时间复杂度高,而且不稳定,因为是跳着交换,基本不用。

示意动图
在这里插入图片描述
Java代码实现

 public static void selectSort(int arr[]) {
        for(int i=0;i<arr.length;i++){//确定需要循环的次数
            int min=arr[i]; //假设一个最小值
            int index=i; //存储遍历出的最小值索引
            //查找未排序列中最小值索引
            for(int j=i+1;j<arr.length;j++){
                if(arr[j]<min){
                    index =j;
                    min=arr[j];
                }
            }
            //交换
            if(index!=i) {//开始交换
                int temp = arr[i];
                arr[i] = arr[index];
                arr[index] = temp;
            }
        }
    }

3,插入排序(重点)

算法描述

  • 从第一个元素开始,该元素可以认为已经被排序
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描
  • 如果该元素小于已排序的元素,交换这两个元素的位置
  • 向前继续比较,重复上面的步骤,直到找到比该元素小的或者与该元素相等的位置,将新元素插入到该位置后面

简单排序:选择,冒泡,插入中,最值得使用的一种,因为速度比选择和冒泡块,而且稳定。

示意动图
在这里插入图片描述
Java代码实现

public static void insertSort(int arr[]) {
        for(int i=1;i<arr.length;i++){
            int temp=arr[i];//把要排序的元素先复制一份
            if(arr[i]<arr[i-1]){//如果前面的元素大则进入循环
                for(int j=i;j>=0;j--){
                    if(j>0&&arr[j-1]>temp){//逐个向前比较
                        arr[j]=arr[j-1];//将数组元素后移,留出插入的位置
                    }else {
                        arr[j]=temp;//找到位置,将元素插入,结束本次循环
                        break;
                    }
                }
            }
        }
    }

4,希尔排序

  • 希尔排序也称递减增量排序是一种:改进的插入排序
  • 因为传统的插入排序中,如果前面的数据都是已经有顺序的,最小的数据出现在最后,这时候就需要将最小的数逐步移动到前面,很浪费时间,这时候就出现了一种排序方法:希尔排序。
  • 希尔排序是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序.。

算法描述

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
以下实例仅做分组示范,不考虑排列数据的大小顺序:

假设待排序序列为:1, 5, 3, 4, 9, 3, 6, 7, 8
序列增量为:9/2=4, 9/4=2, 9/8=1
第一次分组9/2=41, 9, 8
5, 3
3, 6
4, 7
第二次分组 9/4=21, 3, 9, 6, 8
5, 4, 3, 7
第三次分组9/8=11, 5, 3, 4, 9, 3, 6, 7, 8

示意动图
在这里插入图片描述
Java代码实现

public static void shellSort(int arr[]) {
        for(int gap=arr.length/2;gap>0;gap/=2) {//确定步长序列,假设序列长度为9,序列增量为,4,2,1
            for (int i = 0; i <gap; i++) {//进行gap趟排序
                for (int j = i+gap; j<arr.length; j+=gap) {
                    if (arr[j] < arr[j-gap]) {//出现逆序则交换
                        int temp = arr[j];
                        arr[j] = arr[j-gap];
                        arr[j-gap] = temp;
                    }
                }
            }
        }
    }

4,快速排序(重点)

算法描述

  • 从序列中选取一个轴点元素(假设是序列中的第一个元素)
  • 将比轴点元素小的元素放在轴点元素的左边,比轴点元素大的数放在轴点元素的右边,与轴点元素相等的元素,放在轴点元素的左边右边都可以
  • 对左右区间重复第一,二步,直到各区间只有一个元素
  • 快速排序本质:逐渐将每个元素都转换成轴点元素

示意动图
在这里插入图片描述
Java代码实现

 /**
     * 快速排序:使用递归调用
     *
     * @param arr:待排序的数组
     * @param begin:为待排序的数组的首个元素
     * @param end:为待排序元素的最后一个元素
     */
    public static void quickSort(int arr[], int begin, int end) {
        if (begin >= end) return;//如果begin和end指向同一个元素,则排序结束
        int first = begin;//待排序的数组的首个元素
        int last = end; //待排序元素的最后一个元素
        int mid = arr[first];//设置待排序小组的第一个元素为轴点位置
        //开始分组,直到小组起始位置和结束位置相同结束
        while (first < last) {
            while (first < last) {//先从右边开始
                if (mid < arr[last]) {//右边元素>轴点元素
                    last--;
                } else {//右边元素<=轴点元素
                    arr[first] = arr[last];
                    first++;
                    break;
                }
            }
            while (first < last) {//交替到左边
                if (mid > arr[first]) {//左边元素<轴点元素
                    first++;
                } else {//左边元素=>轴点元素
                    arr[last] = arr[first];
                    last--;
                    break;
                }
            }
        }
        arr[first] = mid;//归位操作
        quickSort(arr, begin, first - 1);//左侧子组递归
        quickSort(arr, first + 1, end); //右侧子组递归
    }

6,堆排序(重点)

  • 参考链接:https://blog.csdn.net/u010452388/article/details/81283998

补充知识

  • 堆:其实就是利用完全二叉树的结构来维护的一维数组

  • 按照堆的特点可以把堆分为大顶堆和小顶堆
    大顶堆:每个结点的值都大于或等于其左右孩子结点的值
    小顶堆:每个结点的值都小于或等于其左右孩子结点的值
    在这里插入图片描述

  • 把堆中的结点按层进行编号,将这种逻辑结构映射到数组中如下:
    在这里插入图片描述
    该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
    大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
    小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

  • 升序排列用大顶堆,降序排列用小顶堆

堆排序

  • 堆排序是对简单选择排序的一种优化
  • 简单选择排序每进行一次大循环扫描找出一个最值,时间复杂度为n,一共要进行n次大循环来找出所有的最值,总的时间复杂度为:n^2
  • 堆排序把找最值的循环进行优化,使用堆的方式
  • 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。

算法思想

  • 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n-1个元素的次小值。如此反复执行,便能得到一个有序序列了

Java代码实现

/**
     * 堆:其实就是利用完全二叉树的结构来维护的一维数组
     * 堆排序:
     * 第一步:将无序数组构造成一个大顶堆(升序用大顶堆,降序就用小顶堆)
     * 第二步:交换堆的顶部元素和尾部元素
     * 第三步:去掉尾部元素,将剩余的元素再次建成大顶堆,执行第二步*/
    public static void heapSort(int[] arr) {
        //构造大顶堆
        bigHeap(arr);
        int size = arr.length;
        while (size > 1) {
            //固定最大值
            swap(arr, 0, size - 1);
            size--;
            //构造大顶堆
            heapify(arr, 0, size);
        }

    }
    
    /**
     * 构造大顶堆
     * @param arr:待排序的数组
     */
    public static void bigHeap(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            //当前插入的索引
            int currentIndex = i;
            //父结点索引
            int fatherIndex = (currentIndex - 1) / 2;
            //如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
            //然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
            while (arr[currentIndex] > arr[fatherIndex]) {
                //交换当前结点与父结点的值
                swap(arr, currentIndex, fatherIndex);
                //将当前索引指向父索引
                currentIndex = fatherIndex;
                //重新计算当前索引的父索引
                fatherIndex = (currentIndex - 1) / 2;
            }
        }
    }
    
    /**
     * 将剩余的数构造成大顶堆(通过顶端的数下降)
     * @param arr:剩余的待排序的数组
     * @param index:父亲的索引
     * @param size:剩余待排序的数组大小
     */
    public static void heapify(int[] arr, int index, int size) {
        int left = 2 * index + 1;
        int right = 2 * index + 2;
        while (left < size) {
            int largestIndex;
            //判断孩子中较大的值的索引(要确保右孩子在size范围之内)
            if (arr[left] < arr[right] && right < size) {
                largestIndex = right;
            } else {
                largestIndex = left;
            }
            //比较父结点的值与孩子中较大的值,并确定最大值的索引
            if (arr[index] > arr[largestIndex]) {
                largestIndex = index;
            }
            //如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
            if (index == largestIndex) {
                break;
            }
            //父结点不是最大值,与孩子中较大的值交换
            swap(arr, largestIndex, index);
            //将索引指向孩子中较大的值的索引
            index = largestIndex;
            //重新计算交换之后的孩子的索引
            left = 2 * index + 1;
            right = 2 * index + 2;
        }

    }
    //交换数组中两个元素的值
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

7,归并排序(重点)

算法描述

  • 不断地将当前序列平均分割成2个子序列(直到不能继续分割为止:即每个子序列中只包含一个元素)
  • 不断地将两个子序列合并成一个有序序列(直到最终只剩下1个有序序列:排序完成)

示意图
在这里插入图片描述
Java代码实现

8,计数排序

  • 计数排序:适合对一定范围的整数进行排序,如果这个整数的范围非常大,显然不适合这种方式的排序

算法描述

  • 最简单的计数排序
    第一步:找出待排序的数组中最大的元素(用于创建统计数组,大小为:max+1);
    第二步:统计待排序数组中值为 i 的元素出现的次数,然后将统计数组对应下标为 i 的元素的值加1;
    第三步:遍历统计数组,得到目标数组,排序完成
    在这里插入图片描述

  • 优化的计数排序:增加偏移量,以减少统计数组占据的空间,同时可以对负整数进行排序,改善排序的稳定性
    第一步:找出待排序的数组中最大和最小的元素,max和min;
    第二步:统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i -min项;
    第三步:对所有的计数累加(从数组 C 的第一个元素开始,每一项和前一项相加,因为第零个元素前面没有值);
    第四步:反向遍历待排序数组,填充目标数组:将每个元素 i 放在新数组中,每放一个元素就将 C[i-min] 减去 1;
    注意:必须反向遍历待排序的数组,计数排序才是稳定的
    在这里插入图片描述

示意动图
在这里插入图片描述

Java代码实现

  • 最简单的计数排序
 /**
     * 计数排序:
     * @param arr
     */
    public static void countingSort(int[] arr) {
        //第一步:找出最大值
        int max=arr[0];
        for(int i=1;i<arr.length;i++){
            if(arr[i]>max){
                max=arr[i];
            }
        }
        //开辟一个数组空间(统计数组),存储每个整数出现的次数
        int[] counts=new int[max+1];

        //第二步:统计每个数出现的次数,未赋值的时候,数组里面都是0
        for(int i=0;i<arr.length;i++){
            //arr[i],代表待排序的数,也是统计数组的下标,这个数出现一次,统计数组对应的位置+1
            counts[arr[i]]++;
        }

        //第三步:遍历统计数组,根据整数出现次数,对其进行排序
        int index=0;//目标数组的索引
        for(int i=0;i<counts.length;i++){
            while(counts[i]-- >0) {
                //注意:统计数组中的索引才是要排序的数,统计数组中值为待排序的数出现的次数
                arr[index++] = i;//使用目标数组覆盖待排序的数组,节省空间
                //index++;
                //counts[i]--;
            }
        }
    }
  • 优化的计数排序
    /**
     * 计数排序:
     * @param arr
     */
    public static void countingSort(int[] arr) {
        //第一步:找出最大值最小值
        int max=arr[0];
        int min=arr[0];
        for(int i=1;i<arr.length;i++){
            if(arr[i]>max){
                max=arr[i];
            }
        }
        for(int i=1;i<arr.length;i++){
            if(arr[i]<min){
                min=arr[i];
            }
        }
        //开辟一个数组空间(统计数组),存储次数,
        int[] counts=new int[max-min+1];

        //第二步:统计每个数出现的次数,未赋值的时候,数组里面都是0
        for(int i=0;i<arr.length;i++){
            //此处要注意为了节省空间设置的:偏移量
            counts[arr[i]-min]++;
        }

        //第三步:对所有的统计数累加,从第一个元素开始,加上前面的数,覆盖当前的数
        /*这一步很重要:
        *   1,把所有的统计数加起来就得到了待排序元素的个数
        *   2,这时候counts数组中存放的不是统计数了,而是待排序数组元素在目标数组中的索引
        *   3,排序方式就是,从右向左遍历待排序数组,拿到数减去偏移量,得到counts数组的索引
        *      通过counts[索引]找到待排序的数在目标数组中的位置*/
        for(int i=1;i<counts.length;i++) {
            counts[i]+=counts[i-1];
        }

        //第四步:反向遍历待排序数组,填充目标数组
        int[] titleArr=new int[arr.length];//新建目标数组
        for(int i=arr.length-1;i>=0;i--){//倒序遍历
            //arr[i]为待排序数,arr[i]-min为counts索引,counts[arr[i]-min]为待排序数在目标数组中的位置
            //--counts[arr[i]-min];//不要忘记减操作,而且是先减
            titleArr[--counts[arr[i]-min]]=arr[i];
        }

        //将数组覆盖回来
        for(int i=0;i<arr.length;i++){
            arr[i]=titleArr[i];
        }
    }

9,桶排序

算法描述

  • 先找到数组中元素的最大差值,然后在根据这个差值来划分桶数,最后对每个桶的元素进行排序

注意:如果数组中元素分配不均匀,划分桶的时候,如何分配内存是个要考虑的问题?
分配过小容不下,分配过大浪费空间

算法思想:

  • 设置一个定量的数组当作空桶。
  • 遍历数组,并且把数组中的元素一个一个放到对应的桶中。
  • 对每个不是空的桶进行排序。
  • 从不是空的桶里把元素再放回原来的序列中。

示意动图
在这里插入图片描述

Java代码实现

10,基数排序

算法描述

  • 一种多关键字的排序算法

例如:如果待排序的数最大是三位数,我们先使用个位数排序,在使用十位数排序,在使用百位数排序即可完成排序。

算法思想

  • 取得数组中的最大数,并取得位数
  • 对原始数组最低位的数字进行排序(因为数字范围为0~9:可以选择基数排序的方式进行排序);
  • 然后在使用次高位排序,以此类推,知道使用最高位排序,即完成排序。

Java代码实现

/*基数排序*/
public static int[] RadixSort(int[] arr) {
    //计算最大数的位数
    int d = maxBit(arr);
    //创建一个和原数组同样大的数组
    int[] tempArr = new int[arr.length];
    //计数数组
    int[] count = new int[10];
    int radix = 1;
    //进行d次排序
    for (int i = 1; i <= d; i++) {
        //计数之前先清空计数数组
        for (int j = 0; j < 10; j++) {
            count[j] = 0;
        }
        //对待排序数组进行统计
        for (int j = 0; j < arr.length; j++) {
            int k = (arr[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        //计数数组,从第二位开始,每一位存储的都是前面累计的个数,这样做是为了稳定性
        for (int j = 1; j < 10; j++) {
            count[j] = count[j - 1] + count[j];
        }
        // 倒着遍历原数组,将原数组中的值存放到tempArr数组中,为了稳定性,
        for (int j = arr.length - 1; j >= 0; j--) {
            int k = (arr[j] / radix) % 10;
            // count[k]代表是排序的位数,减1代表数组下标索引
            tempArr[count[k] - 1] = arr[j];
            //将计数数组进行减法操作
            count[k]--;
        }
        //一轮排序结束,将tempArr数组中的内容复制到arr数组中,tempArr临时数组用于存放下一轮排序的数
        for (int j = 0; j < arr.length; j++){
            arr[j] = tempArr[j];
        }
        //乘10操作,用于下次求取十位数的余数
        radix = radix * 10;
    }
    return arr;
}

//辅助函数,求数据的最大位数
public static int maxBit(int[] arr) {
    //假设最大数为数组中的第一个数
    int maxData = arr[0];

    // 先求出最大数
    for (int i = 1; i<arr.length; ++i) {
        if (maxData < arr[i]) {
            maxData = arr[i];
        }
    }
    //求最大数的位数
    int d = 1; //假设为1位
    int p = 10;
    while (maxData >= p) {
        maxData /= 10;
        ++d;
    }
    return d;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

彤彤的小跟班

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

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

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

打赏作者

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

抵扣说明:

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

余额充值