- 排序的基本概念:排序是指将一组数据重新排列顺序,通常按照某个特定的顺序进行排列。排序在计算机科学中是非常基础和重要的操作,被广泛应用于各种数据处理和算法中。其中排序包括内部排序和外部排序,内部排序是排序过程中,必须放在内存中的排序,外部排序是指在排序过程中无法全部放在内存中,必须在内存和外存之间移动的排序
- 插入排序:基本思想是每次将一个待排序的记录按其关键字大小插入到前面待排序的子序列中.
- 直接插入排序:直接插入排序(Straight Insertion Sort)是一种简单直观的排序算法,其工作原理是将待排序的元素一个一个地插入到已经排序好的有序序列中,从而获得一个新的有序序列。插入步骤如下:
1.将待排序的序列看作是一个有序序列,第一个元素就是有序序列的第一个元素。
2.从序列的第二个元素开始,将该元素插入到有序序列中合适的位置,使得插入后的序列仍然保持有序。
3.重复步骤2,直到所有元素都插入到有序序列中。 public static void insertSort(int[] arr){
for (int i=1;i<arr.length;i++){
int temp=arr[i];
int j;
//从i的前一个位置开始向前遍历,
// 若arr[j]>arr[i],则元素一直前移,直到找到合适的位置
for (j=i-1;j>=0&&arr[j]>temp;j--){
//元素前移
arr[j+1]=arr[j];
}
//找到合适的位置
arr[j+1]=temp;
}
}
- 折半插入排序:它利用了二分查找(Binary Search)的思想来提高元素插入过程中查找合适位置的效率。插入步骤如下
1.将待排序序列存放在数组中
2.设置low和high的搜索指针,通过二分查找更新mid值找到合适的插入位置
3.将待插入位置向后移动,找到正确的位置然后插入 public static void binaryInsertSort(int[] arr){
int length = arr.length;
//从第二个元素开始遍历
for (int i=1;i<length;i++){
int key=arr[i];
int low=0;
int high = i-1;
//找插入位置
while (low<=high){
int mid=low+(high-low)/2;
if (arr[mid]>key){
high=mid-1;
}else{
high=mid+1;
}
}
//找到合适位置后将left到i-1位置上的数据全部向后移动
for(int j=i-1;j<=low;j--){
arr[j+1]=arr[j];
}
arr[low]=key;
}
}
- 希尔排序:希尔排序(Shell Sort)是插入排序的一种优化版本,也称为缩小增量排序。它的基本思想是将待排序的序列划分成若干个子序列,每个子序列内部完成插入排序,随后逐步缩小子序列的间隔,直到间隔为1,也就是普通的插入排序。步骤如下:
1.确定间隔序列
2.按照间隔序列进行分组
3.缩小间隔序列,重复步骤2.直到间隔为1 public static void shellSort(int[] arr){
int length=arr.length;
//初始化间隔
int gap=length/2;
//当间隔大于零时按序列排序并减少间隔
while (gap>0){
//按序列进行插入排序
for (int i=gap;i<length;i++){
//待排序数据
int temp=arr[i];
int j;
//从小到大排序
for (j=i;j>=gap&&arr[j-gap]>temp;j-=gap){
//元素向前移动找到插入位置
arr[j]=arr[j-gap];
}
arr[j]=temp;
}
//缩小间隔序列
gap/=2;
}
}
- 交换排序
- 冒泡排序:冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。步骤如下:
1.比较相邻的元素,如果他们位置错误,就把它们交换位置
2.重复步骤二直到没有需要再交换 public static void bubbleSort(int[] arr){
//设置标志位,若为true,则表示已经排好序
boolean swapped;
for (int i=0;i<arr.length-1;i++){
swapped=false;
//每一趟排序都有一个最大值到最后一个位置
for (int j=0;j<arr.length-1-i;j++){
if (arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
//交换位置说明还有元素顺序不对
swapped=true;
}
}
//一趟遍历完所有数据后,若swapped为false,则说明已经排好序
if (!swapped){
break;
}
}
}
- 快速排序:快速排序(Quick Sort)是一种高效的排序算法。它的基本思想是将待排序的序列分成两个子序列,使得左子序列中的所有元素都小于等于基准元素,右子序列中的所有元素都大于等于基准元素。然后递归地对左右子序列进行快速排序。步骤如下:
1.选择一个基准元素
2.将元素分为两个子序列,一个子序列比基准元素大,另一个子序列比基准元素小
3.对这两个子序列进行递归的快速排序 public static int partition(int[] arr,int low,int high){
//设置末尾元素为基准元素
int pivot=arr[high];
//分界索引
int i=low-1;
for (int j=low;j<high;j++){
//将所有小于基准元素的值全部都放在左边
if (arr[j]<=pivot){
i++;
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
//准备将pivot枢纽值放在中间
int temp=arr[i+1];
arr[i+1]=arr[high];
arr[high]=temp;
//返回枢纽值的索引
return i+1;
}
public static void quickSort(int[] arr,int low,int high){
if (low<high){
int pivot=partition(arr,low,high);
//递归排序左子序列
quickSort(arr,low,privot-1);
//递归排序右子序列
quickSort(arr,privot+1,high);
}
}
- 简单选择排序:选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,将其与待排序的数据序列的最前面(或最后面)的元素进行交换,然后缩小待排序数据序列的范围,直到全部待排序的数据元素都排好序为止。
public static void selectSort(int[] arr){
int n=arr.length;
for (int i=0;i<n-1;i++){
int minIndex=i;
//找到最小值索引
for (int j=i+1;j<n;j++){
//找到最小值
if (arr[j]<arr[minIndex]){
minIndex=j;
}
}
//交换位置
int temp=arr[minIndex];
arr[minIndex]=arr[i];
arr[i]=temp;
}
}
- ==堆排序:==堆排序是一种基于比较的排序算法,其基本思想是将待排序的序列构造成一个大顶堆(或小顶堆),然后将堆顶元素与堆尾元素交换,将堆的大小减一,再调整堆,重复这个过程直到堆的大小为1。步骤如下:
1.构建大顶堆,将待排序序列构建成一个大顶堆,从叶节点开始进行堆化操作
2.交换堆顶元素和堆尾元素,从而将最大值放在序列的末尾
3.调整堆,将堆的大小减一,然后从刚刚的位置继续进行堆化操作
4.重复步骤2,3直到堆的大小为1. //堆排序
public static void heapSort(int[] arr){
//找到第一个非叶结点,开始调整
for (int i=arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
//将最后一个与第一个交换,然后调整
for (int i=arr.length-1;i>0;i--){
int temp=arr[i];
arr[i]=arr[0];
arr[0]=temp;
adjustHeap(arr,0,i);
}
}
//将一个数组序列调整成大顶堆
/**
*函数功能:将以i为根的子树调整为大顶堆
* @param arr 待调整数组
* @param i 表示非叶结点在数组中的索引
* @param length 表示堆多少个元素进行调整,length逐渐减少
*/
public static void adjustHeap(int[] arr,int i,int length){
//取出当前元素的值
int temp=arr[i];
//调整节点,k初始值为左子结点
for (int k=2*i+1;k<length;k=k*2+1){
//判断左子结点和右子节点的大小
if (k+1<length&&arr[k]<arr[k+1]){
k++;//将k指向更大的节点
}
if (arr[k]>temp){
arr[i]=arr[k];//将较大的值赋给当前节点
i=k;
}else {
break;
}
}
arr[i]=temp;
}
- 归并排序:是一种采用分治策略的非原地、稳定排序算法。它将待排序的序列分成两半,分别对每一半进行排序,然后将两个已排序的半序列合并成一个完整的有序序列。这种递归地将大问题分解为小问题,再将小问题的解合并为大问题的解的方法体现了分治思想。算法步骤:
1.将待排序序列化为两个子序列,递归的对这两个子序列进行递归排序
2.递归的终止条件是子序列长度为1,此时只有一个元素
3.合并,将两个子序列合并,方法是创建一个临时数组,从两个子序列开始的地方开始比较,每次取较小者放入数组中,当一个序列为空时,将另一个序列全部放入数组后面 public static void mergeSort(int[] arr,int left,int right){
if (left<right){
//获取中间值的索引
int mid=(left+right)>>>1;//相当于/2
//递归排序左子序列
mergeSort(arr,left,mid);
//递归排序右子序列
mergeSort(arr,mid+1,right);
merge(arr,left,mid,right);
}
}
public static void merge(int[] arr,int left,int mid,int right){
//获取两部分序列的大小
int n1=mid-left+1;
int n2=right-mid;
//创建临时数组,存放两个子序列的元素
int[] L = new int[n1];
int[] R = new int[n2];
//将两个子序列元素复制到临时数组
System.arraycopy(arr,left,L,0,n1);
System.arraycopy(arr,mid+1,R,0,n2);
//初始化索引
int i=0,j=0,k=left;
//比较并合并
while (i<n1&&j<n2){
if (L[i]<=R[j]){
arr[k++]=L[i++];
}else {
arr[k++]=R[j++];
//将剩余的元素复制到arr
while (i<n1)
arr[k++]=L[i++];
while (j<n2)
arr[k++]=R[j++];
}
}
}
- 基数排序:基数排序是一种非比较型整数排序算法,特别适用于对整数或可以表示为整数形式的字符串进行排序。其主要思想是将待排序的元素按照位数(即“基数”)进行逐位分解,然后按照每一位上的数字分别进行排序。当所有位数上的排序完成后,整个序列就会变得有序。
步骤:
1.确定最大位数,这是遍历过程中需要遍历的最大基数
2.初始化桶,创建与当前元素位数相等的桶
3.分配元素到桶,当前位数为1时,将元素分配到0-9的桶里
4.收集桶没数据,将元素依次取出
5.递增位数,重复之前的步骤 //基数排序
/**
* 辅助函数:
* 创建桶函数creatBuckets
* 获取位数函数getMaxDigits
* 获取指定位函数getDigit
* @param arr
*/
public static void radixSort(int[] arr){
if (arr==null||arr.length==0){
return;
}
//获取最大值以及位数
int maxVal = Arrays.stream(arr).max().getAsInt();
int maxDigits=getMaxDigits(maxVal);
//创建桶集合
List<List<Integer>> buckets = creatBuckets(10);
//根据个位,十位,等递增循环的分配元素到桶中,再重新取出,再分配
for (int digit=1;digit<=maxDigits;digit++){
//分配元素到桶中
for (int num:arr){
//取出指定数位的值
int bucketIndex = getDigit(num, digit);
buckets.get(bucketIndex).add(num);
}
//一个数位分配完毕,重新取出元素
int index=0;
for (List<Integer> bucket:buckets){
for (int num:bucket){
arr[index++]=num;
}
//清空每个桶
bucket.clear();
}
}
}
//根据大小创建桶
public static List<List<Integer>> creatBuckets(int size){
List<List<Integer>> buckets =new ArrayList<>();
for (int i=0;i<size;i++){
buckets.add(new ArrayList<>());
}
return buckets;
}
//获取这个数有几位
public static int getMaxDigits(int num){
int digits=0;
while (num>0){
num/=10;
digits++;
}
return digits;
}
/**
*
* @param num 数字
* @param digit 目标位1表示个位,2表示十位,以此类推
* @return 获取数字在其指定位上的数字
*/
public static int getDigit(int num,int digit){
//对数取10的余数得到的就是个位上的值,获取地n位的值就应该让此数除以10的n-1次方
return (num/(int)Math.pow(10,digit-1))%10;
}
- 桶排序:桶排序(Bucket Sort)是一种分布式排序算法,其基本思想是将待排序的元素分配到若干个“桶”中,每个桶内元素的范围相对较小,然后对每个桶分别进行排序(通常使用其他适合的小规模排序算法),最后将所有桶中的元素合并成有序序列。以下是桶排序的主要步骤:
1.确定桶的数量
2.计算元素所在桶后,分派元素到桶
3.对每个桶内元素进行排序
4.合并元素到桶 public static void bucketSort(int[] arr){
//1.初始化桶,找到最大最小值
int minValue=Integer.MIN_VALUE;
int maxValue=Integer.MAX_VALUE;
for (int num:arr){
minValue=Math.min(minValue,num);
maxValue=Math.max(maxValue,num);
}
//2.初始化桶,创建桶的集合
int bucketSize=(maxValue-minValue)/arr.length-1;
ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketSize);
//初始化桶集合
for (int i=0;i<bucketSize;i++){
buckets.add(new ArrayList<>());
}
//3.分配元素到桶
for (int num:arr){
//计算元素所属桶的索引
int bucketIndex=(num-minValue)/bucketSize;
//将元素添加到对应的桶中
buckets.get(bucketIndex).add(num);
}
//4.对每个桶进行排序
for (ArrayList<Integer> bucket:buckets){
Integer[] array = (Integer[]) bucket.toArray();
int[] array1 = Arrays.stream(array).mapToInt(Integer::intValue).toArray();
//选择排序
insertSort(array1);
}
//5.合并桶内元素
int index = 0;
for (ArrayList<Integer> bucket:buckets){
for (int num:bucket){
arr[index++]=num;
}
}
}
- 计数排序:是一种非比较型整数排序算法,特别适用于对整数或有限范围内的非负整数进行排序。该算法的核心思想是通过统计每个元素的出现次数,直接计算出每个元素在输出数组中的位置,从而实现排序。计数排序是一种牺牲空间换取时间的算法,尤其在待排序元素值域范围不大且分布均匀时,其效率较高。算法步骤是:
1.统计元素频度,通过最大值-最小值确定值域范围.
2.遍历待排序数组,统计数量在count数组中
3.遍历数组,按一个方向遍历,每遍历一次,count[index]-1;
4.返回输出数组 public static void countSort(int[] arr){
//获取数组最大和最小值
int maxVal = Arrays.stream(arr).max().getAsInt();
int minVal = Arrays.stream(arr).min().getAsInt();
//初始化数组
int range = maxVal - minVal + 1;
int[] count = new int[range];
//统计每个元素出现次数
for (int num:arr){
count[num-minVal]++;
}
//计算前驱元素累计次数
for (int i=1;i<count.length;i++){
count[i]+=count[i-1];
}
//构造输出数组
int[] resultSort = new int[arr.length];
//保证计数排序的稳定性,让重复元素的相对位置不变化
//从后向前遍历,因为下标从零开始,对应元素位置的元素频度-1就是最终元素的位置
for (int i=arr.length-1;i>=0;i--){
int num=arr[i];
//元素减去偏移量是在count数组的下标,对应count[countIndex]是,小于countIndex值的累加量
int countIndex = num - minVal;
//在最终排序序列中的下标是count[countIndex]-1;
resultSort[--count[countIndex]]=num;
}
//将排序结果复制回原来的数组中
System.arraycopy(resultSort,0,arr,0,arr.length);
}
- 排序算法的比较
|
适用场景 |
最好时间复杂度 |
最坏时间复杂度 |
平均时间复杂度 |
空间复杂度 |
稳定性 |
插入排序 |
数据规模小,近乎有序 |
O(n) |
O(n²) |
O(n²) |
O(1)(原地排序) |
稳定 |
希尔排序 |
数据规模较大,且初始状态无明显有序特征 |
取决于所选增量序列,最优情况下可达到O(n log n) |
赖于增量序列的选择 |
赖于增量序列的选择 |
O(1)(原地排序) |
不稳定 |
冒泡排序 |
数据规模小,或近乎有序的数组 |
O(n)-已排序 |
O(n²)-逆序 |
O(n²) |
O(1)(原地排序) |
稳定 |
快速排序 |
大规模,无特殊性质 |
O(n log n) |
(已排序或逆序)为O(n²) |
O(n log n) |
O(log n)(递归栈空间) |
不稳定 |
选择排序 |
数据规模小,对稳定性要求不高 |
O(n²) |
O(n²) |
O(n²) |
O(1)(原地排序) |
不稳定 |
堆排序 |
对数据稳定性要求不高,且数据规模较大的情况 |
O(n log n) |
O(n log n) |
O(n log n) |
O(1)(原地排序) |
不稳定 |
归并排序 |
对稳定性有要求,且数据规模适中,内存空间充足的情况 |
O(n log n) |
O(n log n) |
O(n log n) |
O(n)(需额外空间存储子数组) |
稳定 |
基数排序 |
数据为整数或可以表示为整数的字符串,且位数相差不大时,尤其是数据规模大但k相对较小的情况,k为最大元素的位数(或字符长度) |
O(nk),其中n为元素个数,k为最大元素的位数 |
O(nk) |
O(nk) |
O(n+k) |
稳定 |
桶排序 |
数据均匀分布且能确定合适的桶划分规则时,尤其是数据规模大且能有效利用数据分布特性的场合 |
取决于桶内排序算法,理想情况下桶内排序为O(1),总体为O(n + k) |
取决于桶内排序算法 |
取决于桶内排序算法 |
O(n + k) |
取决于桶内排序算法的稳定性 |
计数排序 |
数据范围有限且值分布均匀,且k远小于n时,其中n为元素个数,k为最大值与最小值之差 |
O(n+k) |
O(n+k) |
O(n+k) |
O(k) |
稳定 |