1.冒泡排序
比较相邻两元素,若前元素大于后元素则交换两元素的值,每次外循环可以将当前未有序序列中最大的元素排到最后,然后再缩小范围继续进行交换直到全体有序
public static int[] bubbleSort(int arr[]) {
int temp;
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]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j+1]=temp;
}
}
}
return arr;
}
优化:添加一个flag标识,判断当前是否已经排序好,若当前已全部有序则提前跳出循环
public static int[] bubbleSort(int arr[]) {
int temp;
int flag=0;
for (int i = 0; i < arr.length-1; i++,flag=0) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j+1]=temp;
flag=1;
}
}
if (flag == 0) {
break;
}
}
return arr;
}
稳定性: 稳定
时间复杂度:
最好:若在代码中加一个标识位进行优化,则最好情况下时间复杂度为O(n),否则为O(n^2)
最坏:O(n^2)
平均:O(n^2)
空间复杂度: O(1)
优点: 稳定,若有序性大时可提前结束循环
缺点: 移动次数多,平均时间复杂度仍然很高
冒泡排序移动次数较多,当无序性大,元素数量多时不适用。
2.选择排序
每次在未排序序列中选取出最大(最小)的元素放在最后(最前)直至全部有序
public static int[] selectSort(int arr[]) {
int temp;
for (int i = 0; i < arr.length; i++) {
int minIndex=i;
for (int j = i; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
if (minIndex!=i) {
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
稳定性: 不稳定
时间复杂度:
平均:O(n^2)
最好:O(n^2)
最坏:O(n^2)
空间复杂度: O(1)
优点: 直观
缺点: 时间复杂度较高
选择排序无论序列的无序性如何时间复杂度都为O(n^2),和冒泡排序相比移动次数少,当每一元素占用的空间较多时,此方法比插入排序快。
3.插入排序
每次将当前元素同前面已排序序列从尾到头对比插入对应的位置直至全部有序
public static int[] insertSort(int arr[]) {
int current;
for (int i = 1; i < arr.length; i++) {
int j;
current = arr[i];
for ( j = i; j > 0 && current < arr[j - 1]; j--) {
arr[j] = arr[j - 1];
}
arr[j] = current;
}
return arr;
}
优化:利用折半查找寻找当前元素的插入点
public static int[] binaryInsertSort(int arr[]) {
int current;
for (int i = 1; i < arr.length; i++) {
int j;
current = arr[i];
int L=0,R=i-1,mid;
while(L<=R)
{
mid=(L+R)/2;
if(current<arr[mid])
{
R=mid-1;
}
else
{
L=mid+1;
}
}
for(j=i;j>R+1;j--)
{
arr[j]=arr[j-1];
}
arr[j]=current;
}
return arr;
}
因为要使用到折半查找,所以只能用于顺序结构。对比于直接插入排序折半插入排序只减少了比较次数,而移动次数不变,所以时间复杂度与直接插入排序相同。
稳定性: 稳定
时间复杂度:
平均:O(n^2)
最好:O(n)
最坏:O(n^2)
空间复杂度: O(1)
优点: 稳定
缺点: 时间复杂度较高
插入排序同样不适用于无序性大,元素数量多的情况,但实际执行起来比冒泡排序快;折半插入排序适用于元素多且无序度较高的情况。
4.希尔排序
首先对增量increment(increment<n)取一个整数,将整个序列按增量分成increment组,对每组进行插入排序,然后将增量increment缩小,继续进行此操作直至increment=1,此时的序列已经基本有序,再进行一次插入排序即使全部序列全部有序
public static int[] shellSort(int arr[]) {
int increment=arr.length/2;
while(increment>=1)
{
for(int i=increment;i<arr.length;i++)
{
int j,temp=arr[i];
for(j=i;j-increment>=0&&temp<arr[j-increment];j-=increment)
{
arr[j]=arr[j-increment];
}
arr[j]=temp;
}
increment/=2;
}
return arr;
}
稳定性: 不稳定
时间复杂度:
平均:O(n^(1.3~2))
空间复杂度: O(1)
优点: 效率高
缺点: 不稳定,只能用于顺序存储结构
希尔排序总的比较次数和移动次数都比插入排序要少,适用于中等大小规模的数据,但对规模非常大的数据排序不是最优选择。
5.归并排序
使用分治法将序列分为若干子序列分别进行2-路归并直至整个序列归并为有序序列
public static int[] mergeSort(int arr[]) {
if (arr.length <= 1) {
return arr;
}
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
return merge(mergeSort(left), mergeSort(right));
}
public static int[] merge(int left[], int right[]) {
//归并相邻两有序子序列
int res[] = new int[left.length + right.length];
int i = 0, j = 0;
for (int index = 0; index < res.length; index++) {
if (i >= left.length) {
res[index] = right[j++];
} else if (j >= right.length) {
res[index] = left[i++];
} else if (right[j] < left[i]) {
res[index] = right[j++];
} else {
res[index] = left[i++];
}
}
return res;
}
稳定性: 稳定(归并中当两个数相等时,先并入left数组中的数再并入right数组的数,使得前后两个相等的数相对位置不变,则算法稳定)
时间复杂度:
平均:O(n log n)
最好:O(n log n)
最坏:O(n og n)
空间复杂度: O(n)
优点: 稳定,不受数据的无序性影响,时间复杂度总为O(n log n),效率较高
缺点: 归并排序不是原地排序算法,需要消耗额外的空间(主要为每次合并时临时申请的数组空间,合并完之后便释放掉,不过在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度为O(n))
归并排序以空间换时间,和快速排序都是用的分治思想,区别在于归并排序方式为自下而上(先解决子问题,再解决父问题);归并排序和选择排序一样不受序列的无序性的影响,但比选择排序更快;
6.快速排序
在序列里找一个元素作为基准数(pivot),再双向遍历从左寻找大于pivot的元素和从右寻找小于pivot的元素再将两元素进行交换,最后使得比pivot小的元素在基准数前面而比pivot大的元素在基准数后面(即确定了当前基准数元素在有序序列中的位置),使用分治法分区重复此过程直至区间缩小为1(确定了每个元素在有序序列中的位置), 此时序列已全部有序
public static void quickSort(int arr[], int left, int right) {
int i,j,temp,pivot;
if(left>right)
{
return ;
}
pivot=arr[left];
i=left;
j=right;
while(i<j)
{
while(arr[j]>=pivot&&i<j)
{
j--;
}
while(arr[i]<=pivot&&i<j)
{
i++;
}
if(i<j)
{
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
arr[left]=arr[i];
arr[i]=pivot;
quickSort(arr,left,i-1);
quickSort(arr,i+1,right);
}
稳定性: 稳定
时间复杂度:
平均:O(n log n)
最好:O(n log n)
最坏:O(n^2)
空间复杂度: O(1)
优点: 效率高
缺点: 不稳定,不适用于链式存储结构
快速排序和归并排序都是用的分治思想,区别在于快速排序方式为自上而下(先解决父问题,再解决子问题);在平均情况下快速排序是所有内部排序算法中速度最快的一种,所以适用于无序性大且元素多的情况,也是最常用的排序算法之一。
7.堆排序
堆排序是一种树形选择排序,将存储在数组中的序列看作完全二叉树进行原地排序;先反向于遍历所有非终端节点将序列构造成大顶堆,然后将堆顶元素(当前堆中最大的元素)取下与末尾元素元素进行交换(也就是将当前最大元素取出放在最后),再将改动后的堆进行堆调整,再次找出堆顶元素(即确定当前未排序序列中最大的元素),反向遍历所有节点重复此过程最终使得序列全部有序
public static int[] heapSort(int arr[]) {
for (int i = (arr.length / 2 - 1); i >= 0; i--) {
//遍历非终端节点,建立大顶堆
heapify(arr, i, arr.length);
}
for (int i = arr.length - 1; i >= 0; i--) {
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
heapify(arr, 0, i);
}
return arr;
}
public static int[] heapify(int arr[], int index, int length) {
//在堆中对下标为index的结点做堆调整
int max = index;
int lchild = 2 * index + 1, rchild = 2 * index + 2;
if (length > lchild && arr[max] < arr[lchild]) {
max = lchild;
}
if (length > rchild && arr[max] < arr[rchild]) {
max = rchild;
}
if (max != index) {
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
heapify(arr, max, length);
}
return arr;
}
稳定性: 不稳定
时间复杂度:
平均:O(n log n)
最好:O(n log n)
最坏:O(n log n)
空间复杂度: O(1)
优点: 效率较高,时间复杂度总为O(n log n)
缺点: 不稳定,不适用于链式存储结构
堆排序建立初始堆时所需的交换次数较多,因此元素较少时不宜采用。堆排序在最坏情况下时间复杂度仍然为O(n log n),相对于快速排序最坏情况下的O(n^2)是个优点,但是从综合性能来看,快速排序性能更好。
8.计数排序
计数排序基于桶排思想,用bucket数组计数原数组元素值与bucket数组下标相同的个数,再正向遍历bucket数组(遍历bucket数组的过程即生成有序序列),按顺序将符合条件的bucket数组值的下标存入原数组。不过为避免开辟不必要的空间,先要找出原数组中的最大最小值,按最大最小之差开辟空间大小,bucket数组计数时下标按原数组的值减去最小值计数,存入原数组的时再加上最小值即可
public static int[] countSort(int arr[]) {
if (arr.length == 0) {
return arr;
}
int max = arr[0], min = arr[0];
for(int i = 0;i < arr.length;i++) {
if (arr[i] < min) {
min = arr[i];
}
if (arr[i] > max) {
max = arr[i];
}
}
int bucket[] = new int[max - min + 1];
Arrays.fill(bucket, 0);//将bucket数组初始化为0
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]-min]++;
}
int index=0;
for (int i = 0; i < bucket.length; i++) {
while (bucket[i] > 0) {
arr[index++]=i+min;
bucket[i]--;
}
}
return arr;
}
稳定性: 稳定
时间复杂度:
平均:O(n+k)
最好:O(n+k)
最坏:O(n+k)
空间复杂度: O(k)
优点: 线性时间复杂度
缺点: 只适用于整数排序,额外开辟的空间取决于数据最大最小的范围,对于范围较大的数据需开辟较大的空间
计数排序不同于比较排序,时间复杂度呈线性使得计数排序快于任何排序算法,但对于范围大的数据需要花费大量的时间和空间。
9.桶排序
桶排序同样是使用了桶排思想,bucket列表中的每个桶可以存放多个数据,在参数列表设置bucketSize参数(每个桶中能存多少个连续的数据),将元素放入桶中时需除以bucketSize来确定放入的桶的下标(这样使得每个桶中存储一定范围的数据),再将每个桶中的多个元素进行排序(缩小bucketSize,再进行一次桶排或其他排序),最后将bucket列表中的数据按顺序取出即为有序序列
public static List<Integer> bucketSort(List<Integer> arr, int bucketSize) {
if (arr == null || arr.size() < 2 || bucketSize < 1) {
return arr;
}
int min = arr.get(0), max = arr.get(0);
for (int i = 0; i < arr.size(); i++) {
if (arr.get(i) > max) {
max = arr.get(i);
}
if (arr.get(i) < min) {
min = arr.get(i);
}
}
int bucketNum = (max - min) / bucketSize + 1;
List<ArrayList<Integer>> bucketList = new ArrayList<>();
for (int i = 0; i < bucketNum; i++) {
bucketList.add(new ArrayList<Integer>());
}
for (int i = 0; i < arr.size(); i++) {
int index = (arr.get(i) - min) / bucketSize;
bucketList.get(index).add(arr.get(i));
}
List<Integer> resList = new ArrayList<>();
for (int i = 0; i < bucketList.size(); i++) {
List<Integer> eachBucket = bucketList.get(i);
if (eachBucket.size() > 0) {
if (bucketNum == 1) {
bucketSize--;
}
List<Integer> sortedBucket = bucketSort(eachBucket, bucketSize);
for (int j = 0; j < sortedBucket.size(); j++) {
resList.add(sortedBucket.get(j));
}
}
}
return resList;
}
稳定性: 稳定
时间复杂度:
平均:O(n+k)
最好:O(n+k)
最坏:O(n^2)
空间复杂度: O(n+k)
优点: 线性时间复杂度快于任何排序算法
缺点: 空间复杂度高
桶排序是计数排序的优化,其元素的存储类似于散列表中的链地址法,bucketSize划分的越小则时间越少,相应的消耗的空间也越大
10.基数排序
基数排序同样基于桶排思想,从低位到高位,通过按各位数上的数字来进行分配从而不断调整序列从而使得序列有序
public static int[] radixSort(int arr[]) {
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
int base=1;
int temp[]=new int[arr.length];
while (max / base > 0) {
int bucket[] = new int[10];
Arrays.fill(bucket,0);
for(int i=0;i<arr.length;i++)
{
bucket[arr[i]/base%10]++;
}
for(int i=1;i<10;i++)
{
bucket[i]+=bucket[i-1];
}
for(int i=arr.length-1;i>=0;i--)
{
temp[bucket[arr[i]/base%10]-1]=arr[i];
bucket[arr[i]/base%10]--;
}
for(int i=0;i<arr.length;i++)
{
arr[i]=temp[i];
}
base=base*10;
}
return arr;
}
稳定性: 稳定
时间复杂度:
(k表示位数)
平均:O(n * k)
最好:O(n * k)
最坏:O(n * k)
空间复杂度: O(n+k)
优点: 稳定且速度快
缺点: 空间复杂度高
基数排序也是桶排的一种,以空间换时间,采用多关键字的排序思想,适用于已知关键字的分布情况