排序算法 (Java)

余生长醉 提交于 2019-12-24 18:32:39

视频参考:B站:马士兵,青岛大学–王卓。

时间复杂度 Big O

算法花费时间随着问题规模的扩大的变化

不考虑必须要做的操作:循环、赋初值、程序初始化…;
不考虑常数项;
不考虑低次项;

一般时间复杂度都是“最差”的情况

E.g.:
访问数组某个位置的值: O(1)
访问链表某个位置的值: O(n)
求数组平均数:O(n)

在这里插入图片描述
在这里插入图片描述

选择排序 SelectionSort

最简单
最没用: O(n^2), 不稳定

  1. 找到最小的数的位置(索引)
  2. 把这个位置上的数和【0】上的数交换
  3. 把除第一个位置之后的剩余部分的数组重复过程,确定位置【1】的数
  4. 循环3的操作,直到最后一个数

时间复杂度:计算执行次数最多语句的时间随着规模扩大的规律,然后忽略常数项、低次项

笔记:外循环i控制每轮要排的最小值的索引;内循环遍历最小值索引后的每一位索引的值,与当前最小值索引上的值作比较,把最小的值换到最小值索引上。

    /**
     * 选择排序
     * @param arr
     */
    public static void selectionSort(int[] arr) {
        //由于要保证i后面还有数才需要排序,所以i最大可以取到[arr.length-2]就行
        for (int i = 0; i < arr.length-1 ; i++) {  //执行n次
            int minPos = i; //每次循环的要求的最小值的位置都加1  //执行n次
            for (int j = i+1; j < arr.length ; j++) {  //执行 (n-1)+(n-2)+...+1 次 = n(n-1)/2
                if(arr[j]<arr[minPos]){
                    //如果后面的值小,交换位置
                    swap(arr,minPos,j);
                }
            }
        }
    }

    /**
     * 交换数组中两个不同位置元素的值
     * @param arr
     * @param i
     * @param j
     */
    static void swap(int[] arr, int i ,int j){
        int temp = arr[j];
        arr[j] = arr[i];
        arr[i] = temp;
    }

    /**
     * 打印数组
     * @param arr
     */
    static void print(int[] arr){
        for (int i : arr) {
            System.out.print(i+"\t");
        }
    }

冒泡排序 BubbleSort

内循环每轮排好一个最大数

    public static void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length-1 ; i++) { //要排arr.length-1轮
            for (int j = 0; j < arr.length-1-i ; j++) { //-i的原因是 在新的一轮,需要比较的数比上一轮少一个
                if(arr[j]>arr[j+1]) swap(arr,j,j+1); //排好一个最大数
            }
        }

    }

每趟不断两两比较,并按规则前小后大交换。
每一趟能够排好一个最大数。

时间复杂度分析:
最好:数组原本有序,比较 n-1 次,移动0次
最坏:数组逆序,比较n(n-1)/2次,移动3n(n-1)/2次

时间复杂度:最差O(n2)O(n^2),最好O(n)O(n),平均O(n2)O(n^2)
空间复杂度:O(1)
稳定性:稳定

插入排序 InsertionSort

类似整理扑克牌,外循环抽出第i张牌(i之前的牌已有序),内循环用这张牌与前面的每个牌作比较,如果比前面一张小,就交换位置,直到检查至第一张牌。

在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
在插入a[i]之前,前半段序列 a[0]~a[i-1] 是有序的,所以a[i]需要在找到有序位置j(0<=j<=i)
插入a[i]时,如果前一个数比a[i]大,那么就与a[i]交换位置,相当于a[i]前进一位,直到a[i]大于等于它前面的数,则排好了a[i]。

        //外层循环控制要排的第i个数
        for (int i = 1; i < arr.length; i++) {
            for (int j = i; j > 0; j--) {
                if(arr[j]<arr[j-1]){
                    Utils.swap(arr,j,j-1);
                }
            }
        }

另一种做法:使用临时变量x存储要排的a[i],如果a[i-1]比x大,那就向后一位,覆盖a[i]。。。以此循环,直到某个数比临时变量x小,那么将x赋值到这个数的后面位置。

    public static void sort(int[] arr){

        for (int i = 1; i < arr.length; i++) {
                int x = arr[i];
            //System.out.println("x:"+x);
            for (int j = i-1; j >= 0 ; j--) {
                //System.out.println("arr[j]:"+arr[j]);
                if(arr[j]>x){
                    arr[j+1] = arr[j];
                    //System.out.println(Arrays.toString(arr));
                    if(j==0) arr[j] = x;
                }else {
                    arr[j+1] = x;
                    //System.out.println("结束内循环---"+Arrays.toString(arr));
                    break;
                }
            }
        }
    }

时间复杂度分析:
最好的情况:数组全部是有序的,每个元素只要比较一次,移动0次。所以要比较n-1次(第一个元素不需要比较)
最坏的情况:数组逆序有序,第二个元素需要和前面的1个元素比较,第三个需要和前面2个比较,第四个需要和前面3个比较…第n个需要和前面(n-1)个元素比较,累计起来要比较 (n)(n-1)/2 次,移动次数大约也是n(n-1)/2次

时间复杂度,最好情况是O(n)O(n),最坏情况是O(n2)O(n^2),平均也是O(n2)O(n^2)

空间复杂度:O(1)O(1),最多只需要一个额外变量,和有多少个元素排序没有关系。

稳定性:稳定

简单排序算法总结

冒泡:基本不用,太慢

选择:基本不用,不稳

插入:样本小且基本有序的时候效率比较高

希尔排序

改进的插入排序

对固定间隔的数 组成的数组进行插入排序,并不断缩小间隔。

以间隔为5为例子:

package sortingAlgorithm.ShellSort;

import sortingAlgorithm.Utils.Utils;

import java.util.Arrays;

public class ShellSortDemo {

    public static void main(String[] args) {
        int[] arr = {81,94,11,96,12,35,17,95,28,58,41,75,15};

        sort(arr);

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


    public static void sort(int[] arr){
        //假设间隔为5
        int h = 5;

        //对数组进行间隔为5的排序, 外循环控制要排的那组数的最后一个数
        //注意j的范围一定要大于4,防止数组越界,因为j-5必须大于等于0
        for (int i = 5; i < arr.length ; i++) {
            for (int j = i; j > 4 ; j-=5) {
                if(arr[j]<arr[j-5]){
                    Utils.swap(arr,j,j-5);
                }
            }
        }

    }
}

加入外循环 :以Knuth序列公式 不断缩小间隔大小,进行排序。

    public static void sort(int[] arr){
        //Knuth序列找出最佳间隔
        int h = 1;
        while(h <= arr.length / 3){
            h = h*3 + 1;
        }

        //不断缩小间隔,直到gap=1
        for(int gap = h; gap > 0; gap = (gap-1)/3){
            //使用当前间隔数,对所有数前面对应间隔的数作比较
            for (int i = gap; i < arr.length; i++) {
                //插入排序间隔组成的新数组
                for(int j = i; j > gap-1 ; j-=gap){
                    if(arr[j]<arr[j-gap]){
                        Utils.swap(arr,j,j-gap);
                    }
                }
            }
        }
    }

时间复杂度:与排序的元素个数和使用的序列有关,约 O(n^(1.25))~O(1.6*n^(1.25))
空间复杂度:O(1)
稳定性:不稳定,在进行某次间隔排序时,相等的值的相对位置可能发生改变。

归并排序 MergeSort

使用递归,把原数组一分为二,对左数组排序,对右数组排序,再排序合并两个有序数组。
过程是递归的,不断一分为二,直到左右两个数组只有一个数时,开始返回。

归并排序是稳定的,所以java等语言里对于对象的排序使用的是归并排序。

public class MergeSort {

    public static void main(String[] args) {
        int[] arr = {1,4,5,8,3,6,9};
        sort(arr,0,arr.length-1);
        Arrays.stream(arr).forEach(x -> {System.out.print(x+" ");});
    }

/*    //排序合并两个有序数组的过程
    public static void merge(int[] arr){
        int[] temp = new int[arr.length]; //定义用于存放合并后数组的空数组
        int i = 0; //第一个有序数组的第一个索引
        int j = arr.length/2+1; //第二个有序数组的第一个索引
        int k = 0; //合并数组的第一个索引

        //当i,j还未到达各自部分的尽头时
        while(i<=arr.length/2 && j< arr.length){
            //如果索引i对应的值小于等于索引j的值,就把该值放入temp数组当前位置,放完后k索引加1,索引i加1
            if(arr[i]<=arr[j]){
                temp[k] = arr[i];
                i++;
                k++;
            }else {
                temp[k] = arr[j];
                j++;
                k++;
            }
        }

        //如果其中一个有序数组还有未到达尽头的部分,直接加到temp数组后面
        while (i<=arr.length/2) temp[k++] = arr[i++];
        while (j<arr.length) temp[k++] = arr[j++];
    }*/

    //改造方法,变得更灵活
    //加入参数:左指针,右指针,右边界
    public static void merge(int[] arr,int leftPtr,int rightPtr,int rightBound){
        int mid = rightPtr - 1;
        int[] temp = new int[rightBound - leftPtr +1];
        int i = leftPtr;
        int j = rightPtr;
        int k = 0;

        while(i<=mid && j< arr.length){
            temp[k++] = arr[i]<=arr[j] ? arr[i++] : arr[j++];
        }

        while (i<=mid) temp[k++] = arr[i++];
        while (j<=rightBound) temp[k++] = arr[j++];

        for (int m=0; m<temp.length;m++) arr[leftPtr+m] = temp[m];
    }

    //左边界, 右边界
    public static void sort(int[] arr,int left,int right){
        if(left == right) return;
        //分成两半
        int mid = (left+right)/2;
        //左边排序
        sort(arr,left,mid);
        //右边排序
        sort(arr,mid+1,right);
        //合并有序数组
        merge(arr,left,mid+1,right);
    }
}

快速排序 QuickSort

pivot: 轴,中心点。
基本思想:
任取一个元素为中心(pivot)(这里代码实现使用了最后一个值为轴)
所有比中心小的元素放在中心左边,比中心元素大的放在中心元素右边。
形成左右两个子表。
再对子表重新选择中心元素,并以此规则划分。
直到每个子表的元素只剩一个。(递归的结束条件)

public class QuickSort {

    //对数组的指定范围进行排序
    public static void sort(int[] arr,int leftBound,int rightBound){
        if(leftBound >= rightBound) return;
        int mid = partition(arr,leftBound,rightBound); //分成左右两部分,得到轴的位置
        //对左右两个部分分别排序
        sort(arr,leftBound,mid-1);
        sort(arr,mid+1,rightBound);
    }

    //以数组最后一个值为轴,将小于等于该值的元素移到轴的左边,大于该值的元素移到轴的右边
    public static int partition(int[] arr,int leftBound,int rightBound){
        int pivlot = arr[rightBound]; //轴
        int left = leftBound; //左指针
        int right = rightBound-1; //右指针

        System.out.println("Initial array:"+Arrays.toString(arr));

        //使用两个指针,不断移动,交换左指针上大于等于轴的值和右指针上小于等于轴的值
        while(left <= right){
            //找到从左指针开始第一个大于等于轴的数
            while (left<=right && arr[left]<=pivlot) left++;
            //找到从右指针开始第一个小于轴的数
            while (left<=right && arr[right]>pivlot) right--;


            //交换这两个数的位置
            if(left<right) {
                //System.out.println("Find two Ptrs to swap: left:"+left+" right:"+right);
                Utils.swap(arr,left,right);
                //System.out.println("After swap:"+ Arrays.toString(arr));
            }
        }

        //将轴的值与中间位置的值交换,即左指针的当前位置,
        //因为左指针此时指向的是右子序列的第一个元素,它必然大于轴
        //这样就形成了  左子序列 轴 右子序列 的排列组合
        Utils.swap(arr,left,rightBound);

        //返回轴的位置索引
        return left;
    }

    public static void main(String[] args) {
        int[] arr  = {49,37,65,97,76,13,27,48};
        System.out.println(partition(arr, 0, arr.length - 1));
    }
}

平均时间复杂度:O(nlogn)O(nlog^n)
空间复杂度:需要O(logn)O(log^n)的栈空间
稳定性:不稳定,在进行某次划分时,相等的值的相对位置可能发生改变。

计数排序 CountSort

适合 量大但是范围小
如何快速得知高考名次

入门demo:

public class CountSortDemo {
    public static void main(String[] args) {
        int[] arr = {2,4,2,3,7,1,1,0,0,5,6,9,8,5,7,4,0,9};

        int[] result = sort(arr);

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


    public static int[] sort(int[] arr){
        //定义结果数组
        int[] result = new int[arr.length];

        //定义计数数组,长度为原数组中元素的种类
        int[] count = new int[10];

        //遍历原数组,将原数组的元素,按照: 索引:原数组元素值  值:出现的次数 放入计数数组中
        for (int i = 0; i < arr.length; i++) {
            count[arr[i]]++;
        }

        //遍历计数数组,将计数数组的内容,按顺序放入结果数组
        for(int i=0,j=0; i < count.length ; i++){
            while(count[i]>0){
                result[j] = i;
                j++;
                count[i]--;
            }
        }

        return result;
    }
}

改进版(稳定):

public class CountSortReview {

    public static void main(String[] args) {
        int[] arr = {4,5,3,2,1,0};

        int[] result = sort(arr);

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

    public static int[] sort(int[] arr){
        int k = 6;//元素的种类
        int[] result = new int[arr.length];

        int[] count = new int[k];

        // 计数数组: 索引:原数组元素值  值:该元素的个数
        for (int i = 0; i < arr.length; i++) {
            count[arr[i]]++;
        }

        //System.out.println("1st count:"+Arrays.toString(count));

        // 改造该数组为增量数组, 每个索引指向的值代表小于等于该索引值的数的个数
        // 索引指向的值代表该索引值在排序好的数组中最后出现的位置
        for (int i = 1; i < count.length; i++) {
            count[i] = count[i] + count[i-1];
        }

        //System.out.println("2nd count:"+Arrays.toString(count));

        //反向遍历原数组
        //每次往向结果数组中放值时,先减去count数组该值对应的数量,-1则代表该值在结果数组里的索引
        // do not forget index j can equal to 0
        for (int j = arr.length-1;j>=0;j--){
            result[--count[arr[j]]] = arr[j];
            //System.out.println(Arrays.toString(result));
        }

        return result;
    }
}

基数排序 RadixSort

package sortingAlgorithm.RadixSort;

import java.util.Arrays;

public class RadixSort {

    public static void main(String[] args) {

        int[] arr = {123,456,325,678,523,256,721,850};

        int[] result = sort(arr);

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

    public static int[] sort(int[] arr){

        int d = findMaxCount(arr);//种类个数(排数字时就是最长数字的位数)
        
        //System.out.println("d:"+d);

        //计数数组,这里是数字,所以有0~9,共10个桶
        int[] count = new int[10];

        //结果数组
        int[] result = new int[arr.length];

        //如果有k位,那么就有k次 分配+组合 的过程
        for(int i = 0;i < d;i++){

            int divid = (int) Math.pow(10,i);
            //用于得到位上的数:
            //求百位上的数,就是用该数除以100再%10,求十位上的数就是该数除以10再%10,求个位上的就是除以1再%10
            // System.out.println(divid);
            //遍历原数组,填充计数数组: 索引:0~9的数字  值:该数字出现的次数
            for (int j = 0; j < arr.length; j++) {
                int num = arr[j]/divid % 10; //得到该位上的数
                count[num]++;
            }

            //System.out.println("1st count:"+ Arrays.toString(count));

            //改变计数数组到增量数组,类似计数排序
            for (int k = 1; k < count.length; k++){
                count[k] = count[k] + count[k-1];
            }

            //System.out.println("2nd count:"+ Arrays.toString(count));

            //反向遍历原数组,放置值到结果数组中
            //与计数排序不同的是 count数组的索引不再是原数组的值,而是原数组的值的某一位的值
            for(int n = arr.length-1;n>=0;n--){
                int num = arr[n]/divid % 10;
                result[--count[num]] = arr[n];
            }

            //将该轮按某位排好的result数组复制到原数组上,完成该轮的组合
            //下一轮将使用排序过的arr再按某位进行排序
            System.arraycopy(result,0,arr,0,arr.length);
            //将count数组归零,用于下一轮某位的排序
            Arrays.fill(count,0);

        }

        return result;
    }

    public static int findMaxCount(int[] arr){
        int max = arr[0];
        for (int i : arr) {
            if(i>max) max=i;
        }
        int count = 0;
        while(max>0){
            max/=10;
            count++;
        }
        return count;
    }

}

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!