算法之基本排序

基本排序算法比较
基本排序算法 平均时间复杂度 最好时间复杂度 最差时间复杂度 空间复杂度 稳定排序否 原地排序否 优点 缺点
冒泡排序 O(n^2) O(n)* O(n^2) O(1) - -
选择排序 O(n^2) O(n^2) O(n^2) O(1) - -
插入排序 O(n^2) O(n) O(n^2) O(1) - -
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n)* 稳定 需额外空间
快速排序 O(nlogn) O(nlogn) O(n^2)* O(1) 不需要额外空间 不稳定*
桶排序 O(n) O(n) O(n) O(n) 时间复杂度低 对数据范围和分布要求极严格
计数排序 O(n) O(n) O(n) O(n) 可是可不是* 时间复杂度低 对数据范围要求极严格
基数排序 O(n) O(n) O(n) O(n) 时间复杂度低 对数据范围要求严格

说几点:

  1. 排序的本质是什么?将逆序对变成正序对 --- 衍生出一道题:求一个数组的逆序对?
  2. 平均时间复杂度:是所有不同逆序对组合排序时间复杂度的平均值,从0对到n*(n-1)/2;
    最好时间复杂度:当数组有序时的时间复杂度
    最坏时间复杂度:当数组逆序或者每次都得不到理想的结果
  3. 稳定排序判断:排序过程中是否能保证相同元素的前后位置保持不变;
    这个指标对于基本类型数组排序没有区别;
    但是对于对象数组是有区别的,因为对象数组排序可能会根据多个字段维度排序:先根据字段1排序再根据字段2排序,如果根据字段2排序是不稳定的,则相同字段2的值元素之前根据字段1排的序就白费了
    比如:淘宝订单先根据下单时间字段排序,然后根据下单金额排序,排序完之后我们希望相同金额的单内部是按时间排序的,则按下单金额排序就必须采用稳定排序了;
    所以,根据不同场景决定是否采用稳定排序。
  4. 每个排序算法说一下注意点:
  • 冒泡排序:可以优化外层循环次数,当没有再进行swap时,则不用再继续了
  • 归并排序:当数组较大的时候,消耗的空间为O(n),可能就不大适合;还有一点,你可能发现了每次一分为二的时候都会new一个数组,空间复杂度不应该是O(nlogn)吗?空间复杂度不是这么算法的,空间复杂度是只某一时间占用额外内存的最大值,这里是单线程,每次new完一个数组使用之后会销毁掉,所以最多占用O(n)的空间
  • 快排最差时间复杂度为O(n^2)出现的场景:每次partition后都是一分为1和n-1
  • 计数排序:可稳定可不稳定,可以有两种写法,见后面的代码部分。
  • 具体选择哪个排序算法,要看排序数字的大小,分布规律,是否要求稳定排序等因素。
  1. 扩展题
  • 求一个数组的逆序对
    归并过程中求解
  • O(n)时间复杂度求数组第k大的元素
    快排算法思想+减治法
  • 10个接口访问10个日志文件,每个日志文件大小300M且都是按照时间排序的,将其合并成一个文件,但是限制内存大小只有1G,问怎么搞?
    归并算法的merge过程,可以先假定只有两个1G的日志文件怎么搞
  • D,a,F,B,c,A,z,b,只有大小写字母,排序将小写字母全部放在大写字母后面,小写字母或者大写字母之间不要求有序? 再问只有大小写字母和数字,将小写字母放左边,数字放中间,大写字母放右边,内部不要求有序,怎么搞?
    外部排序首选:桶排序
各种排序算法实现和优化
  1. 冒泡排序
/**
 * 描述:冒泡排序
 *
 * @author xuery
 * @date 2018/11/20
 */
public class BubbleSort {

    private static final int ARR_SIZE = 10;

    public static void main(String[] args) {
        int[] arr = ArrayUtil.generateArray(ARR_SIZE, 50);
        optimizeBubbleSort(arr);
        ArrayUtil.printArray(arr);
    }
    
    public static void bubbleSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                //注意这里不能大于等于,有等于则是非稳定排序
                if(arr[j] > arr[j+1]){
                    ArrayUtil.swap(arr, j ,j+1);
                }
            }
        }
    }

    public static void optimizeBubbleSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        for (int i = 0; i < arr.length - 1; i++) {
            boolean continueFlag = false;
            for (int j = 0; j < arr.length - 1 - i; j++) {
                //注意这里不能大于等于,有等于则是非稳定排序
                //优化,如果当前内层循环一次都没有进入到swap则说明已经有序了,不需要往下进行了
                if(arr[j] > arr[j+1]){
                    ArrayUtil.swap(arr, j ,j+1);
                    continueFlag = true;
                }
            }
            if (!continueFlag){
                break;
            }
        }
    }

}
  1. 选择排序
/**
 * 描述:选择排序
 *
 * @author xuery
 * @date 2018/11/20
 */
public class SelectSort {

    private static final int ARR_SIZE = 10;

    public static void main(String[] args) {
        int[] arr = ArrayUtil.generateArray(ARR_SIZE, 50);
        selectSort(arr);
        ArrayUtil.printArray(arr);
    }

    public static void selectSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[minIndex] > arr[j]) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                ArrayUtil.swap(arr, i, minIndex);
            }
        }
    }
}
  1. 插入排序
/**
 * 描述:插入排序
 *
 * @author xuery
 * @date 2018/11/20
 */
public class InsertSort {

    public static void main(String[] args) {
        int[] arr = ArrayUtil.generateArray(10, 50);
        insertSort(arr);
        ArrayUtil.printArray(arr);
    }

    public static void insertSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        for (int i = 1; i < arr.length; i++) {
            //从下表i-1遍历到0找到arr[i]插入的位置
            int insertIndex = 0;
            int arri = arr[i];
            for (int j = i - 1; j >= 0; j--) {
                //注意,这里也不能取等号,否则就是不稳定排序了
                if(arr[j] > arri){
                    arr[j+1] = arr[j];
                } else {
                    insertIndex = j+1;
                    break;
                }
            }
            arr[insertIndex] = arri;
        }
    }
}
  1. 归并排序
/**
 * 描述:归并排序及非递归实现
 *
 * @author xuery
 * @date 2018/11/20
 */
public class MergeSort {

    public static void main(String[] args) {
        int[] arr = ArrayUtil.generateArray(7, 50);
        MergeSort mergeSort = new MergeSort();
        ArrayUtil.printArray(arr);
        mergeSort.notRecursionMergeSort(arr);
        ArrayUtil.printArray(arr);
    }

    public void mergeSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }
        recursionMergeSort(arr, 0, arr.length - 1);
    }

    public void recursionMergeSort(int[] arr, int begin, int end) {
        if (begin >= end) {
            return;
        }
        int mid = begin + (end - begin) / 2;
        //divide into two parts
        recursionMergeSort(arr, begin, mid);
        recursionMergeSort(arr, mid + 1, end);
        //merge
        int[] temp = new int[end - begin + 1];
        int i = begin, j = mid + 1, index = 0;
        while (i <= mid && j <= end) {
            //这里需要有等号,保证稳定性
            if (arr[i] <= arr[j]) {
                temp[index++] = arr[i++];
            } else {
                temp[index++] = arr[j++];
            }
        }
        while (i <= mid) {
            temp[index++] = arr[i++];
        }
        while (j <= end) {
            temp[index++] = arr[j++];
        }
        //将temp复制会arr对应的位置
        for (int k = 0; k < end - begin + 1; k++) {
            arr[k + begin] = temp[k];
        }
    }

    /**
     * 非递归实现归并排序
     * 思路很简单:就是没有递,只有归,先1+1合并,再2+2合并,最后n/2+n/2合并
     * 如何编写代码:自己举例,确定两两合并的边界,边界一旦确定直接采用之前的merge就可以了
     * 注意点:两两个数不一定相同,要注意越界条件等的判断
     *
     * bug:一定要注意对某些变量操作完之后,它已经不代表最初的值了,想要最初的值就不能对变量操作或者新建一个变量保存最初的值
     * @param arr
     */
    public void notRecursionMergeSort(int[] arr) {

        //step表示step个元素+step个元素合并
        int step = 1;
        while (step < arr.length) {
            for (int i = 0; i < arr.length; i = i + 2 * step) {
                //i--i+step-1合并,i+step--i+2*step-1合并,都包括边界,注意越界条件判断
                //i+step>=arr.length时右边一半没有,结束本step合并
                if (i + step >= arr.length) {
                    break;
                }
                //可以开始合并
                int mid = i + step - 1, right = Math.min(i + 2 * step - 1, arr.length - 1);
                int[] temp = new int[right - i + 1];
                int leftIndex = i, rightIndex = mid + 1, index = 0;
                while (leftIndex <= mid && rightIndex <= right) {
                    if (arr[leftIndex] <= arr[rightIndex]) {
                        temp[index++] = arr[leftIndex++];
                    } else {
                        temp[index++] = arr[rightIndex++];
                    }
                }
                while (leftIndex <= mid) {
                    temp[index++] = arr[leftIndex++];
                }
                while (rightIndex <= right) {
                    temp[index++] = arr[rightIndex++];
                }
                for (int k = 0; k < right - i + 1; k++) {
                    arr[k + i] = temp[k];
                }
            }
            step = step * 2;
        }
    }
}
  1. 快速排序
/**
 * 描述:快排填坑法再搞一遍
 * 非稳定排序,相同值可能由于partition导致顺序改变
 *
 * @author xuery
 * @date 2018/11/20
 */
public class QuickSort2 {

    private static final Random random = new Random();

    public static void main(String[] args) {
        int[] arr = ArrayUtil.generateArray(10, 20);
        ArrayUtil.printArray(arr);
        quickSort(arr);
        ArrayUtil.printArray(arr);
    }

    public static void quickSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        quickSort(arr, 0, arr.length - 1);
    }

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

        int partition = partition(arr, begin, end);

        //divide into three parts including [begin,partition-1],partition,[partition+1,end]
        quickSort(arr, begin, partition - 1);
        quickSort(arr, partition + 1, end);
    }

    //挖坑:第一个坑是基准所在的位置,最后一个坑填基准值,填坑条件:与基准值比较,每次循环填两个坑,一左一右
    public static int partition(int[] arr, int begin, int end) {
        //随机取其中的一个值作为基准并与最后一个值交换
        int pIndex = begin + random.nextInt(end - begin + 1);
        if (pIndex != end) {
            ArrayUtil.swap(arr, pIndex, end);
        }
        int pivot = arr[end];
        int i = begin, j = end;
        while (i < j) {
            //从左往右找第一个大于pivot的
            while (i < j && arr[i] <= pivot) {
                i++;
            }
            if (i < j) {
                arr[j] = arr[i];
                j--;
            }

            //从右往左找第一个小于pivot的
            while (i < j && arr[j] >= pivot) {
                j--;
            }
            if (i < j) {
                arr[i] = arr[j];
                i++;
            }
        }
        arr[i] = pivot;
        return i;
    }
}
  1. 计数排序
/**
 * 描述:计数排序
 * <p>
 * 算法描述:如其名,遍历数组,数组中的值对应计数数组的下标,将对应计数数组下标对应的值+1操作;之后再遍历计数数组即可完成排序
 * <p>
 * 要求数组中元素的值分布在某个比较小的范围内
 * <p>
 * 可以是不稳定排序或者稳定排序
 *
 * @author xuery
 * @date 2018/11/20
 */
public class CountSort {

    public static void main(String[] args) {
        int[] arr = ArrayUtil.generateArray(10, 20);
        ArrayUtil.printArray(arr);
        stableCountSort(arr);
        ArrayUtil.printArray(arr);
    }

    /**
     * 不稳定的计数排序,无法保证相同数值保证顺序不变,因为是采用从头到尾直接遍历计数数组的方式
     * <p>
     * 假设只有非负整数
     *
     * @param arr
     */
    public static void notStableCountSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        //find max value
        int maxValue = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            if (maxValue < arr[i]) {
                maxValue = arr[i];
            }
        }
        //new a counting arr to count now
        int[] countArr = new int[maxValue + 1];
        for (int i = 0; i < arr.length; i++) {
            countArr[arr[i]]++;
        }

        int index = 0;
        for (int i = 0; i < countArr.length; i++) {
            while (countArr[i]-- > 0) {
                arr[index++] = i;
            }
        }
    }

    /**
     * 上面的方式是不稳定排序,可以采用比较巧妙的方法改写成稳定排序
     * <p>
     * 从头遍历countArr数组,将之前的数值累加到当前位置并放入countSumArr数组中,这样countSumArr[i]就表示当前小于等于i的数值个数为countSumArr[i]
     * 然后从尾部开始遍历countArr, 比如遍历到countArr[i],根据对应的countSumArr[i]的值就知道应该把i放回原数组arr的哪个位置,
     * 并将countArr[i]和countSumArr[i]分别减一, 对于countArr重复该操作,直至countArr[i]变为0
     * 这样可以巧妙的保证相同数值的元素顺序保持不变
     *
     * @param arr
     */
    public static void stableCountSort(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }

        //find max value
        int maxValue = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            if (maxValue < arr[i]) {
                maxValue = arr[i];
            }
        }
        //new a counting arr to count now
        int[] countArr = new int[maxValue + 1];
        for (int i = 0; i < arr.length; i++) {
            countArr[arr[i]]++;
        }

        //calcalate countSumArr just as explained
        int[] countSumArr = new int[maxValue + 1];
        countSumArr[0] = countArr[0];
        for (int i = 1; i < countArr.length; i++) {
            countSumArr[i] = countSumArr[i - 1] + countArr[i];
        }

        for (int i = countArr.length - 1; i >= 0; i--) {
            while(countArr[i]-- > 0){
                arr[--countSumArr[i]] = i;
            }
        }

    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容