Java:排序

发布于:2024-05-04 ⋅ 阅读:(27) ⋅ 点赞:(0)

一、Java对象的比较

1、比较对象的几种方法:

    上一篇文章我们介绍了【优先级队列】,即PriorityQueue,其实,优先级队列在插入元素中有一个要求:插入的元素不能是null或者元素之间必须要能够进行比较,为了简单起见,我们只是插入了Integer类型的数据。

  那么优先级队列能不能插入自定义的类型对象呢? 如果能,他又该如何比较该对象的大小,对其进行排序呢?

答:优先级队列可以插入自定义类型的对象!让我举例来解决这个问题!


  我们先自定义一个Card类型(卡牌类),然后在main方法中实例化一个PriorityQueue队列p,

给该队列添加两个元素,这两个元素是Card对象!

  运行程序,我们发现,程序抛出错误,原因是:该队列每次添加元素时,会对队列中元素进行排序,使其成为一个【小根堆】,那么,每次排序时都需要对元素之间【进行比较】,这里有一个问题:Card类型的对象该如何比较?比较的依据是什么?这些问题程序也不知道怎么做,因此会抛出错误! 

   这个问题其实在上一篇文章【优先级队列】已经介绍过,只需要自定义实现一个【比较器】然后将这个比较器作为参数传入【队列】的构造函数中即可!

  那么,我们先来探究一下,以该Card对象为例,对象之间该如何比较?

【方法1】:用双等号来比较?

    假想一下,数值相同是数字可以用双等号来判断值是否相同!那么,对象是否也可以呢?

首先,我们实例化两个Card对象c1和c2;这两个对象的rank和suit都相同,那么,我们看看,打印一下c1==c2的结果

  很遗憾,结果为false,c1与c2不同,这是因为c1和c2代表的只是【对象的引用】,好比一栋小区的同种类型的房间,内部的结构相同,可是【房间号】不同,而c1和c2的地位就类似于【房间号】,所以用【双等号】是无法比较对象类型是否相同的!  



【方法2】:使用equals方法

  我们都知道,每个对象的类型都默认继承【Object类】,可以说【Object类】是所有类型的【父类】,那么当然,它也是【Card类】的【父类】,这个Object类中里面有这么一个方法equals,它是来比较类型是否相同的方法!

  那么我们在【Card类】中重写该方法 ,写出我们比较这个【Card类】的具体依据:是根据【卡牌值rank】和【卡牌花色suit】来判断两个卡牌对象是否相同?

//自定义一个【卡牌类】
class Card {
    //成员变量
    public int rank;//数值
    public String suit;//花色

    //构造方法
    public Card(int rank,String suit){
        this.rank=rank;
        this.suit=suit;
    }


    //重写equals方法
    @Override
    public boolean equals(Object obj) {
        //自己和自己比较.,如c1.equals(c1)
        if(this==obj){
            return true;
        }
        //如果obj不是Card类,如c1.equals(new Student())
        if(obj==null||!(obj instanceof Card)){
            return false;
        }
        Card c=(Card)obj;
        return this.rank==c.rank&&this.suit==c.suit;

    }
}

  让我们调用一下该方法看看能否比较c1和c2? 结果为true,证明该方法可以以rank和suit为依据比较两个卡牌对象!



  重写equals方法的方式虽然可以比较两个对象的大小,但缺陷是,equals只能按照相等进行比较,不能按照大于、小于的方式进行比较!那么,我们需要学习另外一种方法,基于Comparable接口类的比较!

【方法3】:基于Compareable接口类的比较

Comparble是JDK提供的泛型的比较接口类,源码实现如下:

public interface Comparable<E>{

//比较泛型类的方法
int compareTo(E o);
  //返回值:
  //<0 :表示this指向的对象小于o指向的对象
  //=0 :表示this指向的对象等于o指向的对象
  //>0 : 表示this指向的对象大于o指向的对象
}

  当我们自定义一个类型时(如Card类),如果想要按照大小的方式进行比较时:可以在定义类时,实现Comparble接口即可,然后在该类中重写campareTo方法。 

//自定义一个【卡牌类】
class Card implements Comparable<Card>{//实现Comparable接口
    //成员变量
    public int rank;//数值
    public String suit;//花色

    //构造方法
    public Card(int rank,String suit){
        this.rank=rank;
        this.suit=suit;
    }
    
    //重写compareTo方法:根据卡牌值比较两个对象大小
    @Override
    public int compareTo(Card o) {
        return this.rank-o.rank;
    }
}

代码运行:

如图,调用一下compareTo方法,比较一下两张卡牌,程序运行打印出1,说明c1大于c2;



【方法4】:基于比较器的比较

前面的方法虽然好,但是也有一个缺点,就是,我们只能通过单一的方法对两个对象进行比较,其实,对两个对象的比较也可以根据其他成员变量的内容进行比较,那么单单通过一个compareTo方法就显得不够用了; 

  举例来说:我们定义一个学生类:创建学生A和学生B,我们希望可以比较一下两个对象的大小,即可以从四个方面来讨论,分别为年龄、年级、身高、体重,这都可以成为我们比较对象大小的依据,此时,只通过compareTo方法来比较学生类,就会显得不灵活!因为它一次只能通过某一种具体的判断依据来比较对象的大小!

class Student{
//成员变量
public int age;    //年龄
public int grade;  //年级
public int height; //身高
public int weight; //体重
}

  由此,我们来学习另外一种灵活的比较方法:基于【比较器】的比较 

比较器源码:

class 比较器名 implements Comparator<要比较对象的类名E>{

   //重写compare方法
   public int compare(E o1,E o2){
   //……具体比较方式自己写
   }

}
//1、自定义一个学生类
public class Student {
    //成员变量
    public int age;
    public int grade;
    public int height;
    public int weight;
    
    //构造方法
    public Student(int age,int grade,int height,int weight){
        this.age=age;
        this.grade=grade;
        this.height=height;
        this.weight=weight;
    }
}


//2、实现比较器
//年龄比较器
class AgeComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}
//年级比较器
class GradeComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.grade-o2.grade;
    }
}
//升高比较器
class HeightComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.height-o2.height;
    }
}
//体重比较器
class WeightComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.weight-o2.weight;
    }
}


//3、根据比较依据使用比较器
class test{
    public static void main(String[] args) {
        Student studentA=new Student(10,6,70,160);
        Student studentB=new Student(12,8,60,170);
        //实例化年龄比较器
        AgeComparator ageComparator=new AgeComparator();
        System.out.println(ageComparator.compare(studentA,studentB));
        //实例化年级比较器
        GradeComparator gradeComparator=new GradeComparator();
        System.out.println(gradeComparator.compare(studentA,studentB));
        //实例化生高比较器
        HeightComparator heightComparator=new HeightComparator();
        System.out.println(heightComparator.compare(studentA,studentB));
        //实例化体重比较器
        WeightComparator weightComparator=new WeightComparator();
        System.out.println(weightComparator.compare(studentA,studentB));
    }

   
}



2、给数组的每一个元素排序并且打印

//自定义一个【卡牌类】
class Card implements Comparable<Card>{//实现Comparable接口
    //成员变量
    public int rank;//数值
    public String suit;//花色

    //构造方法
    public Card(int rank,String suit){
        this.rank=rank;
        this.suit=suit;
    }

    //重写compareTo方法:
    @Override
    public int compareTo(Card o) {
        return this.rank-o.rank;
    }

    //重写equals方法
    @Override
    public boolean equals(Object obj) {
        //自己和自己比较.,如c1.equals(c1)
        if(this==obj){
            return true;
        }
        //如果obj不是Card类,如c1.equals(new Student())
        if(obj==null||!(obj instanceof Card)){
            return false;
        }
        Card c=(Card)obj;
        return this.rank==c.rank&&this.suit==c.suit;

    }

    
    //重写toString方法
    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }
}


//实现一个RankComparator比较器
class RankComparator implements Comparator<Card>{
    @Override
    //o2-o1
    public int compare(Card o1, Card o2) {
        return o2.rank-o1.rank;
    }
}


public class Test {
    public static void main(String[] args) {
        //给数组中每一个元素排序并且打印
        Card[] cards=new Card[3];
        cards[0]=new Card(1,"红桃");
        cards[1]=new Card(9,"红桃");
        cards[2]=new Card(6,"红桃");

        //基于Compareable接口类的比较
        Arrays.sort(cards);
        System.out.println(Arrays.toString(cards));

        //基于比较器的比较
        Arrays.sort(cards,new RankComparator());
        System.out.println(Arrays.toString(cards));


    }
}

 



二、排序算法

 所谓的排序,就是使一串记录,按照递增或者递减排列起来。

接下来,让我们来了解一下几种常见的排序算法!

1、直接插入排序

  【基本思想】

把待排序的记录按照其【关键码值】的大小逐个插入到一个【已经排好序】的有序列表中,直到所有的记录插入完为止,得到一个新的有序序列!

举个例子说明:假如给一个数组:10  、6  、4  、3  、7,要求按照从小到大排列!

1、首先,从第一个元素开始,认为该元素已经被排好序

2、取下一个元素,将这个元素的值【存入】tmp中,往前遍历前面的元素

3、如果前面的元素的值【大于】tmp的值,那么该元素向后移动一位

4、重复该操作,直至找到某个元素【小于或者等于】tmp,将tmp存入该元素的下一个位置

5、如果遍历结束,发现前面的元素都【大于】tmp,那么将tmp插入到下标为0的位置


【动画展示】

  我们将【i】下标的元素存入tmp中,然后再通过【j】来遍历下标为【i】之前的元素,如果发现  【j】  下标的元素【大于】tmp,那么将该元素移动到下一位!当【i】超过数组下标的最大值,排序结束!

第一次:

第二次:

第三次:

第四次:


【 代码实现】

  //插入排序
    public static void insertSort(int[] array){
        for(int i=1;i<array.length;i++){
            int j=0;
            int tmp=array[i];
            for(j=i-1;j>=0;j--){
                if(array[j]>tmp){
                    array[j+1]=array[j];
                }else{
                    break;
                }
            }
            array[j+1]=tmp;
        }
    }


2、希尔排序

【基本思想】 

  通过上面【直接插入排序】的学习,其实我们很容易知道,如果一个序列越趋于【有序】,那么插入排序的效率是越高的,如果想不明白,你可以拿着一个【完全逆序】的序列和一个【已经有序】的序列,走一遍【直接插入排序】,看看哪一个步骤比较少!显而易见,越趋于有序步骤越少,那么,下面的希尔排序,正是运用了这个思想:

先使序列趋于有序化!减少直接插入排序的步骤!从而提高了直接插入排序的效率!那么该如何做呢?

  首先,将一组【序列】均分为几组【序列】,先分别对每一组【序列】进行一遍【直接插入排序】。那么,相比于之前的序列,排序后的序列比之前更加有序了。 

  接着,减少分组的组数,将序列继续均分为几组序列,再对每组序列进行【直接插入排序】。

  以此类推,继续减少组数,分组,排序,直至最后分组的组数为1,进行【直接插入排序】,那么该序列就已经有序了! 


【排序分组】

那么现在,又有一个问题,该如何分组,才比较好呢?或许有人会这么想,临近的数据分组。我们来分析一下。

  首先,我们分组希望得到这样一个结果,那么就是【尽量使大的元素往后靠,小的元素往前靠】,那么这样分组明显是达不到我们要的效果的!比如第一组的【9】 和【1】,分组之后【9 】仍然在靠前的位置!

因此,我们需要考虑另外一种更加好的分组方式!比如,跳跃分组!每一组的间隔即为分组的组数,这样它就满足了【大元素往后靠,小元素往前靠】的要求!


 【动画展示】

第一次排序:

组数=元素个数/2=5

第二次排序:

组数=原来组数/2=2

第三次排序

组数=原来组数/2=1

这里也就是最后一次排序,原理相同,不再展示了! 


【代码实现】


    //希尔排序
    public static void shellSort(int [] array){
        //1\第一次的组数=元素个数/2
        int gap= array.length/2;

        //2\循环结束的条件是组数为0
        while(gap>=1){
            insertSort(gap,array);
            gap=gap/2;
        }
    }

   //改良版的【直接插入排序】
    public static void insertSort(int gap,int [] array){
        //注意,这里排序的过程并不是像动画展示那样分别排序,它的排序过程是交替进行的
        //如果不理解,可按照代码逻辑演示一遍
        for(int i=gap;i<array.length;i++){
            int j=i-gap;
            int tmp=array[i];
            for(;j>=0;j=j-gap){
                if(array[j]>tmp){
                    array[j+gap]=array[j];
                }else{
                    break;
                }
            }
            array[j+gap]=tmp;
        }
    }


3、直接选择排序

 【基本思想】

假设一个数组共有n个元素;

第一次,令【i】为0,在array[i]---->array[n-1] 的元素中,找出最小的【元素】,让该最小元素与array[i]交换!

第二次,令i++;重复上述操作,直至i==n-1;


【动画展示】

  该排序过程使用【j】来遍历【i】下标之后的元素,将元素最小值的下标存入【minIndex】中,每次【j】遍历结束,交换array[i]和array[minIndex]


【代码实现】 

   //选择排序
    public static void select(int [] array){
        for(int i=0;i<array.length;i++){
            int j=i+1;
            int minIndex=i;
            for(;j< array.length;j++){
                if(array[j]<array[minIndex]){
                    minIndex=j;
                }
            }
            swap(array,i,minIndex);//交换元素
        }
    }

    //交换元素
    public static void swap(int [] array,int i,int j){
        int tmp=array[i];
        array[i]=array[j];
        array[j]=tmp;
    }


4、冒泡排序

【基本思想】

每次比较相邻两个元素的值,将小元素置于大元素之前,这样,通过第一轮遍历数组后,可以将最大元素置于数组末尾。

再次重复上述操作,将倒数第二大的元素置于倒数第二个位置!

以此类推……


【动画展示】


【代码实现】

  //冒泡排序
    public static void bubbleSort(int [] array){
        //外层循环:冒泡排序的趟数(元素个数-1)
        for(int i=1;i<=array.length-1;i++){
            //内层循环:需要遍历元素的个数
            for(int j=0;j<=array.length-1-i;j++){
                //将大元素置于小元素之后
                if(array[j]>array[j+1]){
                    swap(array,j,j+1);
                }
            }
        }
    }
    //交换元素
    public static void swap(int [] array,int i,int j){
        int tmp=array[i];
        array[i]=array[j];
        array[j]=tmp;
    }



6、快速排序 

【基本思想】 

  【快速排序】是一种利用递归算法的排序! 通过多次调用同一个方法,将一个大的序列的排序看作一个个子序列的排序,通过子序列的不断排序,最终使大序列有序!

   下面就举一个例子,来阐述一下快速排序的原理!

   假设我们要排列以下序列,首先,先规定一个【基准元素】,使用P来代表【基准元素的下标】默认该元素为【数组的最左边元素】,起初,使用L和R分别代表数组的【最左边下标】和【最右边下标】

1、在【L<R】的前提下,先使用R找到小于【基准元素】的元素下标,再使用L找到大于【基准元素的元素下标;

2、各自找到下标后,交换下标为R和L的元素,最后,直至R==L,交换下标为P和下标为L(或者R)的元素!

3、经历过一次这样的排序过后,可以取得这样的效果:

     即【基准元素】会分割整个序列,使得【基准元素】前的元素都是小于或者等于【基准元素】,【基准元素】之后的元素都大于或者等于【基准元素】

4、再将基准元素之前的序列基准元素之后的序列 看作两个【子序列】,各自重复进行1、2操作,直至整个序列有序!


【动画展示】

 


【代码实现】


    //快速排序
    public static void quickSort(int [] array,int start,int end){
        if(start>end){
            return;
        }
        int parIndex=partition(array,start,end);
        quickSort(array,start,parIndex-1);
        quickSort(array,parIndex+1,end);
    }
    //以基准元素分割序列
    public static int partition(int [] array,int left,int right){
        int par=array[left];
        int i=left;//记录par的下标
        while(left<right){
            //一定要先找右边!!!
            //在右边找到【小于】par的元素下标
            while (left<right&&array[right]>=par){
                right--;
            }
            //在左边找到【大于】par的元素下标
            while (left<right&&array[left]<=par){
                left++;
            }
            //此时left<right或者left==right
            swap(array,left,right);

        }
        swap(array,i,left);
        return left;
    }
    //交换元素
    public static void swap(int [] array,int i,int j){
        int tmp=array[i];
        array[i]=array[j];
        array[j]=tmp;
    }



7、归并排序

【基本思想】

  归并排序的排序思路整体分为两个步骤:【分序】和【合序】。

  也就是:先将一个【大的序列】分割为一个个【单独的元素】,然后再将【元素】合并为【小的有序序列】,再将这些【小的有序序列】合并为一个【大的有序序列】! 


【分序】

    数组的分序是通过递归实现的,首先,我们需要使用L和R,分别代表数组的【最左边】下标和【最右边】下标!使用mid代表【(L+R)/ 2】的下标

  然后再根据【mid】分割序列,将数组分割为【L——>mid】的左序列和【mid+1——>R】的右序列,再分别给左序列右序列定义新的R、L、mid,重复上述操作!直至L>=R,分割序列结束


 【合序】

  在【分序】操作中,一个序列会被分割为【左序列】和【右序列】,那么合序的操作就是依次将匹配的左右序列按照【有序序列】合并!

  因此,这里使用了s1,e1和s2,e2分别代表左右序列的起始下标和终止下标,每次比较左右序列起始下标的元素大小,将小的元素放在创建好的临时数组中,直至s1和s2都等于各自的终止下标,才算合序结束!

  这样,通过多次左右序列的合序,最终整个数组趋于有序!


【代码实现】


    //归并排序
    public static void mergeSort(int []array,int left,int right){
        //当最左边下标大于或者等于最右边下标,递归结束
        if(left>=right){
            return;
        }
        //分序
        int mid=(left+right)/2;
        mergeSort(array,left,mid);//左序列
        mergeSort(array,mid+1,right);//右序列
        //合并左序列和右序列
        merge(array,left,right,mid);

    }

    //合并序列并且拷贝到原数组中
    public static void merge(int[] array,int left,int right,int mid){
        //创建一个临时数组合并两个【小序列】元素
        int [] tmp=new int[right-left+1];
        int s1=left;
        int e1=mid;
        int s2=mid+1;
        int e2=right;
        int k=0;
        while (s1<=e1&&s2<=e2){
            if(array[s1]<array[s2]){
                tmp[k++]=array[s1++];
            }else {
                tmp[k++]=array[s2++];
            }
        }
        while(s1<=e1){
            tmp[k++]=array[s1++];
        }
        while (s2<=e2){
            tmp[k++]=array[s2++];
        }
        //将排好序的数组拷贝到原数组
        for(int i=0;i<tmp.length;i++){
            array[i+left]=tmp[i];//这一步的代码建议通过【调试】自行去理解,因为不好解释
        }

    }


网站公告

今日签到

点亮在社区的每一天
去签到