八大排序详解

1.排序概论

3。排序算法的稳定性(重要)

稳定性:若待排序的集合中存在值相等的元素,经过排序之后,相同元素的相对位置没有发生改变。

2.排序分类

(无论是内部排序还是外部排序,最终数据的排序一定再内存中进行)

2.1内部排序

定义:排序过程无需借助外部储存器(如磁盘),所有排序操作均在内存中完成。默认说的排序都是内部排序。

内部排序按照排序思路分为以下四类:

2.1.1 插入排序法 (O(n^2))

概念:基于有序集合插入思想的排序算法


直接插入排序

思想:将待排序的数据分为两个区间,已排序区间与待排序区间。算法刚开始时,已排序空间有一个元素,在待排序空间中选择第一个元素与已排序空间的最后一个元素比较,若比已排序的最大元素大直接放入,若小则进行插入

代码:

package www.sort.java;

/**
 * @Author : YangY
 * @Description :直接插入排序
 * @Time : Created in 20:44 2019/3/7
 */
public class Test2 {
    public static void main(String[] args) {
        int[] arr = new int[]{42,3,4,6,3,10};
        insertSoprt(arr);
        SortHelper.print(arr);
    }
    public static void insertSoprt(int[] arr) {
        int n = arr.length;
        if(n <= 1) {
            return;
        }
        for(int i = 1; i < n; i++) {
            int value = arr[i];
            int j;
            //在已排序集合中找到要插入的位置
            for(j = i-1; j >=0; j--) {
                if(arr[j] <= value) {
                    break;
                }else {
                    arr[j+1] = arr[j];
                }
            }
            //此处注意是 j+1 ,可以用极限法来想
            arr[j+1] = value;
        }
    }
}

  • 时间复杂度:
    最好情况:O(n)
    最坏情况:O(n^2)
    平均复杂度:O(n^2)
  • 算法的内存消耗
    O(1);
  • 算法的稳定性
    属于稳定性排序方法

折半插入排序

相对于直接插入,优化:利用二分查找来节省时间;

package www.sort.java;

/**
 * @Author : YangY
 * @Description :
 * @Time : Created in 9:58 2019/3/10
 */
public class Test3 {
    public static void main(String[] args) {
        int[] arr = new int[]{15,3,4,6,3,10,2,33,1,-1};
        //int[] arr = SortHelper.produceArr(1000,2000,3000);
        insertSoprt(arr);
        SortHelper.print(arr);
    }
    public static void insertSoprt(int[] arr) {
        int n = arr.length;
        if(n <= 1) {
            return;
        }
        for(int i = 1; i < n; i++) {
            int value = arr[i];
            int l = 0;
            int r = i-1;
            int m = l + (r-l)/2;
            while(l <= r) {
                if(value > arr[m]) {
                    l = m + 1;
                }else {
                    r = m - 1;
                }
                m = l + (r-l)/2;
            }
            int j = i - 1;
            for(; j >= r + 1; j--) {
                arr[j+1] = arr[j];
            }
            arr[j+1] = value;

        }
    }
}


希尔排序

时间复杂度会根据分的长度不同而异,所以一般不考虑希尔排序的时间复杂度

    public static void shellSort(int[] arr) {
        int n = arr.length;
        if(n <= 1) {
            return;
        }
        int step = n/2;
        while(step >= 1) {
            for(int i = step; i < n; i++) {
                int value = arr[i];
                int j = i - step;
                for(; j >= 0; j -=step) {
                    if(value < arr[j]) {
                        arr[j+step] = arr[j];
                    }else{
                        break;
                    }
                }
                arr[j+step] = value;
            }
            step /= 2;
        }
    }
}

优化:原先找到插入位置后,元素是一个一个向后移,损耗较大,而希尔排序正是优化了这一点;


2.1.2选择排序

选择排序 (O(n^2))

package www.sort.java;

/**
 * @Author : YangY
 * @Description :选择排序,不稳定排序;
 * @Time : Created in 11:25 2019/3/10
 */
public class Test5 {
    public static void main(String[] args) {
        int[] arr = new int[]{1,5,3,7,8,2};
        selectSort(arr);
        SortHelper.print(arr);
    }
    static void selectSort(int[] arr) {
        double start = System.currentTimeMillis();
        int n = arr.length;
        if(n <= 1) {
            return;
        }
        for(int i = 0; i < n-1; i++) {
            int minIndex = i;
            for(int j = i+1; j < n; j++){
                if(arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            int temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
    }
}

堆排序(二叉树后再完善)


2.1.3 交换排序

冒泡排序(O(n^2))

冒泡排序只会操作相邻的两个元素 。每次对相邻的两个元素做大小比较,看是否满足大小关系。一次冒泡至少会让一个元素移动到最终位置;

注意之处:可以实现小优化:某次交换中若交换次数为0,则代表已经有序,直接返回;

package www.sort.java;

/**
 * @Author : YangY
 * @Description :  冒泡排序,时间复杂度O(n^2);
 * @Time : Created in 19:25 2019/3/7
 */
public class Test1 {
    public static void main(String[] args) {
        int[] arr = new int[]{1,3,7,4,2,10,10,0,-1};
        bubbleSort(arr);
        SortHelper.print(arr);
    }
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for(int i = 0; i < n-1; i++) {
            boolean flag = true;
            for(int j = 0; j < n-i-1; j++) {
                if(arr[j] > arr[j+1]) {
                    flag = false;
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
            //优化
            if(flag) {
                return;
            }
        }
    }
}

总结:

  • 算法的执行效率:
    最好情况:
    数据集本身就是一个有序集合,O(n^2);

    最坏情况:
    数据集完全逆序:,O(n^2);

    平均情况:
    一般与最坏情况一致,O(n^2);

  • 算法的内存消耗
    无需开辟新的空间,仅仅是原有本身数据进行交换
    所以空间复杂度为O(1),为一个原地排序算法;此概念非常重要

  • 算法的稳定性
    冒泡排序由于相邻元素发生大小关系变换才会交换次序,所以当两个元素大小相等时,并不会交换次序,所以为稳定性排序方法;

快速排序

2.1.4归并排序(O(nlogn))

归并与快排都应用分治思想,如何在O(n)内寻找一个无序数组的第K大元素?(经典面试题)
所有使用分治思想解决的问题均可利用递归的技巧完美解决。

    public static void mergeSort(int[] arr) {
        int n = arr.length;
        if(n <= 1) {
            return;
        }
        mergeSortIntern(arr,0,n-1);
    }
    public static void mergeSortIntern(int[] arr, int left, int right) {
        if(left >= right) {
            return;
        }
        int mid = (left + right) / 2;
        mergeSortIntern(arr, left, mid);
        mergeSortIntern(arr,mid+1,right);
        merge(arr,left,mid,right);

    }
    public static void merge(int[] arr, int left, int mid, int right) {
        int[] newArr = new int[right - left + 1];
        int k = 0;
        //前半部分数组的起始
        int i = left;
        //后半部分数组的起始
        int j = mid + 1;
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {     //这一定要为小于等于,这样才是稳定性排序,否则不是稳定性排序
                newArr[k++] = arr[i];
                i++;
            } else {
                newArr[k++] = arr[j];
                j++;
            }
        }
        //总会有一个数组没有拷贝完,此时通过以下方法将剩余的元素拷贝到新数组
        if(i > mid) {
            while(j <=right) {
                newArr[k++] = arr[j++];
            }
        }else {
            while(i <= mid) {
                newArr[k++] = arr[i++];
            }
        }
        //将newArr拷贝到原数组
        for(int n = 0; n < right-left + 1; n++) {
            arr[left+n] = newArr[n];
        }
    }

归并排序复杂度分析:

稳定性:取决于合并函数的写法,arr[i] <= arr[j] ;

时间复杂度:
分组次数:log 2 N

最好:nlogn
最坏:nlogn
平均:nlogn

空间复杂度:O(n),临时数组在合并后空间会释放,就在最后一次合并开的空间最大为n,所以时间复杂度为O(n);

所以归并排序不是原地排序,因此没得到重用;

归并排序小优化: (小⭐)
对合并可进行优化:当左边数组最大元素都小于右边数组最小元素,说明整个数组有序,直接结束排序;


2.1.5 快速排序(⭐)

算法思路:从待排序的数组中选取任意一个元素[l,…r],成为分区点(基准值)
开始遍历过程,每当发现比基准值小的元素就放在基准值左边,每当发现>= 基准值的元素就放在基准值的右边。
当结束一次遍历时,基准元素一定在最终位置;

一路快排:

package www.sort.java;

/**
 * @Author : YangY
 * @Description :一路快排
 * @Time : Created in 19:10 2019/3/14
 */
public class SortQuick {
    public static void main(String[] args) {
        int[] arr = new int[]{1,4,2,7,-1,10,4};
        //int[] arr = SortHelper.produceArr(10000000,1000,10000);
        //int[] arr = SortHelper.produceSortyArr(5500);
        long start = System.currentTimeMillis();
        quickSort(arr);
        long end = System.currentTimeMillis();
        System.out.println("所耗时间为:"+(end-start)+"ms");
        SortHelper.print(arr);

    }
    public static void quickSort(int[] arr) {
        int length = arr.length;
        if(length <= 1) {
            return;
        }
        quickSortInternal(arr,0,length-1);

    }
    private static void quickSortInternal(int[] arr, int l, int r) {
        if(l >= r) {
            return;
        }
        //获取基准值
        int par = patition(arr, l, r);
        quickSortInternal(arr, l, par-1);
        quickSortInternal(arr, par+1, r);
    }
    private static int patition(int[] arr, int l, int r) {
        //随机选取一个下标作为基准值,防止了原数组近乎有序导致的时间复杂度近乎降为O(n^2);
        swap(arr,randomIndex(l,r),l);   //1
        int val = arr[l];              //2     1、2的顺序千万不能反,否则会错
        //并不是不可以反,要反的话就得先定义一个random = randomIndex(l,r),不然你定义两次的话就会产生不同的随机值,这当时坑了我很久
        //[l+1, j]区间内都是小于val
        int j = l;
        //[j+1, i]区间内都是大于等于val
        int i = l+1;
        for(; i<=r;i++) {
            if(arr[i] < val) {
                swap(arr, i, j+1);
                j++;
            }
        }
        swap(arr, l, j);
        return j;
    }
    private static void swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    private static int randomIndex(int l, int r) {
         int random = (int)(Math.random()*(r-l+1)) + l;
         return random;
    }
}

平均时间复杂度:nlogn;

稳定性:不稳定性算法(若基准值为最后一个元素,5 4 3 2 6 1 5)

当待排元素接近有序时,若选区的基准值恰好为最小时,每次分区严重不均衡,导致分区时间复杂度退化为O(n) (假如每次均分,则为O(logn)),再加上合并的O(n),此时总的时间复杂度分层退化成0(n^2),若此时选取得基准值为最大值,则为0(n);

归并与快排都应用分治思想,如何在O(n)内寻找一个无序数组的第K大元素?(经典面试题)

快排优化:

1.当待排序的集合近乎有序时,由于默认选择的第一个元素作为基准值,会导致基准值划分的两个子数组严重不均衡,此时分层下来得结果近乎于n层,此时快排退化为复杂度为O(n^2)排序算法;
优化:选取一个随机下标的数作为基准值,然后将该下标的数与l(最左边)下标的数进行交换,后面过程完全不变;

2.当排序集合中有大量重复元素时,由于基准值相等的元素过多,也会导致划分数组严重不均衡,将时间复杂度退化为O(n^2);
优化:使用二路快排,代码如下:

当重复元素过多,待排元素数量稍大时就会溢出,但优化为二路快排后不仅不容易溢出(原先10w就会溢出,双路后千万都不会溢出),时间也提升了百倍(自己对比而来的)

二路快排:⭐

package www.sort.java;

/**
 * @Author : YangY
 * @Description :  双路快排的实现
 * @Time : Created in 16:49 2019/3/15
 */
public class SortTwoQuick {
    public static void main(String[] args) {
        int[] arr = new int[]{1,3,5,2,7,-10};
        twoQuickSort(arr);
        SortHelper.print(arr);
    }

    public static void twoQuickSort(int[] arr) {
        int length = arr.length;
        if(length <= 1) {
            return;
        }
        twoQuickSortInternal(arr,0,length-1);

    }
    private static void twoQuickSortInternal(int[] arr, int l, int r) {
        if(l >= r) {
            return;
        }
        //获取基准值
        int par = patition(arr, l, r);
        twoQuickSortInternal(arr, l, par-1);
        twoQuickSortInternal(arr, par+1, r);
    }
    private static int patition(int[] arr, int l, int r) {
        //找一个随机下标的数组值作为基准值
        int random = randomIndex(l, r);
        int val = arr[random];
        swap(arr, l, random);
        int i = l + 1;
        int j = r;
        while(true) {
            //此处arr[i]<val和下面的arr[j]>val,不能改为<=和>=,否则起不到优化作用⭐
            while(i<=r && arr[i]<val) {
                i++;
            }
            while(j>=l+1 && arr[j]>val) {
                j--;
            }
            if(i > j) {
                break;
            }
            swap(arr, i, j);
            i++;
            j--;
        }
        swap(arr, l, j);
        return j;
    }
    private static void swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    private static int randomIndex(int l, int r) {
        int random = (int)(Math.random()*(r-l+1)) + l;
        return random;
    }
}

三路快排:(解决大量重复元素的数组)

package www.sort.java;

/**
 * @Author : YangY
 * @Description : 三路快排,对重复元素超多的数组有很大程度的优化,对重复元素较少的相对于二路快排也只慢一点点,值得推荐
 * @Time : Created in 18:57 2019/3/15
 */
public class SortThreeQuick {
    public static void main(String[] args) {
        int[] arr = new int[]{1,4,2,6,-1,10,2,2,9,-10};
        threeQuickSort(arr);
        SortHelper.print(arr);

    }
    public static void threeQuickSort(int[] arr) {
        int length = arr.length;
        if(length <= 1) {
            return;
        }
        threeQuickSortInternal(arr,0,length-1);

    }
    private static void threeQuickSortInternal(int[] arr, int l, int r) {
        if(l >= r) {
            return;
        }
        //找一个随机下标的数组值作为基准值
        int random = randomIndex(l, r);
        int val = arr[random];
        swap(arr, l, random);
        //[l,lt]都小于val
        int lt = l;
        //[lt+1,i-1]都等于val       [i,rt-1]为待比较元素
        int i = l+1;
        //[rt,r]都大于val
        int rt = r + 1;
        while(i < rt) {
            if(arr[i] <val) {
                swap(arr, i, lt+1);
                lt++;
                i++;
            }else if(arr[i] > val) {
                //此种情况i不能自加,因为他是与后面一个数换的,后面那个数还不知道它的情况呢。
                swap(arr, i, rt-1);
                rt--;
            }else {
                i++;
            }
        }
        swap(arr, l, lt);
        threeQuickSortInternal(arr, l, lt-1);    //这里r为lt也没错,但多执行了一步
        threeQuickSortInternal(arr, rt, r);
    }

    private static void swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    private static int randomIndex(int l, int r) {
        int random = (int)(Math.random()*(r-l+1)) + l;
        return random;
    }
}

进阶优化:
当元素个数比较小时(<=15最优),可调用直接插入排序,数量小时,直接插入的时间复杂度接近于O(n);


2.2外部排序

定义:若参与排序的元素过多,数据量过大,内存放不下,需要借助外部存储器来进行排序,如桶排序

2.2.1 桶排序

将元素均匀分桶后进行快排

例如:如何在O(n)时间内按照年龄大小给100万个用户排序

在0~120间分成120个桶,然后每个桶分别进行快排;

2.2.2基数排序(每位排序都进行稳定性排序)

总结

稳定性排序:

冒泡
直接插入
折半插入

非稳定排序:

希尔排序
归并排序(取决于合并过程)
选择排序
快排

O(n^2) :冒泡 直接插入 折半插入 选择

O(n^3/2):希尔排序

O(nlogn): 归并 快排 (随机化 二路 三路)

猜你喜欢

转载自blog.csdn.net/eternal_yangyun/article/details/88605992
今日推荐