常用排序算法实现

好久没有写博客了,就以这篇最近突发奇想的常用排序算法总结做回归吧。

以下是代码

import java.util.concurrent.TimeUnit;

/**
 * @Auther: yubotao
 * @Description:
 * @Date: Created in 19:28 2021/02/21
 * @Modified By:
 */
public class SortTest {

    private static int[] a = {2,3,5,1,6,2,4,7,5,7,3,5,8,19,33,533,12,453};

    public static void main(String[] args) throws Exception {
//        TimeUnit.SECONDS.sleep(50);
        long start = System.nanoTime();
        a = optimizeRadixSort(a);
//        radixSort(a);
//        recursionMergeSort(a);
//        mergeSort(a);
//        heapSort(a, true);
//        selectSort(a);
//        shellSort(a);
//        binaryInsertSort(a);
//        insertSort(a);
//        quickSort(a, 0, a.length - 1);
//        bubble(a);
        System.out.println("耗时:" + (System.nanoTime() - start) + "ns");
        for (int i=0; i<a.length; i++){
            System.out.print(a[i] + " ");
        }
//        TimeUnit.SECONDS.sleep(50);
//        System.gc();
//        System.out.println("----gc completed");
//        TimeUnit.SECONDS.sleep(50);
    }

    /**冒泡 O(n^2)
     * 每趟排序保证第i小的值处于第i个位置
     * 第一趟排序将最小值放到第1位,第二趟将第二小放到第2位,依次类推
     * **/
    public static int[] bubble(int[] a){
        int len = a.length;
        for (int i=0; i<len; ++i){
            for (int j=i+1; j<len; ++j){
                if (a[i]>a[j]){
                    int temp = a[i];
                    a[i] = a[j];
                    a[j] = temp;
                }
            }
        }
        return a;
    }

    /**快排 最快O(nlogn)
     * 保证每次排序都能将基准值放到它的最终位置上,且每次排序后,它的左侧都小于等于它,右侧都大于等于它
     * 具体思想是:获取基准值temp后,取左指针low,右指针high,保证low<high;
     *            从队尾开始扫描,如果a[high]<temp,则将high处的值赋给low,即a[low] = a[high];
     *            之后从low开始扫描,当a[low]>temp,则将low处的值赋给high,即a[high] = a[low];
     *            再从high处开始扫描,如此往复,直到low>=high(low==high),说明找到temp的真正位置,a[low] = temp;
     *            接下来以该位置为界,左侧全部小于等于temp,右侧全部大于等于temp,因此各自再进行快排,最后整个数组有序
     * 参考文章:https://blog.csdn.net/nrsc272420199/article/details/82587933
     * **/
    public static void quickSort(int[] a, int low, int high){
        if (low<high){
            // 寻找基准数据的正确索引
            int index = getIndex(a, low, high);
            // 对index前后的数组进行相同的操作使整个数组有序
            quickSort(a, low, index-1);
            quickSort(a, index+1, high);
        }
    }
    static int getIndex(int[] a, int low, int high){
        // 基准数据
        int temp = a[low];
        while (low < high){
            // 当队尾元素大于等于基准元素时,向前挪动high指针
            while (a[high] >= temp && low<high) {
                high--;
            }
            // 由于队尾元素小于temp,将其值赋给low
            a[low] = a[high];
            // 当队首元素小于temp,向后挪动low指针
            while (a[low] <= temp && low<high) {
                low++;
            }
            // 队首元素大于temp,将其值赋给high
            a[high] = a[low];
        }
        // 此时low和high相等,且是temp元素的最终位置
        a[low] = temp;
        return low;
    }

    /**直接插入排序 O(n^2)
     * 找出第i个值在已经有序的[1,2,...,i-1]的数组中的插入位置k,将[k,...,i-1]的所有元素后移一位,
     * 再将该值插入位置k
     * **/
    public static void insertSort(int[] a){
        int len = a.length;
        for (int i=0; i<len; i++){
            int item = a[i];
            for (int j=0; j<i; j++){
                if (item < a[j]){
                    System.arraycopy(a, j, a, j+1, i-j);
                    a[j] = item;
                    break;
                }
            }
        }
    }

    /**折半插入排序(二分法) O(n^2)
     * 在直接插入排序上的改进,通过二分法查找位置
     * **/
    public static void binaryInsertSort(int[] a){
        int len = a.length;
        for (int i=1; i<len; i++){
            int item = a[i];
            int low=0, high=i;
            while (low < high){
                int middle = (low+high)  >>> 1;
                if (a[middle] <= item){
                    low = middle+1;
                }else {
                    high = middle;
                }
            }
            System.arraycopy(a, low, a, low + 1, i - low);
            a[low] = item;
        }
    }

    /**希尔排序 O(n^2)
     * 简单插入排序的优化,缩小增量排序
     * 思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;
     * 随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
     * 参考文章:https://www.cnblogs.com/chengxiao/p/6104371.html
     * **/
    public static void shellSort(int[] a){
        for (int gap = a.length/2; gap > 0; gap /= 2){
            /**
             * 精妙之处在于从第gap个元素开始比较,然后逐个向后增加;
             * 内部循环会逐渐向左移动,每次移动gap位,直到比较到当前分组最小的值(每一次循环,组内都有序)
             * 新分组也就完成了所有比较,变得有序(有冒泡的影子在)
             * */
            for (int i=gap; i<a.length; i++){
                int j = i;
                while (j-gap>=0 && a[j]<a[j-gap]){
                    // 交换元素,通过求和做差避免空间消耗(创建临时变量)  存在溢出风险!
                    a[j] = a[j] + a[j-gap];
                    a[j-gap] = a[j] - a[j-gap];
                    a[j] = a[j] - a[j-gap];

                    j -= gap;
                }
            }
        }
    }

    /** 简单选择排序 O(n^2)
     * 思路:第i趟排序找到剩下n-i+1个元素中最小的值,进行元素交换,经过n-1趟,排序完成
     */
    public static void selectSort(int[] a){
        for (int i=0; i<a.length-1; i++){
            int k = i;
            for (int j=i+1; j<a.length; j++){
                if (a[j] < a[k]){
                    k = j;
                }
            }
            if (k > i) { // 存在溢出风险!
                a[i] = a[i] + a[k];
                a[k] = a[i] - a[k];
                a[i] = a[i] - a[k];
            }
        }
    }


    /** 堆排序 O(nlogn)
     * 堆一种完全二叉树的顺序存储结构。
     * 大根堆(最大的元素在堆顶):满足 L(i) <= L(2i) 且 L(i) <= L(2i+1)
     * 小根堆(最小的元素在堆顶):满足 L(i) >= L(2i) 且 L(i) >= L(2i+1)
     * 堆排序的关键是构造初始堆,是一个不断调整的过程;插入也是一样;
     * 删除堆顶元素时,先将堆的最后一个元素与堆顶元素交换。
     * 堆排序的思想:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
     *              将其与末尾元素进行交换,此时末尾就为最大值。
     *              然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
     *              如此反复执行,便能得到一个有序序列。
     * 参考文章:https://www.cnblogs.com/chengxiao/p/6129630.html
     * **/
    public static void heapSort(int[] a, boolean increase){
        if (increase) bigTopHeapSort(a, a.length);
        else smallTopHeapSort(a, a.length);
    }
    static void bigTopHeapSort(int[] a, int length){ // 大顶堆
        if (length == 1) return;
        int lastNoLeaf = length/2 - 1; // 完全二叉树,找到最后一个非叶子节点的下标
        while (lastNoLeaf >= 0){
            if (2*(lastNoLeaf + 1)<length && a[lastNoLeaf] < a[2*(lastNoLeaf + 1)]){ // 右子
                swap(a, lastNoLeaf, 2*(lastNoLeaf + 1));
            }
            if (a[lastNoLeaf] < a[2*lastNoLeaf + 1]){ // 左子
                swap(a, lastNoLeaf, 2*lastNoLeaf + 1);
            }
            lastNoLeaf--;
        }
        // 此时建立了大顶堆,交换堆顶元素和最后一个元素
        swap(a, 0, length-1);
        bigTopHeapSort(a, length-1);
    }
    static void smallTopHeapSort(int[] a, int length){ // 小顶堆
        if (length == 1) return;
        int lastNoLeaf = length/2 - 1; // 完全二叉树,找到最后一个非叶子节点的下标
        while (lastNoLeaf >= 0){
            if (2*(lastNoLeaf + 1)<length && a[lastNoLeaf] > a[2*(lastNoLeaf + 1)]){ // 右子
                swap(a, lastNoLeaf, 2*(lastNoLeaf + 1));
            }
            if (a[lastNoLeaf] > a[2*lastNoLeaf + 1]){ // 左子
                swap(a, lastNoLeaf, 2*lastNoLeaf + 1);
            }
            lastNoLeaf--;
        }
        // 此时建立了小顶堆,交换堆顶元素和最后一个元素
        swap(a, 0, length-1);
        smallTopHeapSort(a, length-1);
    }
    static void swap(int[] a, int left, int right){ // 异或操作可以避免溢出
        a[left] = a[left] ^ a[right];
        a[right] = a[left] ^ a[right];
        a[left] = a[left] ^ a[right];
    }


    /**归并排序 O(nlogn)
     * 分治法的应用  这里只讨论朴素的2路归并,目前最优的归并算法时TimSort,这个刚好之前读过paper和源码
     * 思想:通过将待排序序列分解成n个容量为2的单位,分别排序,再将所有单位合并再排序
     * 参考文章:https://www.cnblogs.com/chengxiao/p/6194356.html
     * TimSort算法会整理一篇博客,到时候把地址贴过来
     * **/
    public static void mergeSort(int a[]){ // 自实现,需要优化的点挺多
        int capacity = 2; // 排序的容量单位
        // 分治
        for (int i=0; i<a.length; i += capacity){ // 暂时只考虑2路归并,如果修改容量,则要修改此处代码
            if (i+1<a.length && a[i] > a[i+1]){
                swap(a, i, i+1);
            }
        }
        // 合并
        while (capacity <= a.length){
            int k = 0; // 指针,防止越界
            while (k < a.length){
                int left = k; // 左侧待合并序列第一个元素指针,容量为capacity
                int right = k+capacity; // 右侧待合并序列第一个元素指针
                int[] temp = new int[2*capacity]; // 临时数组 频繁开辟数组
                int tempSize = 0; // 临时数组的真实容量
                while (tempSize < 2*capacity){
                    if (left < a.length && right < a.length){
                        if (left < k+capacity && right < k+2*capacity) {
                            if (a[left] <= a[right]) {
                                temp[tempSize++] = a[left++];
                            } else {
                                temp[tempSize++] = a[right++];
                            }
                        }else{
                            while (left < k+capacity){ // 左侧集合还有剩余
                                temp[tempSize++] = a[left++];
                            }
                            while (right < k+2*capacity){ // 右侧集合还有剩余
                                temp[tempSize++] = a[right++];
                            }
                        }
                    }else {
                        break;
                    }
                }
                // 将排好的临时数组复制回原始数组
                System.arraycopy(temp, 0 , a, k, tempSize);
                k = k+2*capacity;
            }
            capacity *= 2; // 扩容
        }
    }
    // 递归实现
    public static void recursionMergeSort(int[] a){
        int[] temp = new int[a.length]; // 提前新建临时数组,避免频繁开辟空间
        sort(a, 0, a.length-1, temp);
    }
    static void sort(int[] a, int left, int right, int[] temp){
        if (left<right){
            int mid = (left+right)/2;
            sort(a, left, mid, temp); // 左侧归并排序
            sort(a, mid+1, right, temp); // 右侧归并
            merge(a, left, mid, right, temp); // 合并
        }
    }
    static void merge(int[] a, int left, int mid, int right, int[] temp){
        int i=left; // 左侧集合指针
        int j=mid+1; // 右侧指针
        int t=0; // 临时数组指针
        while (i<=mid && j<=right){
            if (a[i] <= a[j]){
                temp[t++] = a[i++];
            }else {
                temp[t++] = a[j++];
            }
        }
        while (i<=mid){
            temp[t++] = a[i++];
        }
        while (j<=right){
            temp[t++] = a[j++];
        }
        // 将临时数组的值复制回原始数组
        t=0;
        while (left<=right){
            a[left++] = temp[t++];
        }
    }
    // 注:通过jconsole的监测得出结果,提前创建最大容量临时数组所占用的空间反而比频繁新建还要多(因为运行中会被gc)
    // 如何测试:https://blog.csdn.net/weixin_44663675/article/details/107089808


    /**基数排序法 O(nlog(r)m)  其中r为所采取的基数,m为堆数
     * 属于分配式排序,通过键值的部分信息,将要排序的元素分配至某些“桶”中,以达到排序的作用
     * 分为最高位优先法——MSD和最低位优先法——LSD
     * 排序思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的前面补零。
     *          然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
     * 参考文章:https://blog.csdn.net/lemon_tree12138/article/details/51695211
     * **/
    public static void radixSort(int[] a){
        int[][] temp = new int[10][a.length]; // 临时数组
        int[] order = new int[10]; // 属于该位的个数
        int digit = 1;
        int maxLength = getMaxLength(a);
        while (maxLength >= digit){
            for (int i=0; i<a.length; i++){
                int digitNum = getDigit(a[i], digit);
                temp[digitNum][order[digitNum]] = a[i];
                order[digitNum]++;
            }
            int aPoint = 0;
            for (int i=0; i<10; i++){
                for (int j=0; j<order[i]; j++){
                    a[aPoint++] = temp[i][j];
                    temp[i][j] = 0;
                }
                order[i] = 0;
            }
            digit++;
        }
    }
    static int getDigit(int num, int digit){
        int result = 0;
        while (digit>0){
            result = num%10;
            num /= 10;
            digit--;
        }
        return result;
    }
    static int getMaxLength(int[] a){
        int maxLength = 0;
        for (int i : a){
            int length = String.valueOf(i).length();
            maxLength = maxLength>length ? maxLength : length;
        }
        return maxLength;
    }

    /** 空间优化
     * 思路:因为创建二维临时数组存在空间浪费,空间利用率过低,因此通过如下方式提高空间利用率
     *      首先我们使用一个和原数组长度相同的临时数组bucket[]来暂存数据,同时通过另一个计数数组count[10]来标识位置
     *      该计数数组长度为10(以10为基底的基数排序),它每位保存的值为前一位保存的个数+当前位的个数
     *      举例说明:假设数组[2314, 5428, 373, 2222, 17],则计数数组count[10]第一次排序以最低位
     *      count[10]  0  1  2  3  4  5  6  7  8  9
     *                 0  0  1  2  3  3  3  4  5  5
     *      上述含义为,最低位为2的有1个数,最低位为3的也是一个数(2 = 1+1),同理最低位为8的是一个数(5 = 4+1)
     *      第二次排序,以倒数第二位排序得
     *      count[10]  0  1  2  3  4  5  6  7  8  9
     *                 0  2  4  4  4  4  4  5  5  5
     *      为什么这么做呢?因为这样临时数组bucket[]的下标就可以直接通过count[]得到。
     *      比如当我们找到某个数的第i位的值时,以373为例,在第二次循环,我们得到的值为7
     *      同时7也是该位最大的值,应该放到数组的末尾,此时我们观察count[7],发现直接取count[7]--
     *      刚好就能得到373在bucket[]中的位置bucket[4]。
     *      这样我们通过以上方式做到了空间压缩。
     * @param array
     * @return
     */
    public static int[] optimizeRadixSort(int[] array) {
        int maxLength = getMaxLength(array);
        return sortCore(array, 1, maxLength);
    }

    private static int[] sortCore(int[] array, int digit, int maxLength) {
        if (digit > maxLength) {
            return array;
        }
        int radix = 10; // 基数
        int arrayLength = array.length;
        int[] count = new int[radix];
        int[] bucket = new int[arrayLength];
        // 统计将数组中的数字分配到桶中后,各个桶中的数字个数
        for (int i = 0; i < arrayLength; i++) {
            count[getDigit(array[i], digit)]++;
        }
        // 将各个桶中的数字个数,转化成各个桶中最后一个数字的下标索引
        for (int i = 1; i < radix; i++) {
            count[i] = count[i] + count[i - 1];
        }
        // 将原数组中的数字分配给辅助数组 bucket
        for (int i = arrayLength - 1; i >= 0; i--) {
            int number = array[i];
            int d = getDigit(number, digit);
            bucket[count[d] - 1] = number;
            count[d]--;
        }
        return sortCore(bucket, digit + 1, maxLength);
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值