几大排序算法的Java实现

Java实现几大常用的排序算法

1,简单插入排序
主要思路将num[i]插入到一个已经排好的数组num[0…i-1]中 ,因为涉及到数据的查找和移动,最坏情况下平均时间复杂度为o(n^2).

public static void insertion_sorting(int[] nums){
        int temp;
        int j,i;
        for (i = 1;i < nums.length; ++i){
            if (nums[i] < nums[i-1]){
                temp = nums[i];
                for (j = i-1;j >= 0&&nums[j]>temp; --j)nums[j+1] = nums[j];
                nums[j+1] = temp;
                }
        }
    }

折半插入排序
插入排序的主要思想就是在一个有序的序列中不停的查找,移动。因此查找过程可以用折半查找代替。

public static void BInsertion_Sorting(int[] nums){
        int temp;
        for (int i = 1;i < nums.length; ++i) {
            if (nums[i] < nums[i - 1]) {
                temp = nums[i];
                int low = 0, height = i - 1;
                while (low <= height) {
                    int m = (low + height) / 2;
                    if (nums[m] <= temp) low = m + 1;
                    else height = m - 1;
                }
                for (int j = i - 1; j >= height + 1; --j) nums[j + 1] = nums[j];
                nums[height + 1] = temp;
            }
        }
    }

引入折半查找的方法后虽然可以减少查找的步骤,但是无法减少数据移动的步骤,因此折半插入排序时间复杂度依然是o(n^2)

2,希尔排序
希尔排序是一种改进的插入排序,因为排序时数组都有序度越高,插入排序的时间复杂度就越低,如果排序前数据已经是有序的,那么插入排序的时间复杂度就是o(n),如果是最坏情况,插入排序的时间复杂度就是o(n^2).
基于这个特点可以先将数组按照一定的规则分成几个部分,先对这几个部分进行初步的插入排序,然后再对整个部分进行插入排序,因为局部已经做到初步的“有序”,所以可以尽量的减少最后一个插入排序的时间复杂度。而划分部分的方法可以通过设定增量的方式进行。

//希尔排序,先将数组按照增量分成若干个小份,先对小份进行插入排序然后在对整个数组进行插入排序
    //增量的选择视数组的情况而定
    public static void Shell_sort(int[] nums){
        int[] dlta = new int[]{5,3,1};  //增量序列
        int temp,k;
        for (int i = 0;i<dlta.length;++i){
            int dk = dlta[i];   //保存增量
            for (int j = dk;j<nums.length;j++){
                if (nums[j]<nums[j-dk]){
                    temp = nums[j];
                    for (k = j-dk;k>=0&&nums[k]>temp;k-=dk)nums[k+dk] = nums[k];
                    nums[k+dk] = temp;
                }
            }
        }
    }

希尔排序的时间复杂度依赖于增量的选择,大致范围是O(nlogn)~O(n2)。

3,冒泡排序
通过“交换”思想进行的排序,通过相邻数字两两交换的方式将最大/小的数放到 最后,然后将次大/小的数放到第二的位置,以此类推。平均时间复杂度o(n^2).

public static void bubble_sort(int[] nums){
        int temp;
        for (int i = nums.length-1;i>0;--i){
            for (int j = 0;j<i;++j){
                if (nums[j]>nums[j+1]){
                    temp = nums[j+1];
                    nums[j+1] = nums[j];
                    nums[j] = temp;
                }
            }
        }
    }

4,快速排序
冒泡排序的改进版,通过一趟排序将记录分割成两部分,其中一部分记录的关键子均比另一部分的关键字小,然后将分别对这两部分记录继续排序,直到整个序列有序。

 //快速排序
    private static int Partition(int[] a,int low,int height){
        int pivotkey = a[low];
        while (low<height){
            while (low<height&&a[height]>=pivotkey)height--;
            a[low] = a[height];
            while (low<height&&a[low]<=pivotkey)low++;
            a[height] = a[low];
        }
        a[low] = pivotkey;
        return low;
    }
    private static void QSort(int[] nums,int low,int height){
        if (low<height){
            int m = Partition(nums,low,height);
            QSort(nums,low,m-1);
            QSort(nums,m+1,height);
        }
    }

    public static void QuickSort(int[] nums){
        QSort(nums,0,nums.length-1);
    }

首先选取记录中第一个元素作为枢轴,然后将小于它的放在其左边,大于它的放在其右边,然后再分别对两部分递归执行上述过程,直到整个记录有序。
快速排序平均时间复杂度o(nlogn),但在最坏情况下为o(n^2)

5,简单选择排序
主要思路就是将排行第几的数字放在排行第几的位置上,与冒泡排序很像,以及与我经常将他们搞混,但是实质上是有区别的。冒泡排序是在遍历的过程中不停的交换相邻的两个元素,将最大/小的元素移动到数组的最后,选择排序是先将最大/小的元素找出来,然后和对应位置的元素交换,只在每一趟的最后进行一次交换,突出一个"选择"二字。

//选择排序
    public static void selection_sort(int[] nums){
        int temp;
        int i,j;
        for (i=0;i<nums.length-1;++i){
            int index = i;
            for (j=i+1;j<nums.length;++j){
                if (nums[i] > nums[j]&&nums[j]<nums[index]){
                    index = j;
                }
            }
            temp = nums[i];
            nums[i] = nums[index];
            nums[index] = temp;
        }
    }

6,堆排序
上面的几种排序算法都是基于一种“比较"的思想,不管是“选择”还是”插入“,都是基于比较结果来进行后续操作,比如甲想要考全班第一,他需要赢过其他所有人,乙想考全班第二,他需要赢过除甲以外的所有人。但是事实上并不用如此,如果甲的成绩比乙高,乙的成绩比丙高,那么自动就可以认为甲的成绩比丙高,必须要再比一次。而堆排序就是基于这种”锦标赛"的思想,建立的。

首先将记录处理成一个堆(关于什么是堆这里不做讨论),之后堆的根节点就是最大/小的节点,然后将最后的元素跟根节点交换,将其再次处理成堆,循环往复,直到有序。

//堆排序,平均时间复杂度为o(nlog(n)),不推荐在数据数量较少的情况下使用,因为建堆和调整堆很费时间
    public static void heapsort(int[] nums){
        //将一个无序的数组调整成一个堆
        for (int i=nums.length/2;i>=0;--i){
            HeapAdjust(nums,i,nums.length);
        }
        //进行堆排序
        for (int i=nums.length-1;i>0;--i){
            int temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
            HeapAdjust(nums,0,i);
        }
    }
    //nums[] 保存当前要进行调整的堆,s表示堆的根节点,m表示堆的节点 数量
    private static void HeapAdjust(int[] nums,int s, int m){
        int temp = nums[s];
        for (int i=s*2;i<m-1;i*=2){
            if (nums[i] < nums[i + 1])i++;
            if (nums[i]<=temp)break;
            nums[s] = nums[i];
            s = i;
        }
        nums[s] = temp;
    }

平均时间复杂度为o(nlog(n))

7,归并排序
“归并"的含义是将两个或者两个以上的有序表组合成一个新的有序表。首先两两合并,然后合并后的序列在两两合并,直到最后两个大的子序列合并。

//归并排序
    private static int[] newNums;  //归并排序的辅助空间
    public static void mergeSort(int[] nums){
        //将辅助空间设定为与待排序列等大,其实不需要等大,在排序 过程中需要多大申请多大就行了,但是我懒省事
        newNums = new int[nums.length];
        //开始排序
        MSort(nums,0,nums.length-1);
    }
    //
    private static void MSort(int[] nums,int s,int t){
        if (s != t){
            int m = (s+t)/2;  //获取数组中间位置
            MSort(nums,s,m);  //递归调用
            MSort(nums,m+1,t); //递归调用
            Merge(nums,s,m,t);  //排序算法
        }
    }

    private static void Merge(int nums[],int s,int m,int t){  //i开始,m中间,n结尾
        //将需要排序的段复制到辅助空间中
        for (int i = s;i<=t;++i){
            newNums[i] = nums[i];
        }
        //以类似起扑克的方式将需要排序的段重新排序,并复制到原数组中
        int i,j;
        int k = s;
        for (i = s,j = m+1;i<=m&&j<=t;){
            if (newNums[i]>=newNums[j]){
                nums[k] = newNums[j];
                j++;
                k++;
            }else {
                nums[k] = newNums[i];
                i++;
                k++;
            }
        }
        if (i > m){
            for (int n = j;n<=t;++n){
                nums[k] = newNums[n];
                k++;
            }
        }else {
            for (int n = i;n<=m;++n){
                nums[k] = newNums[n];
                k++;
            }
        }
    }

时间复杂度o(nlogn)

8,基数排序
基数排序和前面的排序都不相同,它通过链表的方式实现。首先向记录处理成链表,然后再创建出十个链表,对应’0-9’十个数字,然后获取记录中各数据个位上的数,按照上面创建的链表存入,然后将链表连起来,及将记录按照个位上的数进行了一次初排。之后在获取十位上的数,重复上述操作,知道所有位上的数都检查了一遍。

//基数排序
    public static void radixSort(int[] nums){
        //创建十个arrayList用于保存中间生成的list
        ArrayList<Integer> list_0,list_1,list_2,list_3,list_4, list_5,
                list_6,list_7,list_8,list_9;
        //保存数组中的最大值
        int maxNumber = 0;
        //numList用于保存结果列表
        ArrayList<Integer> numList = new ArrayList<>();
        list_0 = new ArrayList<>();list_1 = new ArrayList<>();list_2 = new ArrayList<>();
        list_3 = new ArrayList<>();list_4 = new ArrayList<>();list_5 = new ArrayList<>();
        list_6 = new ArrayList<>();list_7 = new ArrayList<>();list_8 = new ArrayList<>();
        list_9 = new ArrayList<>();
        //查询数组中的最大值,并且将数组处理成静态列表
        for (int i=0;i<nums.length;++i){
            if (nums[i]>=maxNumber)maxNumber = nums[i];
            numList.add(nums[i]);
        }
        //p用于辅助获取每个位上的数字
        int p = 1;
        for (;maxNumber>0; maxNumber/=10){
            for (int i=0;i<numList.size();++i){
                int temp = numList.get(i)/p;
                //获得每个位上的数字,并保存到对应的arrayList中
                if (temp%10 == 0) list_0.add(numList.get(i));
                else if (temp%10 == 1) list_1.add(numList.get(i));
                else if (temp%10 == 2) list_2.add(numList.get(i));
                else if (temp%10 == 3) list_3.add(numList.get(i));
                else if (temp%10 == 4) list_4.add(numList.get(i));
                else if (temp%10 == 5) list_5.add(numList.get(i));
                else if (temp%10 == 6) list_6.add(numList.get(i));
                else if (temp%10 == 7) list_7.add(numList.get(i));
                else if (temp%10 == 8) list_8.add(numList.get(i));
                else if (temp%10 == 9) list_9.add(numList.get(i));
            }
            p *= 10;
            //清除结果序列,并将初步排列的数字重新添加到结果序列中
            numList.clear();
            for (int i=0;i<list_0.size();++i)numList.add(list_0.get(i));
            for (int i=0;i<list_1.size();++i)numList.add(list_1.get(i));
            for (int i=0;i<list_2.size();++i)numList.add(list_2.get(i));
            for (int i=0;i<list_3.size();++i)numList.add(list_3.get(i));
            for (int i=0;i<list_4.size();++i)numList.add(list_4.get(i));
            for (int i=0;i<list_5.size();++i)numList.add(list_5.get(i));
            for (int i=0;i<list_6.size();++i)numList.add(list_6.get(i));
            for (int i=0;i<list_7.size();++i)numList.add(list_7.get(i));
            for (int i=0;i<list_8.size();++i)numList.add(list_8.get(i));
            for (int i=0;i<list_9.size();++i)numList.add(list_9.get(i));

            list_0.clear();list_1.clear();list_2.clear();list_3.clear();
            list_4.clear();list_5.clear();list_6.clear();list_7.clear();
            list_8.clear();list_9.clear();
        }
        //将排完序的序列重新改成数组,并返回
        for (int i = 0;i<numList.size();++i){
            nums[i] = numList.get(i);
        }
    }

基数排序的时间复杂度是o(d*n),最优的情况下甚至可以做到线性时间,但是同样具有局限性。毫无疑问这是一种那空间换时间的算法,需要耗费大量的辅助空间。适用于n很大,关键字较小的序列。是想一下,对{1,10000000002}这个序列排序,用普通排序很容易解决,但是用基数排序需要浪费大量的空间和时间。对于小数和负数的处理同样有些复杂。

9,计数排序
计数排序就是首先用记录中的最大值创建一个Count数组,该数组的空间与记录中最大值等大(最理想情况下是申请Max-Min+1个空间),该数组用于存放记录中的数出现了几次。比如说记录中出现了两个5,就在Count[4]的位置记为2。注意,记录中的值与Count数组中的下标一一对应,及关键字值为5就在Count[4]的位置上加一,直到对整个记录遍历了一遍。然后用计数数组Count还原原数组。及如果Count[4]上的值为2,就在结果数组中添加两个5。因为我们遍历Count数组是顺序遍历的,所以原数组就自动排序了。

//计数排序
    public static void countingSort(int[] nums){
        int maxNum=0,minNum=Integer.MAX_VALUE;
        for (int i=0;i<nums.length;++i){
            if (nums[i]>=maxNum)maxNum = nums[i];
            if (nums[i]<=minNum)minNum = nums[i];
        }
        int[] newNums = new int[maxNum+1];
        for (int i=0;i<nums.length;++i) newNums[nums[i]]++;
        int p = 0;
        for (int i=0;i<newNums.length;++i){
            if (newNums[i]!=0){
                for (int m=newNums[i];m>0;--m){
                    nums[p] = i;
                    p++;
                }
            }
        }
    }

但是第一次看到这个算法时被震撼到了,原来还可以这么简单,而且成功将排序算法的时间复杂度拉到了线性水平o(n+k)。但是后来仔细想想,该方法存在很多局限性,比如要求数组必须是正整数,如果存在负数和小数需要先对数据进行处理。而且和上面同样的问题,如何数组中的最大值和最小值跨度很大,会浪费很多不必要的空间。比如上面的例子对{1,1000000000002}排序,明明只有两个元素却需要至少创建一个1000000000002大小的Count数组。所以只适用于n很大,且关键字大小很集中的记录。

你可能感兴趣的:(Java)