十大排序算法实现(原理+代码)

246 阅读10分钟

博主整理了十大排序算法于此篇博文,从原理到代码实现。

必背的表

没错,这是一张必背的表。不过我们可以理解着来背。

时间复杂度一定要记住平均时间复杂度。

至少要记住插入排序、堆排序、归并排序、快速排序。

注:不稳:两个相等的数,有可能在排序之后的顺序发生变化。
    空间复杂度:不需要额外空间则为1。

验证算法:肉眼不准确,我们可以采用对数器。

    static int[] randomArray(){
        Random r=new Random();
        int[] arr=new int[10000];
        for(int i=0;i<arr.length;i++){
            //10000以内的随机数
            arr[i]=r.nextInt(10000);
        }
        return arr;
    }
    static void check(){
        //一定要拷贝一个新数组,不然永远比较一样的数组就没有意义了
        int[] arr1=randomArray();
        int[] arr2=new int[arr1.length];
        //public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,int length);
        // src:要复制的数组(源数组) srcPos:复制源数组的起始位置 dest:目标数组 destPos:目标数组的下标位置 length:要复制的长度
        System.arraycopy(arr1,0,arr2,0,arr1.length);
        Arrays.sort(arr1);
        //要验证的自己写的算法
        xuanze.sort(arr2);
        boolean flag=true;
        for(int i=0;i<arr1.length;i++){
            if(arr1[i]!=arr2[i]){
                flag=false;
                break;
            }
        }
        System.out.println(flag);
    }

选择排序

思想最简单的排序。

从头到尾,找到最大(最小)的值的下标,和第一位交换;下一轮从第二到尾,找到次大(次小)的值的下标,和第二位交换……一直到找到倒数第二位确定,完毕。

举例:5 2 1 3 4->(交换下标0和2的数字) 1 2 5 3 4-> 1 2 5 3 4->(交换下标2和3的数字) 1 2 3 5 4-> (交换下标3和4的数字)1 2 3 4 5

import java.util.Scanner;
public class xuanze {
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=sc.nextInt();
        }
        //从下标0比较到下标n-2
        for(int i=0;i<n-1;i++){
            int min=i;
            for(int j=i+1;j<n;j++){
                min=arr[j]<arr[min]?j:min;
            }
            swap(arr,i,min);
        }
        print(arr);
    }
    static void print(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }
    static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
}

冒泡排序

从第一个数开始往后两两比较,大的放在后面,一次可以把最大放在最后面。 每一轮都比较到最后一个尚未排序的位置。(像一个泡泡冒到最右边)

举例:8 6 3 4 7-> 6 8 3 4 7-> 6 3 8 4 7->6 3 4 8 7-> 6 3 4 7 8(第一轮完成)->3 6 4 7 8(这时只比较到7)->3 4 6 7 8->3 4 6 7 8(第二轮完成)->3 4 6 7 8-> 3 4 6 7 8(第三轮完成)->3 4 6 7 8(第四轮完成,完毕)

import java.util.Scanner;

public class maopao {
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=sc.nextInt();
        }
        //两两比较,所以比较n-1次
        for(int i=0;i<n-1;i++){
            //n-1-i后面都排好序了(冒好泡了)
            for(int j=0;j<n-1-i;j++){
                if(arr[j]>arr[j+1]){
                    swap(arr,j,j+1);
                }
            }
        }
    }
    static void print(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }
    static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

}

插入排序

适合用于样本小,基本有序的数组。

每次抽出第i个数,逐个跟前面的数比较。

举例:87123- >78123(第一轮)->71823->17823(第二轮)->17283->12783(第三轮)->12738->12378(完毕)

import java.util.Scanner;

public class charu {
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=sc.nextInt();
        }
        //从第二个(下标为1)的数字开始和前面的数字比较
        for(int i=1;i<n;i++){
            for(int j=i;j>0;j--){
                if(arr[j]<arr[j-1]){
                    swap(arr,j,j-1);
                }
            }
        }
        print(arr);
    }
    static void print(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }
    static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

}

希尔排序

前面选择、冒泡、插入是简单排序,接下来七种要稍微难一点。

希尔排序是改进的插入排序。

先看一个例子:假如一个值gap=4,则0 4 8 12等下标的数先插入排序排好序;接下来1 5 9 13等下标的数排序……一共四组排序。然后gap=2再排一遍,最后一定要有gap=1再排一遍。(等下再说gap怎么来的)

希尔排序为何比插入排序快?插入排序时,起始位置和最终位置间隔比较大的数(比如数字1,起始下标在20)需要跟前面的数比较很多次(20、19、……、1)才能挪到排序后的位置,但是希尔排序就可以减少很多次排序(20、16、12、……)迅速挪到相应的位置;并且,起始位置和最终位置间隔比较小的数,移动的距离短(和前面的好处相映相成)。

import java.util.Scanner;

public class shellsort {
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=sc.nextInt();
        }
        //希尔最初的想法就是gap二分
        //gap =n/2……4 2 1(最后一定要到1)
        for(int gap=n/2;gap>0;gap/=2){
            //这里为什么是i++?因为4的位置要比较,5、6……都需要比较,所以i++
            for(int i=gap;i<n;i++){
                //j>gap-1,因为不能越界;j每次减去gap
                for(int j=i;j>gap-1;j-=gap){
                    if(arr[j]<arr[j-gap]){
                        swap(arr,j,j-gap);
                    }
                }
            }
        }
        print(arr);
    }
    static void print(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }
    static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

}

后来出现了Knuth序列,认为是更高效的。

h=1;h=3*h+1;(h<=n/3)

改进后的代码部分:

    int h=1;
    while(h<=n/3){
        h=h*3+1;
    }
    for(int gap=h;gap>0;gap=(gap-1)/3){            
        for(int i=gap;i<n;i++){
            for(int j=i;j>gap-1;j-=gap){
                if(arr[j]<arr[j-gap]){
                    swap(arr,j,j-gap);
                }
            }
        }
    }

归并排序

涉及到递归的思想,暂时不引申。

数组排序->子数组排序->子数组的子数组排序->……->一直到最后只剩两个数,排序,递归返回,完毕。

Java和Python中的对象排序用的就是改进的归并排序。 (对象排序要求稳定,为什么呢?除了数值这个属性,其他属性可能不一样,排序如果不稳定,会把两个数值一样但并不一样的两个对象弄混)

两个已经有序的数组要进行排序,怎么排呢?(假如1 4 6 7 10,还有2 3 5 8 9)

额外开辟一个等长(10)的数组。准备三个指针,i指向第一个数组开始的位置(数值1),j指向第二个数组开始的位置(数值2),k指向新开辟的数组开始的位置。i和j指向的值开始比较,小的赋值给k指向的位置,小的那个指针后移,k后移,直到排序完毕。(一个数组完毕后,另一个数组剩下的数可以直接放在新数组后面)

import java.util.Scanner;

public class shellsort {
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=sc.nextInt();
        }
        sort(arr,0,n-1);
        print(arr);
    }
    private static void sort(int[] arr,int left,int right){
        //跳出递归条件
        if(left==right){return;}
        //分成两半
        int mid=left+(right-left)/2;
        //左边排序
        sort(arr,left,mid);
        //右边排序
        sort(arr,mid+1,right);
        //两两合并
        merge(arr,left,mid,right);
    }
    private static void merge(int[] arr,int left,int mid,int right){
        int i = left;
        int j = mid+1;
        int[] temp=new int[right-left+1];
        int k = 0;
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){
                temp[k++] = arr[i++];
            }else {
                temp[k++] = arr[j++];
            }
        }
        while(i<=mid){
            temp[k++] = arr[i++];
        }
        while(j<=right){
            temp[k++] = arr[j++];
        }
        //排序后在temp里,要把temp的数放进arr里
        for(int m=0;m<temp.length;m++){
            arr[left+m]=temp[m];
        }
    }

    static void print(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }
}

Timsort:多路归并(改进的归并)。

快速排序

最常考的排序之一。

定一个基准,比基准小的在左边,比基准大的在右边。

import java.util.Scanner;

public class quicksort {
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        int[] arr=new int[n];
        for(int i=0;i<n;i++){
            arr[i]=sc.nextInt();
        }
        sort(arr,0,n-1);
        print(arr);
    }
    private static void sort(int[] arr,int low,int high){
        if(low>=high){return;}
        int index=partition(arr,low,high);
        sort(arr,low,index-1);
        sort(arr,index+1,high);
    }
    private static int partition(int[] arr,int low,int high){
        int pivot=arr[low];
        int left=low;
        int right=high;
        while (left<right){
            //基准在左边的第一个数,就先移动right指针,这样最后交换low和left的位置才能找到对的数
            while (left<right&&arr[right]>pivot){
                right--;
            }
            //考虑到重复元素,这里用的是<=
            while (left<right&&arr[left]<=pivot){
                left++;
            }
            if(left<right){
                swap(arr,left,right);
            }
        }
        swap(arr,low,left);
        return left;
    }
    static void print(int[] arr){
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }
    static void swap(int[] arr,int i,int j){
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
}

当数组基本有序,每次取第一个值当基准值,则partition会重复n次,快排退化成O(n^2)。解决办法其一为采用随机基准快排,只需改变一点点代码:(随机数组一个数作为基准,和第一个数交换,别的都一样)

private static int partition(int[] arr,int low,int high){
        //int pivot=arr[low];
        int p=(int)(low+Math.random()*(high-low+1));
        int pivot=arr[p];
        swap(arr,low,p);

堆排序

堆排序会涉及到二叉树的内容,暂时不引申。


计数排序

非比较排序,桶思想的一种特殊情况。相对用的多一些。

适用于量很大,但是数取值范围比较小。

新建数组,长度为取值范围的差(比如0~60,取61为长度)。

遍历题目的数组,每次下标相应符合,对应数字+1。输出的时候,从头开始每一位输出对应数字个的数。

import java.util.Arrays;
import java.util.Scanner;

public class jishu1 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
//        int n = sc.nextInt();
//        int[] arr = new int[n];
//        for (int i = 0; i < n; i++) {
//            arr[i] = sc.nextInt();
//        }
        int[] arr={100,101,105,103,107,104,102,109,109,107,106,106,106,105,104,107,108,109,103,105,106,105,101,102,100,102,109,108,107};
        sort(arr);
    }
    private static void sort(int[] arr){
        int[] res=new int[arr.length];
        //范围为100-109
        //下面这是一个不稳定的算法
//        int[] count=new int[10];
//        for(int i=0;i<arr.length;i++){
//            count[arr[i]-100]++;
//        }
//        for(int i=0,j=0;i<count.length;i++){
//            while (count[i]-->0){res[j++]=i+100;}
//        }
//        System.out.print(Arrays.toString(res));
        //那么稳定的是怎么样的呢
        int[] count=new int[10];
        for(int i=0;i<arr.length;i++){
            count[arr[i]-100]++;
        }
        //累加数组,可以判断每一个重复数的最后一个数的下标
        for(int i=1;i<count.length;i++){
            count[i]+=count[i-1];
        }
        //对原来的数组进行倒序排列
        for(int i=arr.length-1;i>=0;i--){
            res[--count[arr[i]-100]]=arr[i];
        }
        System.out.print(Arrays.toString(res));
    }
}

基数排序

非比较排序,桶思想的一种特殊情况。

多关键字排序。每一步都类似计数排序,有高位优先和低位优先。


桶排序

桶排序用的不多,因为桶的长度不好定。如果是数组ArrayList,排序好排,但扩容容易浪费,如果用链表,空间不浪费了,但排序费时间。 重点掌握还是计数排序和基数排序。

(日后继续会完善)