Java算法之排序

目录

[1. 排序的概念及引用](#1. 排序的概念及引用)

[2. 常见排序算法的实现](#2. 常见排序算法的实现)

[2.1 插入排序](#2.1 插入排序)

[2.1.2 直接插入排序](#2.1.2 直接插入排序)

[2.1.3 希尔排序( 缩小增量排序 )​编辑](#2.1.3 希尔排序( 缩小增量排序 )编辑)

[2.2.2 直接选择排序:](#2.2.2 直接选择排序:)

[2.2.3 堆排序](#2.2.3 堆排序)

2.3.1冒泡排序

[2.3.2 快速排序](#2.3.2 快速排序)

[2.4 归并排序](#2.4 归并排序)

[3 七大排序算法](#3 七大排序算法)

[3.1 核心参数汇总表](#3.1 核心参数汇总表)

[3.2 场景选型指南](#3.2 场景选型指南)

[4 非基于比较的排序算法补充](#4 非基于比较的排序算法补充)

[5.1 计数排序](#5.1 计数排序)

[5.2 桶排序](#5.2 桶排序)

[5.3 基数排序](#5.3 基数排序)

最后


**1.**排序的概念及引用

1.1 排序的概念
排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j] ,且 r[i] 在 r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳 定的;否则称为不稳定的。

内部排序 :数据元素全部放在内存中的排序。
外部排序 :数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

**2.**常见排序算法的实现

2.1****插入排序

2.1.2****直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。

我们默认第一个元素是有序的,从第二个开始遍历,定义一个i下标从第二个开始,定义一个j下标每次等于i-1,i在1下标的位置,j就是i-1,那么我们就把i下标元素放到temp零时变量里面,随后我们开始i下标之前元素和temp比较大小,这里面我们定义的j下标,j下标指定的元素如果比temp大,我们就把arr[j+1]=arr[j]即往后移动,然后j--,直到j<0零时停止,这种排序方法类似于打牌的插入排序

java 复制代码
public class Sort {
    public static void insort(int[] arr) {
        for(int i=1;i<arr.length;i++){
            int temp=arr[i];
            for(int j=i-1;j>=0;j--){
                if(temp>arr[j]){
                    arr[j+1]=arr[j];
                }
                else{
                    arr[j+1]=temp;
                    break;
                }
            }
        }
    }
}

我们的最坏时间复杂度是O(N2),空间复杂度是O(1) 最好时间复杂度是O(N)

2.1.3希尔排序(****缩小增量排序 )

希尔排序法又称缩小增量法。希尔排序法的基本思想是: 先选定一个整数,把待排序文件中所有记录分成多个组, 所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达 =1 时,所有记录在统一组内排好序

java 复制代码
    public static void shellsort(int []arr) {
        int gap=arr.length;
        while(gap>1){
             gap=gap/2;
             shell(arr,gap);
        }
    }
    public static void shell(int[] arr,int gap) {
        for(int i=gap;i<arr.length;i++){
            int temp=arr[i];
            int j=i-gap;
            for( ;j>=0;j-=gap){
                if(arr[j]>temp){
                    arr[j+gap]=arr[j];
                }
                else{
                    arr[j+gap]=temp;
                    break;
                }
            }
            arr[j+gap]=temp;
        }
    }

2.2.2直接选择排序:

在元素集合 array[i]--array[n-1] 中选择关键码最大 ( 小 ) 的数据元素 若它不是这组元素中的最后一个( 第一个 ) 元素,则将它与这组元素中的最后一个(第一个)元素交换 在剩余的array[i]--array[n-2] ( array[i+1]--array[n-1] )集合中,重复上述步骤,直到集合剩余 1 个元素

java 复制代码
  private static void swap1(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
    public static void SelectSort(int[] arr) {
     for(int i=0;i<arr.length;i++){
         int mindex=i;
         for(int j=i+1;j<arr.length;j++){
         if(arr[j]<arr[mindex]){
             mindex=j;
         }
         }
         swap1(arr,i,mindex);
     }

    }

直接选择排序的特性总结

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度: O(N^2)
  3. 空间复杂度: O(1)
  4. 稳定性:不稳定

2.2.3****堆排序

堆排序 (Heapsort) 是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆
来进行选择数据。 需要注意的是排升序要建大堆,排降序建小堆。
直接选择排序的特性总结

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度: O(N*logN)
  3. 空间复杂度: O(1)
  4. 稳定性:不稳定

2.3.1****冒泡排序

一开始将i下标放到tmp里面,随后j在i后面个开始遍历,如果有arr[i]>arr[tmp],就把tmp=j,遍历结束以后,找到这一轮最小值,放到0下标那里,随后我们让i+ +,j在i后面去寻找最小的,然后和i下标的数交换,然后依次这样下去,一直到最后一个
for(int i=0;i<arr.length-1;i+ +)

for(int j=i+1;j<arr.length-1-i;j+ +)

java 复制代码
   public static void bubbleSort(int[] arr) {
for(int i=0;i<arr.length;i++){
    boolean flag=true;
    for(int j=0;j<arr.length-i-1;j++){
        if(arr[j]>arr[j+1]){
            int temp=arr[j];
            arr[j]=arr[j+1];
            arr[j+1]=temp;
            flag=false;
        }
        if(flag){
            break;
        }
    }
}
   }


冒泡排序的特性总结

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度: O(N^2)
  3. 空间复杂度: O(1)
  4. 稳定性:稳定
2.3.2 快速排序

快速排序是 Hoare 于 1962 年提出的二叉树结构的交换排序算法,也是面试中最高频的考点,被称为 "20 世纪十大算法之一"。核心思想 基于分治法的核心思想:

  1. 任取待排序序列中的一个元素作为基准值(pivot);
  2. 遍历序列,将所有小于基准值的元素放到基准值左边,所有大于基准值的元素放到基准值右边,完成一次分区(partition),此时基准值就落在了它最终的排序位置上;
  3. 对基准值左右两个子序列,递归重复上述分区操作,直到所有子序列长度为 1,整个序列就完成了排序。这里实现三种经典的 partition 方法:
  4. Hoare 原版(左右指针法)
java 复制代码
// Hoare版 左右指针分区
private static int partitionHoare(int[] array, int left, int right) {
    int pivot = array[left]; // 选最左元素为基准值
    int i = left;
    int j = right;
    while (i < j) {
        // 右指针往左找小于基准值的元素
        while (i < j && array[j] >= pivot) {
            j--;
        }
        // 左指针往右找大于基准值的元素
        while (i < j && array[i] <= pivot) {
            i++;
        }
        // 交换两个元素
        swap(array, i, j);
    }
    // 基准值归位
    swap(array, left, i);
    return i; // 返回基准值的最终索引
}
  1. 挖坑法
java 复制代码
// 挖坑法 分区
private static int partitionHole(int[] array, int left, int right) {
    int pivot = array[left]; // 基准值,初始坑位在left
    int i = left;
    int j = right;
    while (i < j) {
        // 右指针往左找小于基准值的元素,填到左坑位
        while (i < j && array[j] >= pivot) {
            j--;
        }
        array[i] = array[j]; // 填坑,j位置变成新坑位
        // 左指针往右找大于基准值的元素,填到右坑位
        while (i < j && array[i] <= pivot) {
            i++;
        }
        array[j] = array[i]; // 填坑,i位置变成新坑位
    }
    array[i] = pivot; // 基准值填入最终坑位
    return i;
}
  1. 前后指针法
java 复制代码
// 前后指针法 分区
private static int partitionPoint(int[] array, int left, int right) {
    int prev = left;
    int cur = left + 1;
    int pivot = array[left];
    while (cur <= right) {
        // 找到小于基准值的元素,prev后移,交换
        if (array[cur] < pivot && array[++prev] != array[cur]) {
            swap(array, cur, prev);
        }
        cur++;
    }
    // 基准值归位
    swap(array, left, prev);
    return prev;
}

快速排序递归实现

java 复制代码
// 快速排序 递归主方法
public static void quickSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    quickSortInternal(array, 0, array.length - 1);
}

// 递归内部方法
private static void quickSortInternal(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    // 分区,获取基准值索引
    int pivotIndex = partitionHole(array, left, right);
    // 递归排序左子序列
    quickSortInternal(array, left, pivotIndex - 1);
    // 递归排序右子序列
    quickSortInternal(array, pivotIndex + 1, right);
}

快速排序优化方案

  1. 三数取中法选基准值:避免在序列有序 / 逆序时,选最左 / 最右元素作为基准值导致快排退化为 O (n²),从序列的左、中、右三个位置选中间值作为基准值。
  2. 小区间使用插入排序:当递归到子序列长度很小时(比如长度 < 10),序列已经近乎有序,此时直接插入排序的效率高于递归快排,同时减少递归深度。
  3. 非递归实现:用栈模拟递归过程,避免递归深度过大导致栈溢出。

快速排序非递归实现

java 复制代码
import java.util.Stack;

// 快速排序 非递归实现
public static void quickSortNonR(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    Stack<Integer> stack = new Stack<>();
    int left = 0;
    int right = array.length - 1;
    stack.push(left);
    stack.push(right);
    while (!stack.isEmpty()) {
        right = stack.pop();
        left = stack.pop();
        if (left >= right) {
            continue;
        }
        int pivotIndex = partitionHole(array, left, right);
        // 右子区间入栈
        stack.push(pivotIndex + 1);
        stack.push(right);
        // 左子区间入栈
        stack.push(left);
        stack.push(pivotIndex - 1);
    }
}

核心特性总结

  1. 时间复杂度:最好 / 平均情况 O (n*logn),最坏情况(序列有序 / 逆序,基准值选择不当)O (n²)
  2. 空间复杂度:主要是递归栈的开销,平均 O (logn),最坏 O (n)
  3. 稳定性:不稳定排序
  4. 特点:综合性能最优,实际开发中应用最广泛,因此得名 "快速排序"。

2.4 归并排序

归并排序是建立在归并操作上的高效排序算法,也是分治法的经典应用,是海量数据外部排序的核心方案。

核心思想

  1. 分解:将待排序序列递归拆分成两个子序列,直到每个子序列只有 1 个元素(天然有序);
  2. 合并:将两个有序的子序列合并成一个更大的有序序列,逐层向上合并,最终得到完整的有序序列,这个过程称为二路归并。
java 复制代码
// 归并排序 主方法
public static void mergeSort(int[] array) {
    if (array == null || array.length <= 1) {
        return;
    }
    mergeSortInternal(array, 0, array.length - 1);
}

// 递归分解+合并
private static void mergeSortInternal(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    // 取中间值,拆分左右子序列(避免溢出,不直接用(left+right)/2)
    int mid = left + (right - left) / 2;
    // 递归拆分左半部分
    mergeSortInternal(array, left, mid);
    // 递归拆分右半部分
    mergeSortInternal(array, mid + 1, right);
    // 合并两个有序子序列
    merge(array, left, mid, right);
}

// 合并两个有序区间 [left,mid] 和 [mid+1,right]
private static void merge(int[] array, int left, int mid, int right) {
    // 临时数组,存放合并后的结果
    int[] tmp = new int[right - left + 1];
    int k = 0; // 临时数组的索引
    int s1 = left; // 左区间起始索引
    int s2 = mid + 1; // 右区间起始索引
    // 两个区间都有元素时,按大小合并
    while (s1 <= mid && s2 <= right) {
        if (array[s1] <= array[s2]) {
            tmp[k++] = array[s1++];
        } else {
            tmp[k++] = array[s2++];
        }
    }
    // 处理左区间剩余元素
    while (s1 <= mid) {
        tmp[k++] = array[s1++];
    }
    // 处理右区间剩余元素
    while (s2 <= right) {
        tmp[k++] = array[s2++];
    }
    // 将临时数组的有序结果拷贝回原数组
    for (int i = 0; i < tmp.length; i++) {
        array[left + i] = tmp[i];
    }
}

海量数据外部排序方案归并排序的核心优势,是能解决内存无法容纳全量数据的排序问题。比如内存只有 1G,需要排序 100G 的数据,解决方案如下:

  1. 先把 100G 的大文件切分成 200 份,每份 512M,内存可完整容纳;
  2. 分别对每个 512M 的小文件执行内部排序,生成 200 个有序小文件;
  3. 对 200 份有序文件执行多路归并,最终合并成一个全局有序的大文件,完成排序。

核心特性总结

  1. 时间复杂度:最好 / 最坏 / 平均情况都是 O (n*logn),性能极其稳定
  2. 空间复杂度:O (n),需要额外的临时数组存储合并结果,非原地排序
  3. 稳定性:稳定排序
  4. 适用场景:不仅适合内存中的内部排序,更是海量数据外部排序的唯一核心方案。

3 七大排序算法

3.1 核心参数汇总表

排序算法 最好时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定
直接插入排序 O(n) O(n²) O(n²) O(1) 稳定
直接选择排序 O(n²) O(n²) O(n²) O(1) 不稳定
希尔排序 O(n) O(n^1.3) O(n²) O(1) 不稳定
堆排序 O(n*logn) O(n*logn) O(n*logn) O(1) 不稳定
快速排序 O(n*logn) O(n*logn) O(n²) O(logn)~O(n) 不稳定
归并排序 O(n*logn) O(n*logn) O(n*logn) O(n) 稳定

3.2 场景选型指南

  1. 小规模数据、近乎有序的序列:优先选直接插入排序,效率甚至高于快排;
  2. 追求稳定排序、对空间不敏感:优先选归并排序,其次是冒泡 / 插入排序;
  3. 大规模数据、追求极致平均性能:优先选快速排序,工业界首选;
  4. 大规模数据、要求性能稳定、O (1) 空间:优先选堆排序,尤其适合 TopK 场景;
  5. 海量数据、无法全量加载到内存:必须用归并排序实现外部排序。

我们日常开发中,最常用的就是 JDK 自带的排序方法Arrays.sort(),它的底层实现充分结合了各个排序算法的优势,做到了极致的性能适配:

  1. 对于基本数据类型(int、long、float 等):底层使用双轴快速排序(Dual-Pivot Quicksort),是快排的优化版,性能更优。因为基本类型不关注稳定性,只追求排序效率。
  2. 对于引用数据类型(Object):底层使用TimSort算法,这是归并排序和插入排序的混合优化版,是稳定排序。因为引用类型需要保证排序的稳定性,避免破坏对象的原有相对顺序。

另外,集合的排序Collections.sort(),底层也是调用了Arrays.sort()方法,核心实现完全一致。

4 非基于比较的排序算法补充

除了上述七大基于比较的排序,还有一类不通过元素比较来排序的算法,在特定场景下效率远超基于比较的排序,时间复杂度可以突破 O (n*logn) 的理论下限。

5.1 计数排序

核心思想 :又称鸽巢原理,是哈希直接定址法的变形应用。先统计每个元素出现的次数,再根据统计结果将元素按顺序回写到原序列中。适用场景 :元素的取值范围非常集中的场景,比如年龄排序、成绩排序(0-100 分)。核心特性:时间复杂度 O (MAX (N, 数据范围)),空间复杂度 O (数据范围),稳定排序。

5.2 桶排序

核心思想 :先定义一组 "桶",每个桶对应一个数值范围,将待排序元素分配到对应的桶中,对每个桶内的元素单独排序,最后按桶的顺序将元素依次取出,得到有序序列。适用场景:数据分布相对均匀,取值范围可预估的场景,比如海量日志的时间排序。

5.3 基数排序

核心思想 :按元素的每一位数字依次排序,先按最低位排序,再按次低位排序,直到最高位,最终得到有序序列。适用场景:整数、字符串等可按位拆分的定长数据排序。

最后

排序算法是算法学习的入门,也是进阶的基石。学习排序算法,绝不是死记硬背代码,而是要理解每个算法的核心思想、设计逻辑和适用场景,对于面试而言,快速排序、归并排序、堆排序是绝对的高频考点,必须做到能手写实现、能分析复杂度、能讲清优化方案;对于实际开发而言,我们要理解 JDK 排序 API 的底层逻辑,根据业务场景选择合适的排序方案。

相关推荐
Ricky111zzz2 小时前
leetcode学python记录2
python·算法·leetcode·职场和发展
java1234_小锋2 小时前
Java高频面试题:Redis是单线程还是多线程?
java·redis·面试
查古穆2 小时前
二分查找-搜索二维矩阵
算法
工具罗某人2 小时前
docker实现redis-cluster模式集群部署
java·redis·docker
会编程的土豆2 小时前
【数据结构与算法】堆排序
开发语言·数据结构·c++·算法·leetcode
会编程的土豆2 小时前
【数据结构与算法】希尔排序
数据结构·c++·算法·排序算法
邦爷的AI架构笔记2 小时前
GLM-5.1 接入踩坑记录:用免费开源模型搭个 AI 代码审计小工具
后端·算法
苏宸啊2 小时前
哈希扩展问题
算法·哈希算法
摇滚侠2 小时前
从 Vibe Coding 到 Spec Coding:研发范式演进与高质量交付
java·人工智能·ai编程