几种排序算法的总结与比较

关于我的 Leetcode 题目解答,代码前往 Github:https://github.com/chenxiangcyr/leetcode-answers


基本概念

比较排序和非比较排序:

  • 常见的排序算法都是比较排序

    • 比较排序的时间复杂度通常为 O(n2) 或者 O(nlogn),比较排序的时间复杂度下界就是 O(nlogn)
  • 非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置

    • 非比较排序的时间复杂度可以达到 O(n),但是都需要额外的空间开销

几种排序算法的总结与比较

排序方法 平均时间 最坏时间 辅助空间 稳定性
简单排序 - 冒泡排序 O(n2) O(n^2) O(1) 稳定
简单排序 - 选择排序 O(n2) O(n2) O(1) 不稳定
简单排序 - 插入排序 O(n2) O(n2) O(1) 稳定
快速排序 O(nlogn) O(n2) O(logn) 不稳定
堆排序 O(nlogn) O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(n) 稳定
希尔排序 O(nlogn2) = O(n1.3) O(n2) O(n) 不稳定
计数排序 O(n + k) O(n + k) O(k) 稳定
桶排序 O(n + k) O(n2) O(n) 稳定
基数排序 O(nk) O(nk) O(n + k) 不稳定
Array Sorting Algorithms

冒泡排序

通过与相邻元素的比较和交换来把小的数交换到最前面,或者把大的数交换到最后面。

冒泡排序的时间复杂度为 O(n2)

代码如下:

    // 冒泡排序
    public void bubbleSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        // 只需要循环 n-1 次
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = 0; j < nums.length - i - 1; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);
                }
            }
        }
    }

分析:假设输入为 4, 2, 5, 1, 3
第一次循环后,变为 2, 4, 1, 3, 55 被移动到最后
第二次循环后,变为 2, 1, 3, 4, 54 被移动到最后的第二个位置
以此类推

选择排序

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。
但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。

其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。

选择排序的时间复杂度为 O(n2)

代码如下:

    // 选择排序
    public void selectSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        int minIndex;

        // 只需要循环 n-1 次
        for (int i = 0; i < nums.length - 1; i++) {
            minIndex = i;

            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }

            if (minIndex != i) {
                swap(nums, i, minIndex);
            }
        }
    }

分析:假设输入为 4, 2, 5, 1, 3
第一次循环后,变为 1, 2, 5, 4, 31 被移动到最前
第二次循环后,变为 1, 2, 5, 4, 32 被移动到最前的第二个位置
以此类推

插入排序

插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的。

插入排序的时间复杂度为 O(n2)

代码如下:

    // 插入排序
    public void insertSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        // 假设第一个数位置是正确的
        for (int i = 1; i < nums.length; i++) {
            int j = i;

            int target = nums[j];

            // 后移
            while (j > 0 && nums[j - 1] > target) {
                nums[j] = nums[j - 1];
                j--;
            }

            // 插入
            nums[j] = target;
        }
    }

分析:假设输入为 4, 2, 5, 1, 3
第一次循环后,变为 2, 4, 5, 1, 3
第二次循环后,变为 2, 4, 5, 1, 3
第三次循环后,变为 1, 2, 4, 5, 3
以此类推

快速排序

其实其思想是来自冒泡排序,冒泡排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。

快速排序是不稳定的,其时间复杂度为 O(nlogn)

代码如下:

    // 快速排序
    public void quickSort(int[] nums, int start, int end) {
        if (start >= end)
            return;

        int partitionIdx = partition(nums, start, end);

        quickSort(nums, start, partitionIdx - 1);
        quickSort(nums, partitionIdx + 1, end);
    }

    // partition
    public int partition(int[] nums, int start, int end) {
        if (start == end) {
            return start;
        }

        int pivot = nums[start];

        while (start < end) {
            // 从右往左找到第一个小于 pivot 的元素
            while (start < end && nums[end] >= pivot) {
                end--;
            }
            // 把小的移动到左边
            nums[start] = nums[end];

            // 从左往右找到第一个大于 pivot 的元素
            while (start < end && nums[start] <= pivot) {
                start++;
            }
            // 把大的移动到右边
            nums[end] = nums[start];
        }

        // 最后把pivot赋值到中间
        nums[start] = pivot;

        return start;
    }

分析:假设输入为 4, 2, 5, 1, 3
第一轮递归后,变为 3, 2, 1, 4, 5
第一轮递归后,变为 1, 2, 3, 4, 5
以此类推

堆排序

堆排序是借助堆来实现的选择排序。
注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。

具体参见:堆的使用及相关LeetCode题目

归并排序

归并排序使用了递归分治的思想。
把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列...倒着来看,其实就是先两两合并,然后四四合并...最终形成有序序列。

归并排序空间复杂度为 O(n),时间复杂度为 O(nlogn)

代码如下:

    // 归并排序
    public void mergeSort(int[] arr, int start, int end) {
        if (start >= end)
            return;

        int mid = (start + end) / 2;

        // 递归排序左边
        mergeSort(arr, start, mid);
        // 递归排序右边
        mergeSort(arr, mid + 1, end);

        // 合并
        merge(arr, start, mid, end);
    }

    // 合并两个有序数组
    public void merge(int[] arr, int start, int mid, int end) {
        int[] temp = new int[end - start + 1]; // 中间数组

        int i = start;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= end) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];
            }
        }

        while (i <= mid) {
            temp[k++] = arr[i++];
        }

        while (j <= end) {
            temp[k++] = arr[j++];
        }

        for (int p = 0; p < temp.length; p++) {
            arr[start + p] = temp[p];
        }
    }

希尔排序

希尔排序是插入排序的一种高效率的实现。
简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。
希尔排序就利用了这个特点。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。

希尔排序时间复杂度为 O(nlogn)

代码如下:

    // 希尔排序的一趟插入
    public void shellInsert(int[] nums, int d) {
        for (int i = d; i < nums.length; i++) {
            int j = i - d;
            
            // 记录要插入的数据
            int temp = nums[i];
            // 从后向前,找到比其小的数的位置
            while (j >= 0 && nums[j] > temp) {
                // 向后挪动
                nums[j + d] = nums[j];
                j -= d;
            }

            // 存在比其小的数
            if (j != i - d)
                nums[j + d] = temp;
        }
    }

    // 希尔排序
    public void shellSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        int d = nums.length / 2;
        while (d >= 1) {
            shellInsert(nums, d);
            d /= 2;
        }
    }

计数排序

前提条件:待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。

计数排序空间复杂度为 O(n),时间复杂度为 O(n)

代码如下:

    // 计数排序
    public void countSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        int max = max(nums);

        int[] counts = new int[max + 1];
        Arrays.fill(counts, 0);

        // 计数
        for (int i : nums) {
            counts[i]++;
        }

        int k = 0;
        for (int i = 0; i <= max; i++) {
            for (int j = 0; j < counts[i]; j++) {
                nums[k++] = i;
            }
        }
    }

    public int max(int[] nums) {
        int max = Integer.MIN_VALUE;
        for (int i : nums) {
            if (i > max)
                max = i;
        }

        return max;
    }

桶排序

桶排序算是计数排序的一种改进和推广。
基本思想:使用映射函数将待排序的数组划分成M个的子区间(桶) 。接着对每个桶中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出每个桶中的全部内容即是一个有序序列。
桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1<k2,那么f(k1)<=f(k2)也就是说第 i 个桶中的最小数据都要大于第 i - 1 个桶中最大数据。

代码如下:

    // 桶排序
    public void bucketSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
            max = Math.max(max, nums[i]);
            min = Math.min(min, nums[i]);
        }

        // 桶数
        int bucketNum = (max - min) / nums.length + 1;

        // 创建桶
        ArrayList<ArrayList<Integer>> buckets = new ArrayList<ArrayList<Integer>>(
                bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            buckets.add(new ArrayList<Integer>());
        }

        // 将每个元素放入桶
        for (int i = 0; i < nums.length; i++) {
            int idx = (nums[i] - min) / (nums.length);
            buckets.get(idx).add(nums[i]);
        }

        // 对每个桶进行排序
        for (int i = 0; i < buckets.size(); i++) {
            Collections.sort(buckets.get(i));
        }

        // 还原排好序的数组
        int k = 0;
        for (List<Integer> bucket : buckets) {
            for (int i : bucket) {
                nums[k++] = i;
            }
        }
    }

基数排序

基数排序是一种和前面排序方式不同的排序方式,基数排序不需要进行关键字之间的比较。
基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。
如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级依次增加。

代码如下:

    // 基数排序
    public void radixSort(int[] nums) {
        if (nums == null || nums.length == 0)
            return;

        // 获取最大位数
        int maxBit = getMaxBit(nums);

        /*
         * 先根据个位数排序,再根据十位数排序...
         */
        for (int i = 1; i <= maxBit; i++) {
            // 分配
            List<List<Integer>> buf = distribute(nums, i);

            // 收集
            collect(nums, buf);
        }

    }

    // 分配
    public List<List<Integer>> distribute(int[] nums, int iBit) {
        List<List<Integer>> buf = new ArrayList<List<Integer>>();
        for (int j = 0; j < 10; j++) {
            buf.add(new LinkedList<Integer>());
        }

        for (int i = 0; i < nums.length; i++) {
            buf.get(getNBit(nums[i], iBit)).add(nums[i]);
        }

        return buf;
    }

    // 收集
    public void collect(int[] arr, List<List<Integer>> buf) {
        int k = 0;
        for (List<Integer> bucket : buf) {
            for (int i : bucket) {
                arr[k++] = i;
            }
        }
    }

    // 获取最大位数
    public int getMaxBit(int[] nums) {
        int max = Integer.MIN_VALUE;
        for (int i : nums) {
            max = Math.max(max, (i + "").length());
        }

        return max;
    }

    // 获取数字x的第n位
    public static int getNBit(int x, int n) {
        String str = x + "";
        if (str.length() < n)
            return 0;
        else
            return str.charAt(str.length() - n) - '0';
    }

引用:
面试中的排序算法总结
各种排序算法总结和比较
Big-O Complexity Chart

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

推荐阅读更多精彩内容