用 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 时,希尔排序就是插入排序,所以希尔排序最重要的就是增量的选取。
主要步骤是:
-
- 将待排序数组按照初始增量 d 进行分组
-
- 在每个组中对元素进行直接插入排序
-
- 将增量 d 折半,循环 1、2 、3步骤
-
- 待 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. 归并排序
归并排序是将多个有序表组合成一个新的有序表,该算法是采用分治法的一个典型的应用。即把待排序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为一个整体有序的序列。这里主要以二路归并排序来进行分析。
该排序主要分为两步:
-
- 分解:将序列每次折半拆分
- 合并:将划分后的序列两两排序并合并
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 为关键字位数的个数
参考文章:
- Java 实现八大排序算法
- 《 2022王道数据结构》
- 《算法》
- 八种排序算法模板
- 基数排序就这么简单