用 Java 实现的八种常用排序算法

2023-11-19


八种排序算法可以按照如图分类

在这里插入图片描述


前置知识

1. 算法稳定性

在一个序列中,能保证两个相等的数,经过排序之后,其在序列的前后位置顺序不变(A1 = A2,排序前 A1 在 A2 前面,排序后 A1 还在 A2 前面)

2. 时间复杂度

时间复杂度是用于衡量一个算法的运算时间的一个描述,常用大 O 来表示

假设一个场景:在一个长度为 n 的数组 Array 查找某一特定元素 k,采用遍历的算法来查找。这种算法在不同情况下,时间复杂度也是不一样的。所以,为了描述算法在不同情况下的时间复杂度,我们引入了最好、最坏、平均时间复杂度

考虑以下场景:

  • k 在数组的第一个位置,那么时间复杂度为 O(1)
  • k 在数组的最后一个位置,那么时间复杂度为 O(n)
  • k 不在数组当中,需要遍历完数组才能知道,那么时间复杂度为 O(n)

由以上情况可知,最好时间复杂度就是最理想的情况,也就是 O(1),最坏时间复杂度就是最糟糕的情况,也就是 O(n)

其实最好与最坏都是极端情况,发生的概率并不大。为了能更好的的表示平均情况下的时间复杂度,引入了平均时间复杂度。还是上述的算法,我们知道 k 在数组中出现的情况有 n + 1 种,在数组中有 n 种,不在数组中有 1 种。每种情况要遍历的次数都不一样,我们把这种情况需要遍历的次数累加,然后除以所有情况数,就能得到遍历次数的评价数,即平均时间复杂度为:((1+2+3+…+n) + n) / (n + 1) = n(n+3) / 2(n+1),由此可以得到公式:平均情况时间复杂度 = 每种情况遍历次数累加和 / 所有情况数量

对于大 O 时间复杂度表示法,常量、低阶、系数可以忽略,所以平均时间复杂度是 O(n)

3. 空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个度量,常见的空间复杂度有 O(1)、O(n)、O(n^2)


交换排序

所谓交换,就是序列中任意两个元素进行比较,根据比较结果来交换各自在序列中的位置,以此达到排序的目的。

1. 冒泡排序

冒泡排序是一种简单的交换排序算法,以升序排序为例,其核心思想是:

  1. 从第一个元素开始,比较相邻的两个元素。如果第一个比第二个大,则进行交换。
  2. 轮到下一组相邻元素,执行同样的比较操作,再找下一组,直到没有相邻元素可比较为止,此时最后的元素应是最大的数。
  3. 除了每次排序得到的最后一个元素,对剩余元素重复以上步骤,直到没有任何一对元素需要比较为止。

在这里插入图片描述

用 Java 实现的冒泡排序如下

public void bubbleSortOpt(int[] arr) {

	if(arr == null) {
        throw new NullPoniterException();
    }
    if(arr.length < 2) {
        return;
    }
    int temp = 0;
    for(int i = 0; i < arr.length - 1; i++) {
        for(int j = 0; j < arr.length - i - 1; j++) {
            if(arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
2. 冒泡排序优化

现在有个问题,假如待排序数组是 2、1、3、4、5 这样的情况,按照上述代码实现,第一次循环即可得出正确结果。但循环并不会停止,而是继续执行,直到结束为止。显然,之后的循环遍历是没有必要的。

为了解决这个问题,我们可以设置一个标志位,用来表示当前次循环是否有交换,如果没有,则说明当前数组已经完全排序。

public static int bubbleSortOpt2(int[] arr) {

    if (arr == null) {
        throw new NullPointerException();
    } else if (arr.length < 2) {
        return 0;
    }

    int temp;
    int count = 0;
    for (int i = 0; i < arr.length - 1; i++) {
        int flag = 1;
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = 0;
            }
            count++;
        }
        // 没有发生交换,排序已经完成
        if (flag == 1) {
            return count;
        }
    }
    return count;
}

算法还可以再优化,比如 3、4、2、1、6、7、8 这个数组,第一次循环后,变为 3、2、1、4、6、7、8 的顺序,我们发现,1 之后的 4 、6、7、8 已经有序了,第二次循环就没必要对后面这段再遍历比较。

假设一次循环后数组第 i 个元素后所有元素已经有序,优化目标就是下次循环不再花费时间遍历已经有序的部分。关键在于如何定位 i 这个分界点,其实并不难,可以想象,由于 i 之前的元素是无序的,所以一定有交换发生,而 i 之后的元素已经有序,不会发生交换,最后发生交换的地点,就是我们要找的分界点。

public static int bubbleSortOpt3(int[] arr) {

    if (arr == null) {
        throw new RuntimeException();
    } else if (arr.length < 2) {
        return 0;
    }

    int temp;
    int count = 0;
    int len = arr.length - 1;
    for (int i = 0; i < len; i++) {
        // 记录最后一次交换位置
        int lastChange = 0;
        for (int j = 0; j < len; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                // 每交换一次更新一次
                lastChange = j;
            }
            count++;
        }
        // 没有发生交换,排序已经完成
        if (lastChange == 0) {
            return count;
        }
        len = lastChange;
    }
    return count;
}
3. 快速排序

快速排序的思想很简单,就是先把待排序的数组拆成左右两个区间,左边都比中间的基准数小,右边都比基准数大。接着左右两边各自再做同样的操作,完成后再拆分再继续,一直到各区间只有一个数为止。

举个例子,现在我要排序 4、9、5、1、2、6 这个数组。一般取首位的 4 为基准数,第一次排序的结果是:

2、1、4、5、9、6

可能有人觉得奇怪,2 和 1 交换下位置也能满足条件,为什么 2 在首位?这其实由实际的代码实现来决定,并不影响之后的操作。以 4 为分界点,对 2、1、4 和 5、9、6 各自排序,得到:

1、2、4、5、9、6

不用管左边的 1、2、4 了,将 5、9、6 拆成 5 和 9、6,再排序,至此结果为:

1、2、4、5、6、9

为什么把快排划到交换排序的范畴呢?因为元素的移动也是靠交换位置来实现的。算法的实现需要用到递归(拆分区间之后再对每个区间作同样的操作)

在这里插入图片描述

用 Java 实现的快速排序如下

public void quicksort(int[] arr, int start, int end) {

	if(start < end) {
        // 把数组中的首位数字作为基准数
        int stard = arr[start];
        // 记录需要排序的下标
        int low = start;
        int high = end;
        // 循环找到比基准数大的数和比基准数小的数
        while(low < high) {
            // 右边的数字比基准数大
            while(low < high && arr[high] >= stard) {
                high--;
            }
            // 使用右边的数替换左边的数
            arr[low] = arr[high];
            // 左边的数字比基准数小
            while(low < high && arr[low] <= stard) {
                low++;
            }
            // 使用左边的数替换右边的数
            arr[high] = arr[low];
        }
        // 把标准值赋给下标重合的位置
        arr[low] = stard;
        // 处理所有小的数字
        quickSort(arr, start, low);
        // 处理所有大的数字
        quickSort(arr, low + 1, end);
    }
}

插入排序

插入排序是一种简单的排序方法,其基本思想是将一个记录插入到已经排好序的有序表中,使得被插入数的序列同样是有序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程。

1. 直接插入排序

直接插入排序就是插入排序的粗暴实现。对于一个序列,选定一个下标,认为在这个下标之前的元素都是有序的。将下标所在的元素插入到其之前的序列中。接着再选取这个下标的后一个元素,继续重复操作。直到最后一个元素完成插入为止。我们一般从序列的第二个元素开始操作。

在这里插入图片描述

在这里插入图片描述

用 Java 实现的算法如下:

public void insertSort(int[] arr) {
    // 遍历所有数字
	for(int i = 1; i < arr.length - 1; i++) {
        // 当前数字比前一个数字小
        if(arr[i] < arr[i - 1]) {
            int j;
            // 把当前遍历的数字保存起来
            int temp = arr[i];
            for(j = i - 1; j >= 0 && arr[j] > temp; j--) {
                // 前一个数字赋给后一个数字
                arr[j + 1] = arr[j];
            }
            // 把临时变量赋给不满足条件的后一个元素
            arr[j + 1] = temp;
        }
    }
}
2. 希尔排序

某些情况下直接插入排序的效率极低。比如一个已经有序的升序数组,这时再插入一个比最小值还要小的数,也就意味着被插入的数要和数组所有元素比较一次。我们需要对直接插入排序进行改进。

怎么改进呢?前面提过,插入排序对已经排好序的数组操作时,效率很高。因此我们可以试着先将数组变为一个相对有序的数组,然后再做插入排序。

希尔排序能实现这个目的。希尔排序把序列按下标的一定增量(步长)分组,对每组分别使用插入排序。随着增量(步长)减少,一直到一,算法结束,整个序列变为有序。因此希尔排序又称缩小增量排序。

一般来说,初次取序列的一半为增量,以后每次减半,直到增量为一。

在这里插入图片描述

用 Java 实现的算法如下:

public void shellSort(int[] arr) {
    // gap 为步长,每次减为原来的一半。
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
        // 对每一组都执行直接插入排序
        for (int i = 0 ;i < gap; i++) {
            // 对本组数据执行直接插入排序
            for (int j = i + gap; j < arr.length; j += gap) {
                // 如果 a[j] < a[j-gap],则寻找 a[j] 位置,并将后面数据的位置都后移
                if (arr[j] < arr[j - gap]) {
                    int k;
                    int temp = arr[j];
                    for (k = j - gap; k >= 0 && arr[k] > temp; k -= gap) {
                        arr[k + gap] = arr[k];
                    }
                    arr[k + gap] = temp;
                }
            }
        }
    }
}

选择排序

选择排序是一种简单直观的排序算法,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

1. 简单选择排序

选择排序思想的暴力实现,每一趟从未排序的区间找到一个最小元素,并放到第一位,直到全部区间有序为止。

在这里插入图片描述

用 Java 实现的算法如下:

public static void selectSort(int[] arr) {
    // 遍历所有的数
    for (int i = 0; i < arr.length; i++) {
        int minIndex = i;
        // 把当前遍历的数和后面所有的数进行比较,并记录下最小的数的下标
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                // 记录最小的数的下标
                minIndex = j;
            }
        }
        // 如果最小的数和当前遍历的下标不一致,则交换
        if (i != minIndex) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}
2. 堆排序

我们知道,对于任何一个数组都可以看成一颗完全二叉树,不了解这一方面的同学可以去看这篇博客。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

在这里插入图片描述

像上图的大顶堆,映射为数组,就是 [50, 45, 40, 20, 25, 35, 30, 10, 15]。可以发现第一个下标的元素就是最大值,将其与末尾元素交换,则末尾元素就是最大值。所以堆排序的思想可以归纳为以下两步:

  1. 根据初始数组构造堆
  2. 每次交换第一个和最后一个元素,然后将除最后一个元素以外的其他元素重新调整为大顶堆

重复以上两个步骤,没有元素可操作,就完成排序了。

我们需要把一个普通数组转换为大顶堆,调整的起始点是最后一个非叶子结点,然后从左至右,从下至上,继续调整其他非叶子结点,直到根结点为止。

在这里插入图片描述

/**
 * 转化为大顶堆
 * @param arr 待转化的数组
 * @param size 待调整的区间长度
 * @param index 结点下标
 */
public void maxHeap(int[] arr, int size, int index) {
    // 左子结点
    int leftNode = 2 * index + 1;
    // 右子结点
    int rightNode = 2 * index + 2;
    int max = index;
    // 和两个子结点分别对比,找出最大的结点
    if (leftNode < size && arr[leftNode] > arr[max]) {
        max = leftNode;
    }
    if (rightNode < size && arr[rightNode] > arr[max]) {
        max = rightNode;
    }
    // 交换位置
    if (max != index) {
        int temp = arr[index];
        arr[index] = arr[max];
        arr[max] = temp;
        // 因为交换位置后有可能使子树不满足大顶堆条件,所以要对子树进行调整
        maxHeap(arr, size, max);
    }
}

/**
 * 堆排序
 * @param arr 待排序的整型数组
 */
public static void heapSort(int[] arr) {
    // 开始位置是最后一个非叶子结点,即最后一个结点的父结点
    int start = (arr.length - 1) / 2;
    // 调整为大顶堆
    for (int i = start; i >= 0; i--) {
        SortTools.maxHeap(arr, arr.length, i);
    }
    // 先把数组中第 0 个位置的数和堆中最后一个数交换位置,再把前面的处理为大顶堆
    for (int i = arr.length - 1; i > 0; i--) {
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        maxHeap(arr, i, 0);
    }
}

归并排序

归并排序是建立在归并操作上的一种有效,稳定的排序算法。该算法采用分治法的思想,是一个非常典型的应用。归并排序的思路如下:

  1. 将 n 个元素分成两个各含 n/2 个元素的子序列
  2. 借助递归,两个子序列分别继续进行第一步操作,直到不可再分为止
  3. 此时每一层递归都有两个子序列,再将其合并,作为一个有序的子序列返回上一层,再继续合并,全部完成之后得到的就是一个有序的序列

关键在于两个子序列应该如何合并。假设两个子序列各自都是有序的,那么合并步骤就是:

  1. 创建一个用于存放结果的临时数组,其长度是两个子序列合并后的长度
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入临时数组,并移动指针到下一位置
  4. 重复步骤 3 直到某一指针达到序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

在这里插入图片描述

用 Java 实现的归并排序如下:

/**
 * 合并数组
 */
public static void merge(int[] arr, int low, int middle, int high) {
    // 用于存储归并后的临时数组
    int[] temp = new int[high - low + 1];
    // 记录第一个数组中需要遍历的下标
    int i = low;
    // 记录第二个数组中需要遍历的下标
    int j = middle + 1;
    // 记录在临时数组中存放的下标
    int index = 0;
    // 遍历两个数组,取出小的数字,放入临时数组中
    while (i <= middle && j <= high) {
        // 第一个数组的数据更小
        if (arr[i] <= arr[j]) {
            // 把更小的数据放入临时数组中
            temp[index] = arr[i];
            // 下标向后移动一位
            i++;
        } else {
            temp[index] = arr[j];
            j++;
        }
        index++;
    }
    // 处理剩余未比较的数据
    while (i <= middle) {
        temp[index] = arr[i];
        i++;
        index++;
    }
    while (j <= high) {
        temp[index] = arr[j];
        j++;
        index++;
    }
    // 把临时数组中的数据重新放入原数组
    for (int k = 0; k < temp.length; k++) {
        arr[k + low] = temp[k];
    }
}

/**
 * 归并排序
 */
public static void mergeSort(int[] arr, int low, int high) {
    int middle = (high + low) / 2;
    if (low < high) {
        // 处理左边数组
        mergeSort(arr, low, middle);
        // 处理右边数组
        mergeSort(arr, middle + 1, high);
        // 归并
        merge(arr, low, middle, high);
    }
}

基数排序

基数排序的原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。为此需要将所有待比较的数值统一为同样的数位长度,数位不足的数在高位补零。

在这里插入图片描述

使用 Java 实现的基数排序:

/**
 * 基数排序
 */
public static void radixSort(int[] arr) {
    // 存放数组中的最大数字
    int max = Integer.MIN_VALUE;
    for (int value : arr) {
        if (value > max) {
            max = value;
        }
    }
    // 计算最大数字是几位数
    int maxLength = (max + "").length();
    // 用于临时存储数据
    int[][] temp = new int[10][arr.length];
    // 用于记录在 temp 中相应的下标存放数字的数量
    int[] counts = new int[10];
    // 根据最大长度的数决定比较次数
    for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
        // 每一个数字分别计算余数
        for (int j = 0; j < arr.length; j++) {
            // 计算余数
            int remainder = arr[j] / n % 10;
            // 把当前遍历的数据放到指定的数组中
            temp[remainder][counts[remainder]] = arr[j];
            // 记录数量
            counts[remainder]++;
        }
        // 记录取的元素需要放的位置
        int index = 0;
        // 把数字取出来
        for (int k = 0; k < counts.length; k++) {
            // 记录数量的数组中当前余数记录的数量不为 0
            if (counts[k] != 0) {
                // 循环取出元素
                for (int l = 0; l < counts[k]; l++) {
                    arr[index] = temp[k][l];
                    // 记录下一个位置
                    index++;
                }
                // 把数量置空
                counts[k] = 0;
            }
        }
    }
}

八种排序算法的总结

排序法 最好情形 平均时间 最差情形 稳定度 空间复杂度
冒泡排序 O(n) O(n^2^) O(n^2^) 稳定 O(1)
快速排序 O(nlogn) O(nlogn) O(n^2^) 不稳定 O(nlogn)
直接插入排序 O(n) O(n^2^) O(n^2^) 稳定 O(1)
希尔排序 不稳定 O(1)
直接选择排序 O(n^2^) O(n^2^) O(n^2^) 不稳定 O(1)
堆排序 O(nlogn) O(nlogn) O(nlogn) 不稳定 O(1)
归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定 O(n + logn)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

用 Java 实现的八种常用排序算法 的相关文章

随机推荐

  • 医学图像配准工具Elastix的配置和入门

    一 Elastix介绍 Elastix是一个基于ITK开发的处理医学图像配准问题的工具 Elastix提供了很方便的命令行使用方式以供使用者进行配准应用 同时Elastix是开源的 并且采用模块式构成 可以根据源代码进行开发 或者添加新的模
  • 在LinuxBridge/OVS中使用VxLAN组网以及创建VTEP

    原文来自于 https blog lofyer org E5 9C A8linux bridge ovs E4 B8 AD E4 BD BF E7 94 A8vxlan E7 BB 84 E7 BD 91 E4 BB A5 E5 8F 8A
  • 【满分】【华为OD机试真题2023 JS】箱子之形摆放

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 箱子之形摆放 知识点数组 时间限制 1s 空间限制 128MB 限定语言 不限 题目描述 有一批箱子 形式为字符串 设为str 要求将这批箱子按从上到下以之形的顺序摆放在宽度为n
  • js的事件执行机制(Event loop)

    同步任务 执行主线程上排队执行的任务 只有前一个任务执行完毕 下一个任务才会开始执行 异步任务 不进入主线程 而进入 任务队列 task queue 的任务 只有 任务队列 通知主线程 某个异步任务可以执行了 该任务才会进入主线程执行 事件
  • docker安装nginx太多坑了,果断放弃

    以下是我本人的个人看法 如有不对可在评论区讨论交流 1 listen的端口受限于docker p的参数 一个nginx容器conf文件只能listen同一个端口 2 修改配置文件麻烦 还有docker exec进入到容器内部进行操作 当然
  • 医疗保健软件必备指南

    对许多人来说 软件可能是一种奢侈品 只会给生活在 21 世纪的人们带来一些额外好处 但有时 软件可能是救命稻草 起着生死攸关的作用 根据医疗行业的部分统计数据 我们清醒地发现 美国平均每年约有 25 万至 40 万患者死于本可预防的医疗差错
  • Qt队列的使用

    一 queue 队列 队列是一种先进先出的数据结构 是一个模板类 队列和栈是一种数据逻辑概念 即数据能进行的操作 主要区别是 队列先进先出 First In First Out 栈后进先出 链表和顺序表是一种数据存放方式 主要区别是 链表有
  • 向日葵远程连接Ubuntu出现 “连接中断“ 的解决方法

    向日葵远程连接Ubuntu出现 连接中断 的解决方法 https www cnblogs com wangling1820 p 13448397 html 方法一 参考博客1 https blog csdn net wzf20162016
  • styled-components 的用法

    用于给标签或组件添加样式 给标签或组件添加样式 import styled from styled components styled button 给button标签添加样式 const Button styled button back
  • opencv中归一化函数cv2.normalize()的原理讲解

    本篇文章参考博客 https blog csdn net kuweicai article details 78988886 功能 归一化函数 参数 Python cv2 normalize src dst alpha beta norm
  • 转:在内核里写i2c client 驱动的两种方式

    原文位置 https www cnblogs com simonshi archive 2011 02 24 1963426 html 在内核里写i2c client 驱动的两种方式 前文介 绍了利用 dev i2c 0在应用层完成对i2c
  • 局域网下ROS多机通信的网络连接配置

    1 在路由器设置中固定各机器IP地址 在浏览器中输入路由器的IP地址 例如TP LINK路由器的IP为 192 168 1 1 进入登录页面后 输入用户名和密码登录 用户名一般为admin 密码为自定义 在 基本设置 gt LAN设置 gt
  • WebRTC 之点对点连接——浏览器

    WebRTC 的精髓 点对点连接 上一篇文章中 主要讲了浏览器怎样获取用户设备上的视频流 并且显示在 HTML5
  • java以base64文件格式导出excel表格

    在项目中要求查询数据库并且用base64文件流的格式返回excel表格 自己试了好几种方法 最后找到的答案 错误方式 用HSSFWorkbook直接生成相对应的文件 然后用base64转化 这种解析出来的文件是打不开的 String enc
  • 上海万应云——大数据精准招商系统

    上海万应云数字科技有限公司 基于全国企业大数据与企业特有的经营数据 进行动态分析与整合 形成如下几个业务领域 1 针对地方政府 产业园 形成产业政策分析 产业链路图谱 区域经济报告 高潜企业挖掘 全面把握当地的产业经济状况 投融资实际落地情
  • [JAVA]将Set转换成int[]数组

    今天在写练习的时候 碰到了方法的返回值为int 可我却使用的是HashSet来实现 想return发现类型对不上的问题 于是尝试了toArray方法 但toArray方法返回的是Object类或者是一个包装类 就找到了这个set转换成int
  • 动态一键换肤实现思路和demo

    前言 浏览器切换样式无非是通过css 总共有以下三种方法 内联style 注入style 注入link 那么我们想要实现一键换肤 那么我们为了剥离干净dom和css 我们只能选择style和link这两种方法 核心思路 在html的head
  • Java反射机制和线程

    JAVA反射机制 已经加载过这个类 通过类名来找到这个类的所有相关信息 在运行时判断任意一个对象所属的类 在运行时构造任意一个类的对象 在运行时判断任意一个类所具有的成员变量和方法 在运行时调用任意一个对象的成员变量和方法 生成动态代理 反
  • 远程桌面dos开启

    lt DOCTYPE html PUBLIC WCDTD XHTML StrictEN httpwwwworgTRxhtmlDTDxhtml strictdtd gt 注册表内容 Windows Registry Editor Versio
  • 用 Java 实现的八种常用排序算法

    八种排序算法可以按照如图分类 前置知识 1 算法稳定性 在一个序列中 能保证两个相等的数 经过排序之后 其在序列的前后位置顺序不变 A1 A2 排序前 A1 在 A2 前面 排序后 A1 还在 A2 前面 2 时间复杂度 时间复杂度是用于衡