1.冒泡排序(Bubble Sort)
时间复杂度O(N2)
算法思想与分析
针对一个数组序列,从前到后相邻的元素两两比较,如果前面的元素大于后面的元素则交换位置,比完最后两个元素后,数组中最大的元素就到了末尾;然后再将末尾的元素排除,前面的所有元素再进行一次上述的比较交换方式,第二大的元素就到了原数组倒数第二的位置;按照此方式继续进行下去,最后就形成了一个有序序列。
数组大小为n
- 第一趟从第一个元素开始,比较两相邻元素,前面大于后面则交换位置,一直到最后两个元素比较完,一共需要比较n-1次
- 第二趟从第一个元素开始,一直比较到倒数第二个元素,一共需要比较n-2次
… - 一直到n-1趟,比较最前面的两元素
代码
public class Bubble {
public static void sort(int[] a){
int n=a.length;
for(int i=0;i<n-1;i++){
for(int j=0;j<n-i-1;j++){
//前一个元素大于后一个元素时,交换
if(a[j]>a[j+1]){
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
}
}
2.选择排序(Selection Sort)
算法思想
首先找到最小的元素,让它和数组的第一个元素交换位置,然后在剩下的元素中找到最小的和数组的第二个元素交换位置,如此往复,直到将整个数组排好序。
算法分析
这里规定数组的长度为N
- N次交换:外层每循环一次都要进行交换
- N2/2次比较:0到N-1的任意元素都会N-1-i次比较,所以比较次数为(N-1)+(N-2)+…+2+1=N(N-1)/2~N2
- 特点:运行时间与输入无关(即使输入一个有序数组运行还是没有改变);数据移动是最少的(j交换次数和数组大小是线性关系)
代码
public static void sort(int[] a){
int N=a.length; //数组的长度
for(int i =0;i<N;i++){
int min=i; //最小元素的索引
for(int j=i+1;j<N;j++){
if(a[j]<a[min]) //元素小于min索引处的元素时,将此元素位置赋值给索引
min=j;
}
//交换a[i]和a[min]
int t =a[i];
a[i]=a[min];
a[min]=t;
}
}
3.插入排序(Insertion Sort)
算法思想
拿出一个数,与有序数列比较,插入到适当位置,插入时将其余元素在插入之前都向右移动一位,给插入元素腾出位置。对于一个待排序的数列,我们从第二个元素开始作为索引,默认将第一个元素作为有序序列,索引元素左边的序列始终为有序序列,然后将索引元素插入到左边有序列的合适位置,直到索引达到数组最右端,排序结束。
算法分析
这里规定数组长度为N,需要得到升序排列的数组
- 最坏情况(输入的数组为降序):每一次待插入元素都要和前面的有序序列的每一个元素进行比较切交换,比较次数为:1+2+3+…+N-1=N(N-1)/2~N2/2,交换次数也为N2/2
- 最好情况(输入的数组为升序):N-1次比较,0次交换
代码
public static void sort(int[] a){
int N=a.length; //数组长度
for(int i=1 ; i<N;i++){
for(int j=i;j>0&&a[j]<a[j-1];j--){
//比较,待插入元素比前面的元素小时交换
int t= a[j];
a[j]=a[j-1];
a[j-1]=t;
}
}
}
4.希尔排序(Shell Sort)
算法思想
希尔排序也叫做缩增量排序,确定一个增量,对每组使用直接插入排序,然后随着增量的减少,每组包含的数据越来越少,当增量为1是,整个序列就是一组,就有序了。
代码
public class ShellSort {
public static void shellSort(int arr[]){
//增量为arr.length/2
for(int gap=arr.length/2;gap>0;gap/=2){
for(int i=gap;i<arr.length;i++){
int j=i;
//使用直接插入排序法,对每一个组进行排序
while(j-gap>=0&&arr[j]<arr[j-gap]){
int temp=arr[j];
arr[j]=arr[j-gap];
arr[j-gap]=temp;
j-=gap;
}
}
}
}
}
5.归并排序(Merge Sort)
平均时间复杂度O(NlogN)
算法思想
对于一个待排序的数组,我们可以先递归地将它分成两半分别排序,然后将结果归并起来。简单的说就是把一个数组分成两半,然后把这两半分别排好序,最后将这两部分合在一起进行排序,实际情况下,分成两部分数组的排序仍然是采用归并的方式,所以我们会使用到递归的思想来操作。
原地归并的抽象方法
前提是待排序数组的左右两部分已经有序(后面会通过递归来实现),此方法会将原数组所有的元素复制到一个辅助数组中,然后通过辅助对象把归并的结果放入原数组中。
- 实现过程:
- 代码
//原地归并的抽象方法,升序
public class Merge{
public static void merge(int[] a,int lo,int mid,int hi){
//将a[lo..mid]和[mind+1..hi]归并
int i=lo,j=mid+1;
//将a复制到aux辅助数组
for(int k=lo;k<=hi;k++)
aux[k]=a[k];
//归并回a数组中
for(int k=lo;k<=hi;k++){
if(i>mid) a[k]=aux[j++]; //左边有序数组用尽,取右边的
else if(j>hi) a[k]=aux[i++]; //右边有序数组用尽,取左边的
else if(aux[i]<aux[j]) a[k]=aux[i++]; //左边数组小于右边数组,取左边
else if(aux[i]>aux[j])a[k]=aux[j++]; //右边数组小于左边数组,取右边
else {
a[k++]=aux[i++];
a[k]=aux[j++];
} //相等
}
}
}
自顶向下的归并排序
将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决大问题。
- 实现过程
例:待排序数组a={2,9,5,3,2,7,1,4,10,5}
排序过程中子数组的依赖树
归并调用的顺序:
左边部分排序
merge(a,0,1) → \rightarrow →merge(a,0,2) → \rightarrow →merge(a,3,4) → \rightarrow →merge(a,0,4)
右边部分排序
merge(a,5,6) → \rightarrow →merge(a,5,7) → \rightarrow →merge(a,8,9) → \rightarrow →merge(a,5,9)
归并结果
merge(a,0,9) - 代码
//自顶向下的归并排序
public class MergeTopDown {
public static int[] aux;
public static void sort(int[] a){
aux=new int [a.length];
sort(a,0,a.length-1);
}
private static void sort(int[] a,int lo,int hi){
if(hi<=lo) return;
int mid=lo+(hi-lo)/2;
sort(a,lo,mid); //将左半边排序
sort(a,mid+1,hi); //将右半边排序
Merge.merge(a,lo,mid,hi); //归并结果
}
}
自底向上的归并排序
适用于链表组织的结构,先归并微型数组,再成对归并得到的子数组,比标准的递归方法所需的代码量少。首先进行对两个大小为1的数组进行归并,然后再对两个大小为2的数组进行归并,然后是大小为4的,一直持续下去就可以得到最终归并的结果。
-
实现过程
归并调用顺序:
-
代码
public class MergeBottomUp {
public static void sort(int[] a){
int N=a.length;
for(int sz=1;sz<N;sz=sz+sz) //sz子数组大小
for(int lo=0;lo<N-sz;lo+=sz+sz) //lo:子数组索引
Merge.merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,N-1));
}
}
6.快速排序(Quick Sort)
算法思想
将一个待排序序列分成两部分,将这两部分单独进行排序,当两个子数组都有序时整个数组自然就有序了。和归并排序不同的是递归调用是在处理整个数组之后,左右部分需要一个切割元素进行切分,保证左边的元素小于它,右边的元素大于它。
切分步骤:
- 选择一个切分元素,一般选择序列的第一个元素
- 从数组左端开始向右端扫描直到找到一个比切分元素大的数
- 从数组右端开始向左端扫描直到找到一个比切分元素小的数
- 都找到后交换它们的位置
- 当两指针相遇,将切分元素a[0]和左边部分最右边的元素进行交换,然后返回这个位置
代码
public class QuickSort {
public static void sort(int arr[]){
sort(arr,0,arr.length-1);
}
public static void sort(int arr[],int lo,int hi){
if(hi<=lo)
return;
//返回切割数位置
int j=partition(arr,lo,hi);
//将切割数左边部分排序
sort(arr,lo,j-1);
//将切割数右边部分排序
sort(arr,j+1,hi);
}
//切分操作
private static int partition(int arr[],int lo,int hi){
//左右扫描指针
int i=lo;
int j=hi+1;
//切分元素
int v=arr[lo];
while(true){
//从左到右扫描,找到大于切分元素的数
while(arr[++i]<v){
if(i==hi)
break; }
//从右到左扫描,找到小于切分元素的数
while(arr[--j]>v){
if(j==lo)
break; }
if(i>=j)
break;
//找到后,交换两个元素的位置
swap(arr,i,j);
}
swap(arr,lo,j);
return j;
}
private static void swap(int arr[],int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
7.堆排序(Heap Sort)
算法思想
什么是堆?
首先我们需要了解什么是堆,堆是一个完全二叉树,一共有两种类型的堆
- 大根堆:每一个节点都大于其左右子节点的值
- 小根堆:每一个节点都小于其左右子节点的值
整个排序的过程,以升序为例:
- 将整个序列构建成大根堆
- 然后移除最大元素,将处于根位置的最大元素和堆最后元素进行交换
- 排出这个最大元素后,除了根位置上的元素没有堆化外,其它位置都是已堆化的,所以再将根进行堆化
- 按照以上步骤进行下去最后就得到了升序序列
代码
public class HeapSort {
//对i结点以下堆化
private static void heap(int tree[],int n,int i){
int c1=i*2+1;
int c2=i*2+2;
int max=i;
if(c1<n&&tree[c1]>tree[max]){
max=c1;
}
if(c2<n&&tree[c2]>tree[max]){
max=c2;
}
if(max!=i){
swap(tree,max,i);
heap(tree,n,max);
}
}
//构建堆
private static void buildHeap(int tree[],int n){
int lastNode=n-1;
int parent=(lastNode-1)/2;
for(int i=parent;i>=0;i--){
heap(tree,n,i);
}
}
//整体实现过程,并实现移除操作
public static void sort(int tree[]){
int n=tree.length;
//构建堆
buildHeap(tree,n);
//移除最大元素,继续堆化
for(int i=n-1;i>=0;i--){
swap(tree,i,0);
heap(tree,i,0);
}
}
private static void swap(int arr[],int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
8.计数排序(Counting Sort)
算法思想
使用一个辅助的数组来记录每个数出现的次数,然后再通过这个计数数组将所有数按顺序放入原数组,就达到了排序的目的。
基本步骤
- 找出待排序数组中最大和最小的元素,用于创建计数数组C(数组大小为:max-min+1)
- 遍历原数组,统计数组中每个值为i的元素出现的次数,存入数组C下标为i的位置(实际对应为:i-min)
- 填充原数组,将C数组中记录的元素次数,按顺序填充对应的元素值到原数组中,每放入一个C数组对应位置上的-1
代码
public class CountingSort {
public static void sort(int arr[]){
//如果数组长度为0,直接结束
if(arr.length==0){
return;
}
int min=arr[0];
int max=arr[0];
//求数组最大数和最小数,用于开辟计数数组
for(int i=0;i<arr.length;i++){
min=Math.min(min,arr[i]);
max=Math.max(max,arr[i]);
}
//开辟计数数组
int[] C=new int[max-min+1];
//初始化为0
Arrays.fill(C,0);
//遍历待排序数组,用C来计数
for (int i = 0; i < arr.length; i++) {
C[arr[i]-min]++;
}
int j=0;
//将C中记录的数按顺序放入arr中
for (int i = 0; i < C.length; i++) {
int temp=C[i];
while(temp>0){
arr[j++]=i+min;
temp--;
}
}
}
}
9.桶排序(Bucket Sort)
算法思想
准备有限个桶,每个桶放一段数据值(比如有3个桶,数据范围是[1,30],我们让[1,10]的数放入第一个桶,[11,20]的数放入第二个桶,[21,30]放入第三个桶),然后再对每个桶中的数据分别排序,最后把全部桶的数据合并。
基本步骤
- 找出待排序数组中的最大值和最小值,通过这两个值确定桶的数量
- 用ArryList作为桶,每一个桶里的数据也用ArrayList存储
- 然后遍历待排序数组,将每个数放入对应的桶中
- 遍历桶数组,给每个桶排序,将排好序的桶中数据放入原数组
代码
public class BucketSort {
public static void sort(int arr[]){
int max=Integer.MIN_VALUE;
int min=Integer.MAX_VALUE;
//求最大最小值
for(int i=0;i<arr.length;i++ ){
max=Math.max(max,arr[i]);
min=Math.min(min,arr[i]);
}
//桶的数量
int bucketNum=(max-min)/arr.length+1;
ArrayList<ArrayList<Integer>> bucketArr=new ArrayList<>(bucketNum);
for(int i=0;i<bucketNum;i++){
bucketArr.add(new ArrayList<Integer>());
}
//将元素放入桶中
for(int i=0;i<arr.length;i++){
int num=(arr[i]-min)/(arr.length);
bucketArr.get(num).add(arr[i]);
}
//对每个桶进行排序
for(int i=0;i<bucketNum;i++){
Collections.sort(bucketArr.get(i));
for(int j=0;j<bucketArr.get(i).size();j++){
arr[j]=bucketArr.get(i).get(j);
}
}
}
}
10.基数排序(Radix Sort)
算法思想
将整数按照位数切割成不同的数字,然后按每个位数分别比较。排序过程是将所有待排序数统一成同样长度位数,不够的前面的补零(不用实际操作,在求位数式子就达到目的了),然后从最低位开始,依次进行一次排序,从对低位一直排到最高位,这个数组序列就变成了一个有序序列了。
针对每一个位数的排序是采用的桶排序的思想。
代码
public class RadixSort {
public static void sort(int arr[]){
//如果数组长度小于等于1或者为空,直接结束
if(arr==null&&arr.length<2){
return;
}
//求数组的最大值
int max=Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max=Math.max(max,arr[i]);
}
//根据最大值求最大位数
int maxDigit=0;
while(max!=0){
max/=10;
maxDigit++;
}
//创建桶
ArrayList<ArrayList<Integer>> bucket=new ArrayList<>();
//初始化桶
for (int i = 0; i < 10; i++) {
bucket.add(new ArrayList<Integer>());
}
//取模和做出元素,得到元素每一位的的数
int mod=10;
int div=1;
for(int i=0;i<maxDigit;i++){
//将待排序元素放入对应的桶
for (int j = 0; j < arr.length; j++) {
int num=arr[i]%mod/div;
bucket.get(num).add(arr[j]);
}
//将每个桶中的数排好序后,依次放回原数组
int index=0;
for (int j = 0; j < bucket.size(); j++) {
Collections.sort(bucket.get(j));
for(int k=0;k<bucket.get(j).size();k++){
arr[index++]=bucket.get(j).get(k);
}
bucket.get(j).clear();
}
}
}
}
平均时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|
冒泡 | O(n2) | 1 | 稳定 |
选择 | O(n2) | 1 | 不稳定 |
插入 | O(n2) | 1 | 稳定 |
希尔 | O(nlogn) | 1 | 不稳定 |
归并 | O(nlogn) | n | 稳定 |
快速 | O(nlogn) | logn | 不稳定 |
堆 | O(nlogn) | 1 | 不稳定 |
计数 | O(n+k) | k | 稳定 |
桶 | O(n+k) | n+k | 稳定 |
基数 | O(n+k) | n+k | 稳定 |