常见排序算法及其实现

算法优劣:

  1. 执行效率
  2. 内存消耗,看是否是原地排序(既不需要额外的空间)
  3. 稳定性 ,即相同元素,如果排序前 a1 在 a2 前,排序完依旧是这样

根据比较来进行排序的算法有:冒泡,插入,选择 。 

一 基于比较和交换的算法:

1 冒泡算法:

每次通过比较来判断与后一位的大小关系,来确定是否交换,每次内层循环结束,表明有一个最大的或者是最小的被筛选出来。如果某次循环没有进行过任何的交换,说明此时的数组已经是有序的。

代码:

    public void bubbleSore() {

        int arr[] = {10,8,7,5, 4, 3, 2, 1};
        //外层for循环控制的是冒泡次数
        for (int i = 0; i < arr.length; i++) {
            boolean flag = false;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = true;
                }
            }
            if(!flag){
                System.out.println("没有数据交换");
                break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }

算法分析:

  1. 冒泡排序是原地算法,并不涉及到额外的空间
  2. 是稳定排序,不会改变相同数据的顺序
  3. 最好情况是原数组直接就是有序的,时间复杂度为O(n),如果原数组是倒序的话,需要O(n),平均为O(n)

2 插入排序:

首先, 我们将数组中的数据分为两个区间, 已排序区间和未排序区间。 初始已排序区间只有一个元素, 就是数组的第一个元素。 插入算法的核心思想是取未排序区间中的元素, 在已排序区间中找到合适的插入位置将其插入, 并保证已排序区间数据一直有序。 重复这个过程, 直到未排序区间中元素为空, 算法结束。

代码:

        int arr[] = {10,8,11,5, 4, 3, 2, 1};
        //循环插入每个元素
        for (int i=1;i<arr.length;i++){
            int value = arr[i];
            int j = i-1;
            //将每个元素与已经插入的从后向前进行比较,符合条件 ,则向后移 ,直到遇到不符合条件的
            for (;j>=0;j--){
                if(value<arr[j]){
                    arr[j+1] = arr[j];
                }else{
                    break;
                }
            }
            //找到正确的插入点,赋值
            arr[j+1] = value;
        }
        System.out.println(Arrays.toString(arr));

算法分析:

  1. 插入排序是原地算法,并不涉及到额外的空间
  2. 是稳定排序,不会改变相同数据的顺序
  3. 最好情况是原数组直接就是有序的,时间复杂度为O(n),如果原数组是倒序的话,需要O(n*n),平均为O(n*n)

3 选择排序:

分已排序区间和未排序区间。 但是选择排序每次会从未排序区间中找到最小的元素, 将其放到已排序区间的末尾
代码:

//外层 i 控制的是已经排好序的序列    
public void selectSort(){
        int arr[] = {10, 8, 11, 5, 4, 3, 2, 1};
        for (int i = 0; i<arr.length;i++){
            int min = i ;
            for (int j=i;j<arr.length;j++){
                if(arr[j]<arr[min]){
                    min = j ;
                }
            }
            int temp = arr[i];
            arr[i] = arr[min];
            arr[min] = temp;
        }
        System.out.println(Arrays.toString(arr));

    }

算法分析:

  1. 选择排序是原地算法,并不涉及到额外的空间
  2. 不是稳定排序,由于会改变相同元素大小
  3. 最坏,最好 ,平均都是O(n * n )

上述算法总结:

上述算法都是适用于小规模的数据结构,其中算法选择 插入 > 冒泡 > 选择

选择排序在最后一位的原因是因为不是稳定排序,且没有优化的可能。

插入比冒泡好的原因是:在内层循环内,冒泡需要进行4个赋值操作,插入需要一个赋值操作。下表格为对应的时间耗时:

(环境:Mac prol 8G 内存,不同环境运行时间不同,但是结果应该是一致的)

 10000100000
插入排序33ms2070m
冒泡排序46ms3251m

 

4 归并排序:

把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

这里我们使用递归的编程技巧。

首先找到递推公式:mergeSort(start,end) = merge(mergeSort(start,middle),(middle+1,end))

终止条件:start >= end

    public void mergeSort() {
        int arr[] = {11, 8, 3, 9, 7, 1, 2, 5};
        mergeSort(arr, 0, arr.length - 1);
    }

    public void mergeSort(int[] arr, int start, int end) {
        if (start >= end) {
            return;
        }
        //找出中间点,将数组分为俩部分
        int middle = (start + end) / 2;

        //对前一部分数组进行排序
        mergeSort(arr, start, middle);

        //对后一部分进行排序
        mergeSort(arr, middle + 1, end);

        //进行数组的合并
        merge(arr, start, middle, end);
    }

    public void merge(int[] arr, int start, int middle, int end) {
        int length = end - start + 1;
        //创建临时空间,用来存储
        int[] tempArr = new int[length];
        int curr = 0;
        int firstCursor = start;
        int secondCursor = middle + 1;
        while (firstCursor <= middle && secondCursor <= end) {
            if (arr[firstCursor] < arr[secondCursor]) {
                tempArr[curr++] = arr[firstCursor++];
            } else {
                tempArr[curr++] = arr[secondCursor++];
            }
        }
        //将剩下的放入临时数组
        while (firstCursor <= middle) {
            tempArr[curr++] = arr[firstCursor++];
        }

        while (secondCursor <= end) {
            tempArr[curr++] = arr[secondCursor++];
        }

        //将临时数组的数据替换到原数组
        for (int k = 0; k < length; k++) {
            arr[start + k] = tempArr[k];
        }
        System.out.println(Arrays.toString(tempArr));
    }

算法分析:

  1. 归并排序是非原地算法,涉及到额外的空间,为O(n)
  2. 是稳定排序,不会改变相同元素大小
  3. 最坏,最好 ,平均都是O(nlogn)

5 快速排序:

如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。

我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

代码:

public void quickSort() {
        int arr[] = {11, 8, 3, 9, 7, 1, 2, 5};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    private void quickSort(int[] arr, int start, int end) {
        if (start >= end) {
            return;
        }
        int partition = partition(arr, start, end);
        quickSort(arr, start, partition - 1);
        quickSort(arr, partition + 1, end);
    }
    // 我们将start 和 end中间的数组分为俩个部分,一个是已经处理过的,即小于pivot 的,另一个是未进行处理的
    private int partition(int[] arr, int start, int end) {
        //
        int pivot = arr[end];
        //cursor 即标记处理到哪了,cursor 前面的都是 < pivot 的
        int cursor = start;
        for (   int j = start;j<end;j++){
            //如果小于pivot ,将它与cursor 进行交换
            if(arr[j]<pivot){
                int temp = arr[cursor];
                arr[cursor] =  arr[j];
                arr[j] =temp;
                cursor ++;
            }
        }
        //将end 于cursor 进行交换
        int temp = arr[cursor];
        arr[cursor] =  arr[end];
        arr[end] =temp;
      return cursor;

    }

算法分析:

  1. 快速排序是原地算法,
  2. 是非稳定排序,会改变相同元素大小
  3. 最坏为O(n*n),,最好 ,平均都是O(nlogn)  取决于分区点的选择

区分点的选择:可以使用三数取中法(即从首,末,中各取一个数,从三个数中选择一个中间的数),五数取中发 , 还可以使用随机选择法

二 线性排序,复杂度为O(n)

1 桶排序:

首先,我们来看桶排序。桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序(可以使用快排)。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

要求:桶与桶直接天然有序,且数据在每个桶内均匀分布,如果不均匀分布,会导致数据多的桶排序的时间复杂度很高。

桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

2 计数排序:

特殊的桶排序,假设元素的范围大小为0-m,那么桶的个数就是m +1 。且每个桶里存储的是相同元素的个数。

我们假设现在有10个学生,每个学生都有一个成绩,成绩范围为0-9 , 我们怎么使用计数排序来实现呢。

public static void countingSort(){
        int []arr = new int[10];
        Random random = new Random();
        for (int i = 0;i<10 ;i++){
            arr[i] = random.nextInt(9);
        }

        System.out.println("sort before: " + Arrays.toString(arr));

        int[] bucketArr = new int[10];
        //将对应桶内数据的个数加1
        for (int i = 0 ;i<10; i++){
            bucketArr[arr[i]] ++ ;
        }
        //将每个bucket中的数据为前一个加上当前的
        for (int i=1;i<10;i++){
            bucketArr[i] = bucketArr[i] + bucketArr[i-1];
        }

        //这里是为了展示,也可以新建一个临时数组,从后往前遍历来存储排序之后的数组
        for (int i = 9 ;i>=0 ;i--){
            int value = arr[i];
            int rank = bucketArr[value];
            System.out.println("value:" + value + "   " +"rank: " + rank);
            bucketArr[value] = bucketArr[value] -1 ; ;
        }


    }

 

计数排序只能用在数据范围不大的场景,如果数据范围 k 比要排序的数据 n 大很多,就适合用计数排序了。而且,计数排序只能非负整数排序,如果要排序的数据是其他类型的,要其在不改变相对大小的情况下,转化为非负整数。

3 基数排序:

比较两个数,我们只需要比较高位,高位相同的再比较低位。而且每一位的数据范围不能太大。例子:对手机号进行排序,我们可以对十一位分别进行排序,先排序末尾的,在依次使用进行排序, 最后得到的就是从小到大的列表。 

限制:对于每位的排序算法必须是稳定排序的,且每一位都可与分割出独立的位,且位与位中间与有递进关系,如果数据a的高位比b的数据高位高,那么剩下的地位就不用比较了。

上述三种算法的时间复杂度都是O(n) 。

三 总结:

实现一个比较好的排序算法(一般为logn):

1 我们可以根据不同的数据量使用不同的算法,如小数据量可以使用归并排序,大数据量可以使用快速排序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值