排序算法,最全面的

该文档中记录我在学排序算法中出现的一些问题,可以借鉴一下

program 1

关于接口对象 ,代表什么意思,接口应该不能创建实例的,但是为啥存在接口的实例,关于Comparable接口,Student类继承Comparable接口,创建get Max方法创建新的命令,输入输出采用的就是Comparable对象了。

package test;

import sortAlgorithm.Student;
public class test{
    public static Comparable getMax(Comparable c1,Comparable c2){
        return (c1.compareTo(c2)>=0)?c1:c2;
    }
    public static void main(String[] args){
        Student s1 = new Student();
        s1.setAge(12);
        s1.setName("张三");
        Student s2 = new Student();
        s2.setName("李四");
        s2.setAge(15);
        System.out.println(getMax(s1,s2));


        long startTimes = System.currentTimeMillis();

        long endTimes = System.currentTimeMillis();
        System.out.println("运行时间为"+(endTimes-startTimes));
}
}
package sortAlgorithm;

public class Student implements Comparable<Student>{
    String name;
    int age;

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", studentNum=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return this.getAge()-o.getAge();
    }
}

answer One

接口与抽象类类似,表示一个大类,对外部来讲,相当于一个黑匣子,当接口的实例进行不同的操作时,你会发现接口的方法会根据不同的子类型,进行不同的操作,也就是接口是一个黑匣子的外部接口。

interface Person{
    void eat();
    void sleep();
}
 
class Studentimplements Person{
    public void eat(){
       System.out.println("学生去食堂吃饭!");
    }
    public void sleep(){
       System.out.println("学生回寝室睡觉!");
    }
}
 
class Teacherimplements Person{
    public void eat(){
       System.out.println("教师去教工餐厅吃饭!");
    }
    public void sleep(){
       System.out.println("教师回学校公寓睡觉!");
    }
}
 class Parents implements Person{
    publicvoid eat(){
       System.out.println("家长去招待所饭馆吃饭!");
    }
    public void sleep(){
       System.out.println("家长回招待所睡觉!");
    }
}
 
public class PersonInterface{
         public static void main(String[] args)
         {
                   Person p=new Student(); //创建一个Person接口,没有实例,实例是接口的继承了接口的对象,上面创建了一个学生实例,完成了学生的操作,下面创建了一个老师实例和父母实例。分别按照老师和父母的行为完成了他们相应的操作。
                   p.eat();
                   p.sleep();
                   p=new Teacher();
                   p.eat();
                   p.sleep();
                   p=new Parents();
                   p.eat();
                   p.sleep();
         }
}

program 2

接口 和继承还存在很大的问题,原因缺少实战,以后练完数据结构再练实战项目,先拿到offer,35万年薪offer,五年内年薪破百万。

排序方法中一定要采用交换数据,不能采用直接赋值,直接赋值会对某些元素重复使用。

1.冒泡排序法

简介,比较相邻两个元素值,大的放在后面,小的放在前面,第一遍会直接将最大的值放到最后面,然后不看这个元素,然后再对剩下的元素进行排列,第二遍会将第二大的值放在倒数第二个的位置,同样的第三遍会将第三大的值放在倒数第三大的值放在倒数第三个位置,依次向后排列,然后就会得到一个完整的排序过后的数组。

public static void sort(int[] arr){
    for (int i = arr.length-1 ; i > 0 ; i--){
        for (int j = 0 ; j < i ; j++ ){
            if (arr[j] > arr[i])
                swap(arr,i,j);
        }

    }    }
public static void swap(int[] arr,int i ,int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

总体来说,冒牌排序如下几步

判断数组的左右元素的大小,小的放在左边,大的放在右边,这样相当于每一遍,将最大值放在最右边 遍历一遍找到一个最大值,放在最右边

循环N遍,就得到了有序的数组。

冒泡排序最坏情况下,最坏情况就是逆序。时间复杂度也就是 N+N-1+N-2+…+1=N的平方/2 时间复杂度也就是二次方

2.选择排序法

每一次循环,找出最小的元素放在第一位,下一次循环时不看第一个元素,同样找出第一个元素放在第二个位置

  public static void sort(int[] arr){
        for (int i = 0 ; i < arr.length -1 ; i++ ){
            int temp = i;
            for (int j = i+1 ; j <arr.length ; j++){
                if (arr[j]<arr[temp])
                    temp = j;
            }
            exchange(arr,temp,i);
        }
    }
    public static void exchange(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

选择排序法分为如下几步

1.找到数组中最小元素的索引 遍历数组一遍找到最小元素

2.然后将最小元素与排在前几个元素的位置进行交换,最终得到了有序的数组,遍历N遍得到有序数组

选择排序法时时间复杂度是,N-1+N-2+…+1=N的平方/2-N/2,与冒泡排序法相比,时间复杂度更小一点,时间复杂度正属于N的平方

3.插入排序法

把数组分成两部分,左边是排序数组,右边是未排序数组,初始排序数组只有一个元素,也就是整个数组的第一个元素.

然后将未排序数组中的元素从左到右放进已排序的数组中,若未排序数组第一个大于已排序数组最大的元素,则保持位置不动,相当于放进了已排序数组的最右端,若未排序数组第一个小于已排序数组最大的元素,则开始与左边的元素交换,直到这个元素不大于右边的元素位置。

算法描述

①第i次循环时,默认前面i个元素为有序的,将后面的元素 ai后面,像前面插入,如何插入呢?

用待插入的数据与已插入的数据的最大值进行比较,(也可以最小值比较,算法要重新设计)

若是待插入的值较大,则不要动,若是待插入的值较小,往前排列直到待插入的值大于前面的元素的值

使前面的数据依然使有序的,当i等于数组的长度-1时,排序完成。

public static void sort(int[] arr){
    int j;
    for (int i = 1; i < arr.length ; i++){
        int temp = arr[i];
        //注意是和arr[j-1]比较,不是和arr[j]进行比较
        //因为第一次循环的时候i=j,arr[i] = temp 所以temp = arr[j]  所有相当于和自己进行比较
        //也正是这个原因比较也只能采用temp而不能采用arr[i],因为在下面的循环中会调动arr[i],也就是arr[i]是变化的
        for (j = i ; j > 0 && temp<arr[j-1] ; j--){ 
            arr[j] = arr[j-1];   //  若不符合要求,则根据要求 对元素的位置进行变动
            }
        arr[j] = temp;  // 将指定的元素插入到指定的位置
        }
    }

插入排序共分为以下几步

数组分为已排序和未排序,外层循环将数组未排序元素放进已排序元素

若不能直接放进已排序数组,则需要循环来确定放进已排序数组中的位置。

我们来分析一下最坏时间复杂度。

最坏情况下,每次循环都要进行 i-1次操作,

时间复杂度为1+2+3+…+N-1=N的平方/2-N/2,同样也属于N的平方

最好情况下,每次循环只进行 1次操作

时间复杂度为N-1 时间复杂度属于N

因为插入排序过程中,需要往已排序的数组插入数据,这个过程需要查找,我们常用的查找算法就是暴力算法,即遍历一边数组,但是我们可任意才用更优的算法,就比如二分查找,因为前面数据是有序的,所以可以才用二分查找。这样与暴力法相比,运行时间又会减少,所以整个算法的时间复杂度会更好一点。这也就是插入排序中的二分法插入法。

4.希尔排序法(插入排序的改进版)

分为如下几步:

1.选定好一个增长量h,按照增长量作为分组的依据,对数据进行分组。

2.分别对每一组数据进行插入排序

3.减小增长率,重复操作,直到增长量为1,就得到最终有序的数组。

增长量是影响希尔排序时间复杂度的关键因素。这里采用的增长量是5,2,1,数组长度是10,采用不同长度的数组,增长量不同,时间复杂度会有很大变化的。

public static void sort(int[] arr){
    int h = arr.length/2;
    int j;
    while (h>=1){
       for (int i = h ; i < arr.length ; i++){
           int temp = arr[i];
           for (j = i ; j >= h && temp < arr[j-h] ;j -= h){
               arr[j] = arr[j - h];
           }
           arr[j] = temp;
       }
        h /= 2;
    }
}

该排序方法时间复杂度的计算很难,所有采用估算法,我们利用程序对十万个数据进行排序,查看所需要的时间。

package test;

import java.util.Arrays;

public class test {
    public static void sort(int[] arr){
        int h = arr.length/2;
        int j;
        while (h >= 1){  //划分数组,在此希尔排序中,增量定为h,且以半数不断减少,在此依次是,5,2,1
            // 最外层循环共分为三次 ,每次h的值分别是 5 ,2 ,1
            // 然后进行以增量h分别进行 插入排序
            for (int i = h ; i < arr.length ; i++){
                int temp = arr[i];
                for (j = i ; j >= h && temp < arr[j-h] ; j -= h )
                    arr[j] = arr[j-h];
                arr[j] = temp;
            }
            h /= 2;
        }

    }
    public static void sort2(int[] arr){
        int j;
        for (int i = 1; i < arr.length ; i++){
            int temp = arr[i];
            //注意是和arr[j-1]比较,不是和arr[j]进行比较
            //因为第一次循环的时候i=j,arr[i] = temp 所以temp = arr[j]  所有相当于和自己进行比较
            //也正是这个原因比较也只能采用temp而不能采用arr[i],因为在下面的循环中会调动arr[i],也就是arr[i]是变化的
            for (j = i ; j > 0 && temp<arr[j-1] ; j--){
                arr[j] = arr[j-1];
            }
            arr[j] = temp;
        }
    }

    public static int[] getNums(int n){
        int[] nums = new int[100000];
        for (int i = 0 ; i < nums.length ; i++ ) {
            nums[i] = (int)(Math.random()*100000);
        }
        return nums;
    }
    public static void main(String[] args){
        int[] nums1 = getNums(100000); // 随机生成十万个数据
        long startTimes = System.currentTimeMillis();
        sort(nums1);
        long endTimes = System.currentTimeMillis();
        System.out.println(Arrays.toString(nums1));
        System.out.println("希尔排序方法运行时间为"+(endTimes-startTimes)+"ms");
        int[] nums2 = getNums(100000);
        long startTimes2 = System.currentTimeMillis();
        sort2(nums2);
        long endTimes2 = System.currentTimeMillis();
        System.out.println(Arrays.toString(nums2));
        System.out.println("插入排序方法运行时间为"+(endTimes2-startTimes2)+"ms");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TqSRbqhj-1600168974878)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200829211734919.png)]

运行结果,我们发现希尔排序和插入排序的时间复杂度相差很多。希尔排序运行时间是16ms,插入排序是768毫秒。

希尔排序算法的时间复杂度

最好情况,时间复杂度为NlogN

最坏情况,时间复杂度为N的平方

希尔排序的时间复杂度受序列增量影响很大,所以希尔排序的时间复杂度不太确定。

5.归并排序

首先要分,分半,一直分到子数组中只有一个元素,这样也就可以说每个数组都是有序的,因为每个数组中只有一个元素,然后就开始对数组进行处理,对有序数组进行排序,然后将每个只含有一个元素的数组进行归并,数组不断合并,每次合并得到的都是有序数组

首先第一步 对原数组中数据进行分组,然后对分组之后数组进行排序

 public static void mergeSort(int[] nums){
        // 创造函数所创造的备用数组
        int[] temp = new int[nums.length];
        //  带入递归函数中
        mergeSort(nums,temp,0,nums.length-1);
    }
    public static void mergeSort(int[] nums,int[] tempArr,int low,int high){
        // 判断函数数值被分割大小
        if (low<high){
            // 将数组对半分开
            int mid = (low+high)/2;
            // 对左边数组进行递归排序
            mergeSort(nums,tempArr,0,mid);
            // 对右边数组进行递归排序
            mergeSort(nums,tempArr,mid+1,high);
            // 对两个进行归并
            merge(nums,tempArr,low,mid,high);
        }
    }
    public static void merge(int[] nums,int[] tempArr,int low,int mid,int high){
        // 设置指针 
        // 设置左指针和左边指针边界
        int leftPos = low;
        int leftEnd = mid;
        // 设置右指针和右边指针边界
        int rightPos = mid+1;
        int rightEnd = high;
        // 设置备用数组的指针
        int temPos = leftPos;
        // 需要归并的元素的数量
        int numElements = high-low+1;
        // 对数组中的元素进行归并,将偏小一点的元素值放在前面
        while (leftPos<=leftEnd&&rightPos<=rightEnd){
            if (nums[leftPos]<nums[rightPos]){
                tempArr[temPos++] = nums[leftPos++];
            } else{
                tempArr[temPos++] = nums[rightPos++];
            }
        }
        // 如果左边数组比较长,右边数组都已经归并完成,左边数组还没有归并完成,那么直接将左边数组全部放进备用数组中
        while (leftPos<=leftEnd){
            tempArr[temPos++] = nums[leftPos++];
        }
        // 如果右边数组比较长,左边数组都已经归并完成,右边数组还没有归并完成,那么直接将右边数组全部放进备用数组中
        while (rightPos<=rightEnd){
            tempArr[temPos++] = nums[rightPos++];
        }
        // 现在数组被有序的放到了备用数组中,我们将排好序的元素放进原始数组中
        // i代表元素的数量,rightEnd控制元素存放的位置
        for (int i = 0;i<numElements;i++,rightEnd--){
            nums[rightEnd] = tempArr[rightEnd];
        }
    }

时间复杂度
在这里插入图片描述

我们可以看到,归并排序与希尔排序时间均为15ms左右,所以可以证明他们的时间复杂度为NlogN,与上面相比同样比插入排序速度快很多。

6.快速排序

同样是分组的原理,在数据中找出中间值,然后将大于该中间值的数据分为一组,小于这个元素放在一组,大的数组放在中间值右边,小的元素放在中间值左边,然后对每个小数组进行在排序,最终就会得到有序的数组。

算法描述:

①选取数组中的一个值作为基准值,可以是任意值,一般来说选第一个值,或中间值,然后空出这个位置的值

②在数组后端找到小于基准值的元素放在数组前端。这样,把大的数据放在后面,小的数据放在前面。

③重复执行之前的操作,可以分别在 4 2 1这样的位置执行以上的操作,经过logN次操作基本变换完成。这个过程要用递归。

 
public static void main(String[] args) {
        // 测试数组
        int[] nums = new int[]{1,4,7,2,5,8,3,6,9,0};
        // 输出原始的数组
        System.out.println(Arrays.toString(nums));
        // 对数组进行排序
        quickSort(nums);
        // 输出排序后的数组
        System.out.println(Arrays.toString(nums));
    }
    
    public static void quickSort(int[] arr) {
        int low = 0;
        int high = arr.length-1;
        // 将数组带入递归中
        quickSort(arr,low,high);
    }
    
    private static void quickSort(int[] arr, int low, int high) {
        if(low < high){
            //分区操作,将一个数组分成两个分区,返回分区界限索引
            // 并且这个过程中将大于基准值的数都放进左边,将小于基准值的数第一放进右边,返回中间的位置
            int index = partition(arr,low,high);
            //对左分区进行排序
            quickSort(arr,low,index-1);
            //中间位置的值不用,因为已经不存在左区的元素和右区元素的互相移动了
            //对右分区进行快排
            quickSort(arr,index+1,high);
        }
    }
    private static int partition(int[] arr, int low, int high) {
        //初始化左指针i和右指针j
        int i = low;
        int j= high;
        //将第一个数作为基准值
        int x = arr[low];
        //使用循环实现分区操作
        while(i != j){
            //从右向左,找到第一个小于基准值的值 arr[j],找右边的第一个坑
            while(arr[j]>=x && i<j){
                j--;
            }
            //将右侧找到小于基准数的值加入到左边的(坑)位置, 左指针想中间移动一个位置i++
            if(i<j){
                arr[i] = arr[j];
                i++;
            }
            //从左向右查找,找到第一个大于等于基准值的值 arr[i],找左边的第一个坑
            while(arr[i]<x && i<j){
                i++;
            }
            //将左侧找到的值等于基准值的值加入到右边的坑中,右指针向中间移动一个位置
            if(i<j){
                arr[j] = arr[i];
                j--;
            }
        }

        //使用基准值填坑,这就是基准值的最终位置
        arr[i] = x;
        //返回基准值的位置索引,也就是等同于中间值
        return i;
    }

快速排序的不同种实现方法

我们也可以采用list集合来进行操作,因为list集合对数组操作会快很多

public static void sort(List<Integer> list){
        if (list.size() > 1){
        List<Integer> smaller = new ArrayList<>();
        List<Integer> normal = new ArrayList<>();
        List<Integer> larger = new ArrayList<>();
        Integer chosenItem =list.get(list.size()/2);
        for (Integer i: list){
            if (i>chosenItem)
                larger.add(i);
            else if (i<chosenItem)
                smaller.add(i);
            else
                normal.add(i);
        }
        sort(smaller);
        sort(larger);
        list.clear();
        list.addAll(smaller);
        list.addAll(normal);
        list.addAll(larger);
    }
    }

时间复杂度

最好情况,NlogN

最坏情况,N的平方

这里上面的快速排序和下面的快速排序时间复杂度也是不同的,所以每种排序算法的不同实现方法也会有不同的效果,我们可以来演示一下。我们用十万个随机数来进行测试。

在这里插入图片描述

我们发现,采用List集合快速排序比没有采用的快速排序速度快了接近一倍。

当数据数量较多,数据随机排列时,快速排列是“快速的”,当数据数量较少时或者基准值选取不合适时,快速排序较慢,也就是说快速排序时不稳定的。

7.堆排序

堆是一个部分排序的完全二叉树,将数据存放在数组中

在堆这种数据结构中,堆中某个节点的值总是不大于或不小于其父节点的值。类似于这种形式

每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FsF3m37-1600168974882)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200915165412749.png)]

同时,我们对堆中的结点按层进行编号,然后映射到数组中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cXUrOEPN-1600168974883)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200915165601754.png)]

这个数组就是一个堆结构。

堆排序的思想是,将待排序的数组构造成一个大顶堆,此时,整个数组中的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾元素就成为了最大值,然后将数组中剩余的元素重构造成一个堆,这样每递归一次,就会得到剩下元素中的最大值,将这些最大值,排列起来就能得到一个有序的数组。所以堆排序的核心就是构造一个堆的函数

对数组进行堆排序,

首先,我们得把数组转变为一个堆,然后再进行排序

public static void main(String []args){
    int []arr = {9,8,7,6,5,4,3,2,1};
    sort(arr);
    System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
    //1.构建大顶堆
    for(int i=arr.length/2-1;i>=0;i--){
        //从第一个非叶子结点从下至上,从右至左调整结构
        adjustHeap(arr,i,arr.length);
    }
    //2.调整堆结构+交换堆顶元素与末尾元素
    for(int j=arr.length-1;j>0;j--){
        swap(arr,0,j);
        //将堆顶元素与末尾元素进行交换
        adjustHeap(arr,0,j);
        //重新对堆进行调整
    }

}
// 构造堆函数,核心算法
public static void adjustHeap(int []arr,int i,int length){
    //先取出当前元素i
    int temp = arr[i];
    //从i结点的左子结点开始,也就是2i+1处开始
    for(int k=i*2+1;k<length;k=k*2+1){
        //如果左子结点小于右子结点,k指向右子结点
        if(k+1<length && arr[k]<arr[k+1]){
            k++;
        }
        //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
        if(arr[k] >temp){
            arr[i] = arr[k];
            i = k;
        }else{
            break;
        }
    }
    //将temp值放到最终的位置
    arr[i] = temp;
   
}

public static void swap(int []arr,int a ,int b){
    int temp=arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}

时间复杂度分析

可以看到每次从堆中取出最大元素活着最小元素的时间是O(logN),我们要取N个元素,也就是说,整个过程的时间复杂度是NlogN

8.计数排序

计数排序不适用于一般情况,适用于出现的数字数量比较少,出现的数量比较多的情况,就比如一个数组是1,1,2,3,2,1,1,2,1,2,3,1,2,1,1

这种情况下,我们可以可以先建立一个长度为三的数组,然后安装数量依次将数组中的元素放进新的元素中,我们来试一下

public static void countsort(int[] nums){
    // 创建一个存放每个元素出现次数的数组
    int[] temp = new int[10];
   // 将出现的每个出现的元素的出现次数放进数组中
    for (int i=0;i<nums.length;i++){
        temp[nums[i]]++;
    }
    
    int j=0;
    int i=0;
    // 将备用数组中的元素依次放到原数组中
    while (i<nums.length&&j<temp.length){
        if (temp[j]==0){
            j++;
        }else {
            temp[j]--;
            nums[i++]=j;
        }
    }
}
9.慢排序

这个只要了解一下就好了

慢排序的思想是首先将数组从中间分解,分解到个位数,然后每两个数字进行比较,找出最大值,将最大值取出来,这样循环往复,这样每次找到一个最大值所需的时间是logN,找出N个时间复杂度也就是NlogN,所以慢排序的时间复杂度是NlogN

注:Arrays类中的排序方法sort

Java中自带的一个Arrays类,这个类中存在一个排序方法,那么这个类中的排序方法采用的是那种排序方法呢?

我们可以看一下API
链接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ezatBvBy-1600168974883)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200830173926382.png)]

这里说,他们采用的是Vladimir Yaroslavskiy、Jon Bentley和Joshua Bloch的双轴快速排序算法,这是JDK8的文档说明

我们可以看一下最新的JDK14中的Arrays类中的排序采用的是什么排序。

JDK14中才用的是多种排序。

1.当元素数量少于47个时,采用插入排序

2.元素数量大于等于47个,小于等于286个时,采用的是经典快速排序。

3.元素数量大于286个,如果数组的连续性好,采用归并排序,如果连续性不好,采用的是双轴快速排序。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值