提到数据结构与算法,无法避免的一点就包含排序,熟练的掌握各种排序算法则是一个程序员必备的素质之一,除此之外,排序算法也是当下各大技术公司比较喜欢问的技术点,所以,就这一点青山整理了常见的8种排序算法,希望可以给诸位一点点的参考,有什么错误问题或者更好的解法,欢迎大家在评论区留言,小编一定不遗余力的学习与改正。
排序算法的分类 | 排序算法 | 最好时间 | 最坏时间 | 平均时间 | 辅助空间 | 稳定性 | 备注 |
---|---|---|---|---|---|---|---|
交换排序 | 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 | n小时较好 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不稳定 | n大时较好 | |
插入排序 | 直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 | 大部分元素已经有序 |
希尔排序 | O(n) | O(nlogn) | O(n^k)(1<k<2) | O(1) | 不稳定 | K为所选分组 | |
选择排序 | 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 | n小时较好 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 | n大时较好 | |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | n大时较好 | |
基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+k)) | O(n+k) | 稳定 | 元素大小差别大时较好 |
-
定义
在开始撸代码之前,对于以下几个概念还是要做一个简单的解释的哈,方便理解。
时间复杂度:简单理解的话就是对一个数据规模为n的数据排序所需要的操作次数
空间复杂度:简单理解就是执行排序算法时所需要的存储空间。其中O(1):空间复杂度不随n大小而改变时,是一个常量级别的空间复杂度;O(n):算法的空间复杂度与n成线性比例时的表示;O(logn):一个算法的空间复杂度与以2为底的对数成正比的时候的一种表示,2x = n,那么x就等于log2n;
稳定性:所谓的稳定性就是排序前后,2个相等的元素的相对位置没有发生改变,那么这种排序算法就是稳定的,如: 排序前为A1,A2;排序后则还是A1,A2(A1=A2)。
-
算法
一、冒泡排序(交换排序)
所谓冒泡排序,通俗理解就是通过不断的将相邻的元素进行两两比较和交换,使得关键字较小的记录像水中气泡一样,不断上浮到最左侧;关键字较大的记录像石头一样不断下沉到最右侧,每一次就有一个最大的关键字下沉到最右侧,经过n趟相同操作,使得整个序列有序。代码如下:
import java.util.Arrays;
/**
* 常用排序算法之冒泡排序:
* 算法描述:冒泡排序,不用比喻成泡泡上浮,个人觉得直接上定义,浅显易懂,就是对于一个拥有N个元素的无序序列
* 进行至少N-1次的排序(最后一个元素,单个元素直接是有序的,故不需要再排),每次排序的过程都是相邻的两个元素进行比较,
* 替换操作,因此,每一次至少会比较出一个最大值,
* 而这个最大值就会落在序列的一头,然后对剩下的序列进行同样的操作,直到排序完成。
* 最好时间复杂度:O(n^2),整个序列已经排好队,一次遍历搞定
* 最坏的时间复杂度:O(n^2),
* 平均的时间复杂度:O(n^2),
* 空间复杂度:O(1);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:稳定
*
* //将ArrayList中元素转为int数组中元素
* //int[] arr = list.stream().mapToInt(Integer::intValue).toArray();
*
* 备注:n较小时排序排序效率较好。
*/
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
//冒泡排序(实现算法就这么多代码)
public static void bubbleSort(int[] arr){
//控制一共需要多少轮比较
for (int i = 0; i < arr.length-1; i++) {
//每次比较完,放在最后的元素已经是上轮比较的最大值了,故不需要再作比较
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;
}
}
}
}
}
二、快速排序(交换排序)
所谓快速排序,其实就是冒泡排序的升级版,二者都属于交换排序的范畴,不同的是,快排采用了分治思想,选出一个参照数之后,从头部和尾部分别开始与参照数比较,小于参照数的放在左侧,大于参照数的放在右侧,这样就类似二分的将一个序列分成了更小的两个序列,然后依次对参照数左侧和右侧的序列再进行递归快排,直到所有记录都有序为止。代码如下:
import java.util.Arrays;
/**
* 常用排序算法之快速排序:
* 算法描述:快速排序,所谓快速排序,其实就是冒泡排序的升级版,二者都属于交换排序的范畴,
* 不同的是,快速排序采用了二分思想,选出一个参照数之后,从头部和尾部开始分别与标准数
* 这样,每遍历一次就会将相对参照数小和大的数,分别分散到了参照数的两侧,后面递归左侧和
* 右侧,分别进行如上排序,直到全部有序。
* 最好时间复杂度:O(nlog(n));
* 最坏的时间复杂度:O(n^2),
* 平均的时间复杂度:O(nlog(n)),
* 空间复杂度:O(log(n));
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:不稳定
* 备注:n大时比较好
*/
public class QuickSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
//快速排序
public static void quickSort(int[] arr,int start,int end){
if(start < end){
int min = arr[start];
int low = start;
int high = end;
while(low < high){
while (low < high && min<=arr[high]){
high--;
}
//当不满足的high座标对应的数,肯定比min小,故将其赋值到low位置(min左侧)
arr[low] = arr[high];
while(low < high && min >= arr[low]){
low++;
}
arr[high] = arr[low];
}
arr[low] = min;
//排序min左侧的序列
quickSort(arr,start,low);
//排序min右侧的序列
quickSort(arr,low+1,end);
}
}
}
三、直接插入排序(插入排序)
所谓直接插入排序,就是给定一组记录,初始假设第一个记录自成一个有序序列,其余的序列为无序序列,接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中,直到最后一个记录插入到有序序列中为止。代码如下:
/**
* 常用排序算法之插入排序:
* 算法描述:插入排序,把待排序的记录,按照其关键字的大小,逐个插入到一个已经排好序的有序序列中,
* 直到所有的记录插完为止,得到一个全新的有序数列。
* 最好时间复杂度:O(n);
* 最坏的时间复杂度:O(n^2),
* 平均的时间复杂度:O(n^2),
* 空间复杂度:O(1);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:稳定
* 备注:大部分元素已经有序时效果最好
* @author qingshan
* @time 2020/9/19 - 15:42
*/
public class InsertSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
//插入排序
public static void insertSort(int[] arr) {
//假设第一个记录是有序的,从第二个记录开始插入
for (int i = 1; i < arr.length; i++) {
if(arr[i] < arr[i-1]){
int temp = arr[i];
int j;
//当前位置向前比较,找到合适的位置插入
for(j = i-1;j>=0&&temp<arr[j];j--){
arr[j+1]=arr[j];
}
arr[j+1]=temp;
}
}
}
}
四、希尔排序(插入排序)
所谓希尔排序,就是插入排序的升级版,也叫缩小增量排序,先将待排序的元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对每个子序列进行直接插入排序,这样使得每个子序列都有序,最后再将所有的元素进行一次插入排序。代码如下:
import java.util.Arrays;
/**
* 常用排序算法之希尔排序:
* 算法描述:希尔排序,其实也是插入排序的升级版,也叫缩小增量排序,先将待排序的元素分成多个子序列
* 这每个子序列排序起来速度会快很多,每个子序列都进行插入排序,这样使得每个子序列都有序
* 最后再将所有的元素进行一次插入排序
* 最好时间复杂度:O(n);
* 最坏的时间复杂度:O(n^s,s=1.3),所以约等于O(nlog(n));
* 平均的时间复杂度:O(nlog(n)),
* 空间复杂度:O(1);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:不稳定
* 备注:s为所选的分组
* @author qingshan
* @time 2020/9/19 - 15:42
*/
public class ShellSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
//插入排序
public static void shellSort(int[] arr) {
//遍历所有的步长
for (int d = arr.length/2; d > 0; d/=2) {
//每个步长的插入排序;
for(int i = d;i<arr.length;i++){
for(int j = i-d;j>=0;j-=d){
if(arr[j]>arr[j+d]){
int temp = arr[j];
arr[j] = arr[j+d];
arr[j+d] = temp;
}
}
}
}
}
}
五、简单选择排序(选择排序)
所谓简单选择排序,事先将数组分为已排序和未排序区域,每次从未排序的元素中通过交换将其放到已排序的去区域尾部。
步骤:1,进行数组长度-1轮比较
2,每轮比较找到未排序区域的最小值下标
3,如果最小值下标为非排序区域第一个元素,进行交换后,此时,未排序区域第一个元素则变成了已排序区域的最后一个元素。
4,进行下一轮比较的时候,找到未排序区域的最小值,然后放到已排序区域的最末尾处,直到整个序列有序。
代码如下:
import java.util.Arrays;
/**
* 常用排序算法之选择排序:
* 算法描述:选择排序,将序列分为已排序和未排序,每次从未排序的序列中找到最小的元素
* 最好时间复杂度:O(n^2);
* 最坏的时间复杂度:O(n^2),
* 平均的时间复杂度:O(n^2),
* 空间复杂度:O(1);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:不稳定
* 备注:n小时较好
* @author qingshan
* @time 2020/9/19 - 15:42
*/
public class SelectSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
//选择排序
public static void selectSort(int[] arr) {
//遍历所有的数
for (int i = 0; i < arr.length; i++) {
//初始化最小元素的下标
int minIndex = i;
for (int j = i+1; j < arr.length; j++) {
if(arr[minIndex]>arr[j]){
//记录最小值下标
minIndex = j;
}
}
if(minIndex!=i){
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}
六、堆排序(选择排序)
堆:一种特殊的树形结构,其每个节点都有一个值,通常提到的堆都是指一颗完全二叉树,根节点的值大于(或者小于)两个子节点的值,同时,根节点的两个子树也分别十一个堆。
所谓堆排序,其实就是一种树形选择排序,堆排序中有大顶堆和小顶堆两种,其中大顶堆(升序ASC适用),每个父节点的值都比子节点要大,这样每次排序后都从根节点取出最大值放到队列尾部,将剩下的部分再次构建成大顶堆,再取出,直到所有元素都有序。小顶堆:原理类似,只不过每次找出的是最小数,适合降序(DESC)排列,代码如下:
import java.util.Arrays;
/**
* 常用排序算法之堆排序:
* 算法描述:堆排序,堆排序,主要分为小顶堆和大顶堆,所谓的大顶堆(升序(ASC)适用)就是每个父节点都比子节点要大,这样
* 每次排序后都取出最大的值放到了队列尾部,直到所有的元素有序。
* 小顶堆:原理与大顶堆类似,只不过每次找出的是最小的值,所以适应于降序(DESC)排列
* 最好时间复杂度:O(nlog(n));
* 最坏的时间复杂度:O(nlog(n)),
* 平均的时间复杂度:O(nlog(n)),
* 空间复杂度:O(1);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:不稳定
* 备注:n大的时候较好
*/
public class HeapSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
//使用大顶堆进行堆排序
public static void heapSort(int[] arr) {
int start = (arr.length-2)/2;
for (int i = start;i>=0; i--) {
maxHeap(arr,arr.length,i);
}
for (int i = arr.length-1;i>=0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
maxHeap(arr,i,0);
}
}
/**
* 构造一个大顶堆
* @param arr
* @param size 要构造成大顶堆的元素有多少个
* @param index 从哪个位置开始构造大顶堆,一般为最后一个节点的父节点开始
*/
public static void maxHeap(int[] arr,int size,int index){
int leftNode = 2*index+1;
int rightNode = 2*index+2;
int max = index;
if(leftNode<size&&arr[leftNode]>arr[index]){
max = leftNode;
}
if(rightNode<size&&arr[rightNode]>arr[index]){
max = rightNode;
}
if(max!=index){
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
//处理一下由于交换位置导致的破坏原来的堆结构,
maxHeap(arr,size,max);
}
}
}
七、归并排序
所谓归并排序,就是利用递归和分治的思想将数据划分为越来越小的半子表,然后再对半子表进行排序,最后再用递归的方法将排好序的半子表合并成越来越大的有序列表。代码如下:
import java.util.Arrays;
/**
* 常用排序算法之归并排序:
* 算法描述:归并排序利用递归于分治思想将数据划分成为越来越小的半子表,再对半子表排序
* 最后再用递归方法将排好序的半子表合并为越来越大的有序列表。
* 最好时间复杂度:O(nlog(n));
* 最坏的时间复杂度:O(nlog(n)),
* 平均的时间复杂度:O(nlog(n)),
* 空间复杂度:O(n);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:稳定
* 备注:n大的时候较好
*/
public class MergeSort {
public static void main(String[] args) {
int[] arr = {3,2,6,1,8,4};
System.out.println(Arrays.toString(arr));
mergeSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void mergeSort(int[] arr,int low,int high){
if(low<high){
int middle = low+(high-low)/2;
mergeSort(arr,low,middle);
mergeSort(arr,middle+1,high);
merge(arr,low,high,middle);
}
} //归并排序,将一个数组以middle为分界线,看成两个数组,然后进行递归归并操作
public static void merge(int[] arr,int low,int high,int middle) {
//先创建一个临时数组用来存储归并的元素
int[] temp = new int[high-low+1];
//记录第一个数组的遍历
int i = low;
//记录第二个数组的遍历
int j = middle+1;
//用于记录上面那个临时数组记录的下标(temp)
int index = 0;
while(i<=middle&&j<=high){
if(arr[i]<=arr[j]){
temp[index] = arr[i];
i++;
}else{
temp[index] = arr[j];
j++;
}
index++;
}
//处理其中一个数组的多余的数据
while(i<=middle){
temp[index] = arr[i];
i++;
index++;
}
while(j<=high){
temp[index] = arr[j];
j++;
index++;
}
//把临时数组中的数据放回源数组
for(int k = 0;k<temp.length;k++){
arr[k+low]=temp[k];
}
}
}
八、基数排序
所谓基数排序,就是通过最大值的位数,然后按照每一位的数,放到对应的0-9的数组中,然后按照大小顺序从数组中取数,直到所有位数排完,就是有序了,利用了空间换时间。代码如下:
import java.util.Arrays;
/**
* 常用排序算法之基数排序:
* 算法描述:就是通过最大值的位数,然后按照每一位的数,放到0-9的数组中,然后按照顺序从数组中
* 取数,直到所有位数排完,就是有序了。利用空间换时间
* 第二种解决方法:就是在第一层数组中,使用队列来存取临时取出来的数据,根据队列先进先出
* 的性质取数,直到有序。
* 最好时间复杂度:O(d(n+j));
* 最坏的时间复杂度:O(d(n+j)),
* 平均的时间复杂度:O(d(n+j)),
* 空间复杂度:O(n+j);
*
* 稳定性:所谓稳定性就是在一个序列中多个相同的元素,排序前和排序后的相对次序相同,如A(1),A(2)
* 在排序后仍然是A(1),A(2)
*
* 稳定性:不稳定
* 备注:n个元素中大小差距比较大的效果好
* @author qingshan
* @time 2020/9/19 - 15:42
*/
public class RadixSort {
public static void main(String[] args) {
int[] arr = {32,2,67,109,821,423};
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void radixSort(int[] arr){
//存数组中最大的数字
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
if(arr[i]>max){
max = arr[i];
}
}
//判断最大数字是几位数
int maxLength = (max+"").length();
//用于临时存储数据的数组
int[][] temp = new int[10][arr.length];
//用于记录在temp中相应的数组中存放的数字的数量
int[] counts = new int[10];
//根据最大长度的数决定要比较的次数
for (int i = 0,n=1; i < maxLength; i++,n*=10) {
for (int j = 0; j < arr.length; j++) {
//计算余数
int ys = arr[j] / n % 10;
//把当前遍历的数据放入指定的数组中
temp[ys][counts[ys]] = arr[j];
//记录添加到每个数组中的数量
counts[ys]++;
}
//记录取得元素需要放的位置
int index = 0;
//把数字取出来
for(int k = 0;k<counts.length;k++){
//记录数量的数组中当前余数记录的数量不为0;
if(counts[k]!=0){
for(int l=0;l<counts[k];l++){
//取出元素
arr[index] = temp[k][l];
index++;
}
//把数量置为0
counts[k] = 0;
}
}
}
}
}