十大排序算法
1. 冒泡排序(比较算法)
思路
- 比较相邻的元素,如果升序,如果第一个比第二个大就交换位置
- 一对一对比较,直到最后一对,那么最大的就在最后,至此完成一趟的比较
- 对所有元素重复,除了每趟的最后一个,直到排完序
实现
public staic void bubbleSort(int[] array)
{
for(int i=1;i<array.length;i++) //i作为完成的趟数 总共就是(长度-1)趟 每趟找出当前趟最大值放在后面
{ //i从1开始,作为趟数
for(int j=0;j<=array.length-i-1;j++)
{ //j是数组下标,从0开始。比较j和j+1 所以最大到length-i-1,-1是因为有j+1
//length-i 是因为每趟可以找到最大的一个放在最后或者最小的一个放在最后
if(array[j]>array[j+1])
{
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
复杂度
稳定性是指当两个数相同时,他们的相对顺序不会发生变化
冒泡是稳定的
时间复杂度上:(n-1)+(n-2+...+...+1)=n*(n-1)/2
O(n2)
2. 快速排序(改进冒泡的算法)
快排是对冒泡的一种改进
思路
- 从数列中取出一个数作为基准数
- 分区过程:把比这个数大的放右边,小的放左边
- 再对左右分区重复第二步,直到区间只有一个数
经过一趟之后,基准数左边的数都比它小,右边的数都比它大
每趟的实现:
- 设置 i=0; j=length-1
- 以第一个数为基准数,pivot=A[0]=A[i]
- 从后往前搜索(j–),找到第一个小于pivot的数,交换A[j]和A[i]
- 从前往后搜索(i++),找到第一个大于pivot的数,交换A[i]和A[j]
- 重复3.4步骤,直到i==j;一趟完成
实现
public static void swap(int[] array,int i,int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void quickSort(int[] array,int low,int high)
{
if(low<high)
{
int i = low;
int j = high;
int pivot = array[low];
while(i<j)
{
//从后往前
while(i<j && array[j]>=pivot)
{
j--;
}
//交换
swap(array,i,j);
//从前往后
while(i<j && array[i]<=pivot)
{
i++;
}
swap(array,i,j);
}//至此完成一趟快排
//递归进行左右两边的快排
quickSort(array,low,j-1);
quickSort(array,j+1,high);
}
}
复杂度
不稳定的算法
时间复杂度:O(n*log2n)
最糟糕的情况:每次选取的值是最大或者最小,导致划分出一个空表,那么为O(n2)
改善:选取中间的数,尽可能选组中间数
3. 插入排序(插入算法)
思路
把n个待排序元素看为一个有序表和一个无序表,最开始有序表包含1个元素,无序表包含n-1个元素。
排序时,从无序表选出第一个元素,把它插入到有序表中适当的位置,重复n-1次即可。
实现
//升序
public static void swap(int[] array,int i,int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void insertSort(int[] array)
{
for(int i=1;i<array.length;i++)
{ //循环次数,n-1次
for(int j=i;j>0;j--) //因为i从1开始,所以j从i开始,后面有j-1
{
if(array[j]<array[j-1])
//如果后面的数比前面的小,就交换位置,继续
swap(array,j-1,j);
else break;
//如果位置j的数比j-1的数要大,那么肯定比所有的都大,直接break
}
}
}
复杂度
稳定的算法
时间复杂度:O(n2)
4. 希尔排序(改进插入的算法)
对插入算法的改进
思路
按照元素下标的一定增量分组,对每组使用直接插入排序算法;(初始增量为length/2)
随着增量逐渐减少,每组包含的元素越来越多;(增量每趟之后除以2)
当增量减少至1的时候,整个元素被分为一组,终止。
比如增量为5,那么下标0,5,10, …为一组;1,6,11,…为一组。等等
实现
//升序
public static void swap(int[] array,int i,int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void shellSort(int[] array)
{ //初始增量为长度/2,直到1
for(int gap=array.length/2;gap>0;gap/2)
{
//从gap位置元素开始,逐个对组内的元素进行排序
for(int i=gap;i<array.length;i++)
{
//比较的时候是有增量的,所以j-=gap,j>=gap是为了防止j小于零
for(int j=i;j>=gap;j-=gap)
{
//如果后面的比前面的小就交换位置,跟前面的插入排序是一样的
if(array[j]<array[j-gap])
{
swap(array,j,j-gap);
}
//如果比前面的那个大,那么比前面所有的元素都大
else break;
}
}
}
}
复杂度
不稳定的
复杂度:跟数据的顺序和增量的选择有关
O(n*log2n)~O(n2)
5. 选择排序(选择算法)
思路
以由小到大排序为例,首先在未排序序列中找到最小元素,存放到排序序列的起始位置。
然后再从剩余末排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
以此类推,直到所有元素均排序完毕。
实现
//升序
public static void swap(int[] array,int i,int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void selectionSort(int[] array)
{
//最后一个元素不用确定位置,所以-1,i表示当前剩余元素中最小的位置。
for(int i=0;i<array.length-1;i++)
{
int min_index = i;
for(int j=i+1;j<array.length;j++)
//从i+1开始不断往后比较
{
//找到最小值的位置
if(array[j]<array[min_index])
{
min_index = j;
}
}
//把元素交换
swap(array,i,min_index);
}
}
复杂度
不稳定的
复杂度:O(n2)
6. 堆排序(改进的选择排序)
思路
对于堆:完全二叉树(从上往下,从左往右生成节点);所有父节点的值都要大于或者小于子节点的值。
堆的表示可以直接用数组表示,还可以通过下标来表示父节点,左孩子或者右孩子
比如:下标为i;父节点为(i-1)/2
;左孩子为(2i+1)
;右孩子(2i+2)
。
- 先把待排序列构建为一个大顶堆
- 可以知道最上面的元素为最大的值,但是整个数组并不是有序的
- 把堆顶的数据和最后一个交换,再从堆顶往下进行重构堆
建堆的时候从下往上,重构堆是从上往下,可以保证构建的是符合堆特性的
实现
public static void heapSort(int[] array)
{
//初始化一个最大堆
//建堆的时候,只需要找到最后一个节点的父节点,然后依次往上进行heapify(i--)
int last_node = array.length-1;
int parent = (last_node-1)/2;
for(int i=parent;i>=0;i--)
{
heapify(array,array.length,i);
}
//创建完堆之后,可以知道最上面是最大值
//所以将堆顶和最后一个元素进行交换
for(int j=array.length-1;j>=0;j--)
{
swap(array,j,0);
//交换之后,把最后位置的元素从堆中剔除,所有长度变了,这里长度就是j
heapify(array,j,0);
}
}
//
public static void heapify(int[] tree,int length,int i)
{
if(i>=length)
return;//递归的出口
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
int max = i;
//找到最大值,看是否符合堆
if(c1<length && tree[c1]>tree[max])
{
max = c1;
}
if(c2<length && tree[c2]>tree[max])
{
max = c2;
}
//如果不符合堆的性质,要做交换
if(i != max)
{
swap(tree,max,i);
//递归对它下面的继续检查是否符合
heapify(tree,length,max);
}
}
public static void swap(int[] array,int i,int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
复杂度
不稳定的算法
时间复杂度:O(n*log2n)
7. 归并排序(分治)
分治法的一个典型应用
思路
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子列合并成一个 最终的排序序列
实现
//升序
public static void mergeSort(int[] array,int low,int high)
{
//递归结束的标志,只剩下一个元素
if(low<high)
{
int mid = (low+high)/2
//对左边进行归并排序
mergeSort(array,low,mid);
//对右边进行归并排序
mergeSort(array,mid+1,high);
//合并两个有序数组
merge(array,low,mid,high);
}
}
public static void merge(int[] array,int low,int mid,int high)
{
//合并的时候,从有序数组的最左边开始,升序,逐一比较。
//谁小谁的指针就往后移动,直到某个指针移动完毕
int[] temp = new int[array.length];
//由前面知道,下标是low,mid;mid+1,high
int i = low;
int j = mid+1;
int k = 0;
//比较两个数组最左边的元素,谁小谁的指针就往后移动
while(i<=mid && j<=high){
if(array[i]<=array[j])
{
temp[k++] = array[i++];
}
else temp[k++] = array[j++];
}
//左边有剩余
while(i<=mid)
{
temp[k++] = array[i++];
}
//右边有剩余
while(j<=high)
{
temp[k++] = array[j++];
}
//最后把temp的元素放到array中
k = 0;
while(low<=high)
{
array[k++] = temp[low++]
}
}
复杂度
稳定的
复杂度:O(n*log2n)
8. 计数排序(非比较类算法)
计数排序是一种非比较排序算法,其核在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
思路
- 先找到元素中最大的值max
- 创建一个计数数组,大小为max+1,下标(0,max+1)
- 处理每一个数,比如出现了6,那么在计数数组中下表为6的位置加1
- 最后遍历计数数组,以此把数取出
典型的空间换时间算法
实现
public static void countSort(int[] array)
{
//找到最大值
int max = array[0];
for(int i=1;i<array.length;i++)
{
if(array[i]>max)
{
max = array[i];
}
}
//创建计数数组
int[] count = new int[max+1];
//遍历array
for(int i=0;i<array.length;i++)
{
count[array[i]]++;
}
int k = 0;
//遍历计数数组
for(int i=0;i<count.length;i++)
{
while(count[i])
{
//count的下标代表元素的值,对应的count值是有多少个元素
array[k++] = i;
count[i]--;
}
}
}
复杂度
代码优化:数组的大小可以设置为max-min+1;向数组计数的时候下标减去偏移量min,在输出数组的时候加上偏移量min。
//优化:
public static void countSort(int[] array)
{
//找到最大值
int max = array[0];
for(int i=1;i<array.length;i++)
{
if(array[i]>max)
{
max = array[i];
}
}
//找到最小值
int min = array[0];
for(int i=1;i<array.length;i++)
{
if(array[i]<min)
{
min = array[i];
}
}
//创建计数数组
int[] count = new int[max-min+1];
//遍历array
for(int i=0;i<array.length;i++)
{
count[array[i]-min]++;
}
int k = 0;
//遍历计数数组
for(int i=0;i<count.length;i++)
{
while(count[i])
{
//count的下标代表元素的值,对应的count值是有多少个元素
array[k++] = i + min;
count[i]--;
}
}
}
稳定的算法
复杂度:时间复杂度是O(n+k),其中n是数组长度, k是数据的范围。
因为将n个数放入计数数组的时间复杂度是O(n),从长为k的计数数组中输出元素的时间复杂度为O(k),所以总的时间复杂度为O(n+k)。
9. 桶排序(改进的计数排序)
思路
假设输入数据服从均均分布,将数据分到有限数量的桶里,每个桶再分别排序。
桶排序利用了函数的映射关系把数据分配到桶里,效与否的关键就在于这个映射函数的确定。
步骤:
- 找出待排序数组中的max和min
- 桶的数量:
[(max-min)/array.length]+1
- 遍历数组,计算出(array中)每个元素放入桶中的位置。存放桶的下标=
(array[i]-min)/array.length
桶之间的数据是有序的,桶内的数据是无序的,只需要对桶内的数据进行排序
使用数组或者ArrayList
存储桶,使用ArrayList
存储桶里面的数据
实现
import java.util.ArrayList;
public static void bucketSort(int[] array)
{
//找到最大值
int max = array[0];
for(int i=1;i<array.length;i++)
{
if(array[i]>max)
{
max = array[i];
}
}
//找到最小值
int min = array[0];
for(int i=1;i<array.length;i++)
{
if(array[i]<min)
{
min = array[i];
}
}
//确实桶的数量
ArrayList<ArrayList<Integer>> buckets = new ArrayList<>();
int count = (max-min)/array.length +1 ;
for(int i=0;i<count;i++)
{
//每个桶中再增加一个arraylist
buckets.add(new ArrayList<Integer>());
}
//遍历array,把数据放入对应的桶中
for(int i=0;i<array.length;i++)
{
buckets.get((array[i]-min)/array.length).add(array[i]);
}
//遍历桶,对桶内的数据进行排序
for(int i=0;i<buckets.size();i++)
{
//这里排序可以随便采用其他方法,这里用冒泡
bubbleSort(buckets.get(i));
}
//遍历每个桶里的每个数据
int k = 0;
for(int i=0;i<buckets.size();i++)
{
//获取每个桶里的数据为一个list
ArrayList<Integer> list = buckets.get(i);
for(int j=0;j<list.size();j++)
{
array[k++] = list.get(j);
}
}
}
复杂度
稳定性:取决于桶内的排序算法
时间复杂度:如果要排序的数据有n个,我们把它们分在m个桶中,这样每个桶里的数据平均为k= n/ m。如果每个桶使用快速排序,每个桶内排序的时间复杂度就O(k*log2k)。m个桶就是m * O(k*log~2~k)=m * O((n / m)*log~2~(n /m)=O(nlog~2~(n/m))
。
当桶的个数m接近数据个数n时, log ( n/m )就是一个较小的常数 ,所以时间复杂度接近O(n)。
桶排序的最好情况时间复杂度为0(n) ,如果数据分配到桶的分布情况极度倾斜,比如所有数据都分配到了一个桶,那么这时的时间复杂度就是最坏情况,至于用0表示为多少,就需要看桶排序结合的排序算法的时间复杂度是什么了, 如果是快速排序,那么就是O(n*log2n) ,插入排序则是0(n2)。
10. 基数排序
思路
- 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
- 比如说先比较个位,再去比较十位,再以此类推
- 对数要进行初始化,前面没有的就补0
比如说对个位进行排序,其他位就按照个位进行即可。
- 建立桶数组buckets(下标是0-9)因为个位上的数就只有(0-9)。按照个位把数组中的数添加到桶总,注意桶中只是计数并没有不是记录具体的数
- 根据buckets进行排序,借助temp[]保存排序的结果,找出temp和buckets的映射关系。对于映射关系,把桶里面的数据累加起来形成buckets[]。
temp[数组索引]=buckets[i]-1
- 从后往前遍历原始数组,将元素放入temp中,保证原本的数据顺序不发生变化
实现
public static void radixSort(int[] array)
{
//分解数字 (value/exp)%10 exp:当求个位时,exp=1;求十位时,exp=10;以此类推
//找到最大值
int max = array[0];
for(int i=1;i<array.length;i++)
{
if(array[i]>max)
{
max = array[i];
}
}
//对每位进行操作
for(int exp=1;max/exp>0;exp*=10)
{
//创建桶和数组
int[] buckets = new int[10];
int[] temp = new int[array.length];
//遍历数组元素,计算桶数据个数
for(int i=0;i<array.length;i++)
{
buckets[(array[i]/exp)%10]++;
}
//桶数据累加
for(int i=1;i<buckets.length;i++)
{
buckets[i]+=buckets[i-1];
}
//从后往前遍历array,放入到temp中
for(int i=array.length-1;i>=0;i--)
{
//(array[i]/exp)%10是在计算它的桶号
temp[buckets[(array[i]/exp)%10]-1] = array[i];
//减一是因为可能某位数有多个数据
buckets[(array[i]/exp)%10]--;
}
//把temp给到array
for(int i=0;i<temp.length;i++)
{
array[i] = temp[i];
}
}
}
复杂度
稳定性:基于分别收集分别排序,是稳定的
时间复杂度:O(nm) ,n是数据的个数,m是数据位数