排序算法总结(分析+图+Java实现)

本文详细介绍了各种排序算法,包括选择排序、冒泡排序、插入排序、二分插入排序、希尔排序、归并排序、TimSort、快速排序。深入探讨了它们的思想、时间复杂度和稳定性,特别提到了Java中的对象排序和优化写法。通过对这些经典排序算法的理解,有助于提升编程能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写篇博客自己回忆一下,如果也能帮助到其他人那更好了。
标题中的时间复杂度均指是是平均时间复杂度。

表格

排序算法平均时间复杂度最坏最好空间稳不稳
选择排序n2n2n21不稳
冒泡排序n2n2n1
插入排序n2n2n1
二分插入排序n2n2n1
希尔排序n1.3n2n1不稳
归并排序nlognnlognnlognn
TimSortnlognnlognnn
快速排序nlognn2nlognlogn不稳
堆排序nlognnlognnlogn1不稳
桶排序n+kn2nn+k
计数排序n+kn+kn+kn+k
基数排序n*kn*kn*kn+k

💗🧡💛💚💙💜💗🧡💛💚💙💜💗🧡💛💚💙💜


【选择排序】 O(n2) 不稳


📃思想描述

  • 每轮找最小的,记录下标往前放
  • 最小的记录下标,一轮结束交换位置

最简单但是最没用的排序算法。

思路就是每趟都找剩下的当中最小的那一个。
就是每次都选择最小的那个往前放。
在这里插入图片描述
简单排序的时间复杂度为O(n2),这种排序算法不稳定。

## 0->length-1
### i+1->length
#### 比较更新minPos
public class SelectionSort {

	public static void main(String[] args) {
		
		int[] arr = {5,3,6,8,1,7,9,4,2};
		int minPos;//记录当前轮次最小值的下标
		
		for(int i=0;i<arr.length-1;i++) {
			minPos = i;
			for(int j=i+1;j<arr.length;j++) {
				minPos = arr[j] < arr[minPos] ? j : minPos;
			}
			int temp = arr[i];
			arr[i] = arr[minPos];
			arr[minPos] = temp;
		}
		
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}
		
	}
}



【冒泡排序】 O(n2) 稳

其实我感觉冒泡和选择有点像
不过冒泡是每轮找最大的那个。
但是选择是每轮找最小的那个,并且不用边找边交换位置,而是记录下标,一轮结束了再交换。


📃思想描述

  • 每轮都找最大的往后放
  • 边找边换位置,让大的冒泡到最后

在这里插入图片描述

## 0->length-1
### 0->length-i-1
#### 比较交换位置
public static void main(String[] args) {
		
		int[] arr = {5,3,6,8,1,7,9,4,2};
		
		for(int i=0;i<arr.length-1;i++) {
			for(int j=0;j<arr.length-i-1;j++) {
				if(arr[j]>arr[j+1]) {
					int temp = arr[j];
					arr[j] = arr[j+1];
					arr[j+1] = temp;
				}
			}
		}
		
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}
	}

时间复杂度为O(n2),
冒泡排序是稳定的排序方法。

优化写法

冒泡排序最好的情况是O(n),改进的写法如下:

public void bubbleSort(int arr[]) {
    boolean didSwap;
    for(int i = 0, len = arr.length; i < len - 1; i++) {
        didSwap = false;
        for(int j = 0; j < len - i - 1; j++) {
            if(arr[j + 1] < arr[j]) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
                didSwap = true;
            }
        }
        if(didSwap == false) //如果是false,说明剩下的全部是有序的
            return;
    }    
}


【插入排序】 O(n2) 稳

对于基本有序的数组最好用


📃思想描述

  • 往已经有序的部分中插入下一个数
  • 通过交换位置每轮保证当前部分有序

## 1->length
### i->0
#### 比较交换位置
public static void main(String[] args) {
		
		int[] arr = {5,3,6,8,1,7,9,4,2};
		
		for(int i=1;i<arr.length;i++) {
			for(int j=i;j>0;j--) {
				if(arr[j]<arr[j-1]) {
					int temp = arr[j];
					arr[j] = arr[j-1];
					arr[j-1] = temp;
				}
			}
		}
		
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}

	}

插排最好情况的和冒泡是一致的,改进算法也可以按照冒泡的那个写。


【二分插入排序】

插入排序还可以改进为二分插入排序,在有序部分中找到正确位置这一步骤可以使用二分查找来完成,找到位置再将后面的元素后移。这样主要是节省比较的时间,虽然依然要移动相同数量的元素,但是数组平移比元素一个一个交换还是要快一点点。

public static void main(String[] args) {
		
		int[] arr = {7,4,3,6,5,2,1,8,12,11,9,10};
		
		for(int i=1;i<arr.length;i++) {
			int des = binarySert(0,i-1,arr,arr[i]);
			int temp = arr[i]; //记录待插入的数
			for(int j=i;j>des;j--) { //这里必须逆序,temp记录的是后一个
				arr[j]=arr[j-1];
			}
			arr[des]=temp;
		}
		
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}
		
	}
	
	public static int binarySert(int start,int end, int[] arr, int a) {
		int mid = start+(end-start)/2;
		//这里end是有可能会小于start的,循环终止条件不能写start!=end
		while(start<=end) { 
			if(arr[mid]>a) {
				end=mid-1;
			}else {
				start=mid+1;
			}
			mid = start+(end-start)/2;
		}
		return start;
	}


【希尔排序】 O(n1.3) 不稳

插排的改进,为什么效率比插排好,因为比如像1,通过三步就可以到最前面,而普通排序需要比对很多次。
一次排下来,小的数基本上都放到前面去了,大的很多都在后面了。


📃思想描述

  • 每轮按照指定间隔插入排序
  • 不断缩小间隔直到1,完成全部排序

## gap->0 (gap-1)/3
### gap->length
#### i->gap-1 (-=gap)
##### 比较交换位置
public static void main(String[] args) {
		
		int[] arr = {7,4,27,6,5,2,1,8,12,9,10,11,34,15,23,20,3};
		int gap = 1;
		while(gap<=arr.length/3) {
			gap = gap*3+1;  //这样划分是因为某数学家研究出来这样最优
		}
		
		for(;gap>0;gap = (gap-1)/3) {
			for(int i=gap;i<arr.length;i++) {
				for(int j=i;j>gap-1;j-=gap) {
					if(arr[j]<arr[j-gap]) {
						int temp = arr[j];
						arr[j] = arr[j-gap];
						arr[j-gap] = temp;
					}
				}
			}
		}
		
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}

	}


【归并排序】 O(n*logn) 稳


📃思想描述

  • 将数组排序不断划分为更小的子问题
  • 解决子问题再将子问题合并

首先让最小子问题有序,在下面的代码里,递归的终止是只有一个元素。
然后从两个元素合并开始。
问题就是先不断二分,再合并。
合并的问题是怎样将两个有序数组合并。

## Sort()
### start==end return
### mid = (end-start)/2 + start+1
### sort (start,mid-1) ;sort (mid,end)
### merge (start,mid,end)

## merge()
### 三个指针,两个有序数组,比较放入temp
#### arr[i]<arr[j] i++;k++;  else j++;k++;
### 剩余放入temp
### arr[start:end] = temp[start:end]
public static void main(String[] args) {
		
		int[] arr = {7,4,3,19,6,5,2,13,1,8,12,9,10,11,23};
		int[] temp = new int[arr.length];
		
		sort(arr,0,arr.length-1,temp);
		
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}

	}
	
	static void sort(int[] arr,int start,int end,int[] temp) {
		if(start==end) return;
		//分成两半
		int mid = (end-start)/2 + start+1; //防止中间两int相加越界
		sort(arr,start,mid-1,temp);
		sort(arr,mid,end,temp);
		merge(arr,start,mid,end,temp);
	}
	
	static void merge(int[] arr,int start,int mid,int end,int[] temp) {
		int i=start;
		int j=mid;
		int k=start;
		
		while(i<mid && j<=end) {
			if(arr[i]<arr[j]) {
				temp[k]=arr[i];
				i++;
				k++;
			}else {
				temp[k]=arr[j];
				j++;
				k++;
			}
		}
		while(i<mid) temp[k++] = arr[i++];
		while(j<=end) temp[k++] = arr[j++];
		
		for(int a=start;a<=end;a++) {
			arr[a]=temp[a];//这里注意一定要还给arr
		}
		
	}

Java里的对象排序

对象排序一般要求稳定,所以归并排序是一个比较好的选择。

TimSort也是归并排序,不过不是两个两分,而是一下分很多组,然后再将很多组两两归并。


【TimSort】

TimSort是一个自适应的、混合的、稳定的排序算法,融合了归并算法和二分插入排序算法的精髓。

在Java中是选择当元素数据少于32时,二分插入排序,如果大于32进行归并排序。

TimSort中有一个run的概念,run是单调增或者单调减的一段,并且run必须要保证至少有多少个元素:minrun。

在执行排序算法之前,会计算出这个minrun的值(所以说这个算法是自适应的,会根据数据的特点来进行自我调整),minrun会从32到64(包括)选择一个数字,使得数组的长度除以minrun等于或者略小于2的幂次方。比如长度是65,那么minrun的值就是33;如果长度是165,minrun就是42(注意这里的Java的minrun的取值会在16到32之间)。

不断去找run,遇到递减的run需要反转,反转后压栈。

最终合并栈中相邻的两个run。



【快速排序】O(nlogn) 不稳

就是让某元素左边的元素都小于它,右边的元素都大于它。
快排有很多解法,速度最快的是双指针,如下图。

可以理解为指针指在空的位置上就不动,然后遇到大小需要调换的时候,原本空位置的指针将不空,会让另一个指针不动。


📃思想描述

  • 使用双指针找到temp值的目标位置
  • 目标位置左右继续双指针递归找位置

## quickSort()
### left>=right return
### pos = Partition
### quickSort(left:pos-1)
### quickSort(pos+1:right)

## Partition()
### temp = arr[left]
### left<right
#### arr[right]>temp  right--;
#### arr[left] = arr[right]
#### arr[left]<temp left++;
#### arr[right] = arr[left]
### arr[left] = temp;
### return temp
public static void main(String[] args) {
		
		int[] arr = {7,4,3,19,6,5,2,13,1,8,12,9,10,11,23};
		
		quickSort(arr,0,arr.length-1);
		for(int i=0;i<arr.length;i++) {
			System.out.print(arr[i]+" ");
		}

	}
	
	public static void quickSort(int[] arr,int left,int right) {
		if(left>=right) return;
		int pos = Partition(arr,left,right);
		quickSort(arr,left,pos-1); //对左子区间递归进行快速排序
		quickSort(arr,pos+1,right); //对右子区间递归进行快速排序
	}
	
	public static int Partition(int[] arr,int left,int right) {
		int temp = arr[left];
		while(left<right) {
			while(left<right&&arr[right]>temp)
				right--;
			arr[left] = arr[right];
			while(left<right&&arr[left]<=temp)
				left++;
			arr[right] = arr[left];
		}
		arr[left] = temp;
		return left;
	}

避免最坏的情况(就是数组已经排好了),可以选择随机取一个数,而不是每次都从最左边取数,或者先判断是不是顺序增长或者顺序降低的,如果是就不用快排了。


Java里Arrays类的这个sort(int[] a)方法是使用双轴快排实现的。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值