十大排序算法(附动态图解)- Java版

十大排序算法性能对比

 稳定性:例如[a,b],a和b是相等的,当排完序后a和b的位置互换了就说明不稳定,没有互换就说明稳定。
image-20220328161706745

前言

 算法都有优劣,了解算法的性能才能在实践中选择更好的算法,所以了解算法性能十分的必要。

 在性能上对比归并排序、快速排序、堆排序是相对来说比较好的排序算法,可以重点理解下这些算法;希尔排序相对于以上三个并不是最好,但也可以重点学习理解,面试过程中考察点突发奇想,冒泡选择插入也说不定会考察。

扩展

 在JDK中Array.sort()排序方法中,在数组长度小于286时,使用的是快速排序,而在数组长度小于47时使用的是插入排序。在JDK8中还有一点就是,在数组长度小于286大于47,并且数组具有一定结构,则使用归并排序。

一、选择排序

 算法描述:

  1、将第一位当成最小值,记录值和下标,用最小值向右进行逐个比较,查找是否有更小的,有则更新最小的值和下标。

  2、判断最小值下标是否变动,变动则交换位置,将新的最小值位置赋值为旧的最小值,旧的赋值为新的最小值。

 静态图解
在这里插入图片描述
 动图图解

在这里插入图片描述

 代码部分

/**
* 选择排序:
*  外层循环的作用:
*      选择排序执行的次数
*  内层循环的作用:
*      将第一位当成最小值,记录值和下标,用最小值向右进行逐个比较,查找是否有更小的,有则更新最小的值和下标。
*  判断语句的作用:
*      判断最小值下标是否变动,变动则交换位置,将新的最小值位置赋值为旧的最小值,旧的赋值为新的最小值。
*/
@Test
public void Test() {
    int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12};
    SelectSortFun(arr);
    Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
}

public void SelectSortFun(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) { // 选择排序执行的次数
        int min = arr[i];
        int minIndex = i;
        for (int j = i; j < arr.length; j++) { // 每次遍历确定最左边的一位最小值,所以从i开始。
            if (min > arr[j+1]) {
                min = arr[j+1];
                minIndex = j+1;
            }
        }
        if (i != minIndex) {        // 最小值有变动,不是原来的那个,则交换。
            arr[minIndex] = arr[i]; // 查找到的最小值下标赋值为下标i的数
            arr[i] = min;           // 下标i赋值为最小值
        }
    }
}

二、冒泡排序

 算法描述:两两进行比较,从左开始,5大于2则交换,5大于1则交换,如果有比5大的,则将自身下标设置为更大的值的下标,继续向后比较,直到自身为最大,则确定了数组最右边为第一个最大值,同理逐个确定,直到只剩一个则完成排序。

 静态图解

在这里插入图片描述

 动图图解

在这里插入图片描述
 代码部分

    @Test
    public void Test() {
        int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12};
        BubbleSortFun(arr);
        Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
    }

    public void BubbleSortFun(int[] arr) {
        boolean flag = true; //优化有序数组多次进行循环,当遇到没交换位置的情况则提前终止循环
        for (int i = 0; i < arr.length - 1; i++) { // 冒泡的次数
            for (int j = 0; j < arr.length - i - 1; j++) { // 每次冒泡确定了最后一位数字,所以减i,防止再次遍历后面的数浪费时间。
                if (arr[j] > arr[j + 1]) {
                    flag = false; // 进去了说明有无序数组则不跳过继续循环,没进去才跳过停止循环
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            if (flag) {
                break;
            }
        }
    }

三、插入排序

 算法描述:认为第一个元素已经排序,从第二个元素开始(也就是下标1开始),取出下一个元素,向左扫描,如果取出的元素小于向左扫描的元素,则将扫描到的元素向右移,将取出的元素移动到比他更小的值的前一位。

 动图图解

在这里插入图片描述

 代码部分

    @Test
    public void Test() {
        int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12};
        insertionSort(arr);
        Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
    }

    public int[] insertionSort(int[] arr) {
        int preIndex,current; // pre是下一个需要排序的元素,cur是当前元素的备份值
        for (int i = 1; i < arr.length; i++) {
            preIndex = i - 1; // 移动的下标
            current = arr[i]; // 记录被覆盖的值
            while (preIndex >= 0 && arr[preIndex] > current) { // 
                arr[preIndex + 1] = arr[preIndex];
                preIndex--; 
            }
            arr[preIndex + 1] = current;  // 如果移动:将移动后的位置插入被覆盖的值
        }
        return arr;
    }

四、希尔排序

 算法描述:

  1、确定初始增量大小(增量大小必须大于等于1)

  2、分组进行插入排序

  3、从最小增量2,开始插入排序,排完后再排序增量4,直到整个数组排序。

 静态图解

在这里插入图片描述

 代码部分

&emsp;/**
     * 希尔排序原理:
     *    将数组按照增量gap进行分组,然后将每组内的数字进行插入排序,
     *    排序完后增量减小,继续排序,直至减少到1后再进行最后一次插入排序。
     */
    @Test
    public void Test(){
        int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12};
        sort(arr);
        Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
    }

    public int[] sort(int[] arr) {
        int current;
        /* 按照增量分组,每一组内进行插入排序 */
        /* gap是指用来分组的增量,必须是依次递减的 */
        int gap = arr.length/2;
        while (gap > 0) {
            /* 从gap开始可以节约一点运算时间,少循环几次 */
            for (int i = gap; i < arr.length; i++) {
                int preIndex = i - gap;
                current = arr[i];
                while (preIndex >= 0 && arr[preIndex] > current) {
                    arr[preIndex + gap] = arr[preIndex];
                    preIndex -= gap;
                }
                arr[preIndex + gap] = current;
            }
            gap /= 2;
        }
        return arr;
    }

五、快速排序

 算法描述:

  1、确定基准数

  2、将基准数与最右或者最左交换位置(若基准数本身就在最左最右则不必交换)

  3、一刀切,使用基准数(可以分区大小范围内随机,也可直接设置最左或最右)确定中间值,左边为比基准数更小的,右边是比基准数更大的

  4、细化分区,循环第三步的做法。

  分区操作

   1、如果元素小于等于基准数,则分区指示器右移

   2、在1的基础上,如果当前元素下标(遍历指示器)大于分区指示器下标时,则当前元素和分区指示器元素交换。

   注意:基准数只是一个不会变动的下标,当交换时基准数下标是不会变动的,但是基准数下标的值会不断变动

 动态图解(通俗易懂的)

在这里插入图片描述

 正经点的

在这里插入图片描述

 代码部分

&emsp;/**
     * 快速排序
     */
    @Test
    public void Test(){
        int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12};
        arr = sort(arr, 0, arr.length - 1);
        Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
    }

    // 排序操作
    public int[] sort(int[] arr, int left, int right) {
        if (arr.length < 1 || left < 0 || right >= arr.length || left > right)  return null;
        int index = partition(arr, left, right); //
        if (index > left) { // index为比基准数大的下标位
            sort(arr, left, index - 1);
        } else if(index < right) {
            sort(arr, index + 1, right);
        }
        return arr;
    }

    /**
     * 分区操作:一分为二,左边的是比基准数小的,右边的是比基准数大的。
     * @param arr
     * @param left
     * @param right
     * @return
     */
    public int partition(int[] arr, int left, int right) {
        if (left == right) return left;
        // 设置基准数:如果基准数设置最左或者最左时则此处可以用right或者left代替
        int pivot = new Random().nextInt(arr.length);
        // 设置分区指示器:初始值必须是遍历指示器-1
        int index = left - 1;
        for (int i = left; i <= right; i++) { // i是遍历指示器
            if (arr[i] <= arr[pivot]) { // 遍历值小于等于基准数,则分区指示器+1(向右移一位)
                index++; 
                if (i > index) { // 遍历指示器大于分区指示器的时候,则交换位置
                    swap(arr,i, index);                          
                }
            }
        }
        return index;

    }


    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

六、归并排序

 算法描述:采用的是分而治之的方法,将数组逐个拆分,拆到只剩一个或者两个(拆成多少个都可以排序,但是不能低于0个)再进行比较后从小到大合并合并。

 合并过程:现将左右两边数组第一位进行比较,如果右边的小,则临时数组插入右边数组值,右边下标指示器+1,若左边数组值小于右边数组值,则临时数组插入左边下标值,左边下标指示器+1,若下标指示器等于数组长度时,说明一遍数组已经排序完毕,则直接一次插入右边数组的值即可。

 静态图解

在这里插入图片描述

 动态图解

在这里插入图片描述

 代码部分

&emsp;/**
     * 归并排序:
     *  采用的是分而治之的方法,将数组逐个拆分,拆到只剩一个或者两个(拆成多少个都可以排序,但是不能低于0个)再进行比较后从小到大合并合并。
     */
    @Test
    public void Test() {
        int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12, -55, 5, 6, 43, 65};
        arr = margeSort(arr);
        Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
    }


    public int[] margeSort(int[] arr) {
        if (arr.length < 2) return arr;
        int mid = arr.length / 2;
        int[] left = Arrays.copyOfRange(arr, 0, mid);
        int[] right = Arrays.copyOfRange(arr, mid, arr.length);
        return marge(margeSort(left), margeSort(right));
    }


    public int[] marge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        for (int index = 0, i = 0, j = 0; index < result.length; index++) {
            if (i >= left.length)           // 左边数组已经取完,直接拿右边数组的值即可
                result[index] = right[j++];
            else if (j >= right.length)     // 右边数组已经取完,直接拿左边数组的值即可
                result[index] = left[i++];
            else if (left[i] > right[j])    // 左边数组元素值大于右边数组,取右边数组
                result[index] = right[j++];
            else                            // 右边数组元素值大于左边数组,取左边数组
                result[index] = left[i++];
        }
        return result;
    }&emsp;

七、堆排序

 算法描述:

  1、将原始数组构造为最大堆。

  2、不停的将头元素和尾元素进行交换,交换后尾元素则为最大值,将他存入结果数组中。

  3、交换后再次构造最大堆,重复步骤2和3,最终排序完成。

扩展-二叉堆的定义:二叉堆是一个完全二叉树的结构,同时满足堆的性质,子节点的键值或者索引总是小于(或大于)它的父节点。在一个二叉堆中,根节点总是最大(或者最小)节点,这样的堆称之为最大(小)堆

扩展-完全平方二叉树:

在这里插入图片描述

推论1:对于位置为K的节点,左子节点=2xK+1,右子节点=2x(K+1) 验证:C=2 2x2+1=5 2x(2+1)=6

推论2:最后一个非叶子节点的位置为(N/2)-1,N为数组长度 验证:数字长度为6,(6/2)-1=2

 动态图解

在这里插入图片描述

 代码部分

    /**
     * 定义长度为全局变量,用于记录堆排序的剩余数组长度
     */
    public static int len;

    /**
     * 堆排序
     */
    @Test
    public void Test() {
        int[] arr = new int[]{-12, -55, -23, 43, 24, 34, 12, -55, 5, 6, 43, 65};
        sortArray(arr);
        MyArrayUtil.printArray(arr);
    }

    public int[] sortArray(int[] arr) {
        len = arr.length;
        if (len < 1) { return arr; }
        /* 1.构建最大堆 */
        buildMaxHeap(arr);
        /* 2.循环将堆首位(最大值)与未排序数据末位交换,然后调整为最大值*/
        while (len > 0) {
            /* 交换首尾,将末尾元素放置最大值 */
            MyArrayUtil.swap(arr, 0, len - 1);
            len--;
            buildMaxHeap(arr);
            MyArrayUtil.printArray(arr);
        }
        return arr;
    }

    /**
     * 构建最大堆
     * @param arr
     */
    public static void buildMaxHeap(int[] arr) {
        for (int i = (len/2 -1); i >= 0; i--) {
            /* i为最小非叶子结点在数组中的下标 */
            adjustHeap(arr,i);
        }
    }

    /**
     * 调整最大堆
     * @param arr
     * @param i
     */
    public static void adjustHeap(int[] arr, int i) {
        int maxIndex=i;
        int left = 2*i-1;
        int right = 2*(i+1);
        if (left < len && arr[left] > arr[maxIndex]) {
            maxIndex = left;
        }
        if (right < len && arr[right] > arr[maxIndex] && arr[right] > arr[left]) {
            maxIndex = right;
        }
        if (maxIndex != i) {
            /* 将最大值和父节点进行交换,使父节点成为最大值 */
            MyArrayUtil.swap(arr,maxIndex,i);
            adjustHeap(arr,maxIndex);
        }
    }

八、计数排序

 算法描述:

  1、构建最大值长度的数组。

  2、遍历数组,将遍历的数字作为构建数组的下标,遍历一次则+1计数。

  3、从数组最小值到数组最大值遍历构建数组(节约遍历空间),当数组下标对应的值不为0,则结果数组的值设置为下标值。

 实际应用:实际应用会找出数组中的最大值和最小值,主要为了节省空间,例如【1001,1002,1005,1002】这样的数组没必要建立1005个长度的数组。

 适用范围:适用于排序元素分布连续,且跨度小的情况。

 存在的问题:无法解决小数情况,且负数情况需要进一步处理

 动态图解

在这里插入图片描述

 代码部分

    /**
     * 计数排序
     */
    @Test
    public void Test() {
        int[] arr = new int[]{12, 55, 23, 43, 24, 34, 12};
        sortArray(arr);
        MyArrayUtil.printArray(arr);
    }

    public int[] sortArray(int[] arr) {
        int maxVal = arr[0];
        int minVal = arr[0];
        // 查找最大值和最小值,构建临时数组的长度(节约空间)
        for (int i = 1; i < arr.length; i++) {
            if (maxVal < arr[i]) {
                maxVal = arr[i];
            }
            if (minVal > arr[i]) {
                minVal = arr[i];
            }
        }
        int[] temp = new int[maxVal-minVal+1];
        // 遍历数组中每个值,用于临时数组的下标
        for (int k : arr) {
            // 在指定下标+1,计数
            temp[k - minVal]++;
        }
        // 第一种写法
        /*for (int i = 0,j=0; i < temp.length; i++) {
            while (temp[i] != 0) {
                arr[j] = i + minVal;
                j++;
                temp[i]--;
            }
        }*/
        // 第二种写法
        /* 访问原数组的下标指示器 */
        int index = 0;
        /* 访问计数数组的下标指示器 */
        int i = 0;
        while (index < arr.length) {
            if (temp[i] != 0) {
                arr[index] = i + minVal;
                index++;
                temp[i]--;
            } else {
                i++;
            }
        }
        return arr;
    }

九、桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

 算法描述:

  1、创建一定量的数组当做桶。

  2、遍历数组将每个数据放入规定范围(可能是[0,10],[10,20],[20,30)这个样子的)的桶中

  3、对每个桶进行排序

 存在的问题:1、排序的数组可能趋近一个范围内,导致所有数组中所有数都放入一个桶中,失去了桶排序的意义。 2、事先不知道输入的数据是什么,为了保证每个元素均匀分布到各个桶中,需要建立多少个桶,每个桶的范围是多少并不知道。

 实际应用:简单点的有,限定桶的容量,根据元素最大值和最小值相减来决定桶的数量。

 动态图解

在这里插入图片描述

 代码部分

    /**
     * 桶排序
     */
    @Test
    public void test() {
        ArrayList<Integer> arr = new ArrayList<Integer>();
        arr.add(-12);
        arr.add(-55);
        arr.add(23);
        arr.add(34);
        arr.add(31);
        arr.add(83);
        arr.add(34);
        arr.add(65);
        arr.add(23);
        arr = bucketSort(arr, 2);
        System.out.println(arr);


    }

    public ArrayList<Integer> bucketSort(ArrayList<Integer> array, int bucketCap) {
        if (array == null || array.size() < 2) {
            return array;
        }
        int max = array.get(0), min = array.get(0);

        /* 找到最大值和最小值 */
        for (Integer num : array) {
            if (max < num) {
                max = num;
            }
            if (min > num) {
                min = num;
            }
        }

        /* 获得桶的数量 */
        int bucketCount = (max - min) / bucketCap + 1;
        /* 构建桶 */
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        ArrayList<Integer> resultArr = new ArrayList<>();
        for (int i = 0; i < bucketCount; i++) {
            bucketArr.add(new ArrayList<>());
        }

        /* 将原始数据分配到桶中(分配的过程中就可以排好了一部分顺序):
              分配规则为,
                下标值=(遍历的值-最小值)/ 桶容量
                数据=遍历值
        */
        for (int i = 0; i < array.size(); i++) {
            bucketArr.get((array.get(i) - min) / bucketCap).add(array.get(i));
        }

        /* 查看桶中的分布情况 */
        for (int i = 0; i < bucketArr.size(); i++) {
            System.out.print("第" + i + "个桶包含数据:");
            System.out.println(bucketArr.get(i));
        }

        /* 遍历所有桶 */
        for (int i = 0; i < bucketCount; i++) {
            if (bucketCap == 1 ){
                resultArr.addAll(bucketArr.get(i));
            } else {
                if (bucketCount == 1) {
                    bucketCap--;
                }
                /* 对桶中数据再次进行桶排序 */
                List<Integer> temp = bucketSort(bucketArr.get(i), bucketCap);
                /* 将所有排序好的桶进行合并 */
                resultArr.addAll(temp);
            }
        }
        return resultArr;
    }

十、基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

 算法描述:

  1、遍历数组,获取最大值,并获取最大值的位数,确定遍历轮数。

  2、遍历数组,根据个位(十位、百位等)按照下标入桶。

  3、取出桶中所有数字,循环执行步骤2和步骤3

 存在的问题:该代码不可用于有小数类型的数组,负数需要进一步处理。

 动态图解

 代码部分

    /**
     * 基数排序
     */
    @Test
    public void Test() {
        int[] arr = new int[]{32, 5, 52, 34, 43, 24, 34, 12};
        sortArray(arr);
        Arrays.stream(arr).forEach(i -> System.out.print(i + " "));
    }

    public int[] sortArray(int[] arr) {
        /* 找出最大值的位数,确定需要做几轮排序 */
        int max = 0;
        for (int num : arr) {
            max = Math.max(max, num);
        }
        /* 确定位数 */
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        /* 用于获得数字指定位数 */
        int mod = 10, div = 1;
        /* 构建桶 */
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
        /* 初始化桶 */
        for (int i = 0; i < 10; i++) {
            bucketList.add(new ArrayList<>());
        }

        /* 外层循环:几轮排序 */
        for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
            /* 遍历数组,投入桶中 */
            for (int k : arr) {
                int num = (k % mod) / div;
                bucketList.get(num).add(k);
            }
            /* 从桶中取出 */
            int index = 0;
            for (ArrayList<Integer> integers : bucketList) {
                for (Integer integer : integers) {
                    arr[index++] = integer;
                }
                /* 清除桶,准备下一轮排序 */
                integers.clear();
            }
        }
        return arr;
    }

末尾表达歉意:所有动图都通过互联网搜索而来,如有冒犯请私信联系我。静态图有些是自己话的哈。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值