大数据算法系列2:从排序说起,估计算法复杂度

14 篇文章 1 订阅

一. 排序的分治算法

1.1 分而治之

image.png

分而治之的思路:
image.png

算分分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjBMfA72-1665307411181)(https://upload-images.jianshu.io/upload_images/2638478-5b9985f1ed3ae528.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

二. Java实现排序算法

image.png

2.1 冒泡排序

从要排序序列的第一个元素开始,不断比较相邻元素的值,发现逆序则交换,将值较大的元素逐渐从前向后移动。

每找到待排序序列的最大值时,就将该最大值固定在待排序序列的尾部,且每找到一个待排序序列最大值需要循环一次,n 个值则需要循环 n 次,但最后一个值无需比较,则实际需循环 n-1 次,即 i < arr.length - 1 。

package com.suanfa.排序;

public class 冒泡排序 {
    public static void main(String[] args) {
        int[] arr1 = {2,3,4,5,6,7,8};
        bubbleSort2(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    //传统的冒泡排序写法
    public static void bubbleSort1(int[] arr){
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - i - 1; j++ ) {
                //System.out.println("i=" + i + ",j=" + j );
                if (arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j +1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

    //设置一个flag标签,如果前面循环已经是排好序的,直接返回即可
    public static void bubbleSort2(int[] arr) {
        boolean flag = false;
        for (int i = 0; i < arr.length - 1; i++) {
            flag = false;
            for (int j = 0; j < arr.length - i - 1; j++){
                System.out.println("i=" + i + ",j=" + j );
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    flag = true;
                }
            }
            if (!flag) {
                break; //数组已有序
            }
        }

    }
}

2.2 选择排序

选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。同样,选择排序也需要比较 n - 1轮,最后一轮无需比较。

package com.suanfa.排序;

public class 选择排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        selectSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void selectSort (int[] arr) {
        for (int i = 0; i < arr.length - 1; i++){
            int minIndex = i; //初始化最小值索引
            for (int j = i + 1; j < arr.length; j++){
                System.out.println("i=" + i + ",j=" + j );
                if (arr[j] < arr[minIndex]) {
                        minIndex = j;
                }
            }
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }
}

2.3 直接插入法

把 n 个待排序的元素看成为一个有序表和一个无序表,开始时 有序表 中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,与有序表中的元素进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

package com.suanfa.排序;

public class 直接插入排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        insertSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void insertSort (int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i]; //记录待排序的数值
            int j = i;
            System.out.println("i=" + i + ",j=" + j );

            while (j > 0 && arr[j - 1] > temp) {
                arr[j] = arr[j - 1];
                //System.out.println("arr[j]=" + arr[j]);
                j--;
            }
            if (j != i) {
                arr[j] = temp;
            }
        }
    }
}

2.4 希尔排序

希尔排序,也被称为递减增量排序,是直接插入排序的一种改进版本。简单插入排序]可能存在的问题:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。

package com.suanfa.排序;

public class 希尔排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        shellSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

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


}

2.5 归并排序

归并排序的核心思想是分治法,把一个复杂问题拆分成若干个子问题来求解。

归并排序的算法思想是:把数组从中间划分为两个子数组,一直递归地把子数组划分成更小的数组。长度为 1 序列是有序的,因此应递归分解直到子数组里面只有一个元素的时候开始排序。排序的方法就是按照大小顺序合并两个元素。接着依次按照递归的顺序返回,不断合并排好序的数组,直到把整个数组排好序。

package com.suanfa.排序;

public class 归并排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        mergeSort(arr1, 0, arr1.length - 1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void mergeSort(int[] arrs, int first, int last){
        /*
         * 第一步: 分  将数组进行划分,1分为2
         */

        // 设置递归退出条件
        if (first >= last){
            return;
        }

        int middle = (first + last)/2;

        //开始递归 每次递归都把1个数组分为2个,直到不可分为止
        mergeSort( arrs, first, middle );
        mergeSort( arrs, middle + 1, last);

        /*
         * 第二步: 治 进行合并,每次把2个合并好的数组进行合并
         */

        // 先定义一个数组用于临时存放数据,其实这个放在全局变量比较好多次递归就不用多次生成
        int[] temp = new int[last+1];
        // 定义指针 i j t
        int t = 0;
        int i = first;
        int j = middle + 1;

        //进行排序,哪个元素小,就把这个元素先放在临时数组中
        while (i <= middle && j <= last) {
            // 其实可以写两层循环,但是这么写更巧妙
            if (arrs[j] <= arrs[i]){
                temp[t++] = arrs[j++];
            } else {
                // 第一轮只有2个元素,之后的都是排序号的数组,所以这么写没问题
                temp[t++] = arrs[i++];
            }

        }

        //执行到这里,要么i不满足条件退出,要么j不满足条件退出
        while (i <= middle){
            temp[t++] = arrs[i++];
        }
        while (j <= last){
            temp[t++] = arrs[j++];
        }

        //将临时数组中元素复制到原始数组中去
        int z = 0;
        for (int k = first; k<= last; k++){
            arrs[k] = temp[z++];
        }

    }
}

2.6 快速排序

快速排序也采用了分治的思想,如果说归并排序是先拆分再排序,那么快速排序就是先排序再划分。

快速排序的基本思想是:首先从待排序列中选定一个记录,称之为 枢纽 ,通过关键字与枢纽的比较将待排序列的序列划分成位于枢纽前后的两个子序列,其中枢纽之前的子序列的所有关键字都不大于枢纽,枢纽之后的子序列的所有关键字都不小于枢纽;此时枢纽已到位,再按同样方法对这两个子序列分别递归进行快速排序,最终使得整个序列有序。

package com.suanfa.排序;

public class 快速排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        quickSort(arr1, 0, arr1.length - 1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void quickSort(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }

        int pivot = arr[left];
        int i = left, j = right;
        while (i < j) {
            while (i < j && arr[j] >= pivot)
                j--;
            arr[i] = arr[j];
            while (i < j && arr[i] <= pivot)
                i++;
            arr[j] =  arr[i];
        }
        arr[i] = pivot;
        quickSort( arr, left, i -1 );
        quickSort( arr, j + 1, right );
    }

}

2.7 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一类完全二叉树,具有以下特性:

大顶堆: 每个结点的值都大于或等于其左右孩子结点的值

小顶堆: 每个结点的值都小于或等于其左右孩子结点的值
image.png

堆排序基本步骤:
将待排序数组构造成一个大顶堆,即从右至左、从下至上进行下沉操作。此时,整个序列的最大值就是堆顶的根节点。一般升序采用大顶堆,降序采用小顶堆
将其与末尾元素进行交换,此时末尾就为最大值。
然后将剩余n-1个元素进行调整重新构造成一个堆。再将堆顶元素与末尾元素交换,如此反复执行,便能得到一个有序序列了。

package com.suanfa.排序;

public class 堆排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        heapSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    //数组第 0 个位置有元素
    public static void heapSort(int[] arr) {
        int N = arr.length;
        //构建大顶堆
        for (int i = N / 2 - 1; i >= 0; i--) {
            sink(arr, i, N);
        }
        while (N > 1) {
            swap(arr, 0, --N);  //堆顶与堆尾结点交换,堆长度减1
            sink(arr, 0, N);  //筛选新的堆顶结点
        }
    }
    //下沉
    private static void sink(int[] arr, int pos, int N) {
        while (pos <= N / 2 - 1) {
            int j = 2 * pos + 1;
            if (j < N - 1 && arr[j] < arr[j + 1]) {
                j++;    // j 为左、右孩子中优先者的位置
            }
            if (arr[j] < arr[pos]){
                return;
            }
            swap(arr, pos, j);
            pos = j;
        }
    }

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

}

2.8 记数排序

比较和非比较的区别
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

适用条件:
计数排序需要占用大量空间,它仅适用于数据比较集中的情况,如[0,100],高考学生成绩。

package com.suanfa.排序;

public class 计数排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        countSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void countSort(int[] arr) {
        int maxValue = arr[0];
        //获取最大值
        for (int num : arr) {
            if (maxValue < num) {
                maxValue = num;
            }
        }

        int[] bucket = new int[maxValue + 1];
        for (int num : arr) {
            bucket[num]++;
        }
        

        for (int i = 0, j = 0; i <= maxValue; i++){
            while (bucket[i] -- > 0) {
                arr[j++] = i;
            }
        }
    }
}

2.9 桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
    同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

什么时候最快:
当输入的数据可以均匀的分配到每一个桶中。

什么时候最慢:
当输入的数据被分配到了同一个桶中。

package com.suanfa.排序;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class 桶排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        bucketSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void bucketSort(int[] arr) {
        // 计算最大值与最小值
        int max = arr[0];
        int min = arr[0];
        for (int num : arr) {
            if (num > max) {
                max = num;
            } else if (num < min) {
                min = num;
            }
        }
        //计算桶的数量
        int bucketNum = (max - min) / arr.length + 1;
        List<List<Integer>> bucketArr = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            bucketArr.add( new ArrayList<>() );
        }
        // 利用映射函数将数据分配到各个桶中
        for (int i = 0; i < arr.length; i++) {
            int hash = (arr[i] - min)/ (arr.length);
            bucketArr.get( hash ).add( arr[i] );
        }
        // 对每个桶进行排序
        for (int i = 0; i < bucketArr.size(); i++) {
            Collections.sort( bucketArr.get( i ) );
        }
        // 将桶中的元素赋值到原序列
        int index = 0;
        for (int i = 0; i < bucketArr.size(); i++) {
            for (int j = 0; j < bucketArr.get( i ).size(); j++){
                arr[index++] = bucketArr.get( i ).get( j );
            }
        }
    }
}

2.10 基数排序

基数排序是一种非比较型排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。

将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。 这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

package com.suanfa.排序;

public class 基数排序 {
    public static void main(String[] args) {
        int[] arr1 = {9,3,4,10,6,11,8};
        radixSort(arr1);
        for (int num : arr1) {
            System.out.print(num + "、");
        }
    }

    public static void radixSort(int[] arr) {
        //定义桶数组, 共10个桶, 每个桶是一个一维数组
        int[][] bucket = new int[10][arr.length];
        //每个桶放入元素的个数
        int[] bucketCount = new int[10];
        int maxValue = arr[0];
        //获取最大值
        for (int num : arr) {
            if (num > maxValue) {
                maxValue = num;
            }
        }
        //获取最大位数
        int maxLength = (maxValue + "").length();
        //在对应位上进行排序
        for (int digit = 0, n = 1; digit < maxLength; digit++, n*= 10) {
            for (int i = 0; i < arr.length; i++) {
                //获取元素对应位的值
                int value = arr[i] / n % 10;
                bucket[value][bucketCount[value]++] = arr[i];
            }
            int index = 0;
            //将桶中的数据放回原数组
            for (int j=0; j<10; j++ ) {
                for (int k=0; k<bucketCount[j]; k++) {
                    arr[index++] = bucket[j][k];
                }
                bucketCount[j] = 0; //清0
            }
        }
    }
}

2.11 基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  1. 计数排序:每个桶只存储单一键值;
  2. 桶排序:每个桶存储一定范围的数值;
  3. 基数排序:根据键值的每位数字来分配桶;

2.12 总结

  1. 当数据规模较小时,可以使用直接插入排序。
  2. 当数组初始时已经基本有序,可以用直接插入排序和冒泡排序。
  3. 当数据规模较大时,可以考虑使用快速排序,速度最快。当记录随机分布的时候,快速排序平均时间最短。
  4. 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
  5. 若要求排序稳定,则可选用归并排序。

参考:

  1. http://www.dataguru.cn/article-5747-1.html
  2. https://blog.csdn.net/KIMTOU/article/details/121259340
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值