数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序

目录

1.Java比较器

(1)Comparable

(2)Comparator

(3)Comparable和Comparator使用对比

2.后面所有排序算法类继承的父类

3.冒泡排序

3.1 基本思想

3.2 图解原理:

3.3 Java代码实现

3.4 冒泡排序的优化

(1)优化切入点发现

(2)优化实现

3.5 性能分析


1.Java比较器

Java中的对象只能进行比较操作==或!=,不能使用>或<的,但是在开发场景中我们需要对多个对象进行排序,言外之意,就需要比较对象的大小,Java中使用Comparable或Comparator接口中任何一个

(1)Comparable

java.lang.Comparable

使用方法:

  • 实现Comparable接口
  • 重写compareTo(obj)方法,重写规则如下:
    • 如果当前对象this大于形参对象obj,则返回正整数,一般用1
    • 如果当前对象this小于形参对象obj,则返回负整数,一般用-1
    • 如果当前对象this等于形参对象obj,则返回零

像String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象的方式

String示例:

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第1张图片

使用String的该接口实现自然排序:

import java.util.Arrays;

public class ComparableDemo {
    public static void main(String[] args) {
        String[] arr = new String[]{"AA","CC","GG","BB","MM","DD"};
        //可以发现此方法对原数组中元素的顺序进行了改变
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第2张图片

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第3张图片

可以发现最终还是使用Comparable接口的compareTo方法在进行比较

自定义类实现Comparable接口实现自然排序:

  • 对于自定义类来说,如果需要排序,我们可以让自定义类来实现Comparable接口,重写compareTo(obj)方法,在compareTo(obj)方法中指明如何排序
  • 我们把这种实现叫做自然排序
public class Goods implements Comparable{
    private String name;
    private double price;

    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    //指明比较商品大小的方式:按照价格从低到高排序,再按照产品名称从低到高排序
    @Override
    public int compareTo(Object o) {
        if(o instanceof Goods){
            Goods goods = (Goods)o;
            if(this.price > goods.price){
                return 1;
            }else if(this.price < goods.price){
                return -1;
            }else {
                //return 0;
                return this.name.compareTo(goods.name);
            }
        }

        throw new RuntimeException("传入的数据类型不一致!");
    }
}
import java.util.Arrays;

public class ComparableDemo {
    public static void main(String[] args) {
        Goods[] arr = new Goods[4];
        arr[0] = new Goods("lenovoMouse",43);
        arr[1] = new Goods("huaweiMouse",65);
        arr[2] = new Goods("xiaomiMouse",25);
        arr[3] = new Goods("dellMouse",43);

        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

(2)Comparator

java.util.Comparator

  • 我们可以使用Comparator来实现定制排序
  • 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,或者实现了Comparable接口的排序规则不适合当前的操作,那么可以考虑用Comparator的对象来排序,强行对多个对象进行整体排序的比较
  • 使用方法:
    • 创建Comparator的实现类,在泛型中传入我们要比较的对象的类型
    • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小
      • 如果对象o1大于对象o2,则返回正整数,一般用1
      • 如果对象o1小于对象o2,则返回负整数,一般用-1
      • 如果对象o1等于对象o2,则返回零

示例代码1:

import java.util.Arrays;
import java.util.Comparator;

public class ComparableDemo {
    public static void main(String[] args) {
        String[] arr = new String[]{"AA","CC","GG","BB","MM","DD"};

        //String本身实现Comparable接口从小到大进行排序,
        // 我们想要从大到小进行排序,所以我们需要实现Comparator接口的compare方法来实现这种定制
         Arrays.sort(arr, new Comparator() {
            //按照字符串从大到小进行排列
            @Override
            public int compare(String o1, String o2) {

                    return -s1.compareTo(s2);

            }
        });
        System.out.println(Arrays.toString(arr));
    }
}

示例代码2:

public class Goods implements Comparable{
    private String name;
    private double price;

    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    //指明比较商品大小的方式:按照价格从低到高排序,再按照产品名称从低到高排序
    @Override
    public int compareTo(Object o) {
        if(o instanceof Goods){
            Goods goods = (Goods)o;
            if(this.price > goods.price){
                return 1;
            }else if(this.price < goods.price){
                return -1;
            }else {
                //return 0;
                return this.name.compareTo(goods.name);
            }
        }

        throw new RuntimeException("传入的数据类型不一致!");
    }
}
import java.util.Arrays;
import java.util.Comparator;

public class ComparableDemo {
    public static void main(String[] args) {
        Goods[] arr = new Goods[4];
        arr[0] = new Goods("lenovoMouse",43);
        arr[1] = new Goods("huaweiMouse",65);
        arr[2] = new Goods("xiaomiMouse",25);
        arr[3] = new Goods("dellMouse",43);

        Arrays.sort(arr, new Comparator() {
            //定义新的比较商品大小的方式:按照价格从低到高排序,再按照产品名称从高到低排序
            @Override
            public int compare(Goods o1, Goods o2) {
                if(o1.getPrice() == o2.getPrice()){
                    return -o1.getName().compareTo(o2.getName());
                }else {
                    return Double.compare(o1.getPrice(),o2.getPrice());
                }

            }
        });
        System.out.println(Arrays.toString(arr));
    }
}

  • 可以将Comparetor传递给sort方法(如Collections.sort或Arrays.sort)从而允许在排序顺序上实现精确控制
    • 数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第4张图片
  • 还可以使用Comparator来控制某些数据结构(如有序set或有序map)的顺序,或者为那些没有自然顺序的对象collection提供排序

(3)Comparable和Comparator使用对比

  • Comparable是我们在定义业务类的时候,通过它来指定排序的规则,一旦指定,保证Comparable实现类的对象在任何位置都可以比较大小
  • 而Comparetor是我们在使用时临时定制提供它的排序规则,业务类如果本身没有实现Comparable接口的话,下次使用还得再次Comparator类的实现类对象

2.后面所有排序算法类继承的父类

写如下父类的定义是为了在父类中定义比较和交换方法

public abstract class Sort {
    /**
     * 排序方法
     * 
     * 由各个具体的排序类自己去实现
     */
    public abstract void sort(Comparable[] arr);

    /**
     * 交换数组指定两个位置的元素
     */
    protected void swap(Comparable[] arr,int i,int j){
        Comparable temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    /**
     * 比较数组中位置i处的元素和位置j处的元素
     *
     * 返回值为正整数:arr[i] > arr[j]
     * 返回值为负整数:arr[i] < arr[j]
     * 返回值为零:arr[i] = arr[j]
     */
    protected int compare(Comparable[] arr,int i,int j){
        return arr[i].compareTo(arr[j]);
    }
}

3.冒泡排序

3.1 基本思想

  • 通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样向上冒一样

3.2 图解原理:

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第5张图片

小结:

  • 一共进行n-1次排序(n为数组的长度)
  • 每一趟遍历都会将未排序序列中的最大值“冒泡”(比较和交换)到未排序序列的最右边
  • 每一趟排序的比较的次数都在减少

3.3 Java代码实现

import java.util.Arrays;

public class BubbleSort extends Sort{
    @Override
    public void sort(Comparable[] arr) {

        //遍历n-1趟
        for(int i = 0;i < arr.length-1;i++){
            //内部循环为每一趟遍历arr.length-i个位置

            //错误写法
            //遍历到最后一个位置会让数组访问越界
//            for(int j = 0;j < arr.length-i;j++){
//                //每个位置和它后面的一个元素做比较
//                if(compare(arr,j,j+1) > 0){
//                    //前一个元素大于后面的元素,它们就交换
//                    swap(arr,j,j+1);
//                }
//            }

            //分析上述越界原因:因为我们总是和后面的一个元素做比较,上述的写法每一趟总是会对比多一个元素
            //              所以我们的当前元素只需要遍历到n-1位置的元素即可,这样后面一个元素就是n位置的元素
            //              这样整体上会把每次未排序的序列刚好遍历完

            //正确写法
            //遍历比较未排序序列的相邻连个值
            for(int j = 0;j < arr.length-1-i;j++){
                //每个位置和它后面的一个元素做比较
                if(compare(arr,j,j+1) > 0){
                    //前一个元素大于后面的元素,它们就交换
                    swap(arr,j,j+1);
                }
            }
            //以下两步不是必须,此处打印只是为了结果能清楚的观察
            System.out.println("第"+(i+1)+"趟排序后的数组为:");
            System.out.println(Arrays.toString(arr));
        }
    }


    //写法2:
/*
    @Override
    public void Sort(Comparable[] arr) {
		//外部为每次要排好序的位置,即每趟末尾都是最大值
        //而该i就相当于一个到此结束的标志位置
		for (int i = arr.length-1; i > 0; i--)
		{
			//每次要排好序的是i,让j循环到i-1,然后在比较的时候由于是j和j+1相比较和交换,所以正好会对比到i位置
			for (int j = 0; j < i; j++)
			{
				//每个位置进行比较,大于就交换
				if (compare(arr,j,j+1) > 0)
					swap(arr,j,j+1);
				//否则就继续迭代下一个位置
			}
		}
	}
*/

}
import java.util.Arrays;

public class SortTest {
    public static void main(String[] args) {
        Integer[] arr = new Integer[]{3, 9, -1, 10, -2};
        System.out.println("未排序的数组为:");
        System.out.println(Arrays.toString(arr));
        BubbleSort bubbleSort = new BubbleSort();
        bubbleSort.sort(arr);
    }
}

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第6张图片

3.4 冒泡排序的优化

(1)优化切入点发现

上述代码我们把测试用例改成

运行结果为:

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第7张图片

可以发现上面的后3趟并没有发生任何交换,做的是无用的比较

将测试用例修改为

运行结果为:

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第8张图片

可以发现上面的排好序的数组仍然要进行n-1趟,所以上述这种原始的冒泡排序的写法与输入无关,不管输入的数组是否有序,都会遍历n-1趟,比较次数为(n-1)+(n-2)+...+2+1=n(n-1)/2次,交换次数为倒置的数量

  • 倒置:即数组中两个顺序颠倒的元素
    • 没有倒置
    • 的倒置有(3,-1)
    • 的倒置有(3,-1)、(3,-2)、(9,-1),(9,-2),(-1,-2),(10,-2)

不过通过如上可以发现,排好序,就不会再发生交换,我们思考当不发生交换的时候,是否能表明已经排好序,答案是肯定的,此处不做证明

(2)优化实现

优化思路:

  • 如果我们在某一次排序中未发现任何交换,则表明该数组已经排好序,就可以提前结束排序

代码实现:

import java.util.Arrays;

public class BubbleSortImprove extends Sort{
    private boolean isSwap = false;  //标识变量,表示是否进行过交换

    @Override
    public void sort(Comparable[] arr) {

        //遍历n-1趟
        for(int i = 0;i < arr.length-1;i++){
            //内部循环为每一趟遍历arr.length-i个位置

            //遍历比较未排序序列的相邻连个值
            for(int j = 0;j < arr.length-1-i;j++){
                //每个位置和它后面的一个元素做比较
                if(compare(arr,j,j+1) > 0){
                    isSwap = true;
                    //前一个元素大于后面的元素,它们就交换
                    swap(arr,j,j+1);
                }
            }

            System.out.println("第"+(i+1)+"趟排序后的数组为:");
            System.out.println(Arrays.toString(arr));

            if(!isSwap){   //在一趟中未发生任何交换
                break;
            }else {    //发生了交换,重置标识变量,用于下次判断是否发生过交换
                isSwap = false;
            }
        }
    }

}
public class SortTest {
    public static void main(String[] args) {
        Integer[] arr = new Integer[]{3, 9, -1, 10, 20};

        BubbleSortImprove bubbleSortImprove = new BubbleSortImprove();
        bubbleSortImprove.sort(arr);
    }
}

数据结构和算法——排序算法(1)——Java比较器、基于比较的O(n^2)的排序算法(1)——冒泡排序_第9张图片

将测试用例修改为

运行结果为:

3.5 性能分析

优化前的冒泡排序:

  • 运行时间与输入无关,即与输入的数组是否排序无关
  • 遍历次数:n-1(n为数组长度)
  • 比较次数:(n-1)+(n-2)+...+2+1=n(n-1)/2
  • 交换次数:倒置的个数

优化后的冒泡排序:

  • 运行时间与输入的数组是否排序有关
  • 遍历次数:取决于哪一趟让数组已经完成排序,最差情况是第n-1趟完成排序
  • 比较次数:与遍历的趟数有关,最差情况是
  • 交换次数:倒置的个数

说明:关于性能测试的代码,我把排序算法实现类中的打印语句进行了注释,此时打印并不是我们关心的,还影响运行结果

优化前后性能测试:(输入是随机数据)

public class SortPerformanceTest {
    public static void main(String[] args) {
        //创建100000个随机数据
        Double[] arr1 = new Double[100000];
        for (int i = 0; i < arr1.length; i++) {
            arr1[i] = (Double) (Math.random() * 10000000);  //这里使用10000000是为了让数据更分散
        }

        //赋值上述创建的数组arr1的值到数组arr2
        Double[] arr2 = new Double[100000];
        for (int i = 0; i < arr1.length; i++) {
            arr2[i] = arr1[i];
        }
        
        //创建两种排序类的对象
        BubbleSort bubbleSort = new BubbleSort();
        BubbleSortImprove bubbleSortImprove = new BubbleSortImprove();
        
        //使用优化前的冒泡排序对arr1进行排序
        long bubbleSort_start = System.currentTimeMillis();
        bubbleSort.sort(arr1);
        long bubbleSort_end = System.currentTimeMillis();

        System.out.println("优化前的冒泡排序所用的时间为:"+(bubbleSort_end - bubbleSort_start)+"ms");

        //使用优化后的冒泡排序对arr2进行排序
        long bubbleSortImprove_start = System.currentTimeMillis();
        bubbleSortImprove.sort(arr2);
        long bubbleSortImprove_end = System.currentTimeMillis();

        System.out.println("优化后的冒泡排序所用的时间为:"+(bubbleSortImprove_end - bubbleSortImprove_start)+"ms");
    }
}

可以发现,对于随机输入,优化后的冒泡排序与优化前相比性能有改善但不是特别明显,甚至有时候因为我们多了对每次遍历对flag判断的代码,反而优化后比优化前所花费的时间还要长

你可能感兴趣的:(数据结构与算法)