当前位置: 首页 > news >正文

[Java数据结构与算法]详解排序算法

目录

一、引言:排序的重要性

排序的概念

排序的定义

稳定性

内部排序和外部排序

二、常见排序算法原理与实现

2.1 插入排序

2.1.1 直接插入排序

算法实现思路

代码实现

性能分析

2.1.2 希尔排序

算法实现思路

代码实现

性能分析

2.2 选择排序

2.2.1 直接选择排序

算法实现思路

代码实现

性能分析

算法优化

2.2.2 堆排序

算法实现思路

代码实现

性能分析

2.3 交换排序

2.3.1 冒泡排序

算法实现思路

代码实现

性能分析

算法优化

2.3.2 快速排序

算法实现思路

Hoare法

代码实现

Δ挖坑法

代码实现

前后指针法

代码实现

性能分析

算法优化

非递归实现快速排序

算法实现思路

代码实现

2.4 归并排序

算法实现思路

代码实现

性能分析

非递归实现归并排序

算法实现思路

代码实现

2.5 计数排序(非基于比较的排序)

算法实现思路

代码实现

性能分析


一、引言:排序的重要性

排序是计算机科学中最基础且重要的主题之一,无论是学术研究还是实际开发,都离不开排序算法的应用。

本文将系统介绍常用且重要的排序算法,分析它们的性能特点。

排序的概念

排序的定义

排序是将一串记录按照某个或某些关键字的大小,递增或递减排列起来的操作。

简单来说,就是将一组无序的数据变成有序的过程

稳定性

稳定性是排序算法的重要特性。

假设在待排序序列中存在多个相同关键字的记录,如果排序后这些记录的相对次序保持不变,则称该算法是稳定的;否则称为不稳定。

例如:序列 [9, 5a, 2, 7, 5b, 8] 经过稳定排序后,5a仍然在5b之前

内部排序和外部排序

  • 内部排序:数据元素全部放在内存中进行排序
  • 外部排序:数据量太大,无法全部放入内存,需要在内外存之间移动数据

二、常见排序算法原理与实现

因为比较排序算法离不开大小比较,因此小编先把交换方法swap写在前面:

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

2.1 插入排序

2.1.1 直接插入排序

算法实现思路
  1. 先把第一个元素视作已有序的元素
  2. 从第二个元素开始,与前面的元素进行大小比较:
  3. 若比前面的元素小,就交换
  4. 若比前面的元素大,就停止交换

图示如下:

代码实现
// 直接插入排序
public static void insertSort (int[] arr) {for (int i = 1; i < arr.length; i++) {int temp = arr[i];int j = i - 1;for (; j >= 0; j--) {if (arr[j] > temp)arr[j+1] = arr[j];else {arr[j+1] = temp;break;}}arr[j+1] = temp;}
}
性能分析
  • 时间复杂度:O(N²) —— 元素集合越接近有序,效率越高
  • 空间复杂度:O(1)
  • 稳定性:稳定

2.1.2 希尔排序

算法实现思路

是对直接插入排序的优化

分组插入排序+缩小增量gap。

当gap>=1,属于预排序。

希尔排序采用跳跃式分组(按照gap进行分组),好处是能够把大的数据放到更靠后的位置,随着分的组数越来越少,数据逐渐趋于有序

  1. 每一次按照数据长度的一半来分组,每一组交替进行插入排序
  2. 当gap=1时,就全部排序完毕

图示如下:

代码实现
// 希尔排序
public static void shellSort (int[] arr) {// 让gap等于数据的长度int gap = arr.length;while (gap > 1) {// 每次按照gap的一半进行分组gap /= 2;// 每组进行直接插入排序shell(arr,gap);}
}
private 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;}
}
性能分析
  • 时间复杂度:约为O(n^1.25)到O(1.6*n^1.25)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

2.2 选择排序

2.2.1 直接选择排序

算法实现思路
  1. 遍历数据,第一次默认第一个元素是最小的然后往后面走;当遇到比前面认定最小元素还小的值就记录下标;遍历完数据后再将两个值调换
  2. 当遍历走完,数据就有序了

图示如下:

代码实现
// 直接选择排序
public static void selectSort2 (int[] arr) {for (int i = 0; i < arr.length; i++) {// 默认第i个数据最小int minIndex = i;for (int j = i+1; j < arr.length; j++) {// 进行比较,找到比min还小的数if (arr[j] < arr[minIndex])minIndex = j;}// 执行到这里时,已经找到/或者没有更小的数// 进行交换操作swap(arr,i,minIndex);}
}
性能分析
  • 时间复杂度:O(N²) —— 不管数据本身是否有序,都是O(N²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
算法优化
  1. 遍历数据,默认第一个元素(left所在位置的值)是最小值和最大值;往后遍历数据的同时找到数据中的最小值和最
  2. 大值,并记录到 minlndex 和 maxlndex;然后把最小值放到第一个位置,把最大值放到最
  3. 后位置
  4. 注意当数据的第一个数就是最大数时,最大值就变成minlndex了,需要改一下

图示如下:

// 优化版
public static void selectSort (int[] arr) {int left = 0;int right = arr.length - 1;while (left < right) {// 默认left所在位置的值是最小值和最大值int minIndex = left;int maxIndex = left;// 找到数据中的最小值和最大值for (int i = left+1; i <= right; i++) {if (arr[i] < arr[minIndex])minIndex = i;if (arr[i] > arr[maxIndex])maxIndex = i;}// 把最小值放到第一个位置swap(arr,left,minIndex);// 当数据的第一个数就是最大值时,由于先换了最小值,所以此时第一个数在minIndex位置if (maxIndex == left)maxIndex = minIndex;// 把最大值放到最后位置swap(arr,right,maxIndex);left++;right--;}
}

2.2.2 堆排序

算法实现思路
  • 升序(从小到大)——> 建立大根堆
  • 降序 (从大到小) ——> 建立小根堆

升序为例:

  1. 将堆顶元素(即下标为0的元素)和堆底元素end交换
  2. 向下调整,每一次调整end都要减一,堆从后往前就逐渐由大到小排序了
  3. 当end大于0时才进行以上操作,否则结束循环

图示如下:

代码实现
// 堆排序
public static void heapSort (int[] arr) {// 创建堆createHeap(arr);// 标记的最后元素的位置,每一次调整后都要减一int end = arr.length - 1;while (end > 0) {// 将第一个元素和最后元素交换swap(arr,0,end);// 向下调整以第一个元素为堆顶的堆shiftDown(arr,0,end);// 标记最后元素位置的end自减1end--;}
}
private static void createHeap(int[] arr) {for (int parent = (arr.length-2)/2; parent >= 0; parent--) {// 向下调整建堆shiftDown(arr,parent,arr.length);}
}
private static void shiftDown(int[] arr, int parent, int length) {// param:                  目标数据    起始范围      结束范围int child = parent * 2 + 1;while (child < length) {// 找到较大的数:确保下标位置合法if ((child+1)<length && arr[child]<arr[child+1]) {child++;}// 与parent的值比较if (arr[child] > arr[parent]) {swap(arr,child,parent);// 往子树走parent = child;child = parent * 2 + 1;} else {break;}}
}
性能分析
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

2.3 交换排序

2.3.1 冒泡排序

算法实现思路
  1. 第一次从数组的第一个元素开始遍历数据直到最后,两两与相邻的元素比较大小;
  2. 第二次遍历数据直到倒数第二个元素,因为最后元素已经在第一趟排好位置了
  3. 循环此操作就可得到升序的数据

图示如下:

代码实现
// 冒泡排序
public static void bubbleSort2 (int[] arr) {// i控制比较的趟数for (int i = 0; i < arr.length-1; i++) {// j控制每一趟比较的次数for (int j = 0; j < arr.length-1-i; j++) {if (arr[j] > arr[j+1]) {swap(arr,j,j+1);}}}
}
性能分析
  • 时间复杂度:O(N²) ——> 优化以后可能达到O(N)
  • 空间复杂度:O(1)
  • 稳定性:稳定
算法优化
  1. 使用一个布尔变量,当数据交换一次就标记
  2. 若数据本身有序就可以避免多次遍历和比较了
// 优化版
public static void bubbleSort (int[] arr) {// i控制比较的趟数for (int i = 0; i < arr.length-1; i++) {// 每一次排序时都默认标记有序,表示数据是有序的boolean isOrder = true;// j控制每一趟比较的次数for (int j = 0; j < arr.length-1-i; j++) {if (arr[j] > arr[j+1]) {swap(arr,j,j+1);// 一旦交换一次,就改变标记,表示数据是无序的isOrder = false;}}// 若标记始终有序,就直接退出比较if (isOrder) {break;}}
}

2.3.2 快速排序

算法实现思路
  1. 采用分治策略,选取一个基准值,将序列划分成左右两部分:左边均小于基准值,右边均大于基准值
  2. 然后递归处理左右子序列

有三种划分方法:

  1. Hoare法:左右指针向中间扫描
  2. 挖坑法:将基准值保存,形成坑位
  3. 前后指针法:使用前后两个指针进行划分

小编在这里逐一配图来给读者解析

Hoare法
  1. 使用两个引用(left和right) 分别从前后往中间遍历数据;以数据第一个元素为基准,从后查找比该元素小的数,从前查找比该元素大的数然后交换这两个数;当两个引用相遇时把基准与相遇时位置(pivot)的值进行交换,该位置的左边全是比它小的数,右边全是比它大的数
  2. 接着开始递归遍历以相遇点位置(pivot)为根的二叉树,每一棵子树都重复 找基准值并划分 的操作,直到遍历完全部数据

图示如下:

代码实现
// hoare法
public static void quickSortHoare (int[] arr) {quickHoare(arr,0,arr.length-1);
}
private static void quickHoare(int[] arr, int start, int end) {// 当范围不合法就退出if (start >= end)return;// 将数据以基准值划分int pivot = patitionHoare(arr,start,end);// 递归quickHoare(arr,start,pivot-1);quickHoare(arr,pivot+1,end);
}
private static int patitionHoare (int[] arr, int left, int right) {// 基准值int base = arr[left];int baseIndex = left;// 当两个引用还没相遇时进行操作while (left < right) {// 若值没有基准值小if (left<right && arr[right]>=base) {right--;}// 若值没有基准值大if (left<right && arr[left]<=base) {left++;}// 交换min和max的值swap(arr,left,right);}// 当两个引用相遇时,将基准值与相遇位置的值交换swap(arr,baseIndex,left);return left;
}

Δ挖坑法
  1. 与Hoare法不同的是:先暂时存储基准值于临时变量temp, 当从后遍历的引用 right 找到数后,直接将其与从前遍历的引用 left 所在位置的数交换;
  2. 当从前遍历的引用 left 找到数后,将其放到从后遍历的引用 right 所在位置;
  3. 当两个引用相遇,把基准值放到相遇位置(pivot)

图示如下:

代码实现
// 挖坑法
private static int patition (int[] arr, int left, int right) {// 把基准值暂存至tempint temp = arr[left];// 当两个引用还没相遇时进行操作while (left < right) {// 若值没有基准值小while (left<right && arr[right]>=temp) {right--;}// 将最小的数放到left位置arr[left] = arr[right];// 若值没有基准值大while (left<right && arr[left]<=temp) {left++;}// 将最大的数放到right位置arr[right] = arr[left];}// 当两个引用相遇时,将基准值放到相遇位置arr[left] = temp;return left;
}

前后指针法
  1. 使用两个引用(prev和cur) 从前往后遍历数据。保证 prev 位置的值都是比基准值小的数:当cur位置的值比基准数小并且cur和prev+1不在同一位置,就将两个位置的值进行交换
  2. 当cur遍历完数据,将prev位置的值与基准值交换

图示如下:

代码实现
// 前后指针法
private static int patitionPCPtr (int[] arr, int left, int right) {// 定义两个引用int prev = left;int cur = left + 1;// 合法范围内进行操作while (cur <= right) {// 找到比基准值大的数if (arr[cur]<arr[left] && arr[++prev]!=arr[cur]) {swap(arr,prev,cur);}cur++;}// 将prev的值和基准值交换swap(arr,prev,left);return prev;
}
性能分析
  • 时间复杂度:最好情况O(N*logN) 最坏情况O(N2)
  • 空间复杂度:最好情况O(logN) 最坏情况O(N)
  • 稳定性:不稳定
算法优化

由于快速排序是递归进行的,当数据量过大时,不断递归可能会导致栈溢出。

因此,需要优化递归——减少递归的次数。

有两种方法可以减少递归的次数:

  1. 三数取中法:找到left和right值的中位数,然后以中位数作为基准值,目的是减少单分支递归
  2. 直接插入法 (针对一定范围):对二叉树的后两层(小区间)使用插入排序而不使用递归

三数取中法:

  1. 分两种大情况:start < end ; start > end
  2. 每一种大情况又分为三种小情况:mid < start/end < end/start ; start/end< mid < end/start; start/end <  end/start < mid

如图:

优化后的完整快速排序算法:

public static void quickSort (int[] arr) {quick(arr,0,arr.length-1);
}
private static void quick(int[] arr, int start, int end) {// 当范围不合法就退出if (start >= end)return;// 递归到小范围的数据时,使用直接插入排序,减少递归的次数if (end-start+1 <= 7) {insertSortRange(arr,start,end);return;}// 三数取中找中位数并以中位数位基准值int midIndex = getMiddleNum(arr,start,end);swap(arr,start,midIndex);// 将数据以基准值划分int pivot = patition(arr,start,end);// 递归quick(arr,start,pivot-1);quick(arr,pivot+1,end);
}
private static int patition (int[] arr, int left, int right) {// 把基准值暂存至tempint temp = arr[left];// 当两个引用还没相遇时进行操作while (left < right) {// 若值没有基准值小while (left<right && arr[right]>=temp) {right--;}// 将最小的数放到left位置arr[left] = arr[right];// 若值没有基准值大while (left<right && arr[left]<=temp) {left++;}// 将最大的数放到right位置arr[right] = arr[left];}// 当两个引用相遇时,将基准值放到相遇位置arr[left] = temp;return left;
}
// 三数取中法
private static int getMiddleNum(int[] arr, int start, int end) {int mid = (start + end) / 2;if (arr[start] < arr[end]) {if (arr[mid] < arr[start]) {return start;} else if (arr[mid] > arr[end]) {return end;} else {return mid;}} else {if (arr[mid] > arr[start]) {return start;} else if (arr[mid] < arr[end]) {return end;} else {return mid;}}
}
// 直接插入法(针对一定范围)
private static void insertSortRange (int[] arr, int start, int end) {for (int i = start+1; i <= end; i++) {int temp = arr[i];int j = i - 1;for (; j >= start; j--) {if (arr[j] > temp)arr[j+1] = arr[j];else {arr[j+1] = temp;break;}}arr[j+1] = temp;}
}
非递归实现快速排序
算法实现思路
  1. 以 基准值 为界划分数组之后,把 pivot 左边部分的 start 和 end 压入栈中(当pivot>start+1),再压右边部分的(当pivot<end-1)
  2. 只要栈不为空就取出两个栈顶元素并进行partition划分

图示如下:

代码实现
// 非递归快速排序
public static void quickSortNonTra (int[] arr) {quickNonTra(arr,0,arr.length-1);
}
private static void quickNonTra(int[] arr, int start, int end) {Stack<Integer> stack = new Stack<>();// 找到基准值int pivot = patition(arr,start,end);// 若基准值的左边/右边至少有两个元素,就需要排序,故入栈if (pivot > start+1) {stack.push(start);stack.push(pivot-1);}if (pivot < end-1) {stack.push(pivot+1);stack.push(end);}// 当栈不为空就持续排序while (!stack.isEmpty()) {// 弹出的第一个元素是end,第二个元素是startend = stack.pop();start = stack.pop();// 找到基准值pivot = patition(arr,start,end);// 若基准值的左边/右边至少有两个元素,就需要排序,故入栈if (pivot > start+1) {stack.push(start);stack.push(pivot-1);}if (pivot < end-1) {stack.push(pivot+1);stack.push(end);}}
}

2.4 归并排序

算法实现思路
  1. 先递归分解
  2. 再合并:合并两个有序数组

图示如下:

合并的具体操作详见链表面试题中的:合并两个有序链表,思路是一样的。

代码实现
// 归并排序
public static void mergeSort (int[] arr) {splitAndMerge(arr,0,arr.length-1);
}
private static void splitAndMerge(int[] arr, int left, int right) {// 分解if (left >= right)return;int mid = (left + right) / 2;splitAndMerge(arr,left,mid);splitAndMerge(arr,mid+1,right);// 合并merge(arr,left,mid,right);
}
private static void merge(int[] arr, int left, int mid, int right) {int[] ret = new int[arr.length];int k = 0;int fs = left;//int fe = mid;int ls = mid + 1;//int le = right;// 当两个有序数组都不为空while (fs<=mid && ls<=right) {if (arr[fs] < arr[ls]) {ret[k++] = arr[fs++];} else {ret[k++] = arr[ls++];}}while (fs <= mid) {ret[k++] = arr[fs++];}while (ls <= right) {ret[k++] = arr[ls++];}// 此时ret数组已经存入全部数据,将ret数组接入arr数组for (int i = 0; i < k; i++) {arr[i+left] = ret[i];}
}

性能分析

  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(N)
  • 稳定性:稳定

非递归实现归并排序

算法实现思路
  1. 第一次每一个数都是一组有序的数,然后第二次两个看成有序一组,以此类推依次乘二,当达到数组长度就结束
  2. 每一次定义三个下标left、mid和right, 通过下标实现合并数组操作
  3. 注意检查下标是否越界

图示如下:

合并操作传送:合并两个有序链表

代码实现
// 非递归归并排序
public static void mergeSortNonTra (int[] arr) {// 最开始每一个元素看成一组有序的数组int gap = 1;// 确保gap不超过数组长度while (gap < arr.length) {// 遍历数据,每次走i+2*gap步for (int i = 0; i < arr.length; i = i+2*gap) {int left = i;int mid = left + gap - 1;// 判断mid是否合法if (mid >= arr.length) {mid = arr.length - 1;}int right = mid + gap;// 判断right是否合法if (right >= arr.length) {right = arr.length - 1;}// 合并数组merge(arr,left,mid,right);}// 每一轮结束后让gap乘2gap *= 2;}
}

2.5 计数排序(非基于比较的排序)

算法实现思路
  1. 使用一个计数数组存储0~9数字每个数字出现的次数(将每个数字减去最小值的结果作为计数数组的下标)
  2. 然后按照计数数组按顺序打印每个数字对应的个数
  3. 注意:1. 场景:数据集中在某个范围内,对于太发散的数据不适用;2. 需要先观察数据的最大和最小值,再计算计数数组的长度len = max - min+ 1

图示如下:

代码实现
// 计数排序(非基于比较的排序)
public static void countSort (int[] arr) {// 1.找数据中的最大值和最小值,然后确定 计数数组 的长度int minVal = arr[0];int maxVal = arr[0];for (int i = 0; i < arr.length; i++) {if (arr[i] < minVal) {minVal = arr[i];}if (arr[i] > maxVal) {maxVal = arr[i];}}// 计算 计数数组 的长度int len = maxVal - minVal + 1;int[] count = new int[len];// 2.遍历原数组,通过 计数数组 统计每一个数个位数字出现的个数for (int i = 0; i < arr.length; i++) {count[arr[i]-minVal]++;}// 3.遍历计数数组,按顺序覆盖原数组int index = 0;for (int i = 0; i < count.length; i++) {while (count[i] != 0) {arr[index++] = i + minVal;count[i]--;}}
}

性能分析

  • 时间复杂度:O(N + k),k为数据范围
  • 空间复杂度:O(k)
  • 稳定性:稳定

至此,八大排序就全部讲解完毕啦,若有不正确的,请尽管指出!

希望读者朋友们能够学到知识~


http://www.dtcms.com/a/507358.html

相关文章:

  • 工业级时序数据库选型指南:技术架构与场景化实践
  • 精选五款电脑USB接口控制软件,助您高效管控USB端口
  • 有个性的个人网站js打开网站
  • tesla 2025 年在自动驾驶投入 多少钱
  • 做调查报告的网站钟点工
  • 在 Vue 3.5 中优雅地集成 wangEditor,并定制“AI 工具”下拉菜单(总结/润色/翻译)
  • 【YOLO模型】(4)--YOLO V3超超超超详解
  • idea 的全局的配置的修改
  • 永久免费云服务器推荐电子商务网站优化方案
  • Altium Designer(AD24)IEEE Symbols按钮总结
  • 阿里云k8s1.33部署yaml和dockerfile配置文件
  • 有口碑的盐城网站建设wordpress配置ip访问
  • LINUX15--进程间的通信-信号量
  • 在 Linux 内核中加载驱动程序(一)
  • yarn面试题
  • Android跨进程通信: Binder 进程间通信机制解析
  • 【Day 80】Linux-虚拟化
  • 建设厅官方网站网站主题的分类
  • 广州营销网站建设公司php网站开发实例项目
  • Kubernetes 核心概念解析与集群部署实战(基于 Docker+Flannel)
  • 【课堂笔记】复变函数-6
  • OpenBMC: BmcWeb处理WebScoket1 判断是否为WebSocket
  • 操作系统学习 进程(1)进程的概念与状态
  • Genie Envisioner--智元机器人--世界模型框架--2025.8.7
  • 权威网站有哪些wordpress分类目录浏览权限
  • Vue 缓存之坑,变量赋值方式和响应式数据
  • AWS CloudFormation —— 自动化部署的“云中脚本大师”
  • 响应式网站下载长沙排名推广
  • 【软考备考】 安全协议:SSL/TLS, IPSec 详解
  • 在 HarmonyOS 中平滑切换“点状粒子”与“图片粒子”(含可运行 ArkTS 示例)