跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

Java 常见排序算法实现与原理分析

综述由AI生成系统讲解了 Java 中常见的排序算法,涵盖插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序(含霍尔法、挖坑法、前后指针法及优化)、归并排序(递归与非递归)以及非比较排序(计数、基数、桶排序)。内容包含各算法的核心思想、Java 代码实现、时间复杂度、空间复杂度及稳定性分析,适合算法学习与面试准备。

ApiHolic发布于 2026/3/29更新于 2026/6/134 浏览
Java 常见排序算法实现与原理分析

排序

  1. 稳定的排序:排序之前和排序之后它们俩的相对顺序没有发生变化
  2. 内部排序:在内存上的排序
  3. 外部排序:需要借助磁盘等外部空间进行的排序

插入排序

  1. 思想:假设这组数据的第一个元素是有序的,从第二个元素和前面的元素进行比较,找到合适的位置进行插入
// 插入排序
public static void insertSort(int[] array) {
    for (int i = 1; i < array.length; i++) {
        int j = i - 1;
        int tmp = array[i];
        for (; j >= 0; j--) {
            if (array[j] > tmp) {
                array[j + 1] = array[j];
            } else {
                break;
            }
        }
        array[j + 1] = tmp;
    }
}
分析
  1. 时间复杂度:O(N^2) 最坏情况:O(N^2) 最好情况:有序的数组,O(N) 数据越有序,排序越快 适用于待排序数组基本上趋于有序了,时间复杂度趋于 O(N)
  2. 空间复杂度:O(1)
  3. 稳定性:是一个稳定的排序 例子:5 5 7 稳定的排序可以改成不稳定的排序,但是不稳定的排序不能改成稳定的排序

希尔排序

  1. 对直接插入排序进行了优化,如果是 5 4 3 2 1 会退化为 O(N^2)
  2. 分组:分完组后,每组都采用直接插入排序
  3. 中间隔一些元素进行分组的好处:比较大的元素都往后走了,比较小的元素都往前走了
  4. 缩小增量到最后会把整体看成是一组,5 3 1 组,前面的 5 3 都是预排序,真正的排序是最后的一组
  5. 缩小增量的目的:为了让排序更接近 O(N),为了让排序更快
// 希尔排序
public static void shellSort(int[] array) {
    int gap = array.length;
    while (gap > 1) {
        gap /= 2;
        shell(array, gap);
    }
}

// 对每组进行插入排序
public static void shell(int[] array, int gap) {
    for (int i = gap; i < array.length; i++) {
        int j = i - gap;
        int tmp = array[i];
        for (; j >= 0; j -= gap) {
            if (array[j] > tmp) {
                array[j + gap] = array[j];
            } else {
                break;
            }
        }
        array[j + gap] = tmp;
    }
}
分析
  1. 时间复杂度:O(N^1.3 - N ^ 1.5),时间复杂度不好计算
  2. 空间复杂度:O(1)
  3. 稳定性:不稳定的排序

检测排序速度:

public static void testInsert(int[] array) {
    long startTime = System.currentTimeMillis();
    int[] tmp = Arrays.copyOf(array, array.length);
    Sort.insertSort(tmp);
    long endTime = System.currentTimeMillis();
    System.out.println(" " + (endTime - startTime));
}

public static void testShell(int[] array) {
    long startTime = System.currentTimeMillis();
    int[] tmp = Arrays.copyOf(array, array.length);
    Sort.shellSort(tmp);
    long endTime = System.currentTimeMillis();
    System.out.println(" " + (endTime - startTime));
}

// 逆序初始化
public static void initOrder(int[] array) {
    for (int i = 0; i < array.length; i++) {
        array[i] = array.length - i;
    }
}

public static void main(String[] args) {
    int[] arr = new int[10000];
    initOrder(arr);
    testInsert(arr);
    testShell(arr);
}

选择排序

在当前 i 下标对应的值的后面,找到后面最小的值和 i 下标对应的值交换

// 交换
public static void swap(int i, int j, int[] array) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

// 选择排序
public static void selectSort(int[] array) {
    // 在 i 下标的后面找到比 i 下标对应的值的最小值,然后交换
    int n = array.length;
    for (int i = 0; i < n; i++) {
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (array[j] < array[i]) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
        }
        swap(i, minIndex, array);
    }
}

双向选择排序 时间复杂度还是 O(N^2) 左边找最大的,右边找最小的,和第一个数和最后一个数交换

存在缺陷,maxIndex 下标可能是在 0 下标是最大的,0 下标会和最小值小标的值交换,那么 0 下标就不是最大值下标,应该更新为 maxIndex = minIndex

public static void selectSort2(int[] array) {
    // 在 i 下标的后面找到比 i 下标对应的值的最小值,然后交换
    int left = 0;
    int right = array.length - 1;
    while (left < right) {
        int minIndex = left;
        int maxIndex = left;
        for (int i = left + 1; i <= right; i++) {
            if (array[i] < array[minIndex]) {
                minIndex = i;
            }
            if (array[i] > array[maxIndex]) {
                maxIndex = i;
            }
        }
        swap(left, minIndex, array);
        // 第一个数是最大的数,防止最小的下标和第一个数换了,最大值就在 minIndex 的位置了
        if (maxIndex == left) {
            maxIndex = minIndex;
        }
        swap(right, maxIndex, array);
        left++;
        right--;
    }
}
分析
  1. 时间复杂度:O(N^2)
  2. 空间复杂度:O(1)
  3. 稳定性:不稳定的排序

堆排序

// 堆排序
private static void shiftDown(int[] array, int parent, int len) {
    int child = 2 * parent + 1;
    while (child < len) {
        if (child + 1 < len && array[child] < array[child + 1]) {
            child++;
        }
        if (array[child] > array[parent]) {
            swap(child, parent, array);
            parent = child;
            child = 2 * parent + 1;
        } else {
            break;
        }
    }
}

private static void createHeap(int[] array) {
    // 建立大根堆
    for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
        shiftDown(array, parent, array.length);
    }
}

public static void heapSort(int[] array) {
    createHeap(array);
    int end = array.length - 1;
    while (end > 0) {
        swap(end, 0, array);
        shiftDown(array, 0, end);
        end--;
    }
}
分析
  1. 时间复杂度:O(N * logN)
  2. 空间复杂度:O(1)
  3. 稳定性:不稳定的排序

冒泡排序

// 冒泡排序
public static void bubbleSort(int[] array) {
    // i 是趟数
    for (int i = 0; i < array.length; i++) {
        // j 是比较的大小的
        boolean flag = true;
        for (int j = 0; j < array.length - i - 1; j++) {
            if (array[j] > array[j + 1]) {
                swap(j, j + 1, array);
                flag = false;
            }
        }
        if (flag) {
            break;
        }
    }
}
分析
  1. 时间复杂度:O(N ^ 2)
  2. 空间复杂度:O(1)
  3. 稳定性:稳定的排序

快速排序

霍尔法
  1. 根据二叉树进行递归划分
// 快速排序
public static void quickSort(int[] array) {
    quick(array, 0, array.length - 1);
}

private static void quick(int[] array, int start, int end) {
    if (start >= end) {
        return;
    }
    int pivot = partitionHoare(array, start, end);
    quick(array, start, pivot - 1);
    quick(array, pivot + 1, end);
}

private static int partitionHoare(int[] array, int left, int right) {
    // 基准元素
    int tmp = array[left];
    // 记录第一个基准下标
    int i = left;
    while (left < right) {
        // 必须先找先右再左
        // 找小
        while (left < right && array[right] >= tmp) {
            right--;
        }
        // 找大
        while (left < right && array[left] <= tmp) {
            left++;
        }
        swap(left, right, array);
    }
    swap(i, left, array);
    return left;
}
  1. 为什么有等于号? 没有等于号,会死循环,比如两端都是 6

为什么先从右边开始,不先从左边开始? 先走左边的话,先遇到大的停下来,如果相遇的话,那么相遇位置的值就是大于基准元素的,这时候交换的话,6 的左边有一个数比 6 大

分析
  1. 时间复杂度: 最好情况下:O(N * logN) 每层都有 N 个节点,高度为 logN,需要每个节点都遍历到,N * logN 次遍历 最坏情况下:O(N^2),有序/逆序,单分支的树
  2. 空间复杂度: 最好情况下:O(logN) 最坏情况下:O(N),有序/逆序,单分支的树 递归左边再递归右边,递归右边左边没有释放
  3. 稳定性:不稳定的排序
挖坑法找基准
  1. 先走右边再走左边,以第一个元素为基准,并且拿出基准元素,基准元素的位置就是坑,如果右边找到比基准小的,把它放入坑中,左边找到比基准元素大的放到坑中,最后两者相遇,把基准元素放入其中
// 挖坑法
private static int partitionHole(int[] array, int left, int right) {
    // 基准元素
    int tmp = array[left];
    while (left < right) {
        // 必须先找先右再左
        // 找小
        while (left < right && array[right] >= tmp) {
            right--;
        }
        array[left] = array[right];
        // 找大
        while (left < right && array[left] <= tmp) {
            left++;
        }
        array[right] = array[left];
    }
    array[left] = tmp;
    return left;
}
前后指针法
  1. 如果 cur 比基准元素小并且 cur 下标的值和 prev 下标的值不相等
// 前后指针法
public static int partition(int[] array, int left, int right) {
    int prev = left;
    int cur = left + 1;
    while (cur <= right) {
        while (array[cur] < array[left] && array[++prev] != array[cur]) {
            swap(cur, prev, array);
        }
        cur++;
    }
    swap(prev, left, array);
    return prev;
}
题目
  1. 优先试挖坑法,其次是 Hoare,最后是前后指针法
快排的优化
  1. 均匀的分割数组
  2. 让递归的次数变少
三数取中法
  1. 三数取中法:left,right,mid 三个下标中的中间值和第一个数交换位置,然后右边找比基准元素小的值,左边找比基准元素大的值

规定 array[mid] <= array[left] <= array[right]

// 三数取中法,求中位数的下标
private static int middleNum(int[] array, int left, int right) {
    int mid = (left + right) / 2;
    if (array[left] < array[right]) {
        // 1. x < 3 < 9
        // 2. 3 < 9 < x
        // 3. 3 < x < 9
        if (array[mid] < array[left]) {
            return left;
        } else if (array[mid] > array[right]) {
            return right;
        } else {
            return mid;
        }
    } else {
        // 9 > 3 == left > right
        // 1. x > left > right
        if (array[mid] > array[left]) {
            return left;
        } else if (array[right] > array[mid]) {
            // 2. left > right > x
            return right;
        } else {
            // 3. left > x > right
            return mid;
        }
    }
}

private static void quick(int[] array, int start, int end) {
    if (start >= end) {
        return;
    }
    // 1 2 3 4 5 6 7
    // 中间值的下标和第一个数交换,作为新的基准元素
    int index = middleNum(array, start, end);
    swap(index, start, array);
    // 4 2 3 1 5 6 7
    // 为了让左右分布更均匀,降低树的高度,减少递归的次数
    int pivot = partition(array, start, end);
    quick(array, start, pivot - 1);
    quick(array, pivot + 1, end);
}

只剩最后几层时,使用插入排序进行优化,降低递归次数,可以使用插入排序是因为前面递归成有序的序列了

public static void insertSort(int[] array, int left, int right) {
    for (int i = left + 1; i <= right; i++) {
        int j = i - 1;
        int tmp = array[i];
        for (; j >= left; j--) {
            if (array[j] > tmp) {
                array[j + 1] = array[j];
            } else {
                break;
            }
        }
        array[j + 1] = tmp;
    }
}

private static void quick(int[] array, int start, int end) {
    if (start >= end) {
        return;
    }
    if (end - start + 1 <= 15) {
        // 减少递归的次数
        // 因为最后几层节点数最多,递归次数也多
        insertSort(array, start, end);
        return;
    }
    // 1 2 3 4 5 6 7
    // 中间值的下标和第一个数交换,作为新的基准元素
    int index = middleNum(array, start, end);
    swap(index, start, array);
    // 4 2 3 1 5 6 7
    // 为了让左右分布更均匀,降低树的高度,减少递归的次数
    int pivot = partition(array, start, end);
    quick(array, start, pivot - 1);
    quick(array, pivot + 1, end);
}
非递归实现快排
  1. 找基准里面会进行交换元素,进行排序,外面就进行找到左右下标位置
// 非递归实现快速排序
public static void quickSortNor(int[] array) {
    int start = 0;
    int end = array.length - 1;
    Stack<Integer> stack = new Stack<>();
    int pivot = partitionHoare(array, start, end);
    if (pivot - 1 > start) {
        // 左边有两个元素
        stack.push(start);
        stack.push(pivot - 1);
    } else if (pivot + 1 < end) {
        // 右边至少有两个元素
        stack.push(pivot + 1);
        stack.push(end);
    }
    // 找基准里面会互换元素进行排序
    while (!stack.empty()) {
        end = stack.pop();
        start = stack.pop();
        pivot = partitionHoare(array, start, end);
        if (pivot - 1 > start) {
            // 左边有两个元素
            stack.push(start);
            stack.push(pivot - 1);
        } else if (pivot + 1 < end) {
            // 右边至少有两个元素
            stack.push(pivot + 1);
            stack.push(end);
        }
    }
}

归并排序

  1. 分解:根据 mid 进行分解 合并:第一个有序段和第二个有序段,比较大小放入一个新的数组中
// 归并排序
public static void mergeSort(int[] array) {
    merge(array, 0, array.length - 1);
}

private static void merge(int[] array, int start, int end) {
    if (start >= end) {
        return;
    }
    int mid = (start + end) / 2;
    // 分解
    merge(array, start, mid);
    merge(array, mid + 1, end);
    // 合并
    mergeHeB(array, start, mid, end);
}

private static void mergeHeB(int[] array, int left, int mid, int right) {
    int s1 = left;
    int e1 = mid;
    int s2 = mid + 1;
    int e2 = right;
    // 新数组的下标
    int k = 0;
    int[] tmpArray = new int[right - left + 1];
    while (s1 <= e1 && s2 <= e2) {
        if (array[s1] < array[s2]) {
            tmpArray[k++] = array[s1++];
        } else {
            tmpArray[k++] = array[s2++];
        }
    }
    while (s1 <= e1) {
        tmpArray[k++] = array[s1++];
    }
    while (s2 <= e2) {
        tmpArray[k++] = array[s2++];
    }
    // left 是因为有左边还有右边的数组
    // tmpArray 是临时数组,会销毁的,需要拷贝回原来的数组
    for (int i = 0; i < tmpArray.length; i++) {
        array[i + left] = tmpArray[i];
    }
}
分析
  1. 时间复杂度:O(N * logN)
  2. 空间复杂度:O(N),因为每次合并数组的时候要开 O(N) 大小的额外空间
  3. 稳定性:稳定的排序
非递归实现归并排序
  1. 先一个一个有序,两个两个有序,四个四个有序,八个八个有序…
  2. gap = 1 left = i mid = left + gap - 1 right = mid + gap gap *= 2
  3. 每组合并完,最终就有序了
// 非递归实现归并排序
public static void mergeSortNor(int[] array) {
    // gap 表示每组的数据个数
    int gap = 1;
    while (gap < array.length) {
        for (int i = 0; i < array.length; i = i + 2 * gap) {
            int left = i;
            // mid 和 right 可能会越界
            // 比如只有 5 个元素
            // 分解右边一半的时候
            // l = i mid = 5 r = 7
            int mid = left + gap - 1;
            int right = mid + gap;
            if (mid >= array.length) {
                mid = array.length - 1;
            }
            if (right >= array.length) {
                right = array.length - 1;
            }
            mergeHeB(array, left, mid, right);
        }
        gap *= 2;
    }
}

海量数据的排序

  1. 使用归并排序进行外部排序,如果内存中不够空间的话

非比较的排序

计数排序
  1. 通过统计每次数字出现的次数,最后按照顺序从小到大输出这些数字
  2. 适用的场景:指定范围内的数据
// 计数排序,指定范围内的数据进行排序
// O(Max(N,范围))
public static void countSort(int[] array) {
    int minVal = array[0];
    int maxVal = array[0];
    // O(N)
    for (int i = 0; i < array.length; i++) {
        if (minVal > array[i]) {
            minVal = array[i];
        }
        if (maxVal < array[i]) {
            maxVal = array[i];
        }
    }
    int len = maxVal - minVal + 1;
    int[] count = new int[len];
    // O(N)
    for (int i = 0; i < array.length; i++) {
        count[array[i] - minVal]++;
    }
    // 写回 array 数组中
    // O(范围)
    // 因为 count 数组集中于一个数字,那么也是 O(范围)
    // 如果 count 数组不集中于一个数子,也是 N 倍的范围
    // 也是 O(范围)
    int k = 0;
    for (int i = 0; i < count.length; i++) {
        while (count[i] > 0) {
            array[k++] = i + minVal;
            count[i]--;
        }
    }
}
分析
  1. 时间复杂度:O(Max(N,范围))
  2. 空间复杂度:O(范围)
  3. 稳定性:稳定的排序
基数排序
  1. 开 10 个大小的空间,分别依次比较个位大小,十位大小和百位大小,最终达到有序,每个空间都是一个队列
桶排序
  1. 先把数字放入一个范围的区间内进行排序,排好序依次拿出来,最终就有序了

目录

  1. 排序
  2. 插入排序
  3. 分析
  4. 希尔排序
  5. 分析
  6. 选择排序
  7. 分析
  8. 堆排序
  9. 分析
  10. 冒泡排序
  11. 分析
  12. 快速排序
  13. 霍尔法
  14. 分析
  15. 挖坑法找基准
  16. 前后指针法
  17. 题目
  18. 快排的优化
  19. 三数取中法
  20. 非递归实现快排
  21. 归并排序
  22. 分析
  23. 非递归实现归并排序
  24. 海量数据的排序
  25. 非比较的排序
  26. 计数排序
  27. 分析
  28. 基数排序
  29. 桶排序
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Python 数据分析与可视化:折线图、柱状图与数据转置
  • Git 入门教程
  • Docker 跨平台安装与配置实战指南
  • 基于 FPGA 的数字频率计设计与实现
  • Java 项目实战:AI 辅助开发电商系统核心功能模块
  • Spring AI 实战:Java 原生接入大模型快速上手
  • Python 爬虫入门与分布式架构原理
  • OpenClaw 配合 cpolar 实现本地 AI 服务远程访问
  • 基于 cocotb 与 VCS 验证 Xilinx FPGA IP 核实战
  • C++ WinRT 中的异步
  • 基于 Dify+LangBot 实现飞书智能体对话机器人
  • AI 核心概念解析:Skill、MCP 与 Function Call
  • 前缀和与哈希表实战:解决和为 K 及整除子数组问题
  • Mac 系统下利用虚拟机搭建抖音直播推流环境
  • 滑动窗口经典算法面试题解析
  • 基于AIA改进的移相干涉抗振算法
  • Openclaw 与飞书多机器人配置指南
  • Ubuntu20.04 下使用 KITTI-07 数据集运行 LIO-SAM 及 EVO 评测
  • 大模型 Agent 开发指南:基于 LangChain 的实战应用
  • Seedance 2.0 多模态视频创作实战指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online