用 Java 实现常见的 8 种内部排序算法

简介: 来看看用 Java 实现常见的 8 种内部排序算法

一、插入类排序

插入类排序就是在一个有序的序列中,插入一个新的关键字。从而达到新的有序序列。插入排序一般有直接插入排序、折半插入排序和希尔排序。

1. 插入排序

1.1 直接插入排序

/**
* 直接比较,将大元素向后移来移动数组
*/
public static void InsertSort(int[] A) {
   
   
    for(int i = 1; i < A.length; i++) {
   
   
        int temp = A[i];                           //temp 用于存储元素,防止后面移动数组被前一个元素覆盖
        int j;
        for(j = i; j > 0 && temp < A[j-1]; j--) {
   
    //如果 temp 比前一个元素小,则移动数组
            A[j] = A[j-1];
        }
        A[j] = temp;                              //如果 temp 比前一个元素大,遍历下一个元素
    }
}

/**
* 这里是通过类似于冒泡交换的方式来找到插入元素的最佳位置。而传统的是直接比较,移动数组元素并最后找到合适的位置
*/
public static void InsertSort2(int[] A) {
   
    //A[] 是给定的待排数组
    for(int i = 0; i < A.length - 1; i++) {
   
      //遍历数组
        for(int j = i + 1; j > 0; j--) {
   
    //在有序的序列中插入新的关键字
            if(A[j] < A[j-1]) {
   
             //这里直接使用交换来移动元素
                int temp = A[j];
                A[j] = A[j-1];
                A[j-1] = temp;
            }
        }
    }
}

/**
* 时间复杂度:两个 for 循环 O(n^2) 
* 空间复杂度:占用一个数组大小,属于常量,所以是 O(1)
*/

1.2 折半插入排序

/*
* 从直接插入排序的主要流程是:1.遍历数组确定新关键字 2.在有序序列中寻找插入关键字的位置
* 考虑到数组线性表的特性,采用二分法可以快速寻找到插入关键字的位置,提高整体排序时间
*/
public static void BInsertSort(int[] A) {
   
   
    for(int i = 1; i < A.length; i++) {
   
   
        int temp = A[i];
        //二分法查找
        int low = 0;
        int high = i - 1;
        int mid;
        while(low <= high) {
   
   
            mid = (high + low)/2;
            if (A[mid] > temp) {
   
   
                high = mid - 1;
            } else {
   
   
                low = mid + 1;
            }
        }
        //向后移动插入关键字位置后的元素
        for(int j = i - 1; j >= high + 1; j--) {
   
   
            A[j + 1] = A[j];
        }
        //将元素插入到寻找到的位置
        A[high + 1] = temp;
    }
}

2. 希尔排序

希尔排序又称缩小增量排序,其本质还是插入排序,只不过是将待排序列按某种规则分成几个子序列,然后如同前面的插入排序一般对这些子序列进行排序。因此当增量为 1 时,希尔排序就是插入排序,所以希尔排序最重要的就是增量的选取

主要步骤是:

    1. 将待排序数组按照初始增量 d 进行分组
    1. 在每个组中对元素进行直接插入排序
    1. 将增量 d 折半,循环 1、2 、3步骤
    1. 待 d = 1 时,最后一次使用直接插入排序完成排序

/**
* 希尔排序的实现代码还是比较简洁的,除了增量的变化,基本上和直接插入序列没有区别
*/
public static void ShellSort(int[] A) {
   
   
    for(int d = A.length/2; d >= 1; d = d/2) {
   
        //增量的变化,从 d = "数组长度一半"到 d = 1
        for(int i = d; i < A.length; i++) {
   
           //在一个增量范围内进行遍历[d,A.length-1]
            if(A[i] < A[i - d]) {
   
                      //若增量后的元素小于增量前的元素,进行插入排序
                int temp = A[i];
                int j;
                for(j = i - d; j >= 0 && temp < A[j-d]; j -= d) {
   
    //对该增量序列下的元素进行排序
                    A[j + d] = A[j];                 //这里要使用i + d 的方式来移动元素,因为增量 d 可能大于数组下标
                }                                    //造成数组序列超出数组的范围
                A[j + d] = temp;
            }
        }
    }
}

复杂度分析

排序方法 空间复杂度 最好情况 最坏情况 平均时间复杂度
直接插入排序 O(1) O(n^2) O(n^2) O(n^2)
折半插入排序 O(1) O(nlog2n) O(n^2) O(n^2)
希尔排序 O(1) O(nlog2n) O(nlog2n) O(nlog2n)

二、交换类排序

交换,指比较两个元素关键字大小,来交换两个元素在序列中的位置,最后达到整个序列有序的状态。主要有冒泡排序和快速排序

3. 冒泡排序

冒泡排序就是通过依次比较序列中两个相邻元素的值,根据需要的升降序来交换这两个元素。最终达到整个序列有序的结果。

/**
* 冒泡排序
*/
public static void BubbleSort(int[] A) {
   
   
    for (int i = 0; i < A.length - 1; i++) {
   
           //冒泡次数,遍历数组次数,有序元素个数
        for(int j = 0; j < A.length - i - 1; j++) {
   
    //对剩下无序元素进行交换排序
            if(A[j] > A[j + 1]) {
   
   
                int temp = A[j];
                A[j] = A[j + 1];
                A[j + 1] = temp;
            }
        }
    }
}

4. 快速排序

快速排序实际上也是属于交换类的排序,只是它通过多次划分操作实现排序。这就是分治思想,把一个序列分成两个子序列它每一趟选择序列中的一个关键字作为枢轴,将序列中比枢轴小的移到前面,大的移到后边。当本趟所有子序列都被枢轴划分完毕后得到一组更短的子序列,成为下一趟划分的初始序列集。每一趟结束后都会有一个关键字达到最终位置。

 /**
     * 快速排序算是在冒泡排序的基础上的递归分治交换排序
     * @param A 待排数组
     * @param low 数组起点
     * @param high 数组终点
     */
    public static void QuickSort(int[] A, int low, int high) {
   
   
        if(low >= high) {
   
                                //递归分治完成退出
            return;
        }
        int left = low;                               //设置左遍历指针 left
        int right = high;                             //设置右遍历指针 right
        int pivot = A[left];                          //设置枢轴 pivot, 默认是数组最左端的值
        while(left < right) {
   
                            //循环条件
            while(left < right && A[right] >= pivot) {
   
   //若右指针所指向元素大于枢轴值,则右指针向左移动
                right--;
            }
            A[left] = A[right];                       //反之替换
            while (left < right && A[left] <= pivot) {
   
   //若左指针所指向元素小于枢轴值,则左指针向右移动
                left++;
            }
            A[right] = A[left];                       //反之替换
        }
        A[left] = pivot;                              //将枢轴值放在最终位置上
        QuickSort(A, low, left - 1);            //依此递归枢轴值左侧的元素
        QuickSort(A, left + 1, high);            //依此递归枢轴值右侧的元素
    }

复杂度分析

排序方法 空间复杂度 最好情况 最坏情况 平均时间复杂度
冒泡排序 O(1) O(n^2) O(n^2) O(n^2)
快速排序 O(log2n) O(nlog2n) O(n^2) O(nlog2n)

三、选择排序

选择排序就是每一趟从待排序列中选择关键字最小的元素,直到待排序列元素选择完毕。

5. 简单选择排序

/**
 * 简单选择排序
 * @param A 待排数组
 */
public static void SelectSort(int [] A) {
   
   
    for (int i = 0; i < A.length; i++) {
   
   
        int min = i;                             //遍历选择序列中的最小值下标
        for (int j = i + 1; j < A.length; j++) {
   
    //遍历当前序列选择最小值
            if (A[j] < A[min]) {
   
   
                min = j;
            }
        }
        if (min != i) {
   
                             //选择并交换最小值
            int temp = A[min];
            A[min] = A[i];
            A[i] = temp;
        }
    }
}

6.堆排序

堆是一种数据结构,可以把堆看成一颗完全二叉树,而且这棵树任何一个非叶结点的值都不大于(或不小于)其左右孩子结点的值。若父结点大子结点小,则这样的堆叫做大顶堆;若父结点小子结点大,则这样的堆叫做小顶堆。

堆排序的过程实际上就是将堆排序的序列构造成一个堆,将堆中最大的取走,再将剩余的元素调整成堆,然后再找出最大的取走。这样重复直至取出的序列有序。

排序主要步骤可以分为(以大顶堆为例):

(1) 将待排序列构造成一个大顶堆:BuildMaxHeap()

(2) 对堆进行调整排序:AdjustMaxHeap()

(3) 进行堆排序,移除根结点,调整堆排序:HeapSort()

/**
     * 堆排序(大顶堆)
     * @param A 待排数组
     */
public static void HeapSort(int [] A) {
   
   
    BuildMaxHeap(A);                            //建立堆
    for (int i = A.length - 1; i > 0; i--) {
   
       //排序次数,需要len - l 趟
        int temp = A[i];                        //将堆顶元素(A[0])与数组末尾元素替换,更新待排数组长度
        A[i] = A[0];
        A[0] = temp;
        AdjustMaxHeap(A, 0, i);                    //调整新堆,对未排序数组再次进行调整
    }
}

/**
     * 建立大顶堆
     * @param A 待排数组
     */
public static void BuildMaxHeap(int [] A) {
   
   
    for (int i = (A.length / 2) -1; i >= 0 ; i--) {
   
    //对[0,len/2]区间中的的结点(非叶结点)从下到上进行筛选调整
        AdjustMaxHeap(A, i, A.length);
    }
}

/**
     * 调整大顶堆
     * @param A 待排数组
     * @param k 当前大顶堆根结点在数组中的下标
     * @param len 当前待排数组长度
     */
public static void AdjustMaxHeap(int [] A, int k, int len) {
   
    
    int temp = A[k];
    for (int i = 2*k + 1; i < len; i = 2*i + 1) {
   
    //从最后一个叶结点开始从下到上进行堆调整
        if (i + 1 < len && A[i] < A[i + 1]) {
   
        //比较两个子结点大小,取其大值
            i++;
        }
        if (temp < A[i]) {
   
                             //若结点大于父结点,将父结点替换
            A[k] = A[i];                          //更新数组下标,继续向上进行堆调整
            k = i;
        } else {
   
   
            break;                                  //若该结点小于父结点,则跳过继续向上进行堆调整
        }
    }
    A[k] = temp;                                 //将结点放入比较后应该放的位置
}

复杂度分析

排序方法 空间复杂度 最好情况 最坏情况 平均时间复杂度
简单选择排序 O(1) O(n^2) O(n^2) O(n^2)
堆排序 O(1) O(log2n) O(nlog2n) O(nlog2n)

四、其他内部排序

7. 归并排序

归并排序是将多个有序表组合成一个新的有序表,该算法是采用分治法的一个典型的应用。即把待排序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为一个整体有序的序列。这里主要以二路归并排序来进行分析。

该排序主要分为两步:

    1. 分解:将序列每次折半拆分
    2. 合并:将划分后的序列两两排序并合并

private static int[] aux;          
/**
     * 初始化辅助数组 aux
     * @param A 待排数组
     */
public static void MergeSort(int [] A) {
   
   
    aux = new int[A.length];      
    MergeSort(A,0,A.length-1);
}

/**
     * 将数组分成两部分,以数组中间下标 mid 分为两部分依此递归
     * 最后再将两部分的有序序列通过 Merge() 函数 合并
      * @param A 待排数组
     * @param low 数组起始下标
     * @param high 数组末尾下标
     */
public static void MergeSort (int[] A, int low, int high) {
   
   
    if (low < high) {
   
   
        int mid = (low + high) / 2;
        MergeSort(A, low, mid);
        MergeSort(A, mid + 1, high);
        Merge(A, low, mid, high);
    }
}

/**
     * 将 [low, mid] 有序序列和 [mid+1, high] 有序序列合并 
     * @param A 待排数组
     * @param low 数组起始下标
     * @param mid 数组中间分隔下标
     * @param high 数组末尾下标
     */
public static void Merge (int[] A, int low, int mid, int high) {
   
   
    int i, j, k;
    for (int t = low; t <= high; t++) {
   
   
        aux[t] = A[t];
    }
    for ( i = low, j = mid + 1, k = low; i <= mid && j <= high; k++) {
   
   
        if(aux[i] < aux[j]) {
   
   
            A[k] = aux[i++];
        } else {
   
   
            A[k] = aux[j++];
        }
    }
    while (i <= mid) {
   
   
        A[k++] = aux[i++];
    }
    while (j <= high) {
   
   
        A[k++] = aux[j++];
    }
}

8. 基数排序

基数排序比较特别,它是通过关键字数字各位的大小来进行排序。它是一种借助多关键字排序的思想来对单逻辑关键字进行排序的方法。

它主要有两种排序方法:

  • 最高位优先法(MSD):按照关键字位权重高低依此递减来划分子序列
  • 最低位优先法(LSD) :按照关键字位权重低高依此增加来划分子序列

基数排序的思想:

  • 分配
  • 回收

/**
     * 找出数组中的最长位数
     * @param A 待排数组
     * @return MaxDigit 最长位数
     */
public static int MaxDigit (int [] A) {
   
   
    if (A == null) {
   
   
        return 0;
    }
    int Max = 0;
    for (int i = 0; i < A.length; i++) {
   
   
        if (Max < A[i]) {
   
   
            Max = A[i];
        }
    }
    int MaxDigit = 0;
    while (Max > 0) {
   
   
        MaxDigit++;
        Max /= 10;
    }
    return MaxDigit;
}

/**
     * 将基数排序的操作内化在一个二维数组中进行
     * @param A 待排数组
     */
public static void RadixSort(int [] A) {
   
   
    //创建一个二维数组,类比于在直角坐标系中,进行分配收集操作
    int[][] buckets = new int[10][A.length];
    int MaxDigit = MaxDigit(A);
    //t 用于提取关键字的位数
    int t = 10;
    //按排序趟数进行循环
    for (int i = 0; i < MaxDigit; i++) {
   
   
        //在一个桶中存放元素的数量,是buckets 二维数组的y轴
        int[] BucketLen = new int[10];
        //分配操作:将待排数组中的元素依此放入桶中
        for (int j = 0; j < A.length ; j++) {
   
   
            //桶的下标值,是buckets 二维数组的x轴
            int BucketIndex = (A[j] % t) / (t / 10);
            buckets[BucketIndex][BucketLen[BucketIndex]] = A[j];
            //该下标下,也就是桶中元素个数随之增加
            BucketLen[BucketIndex]++;
        }
        //收集操作:将已排好序的元素从桶中取出来
        int k = 0;
        for (int x = 0; x < 10; x++) {
   
   
            for (int y = 0; y < BucketLen[x]; y++) {
   
   
                A[k++] = buckets[x][y];
            }
        }
        t *= 10;
    }
}

复杂度分析

排序方法 空间复杂度 最好情况 最坏情况 平均时间复杂度
归并排序 O(n) O(nlog2n) O(nlog2n) O(nlog2n)
基数排序 O(rd) O(d(n+rd)) O(d(n+rd)) O(d(n+rd))

备注:基数排序中,n 为序列中的关键字数,d为关键字的关键字位数,rd 为关键字位数的个数

参考文章:

  1. Java 实现八大排序算法
  2. 《 2022王道数据结构》
  3. 《算法》
  4. 八种排序算法模板
  5. 基数排序就这么简单
目录
相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
65 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
3月前
|
负载均衡 NoSQL 算法
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
这篇文章是关于Java面试中Redis相关问题的笔记,包括Redis事务实现、集群方案、主从复制原理、CAP和BASE理论以及负载均衡算法和类型。
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
|
3月前
|
搜索推荐 算法 Java
手写快排:教你用Java写出高效排序算法!
快速排序(QuickSort)是经典的排序算法之一,基于分治思想,平均时间复杂度为O(n log n),广泛应用于各种场合。在这篇文章中,我们将手写一个Java版本的快速排序,从基础实现到优化策略,并逐步解析代码背后的逻辑。
141 1
|
1月前
|
算法 搜索推荐 Java
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
这篇文章介绍了如何使用Java后端技术,结合Graphics2D和Echarts等工具,生成包含个性化信息和图表的海报,并提供了详细的代码实现和GitHub项目链接。
103 0
java 后端 使用 Graphics2D 制作海报,画echarts图,带工具类,各种细节:如头像切割成圆形,文字换行算法(完美实验success),解决画上文字、图片后不清晰问题
|
1月前
|
算法 Java Linux
java制作海报一:java使用Graphics2D 在图片上写字,文字换行算法详解
这篇文章介绍了如何在Java中使用Graphics2D在图片上绘制文字,并实现自动换行的功能。
85 0
|
1月前
|
算法 Java 测试技术
数据结构 —— Java自定义代码实现顺序表,包含测试用例以及ArrayList的使用以及相关算法题
文章详细介绍了如何用Java自定义实现一个顺序表类,包括插入、删除、获取数据元素、求数据个数等功能,并对顺序表进行了测试,最后还提及了Java中自带的顺序表实现类ArrayList。
18 0
|
3月前
|
设计模式 缓存 算法
揭秘策略模式:如何用Java设计模式轻松切换算法?
【8月更文挑战第30天】设计模式是解决软件开发中特定问题的可重用方案。其中,策略模式是一种常用的行为型模式,允许在运行时选择算法行为。它通过定义一系列可互换的算法来封装具体的实现,使算法的变化与客户端分离。例如,在电商系统中,可以通过定义 `DiscountStrategy` 接口和多种折扣策略类(如 `FidelityDiscount`、`BulkDiscount` 和 `NoDiscount`),在运行时动态切换不同的折扣逻辑。这样,`ShoppingCart` 类无需关心具体折扣计算细节,只需设置不同的策略即可实现灵活的价格计算,符合开闭原则并提高代码的可维护性和扩展性。
63 2
|
3月前
|
安全 算法 Java
java系列之~~网络通信安全 非对称加密算法的介绍说明
这篇文章介绍了非对称加密算法,包括其定义、加密解密过程、数字签名功能,以及与对称加密算法的比较,并解释了非对称加密在网络安全中的应用,特别是在公钥基础设施和信任网络中的重要性。
|
3月前
|
算法 Java
LeetCode经典算法题:矩阵中省份数量经典题目+三角形最大周长java多种解法详解
LeetCode经典算法题:矩阵中省份数量经典题目+三角形最大周长java多种解法详解
50 6
|
3月前
|
存储 算法 Java
LeetCode经典算法题:打家劫舍java详解
LeetCode经典算法题:打家劫舍java详解
67 2