LeetCode刷题-04-排序

时间:2020-11-30
本文章向大家介绍LeetCode刷题-04-排序,主要包括LeetCode刷题-04-排序使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

第四讲 排序

4.1 巨经典的排序算法

1. 冒泡排序(很简单)

  • 平均时间复杂度 O(n^2) ,空间复杂度 O(1),稳定

  • 基本思想

    • 两个数比较大小,较大的数下沉,较小的数冒起来。
  • 演示(图片来自菜鸟教程)

  • 代码

    /**
     * 冒泡排序
     * @param array 待排序的数组
     */
    public static void bubbleSort(int[] array) {
        for(int i=0; i<array.length-1; i++){
            for(int j=array.length-1; j>i; j--){
                if(array[j] < array[j-1]){
                    int temp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = temp;
                }
            }
        }
    }
    

2. 选择排序

  • 平均时间复杂度 O(n^2) ,空间复杂度 O(1),不稳定

  • 基本思想

    • 遍历数组,依次找到第 i 小的元素,然后把它和第i 个元素交换位置
  • 演示

  • 代码

    /**
     * 选择排序
     * @param array 待排序的数组
     */
    public static void selectSort(int[] array) {
        for(int i=0;i<array.length-1;i++){
            int minIndex = i;
            for(int j=i+1;j<array.length;j++){
                if(array[j]<array[minIndex]){
                    minIndex = j;
                }
            }
            if(minIndex != i){
                int temp = array[i];
                array[i] = array[minIndex];
                array[minIndex] = temp;
            }
        }
    }
    

3. 插入排序

  • 平均时间复杂度 O(n^2) ,空间复杂度 O(1),稳定

  • 基本思想

    • 在要排序的一组数中,假定前 i 个数已经排好序了,那么第 i+1 个数只需要插入到前面已经排好序的数组中即可
  • 演示(图片来自菜鸟教程)

  • 代码

    /**
     * 插入排序
     * @param array 待排序的数组
     */
    public void insertSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = i + 1; j > 0; j--) {
                if (array[j] < array[j-1]) {
                    int temp = array[j-1];
                    array[j-1] = array[j];
                    array[j] = temp;
                } else {
                    break;
                }
            }
        }
    }
    

4. 归并排序

  • 平均时间复杂度 O(n log(n)) ,空间复杂度 O(n),稳定

  • 基本思想(分治)

    • 首先考虑如何合并两个有序数组
      • 我们只需要依次比较两个数组中的第一个数即可,谁小放到新数组中
    • 归并排序的主要思想就是归并两个有序数组。
      • 将原数组分成二组 A,B。如果A,B都是有序的,归并即可。
      • 但是A,B都不是有序的,那么就继续分。直到这个数组中只有1个数据的时候。
      • 这个时候可以认为他是有序的,然后依次向上合并
    • 总结:递归分解数组,然后再从小到大合并
  • 演示(图片来自菜鸟教程)

  • 代码

    /**
     * 归并排序
     * @param array 需要排序的数组
     * @return 排好序的数组
     */
    public int[] mergeSort(int[] array) {
        // 创建额外的空间
        int[] res = Arrays.copyOf(array, array.length);
        if (res.length < 2) {
            return res;
        }
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(mergeSort(left), mergeSort(right));
    }
    
    /**
     * 归并两个有序数组
     * @param array1 有序数组1
     * @param array2 有序数组2
     * @return 归并后的新数组
     */
    private int[] merge(int[] array1, int[] array2) {
        int[] res = new int[array1.length + array2.length];
        int p = 0;
        int i = 0;
        int j = 0;
        while (i < array1.length && j < array2.length) {
            if (array1[i] <= array2[j]) {
                res[p++] = array1[i++];
            } else {
                res[p++] = array2[j++];
            }
        }
        // 剩下了left
        while (i < array1.length) {
            res[p++] = array1[i++];
        }
        // 剩下的是right
        while (j < array2.length) {
            res[p++] = array2[j++];
        }
        return res;
    }
    

5. 快速排序(很常用)

  • 平均时间复杂度 O(n log(n)) ,空间复杂度 O(log n),不稳定

  • 算法思想(分治)

    • 选取一个key,对于比key大的,全部放到右边,对于比key小的,全部放到key的左边。这样key的位置就可以唯一确定了。然后对key两边的区间重复操作就可以确定所有元素的位置了
  • 演示(图片来自菜鸟教程)

  • 具体操作

    1. i=0,j=n-1,key=array[i]
    2. j 向前移动,找到第一个比 key 小的元素,把这个元素放到 i
    3. i 向后移动,找到第一个比 key 大的元素,将它放到 j 的位置
    4. i 的地方放上 key
    5. 经过这么一步操作,就可以使得 [0,i-1] 中所有的元素都是比 i 小的,[j,n-1] 中的所有元素都是比 i 大 的,然后重复操作,直到 i==j 这样就满足了 array[0,i-1] < array[i] < array[i+1,n-1] 。然后 i左右两边的区间重复操作。
  • 代码

    /**
     * @param array 需要排序区间所在的数组
     * @param left 区间的起始下标
     * @param right 区间的结束下标
     */
    public void quickSort(int[] array, int left, int right) {
        if (left < right) {
            int i = left;
            int j = right;
            int key = array[left];
            while (i < j) {
                // 从j开始向左寻找到第一个比 key 小的数
                while (i < j && array[j] >= key) {
                    j--;
                }
                if (i < j ) {
                    array[i] = array[j];
                    i++;
                }
                // 从i开始向右寻找第一个大于等于 key 的数
                while (i < j && array[i] < key) {
                    i++;
                }
                if (i < j) {
                    array[j] = array[i];
                    j--;
                }
            }
            array[i] = key;
            quickSort(array, left, i-1);
            quickSort(array, i+1, right);
        }
    }
    

6. 堆排序

  • 平均时间复杂度 O(n+k) ,空间复杂度 O(k),稳定

  • 什么是堆?

    • 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。(注意是左右孩子而不是左右子树)
  • 基本思想

    • 将待排序的数组构造成一个大顶堆。此时,堆顶的元素就是最大的,将堆顶元素和最后一个元素交换位置,然后重新构造大顶堆。
    • 我们不需要想构造一颗二叉树一样去构造堆,我们只需要按照以下规则进行对应即可:
      • 即数组下标为 i 的元素,他的左右孩子的下标是 2i+12i+2
    • 如此一来,我们就不需要额外的空间
  • 演示

  • 代码

    /**
     * 堆排序
     * @param array 待排序的数组
     */
    public void heapSort(int[] array) {
        // len表示的是未进行排序的长度
        int len = array.length;
        for (int i = 0; i < array.length; i++) {
            // 从最后一个非叶子节点开始调整,使其满足大顶堆的性质
            int last = len / 2 - 1;
            for (int j = last; j >= 0; j--) {
                int left = 2 * j + 1;
                int right = left + 1;
                if (array[left] > array[j]) {
                    swap(array, left, j);
                }
                if (right < len && array[right] > array[j]) {
                    swap(array, right, j);
                }
            }
            len--;
            // 将堆顶元素和放到正确的地方
            swap(array, 0, array.length - 1 - i);
        }
    }
    
    /**
     * 交换数组中的两个元素
     * @param array 数组
     * @param i1 元素1
     * @param i2 元素2
     */
    private void swap(int[] array, int i1, int i2) {
        int temp =  array[i1];
        array[i1] = array[i2];
        array[i2] = temp;
    }
    

7. 计数排序

  • 平均时间复杂度 O(n+k) ,空间复杂度 O(k),稳定。其中,k是整数的范围

  • 基本思想

    • 遍历数组,得出最大值、最小值、差距三者中的两个。
    • 申请一个新数组,数组长度为差距。然后对原数组中的数进行遍历,记录出现的次数
    • 最后遍历新数组,就可以得到排序后的结果
  • 演示

  • 代码

    /**
     * 计数排序
     * @param array 待排序的数组
     */
    public void countingSort(int[] array) {
        int min = array[0];
        int max = array[0];
        // 找最大值和最小值
        for (int i : array) {
            min = Math.min(i, min);
            max = Math.max(i, max);
        }
    
        // 申请额外的空间,大小为最值之间的范围
        int[] temp = new int[max - min + 1];
    
        // 填充新数组
        for (int i : array) {
            temp[i - min]++;
        }
    
        // 遍历新数组,然后填充原数组
        int index = 0;
        for (int i = 0; i < temp.length; i++) {
            if (temp[i] != 0) {
                Arrays.fill(array, index, index + temp[i], i + min);
                index += temp[i];
            }
        }
    }     
    
  • 注意:计数排序对于一定范围内的整数进行排序具有最高的效率,前提是整数的范围不要太大

8. 桶排序

  • 平均时间复杂度 O(n) ,空间复杂度 O(n+k),稳定。其中k是桶的数量

  • 基本思想

    • 首先划分若干个范围相同的 “桶” ,
    • 然后将待排序的数据扔到属于他自己的桶中
    • 对桶中的数据进行排序
    • 依次输出所有桶中的数据
  • 关键

    • 计数排序的优化,适合于对于大量有范围的数据
    • 桶的划分要尽量保证元素应当可以均匀分散到所有桶中,如果说所有元素都集中在一个桶中,那么桶排序就会失效
  • 演示

  • 代码

    • 桶排序的关键在于桶的划分和选取。需要根据实际使用场景按需进行设计,所以这里不做展示。

4.2 快速选择

215. 数组中的第K个最大元素(Medium)

  • 思路

    • 快速选择算法是快速排序的一个变形
    • 从上文对于快速排序的讲解中不难发现。对于一个 key 来说,在进行一次快速排序的过程中,是可以确定出这个 key 所在有序数组中的位置。
    • 那么我们只需要关注这个位置是不是我们需要的 K 即可。如果比 k 大,则在 key 的右边再做一次快速排序即可;反之,则在左边做一次快速排序
  • 时间复杂度 O(n),空间复杂度 O(1)

  • 代码

    public int findKthLargest(int[] nums, int k) {
        int left = 0;
        int right = nums.length - 1;
        int target = nums.length - k;
        while (left < right) {
            int p = quickSort(nums, left, right);
            if (p < target) {
                left = p + 1;
            } else if (p > target) {
                right = p - 1;
            } else {
                return nums[p];
            }
        }
        return nums[left];
    }
    
    // 快速排序函数,返回key的下标
    public int quickSort(int[] array, int left, int right) {
        int i = left;
        int j = right;
        int key = array[left];
        while (i < j) {
            while (i < j && array[j] >= key) {
                j--;
            }
            if (i < j) {
                array[i] = array[j];
                i++;
            }
            while (i < j && array[i] < key) {
                i++;
            }
            if (i < j) {
                array[j] = array[i];
                j--;
            }
        }
        array[i] = key;
        return i;
    }
    
  • 注意点

    • k=1 时,函数可能不会在循环体中返回结果。此时,退出循环后left=5 ,所以要返回 nums[left]

4.3 桶排序

347. 前 K 个高频元素(Medium)

  • 思路

    • 典型的桶排序的使用场景,来回顾一下桶排序
      • 桶排序就是把需要排序的元素放到属于他的桶中,然后再对桶中的元素进行排序,最后把所用桶中的元素输出就是排序之后的结果。重点是让所有元素均匀的分布到所有桶中
    • 如果我们的桶中只放这个元素自己,那么桶的大小就是这个元素的出现次数,然后根据桶的大小进行排序,就可以很方便的得到出现频率前K个高频元素
    • 重点:将元素和出现频率进行对应
    • 时间 O(n log(n)), 空间 O(n)
  • 代码

    public int[] topKFrequent(int[] nums, int k) {
        int[] res = new int[k];
        // 用哈希表做桶排序,每个元素作为key,出现次数作为value
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0)+1);
        }
        int[][] bucket = new int[map.size()][2];
        int p = 0;
        // 利用数组将元素和出现次数进行对应
        for (Integer i : map.keySet()) {
            bucket[p][0] = i;
            bucket[p++][1] = map.get(i);
        }
        // 降序排序
        Arrays.sort(bucket, ((o1, o2) -> o2[1]-o1[1]));
        for (int i = 0; i < k; i++) {
            res[i] = bucket[i][0];
        }
        return res;
    }
    
  • 优化

    • 在得到每个元素的频率之后,可以再对频率进行一次桶排序,这样的话可以把时间复杂度降低到 O(n)
  • 代码

    // 再对频率进行一次桶排序,这样就可以得到前K个高频的元素
    // 对于频率最高的元素,放在最前面
    List<Integer>[] bucket = new List[maxFrequency + 1];
    for (int i : map.keySet()) {
        int f = maxFrequency - map.get(i);
        if (bucket[f] == null) {
            bucket[f] = new ArrayList<>();
        }
        bucket[f].add(i);
    }
    List<Integer> res = new ArrayList<>();
    int i = 0;
    while (k > 0) {
        List<Integer> list = bucket[i++];
        if (list != null) {
            res.addAll(list);
            k -= list.size();
        }
    }
    

4.4 基础练习

451. 根据字符出现频率排序(Medium)

  • 思路

    • 桶排序,每个字母一个桶,最后降序输出
  • 代码

    public String frequencySort(String s) {
        Map<Character, Integer> map = new HashMap<>();
        // 对字母进行桶排序,得到每个字母出现的频率
        int max = 0;
        for (int i = 0; i < s.length(); i++) {
            map.put(s.charAt(i), map.getOrDefault(s.charAt(i), 0) + 1);
            max = Math.max(max, map.get(s.charAt(i)));
        }
        // 再对频率进行一次桶排序
        ArrayList<Character>[] bucket = new ArrayList[max + 1];
        for (char c : map.keySet()) {
            int f = map.get(c);
            if (bucket[f] == null) {
                bucket[f] = new ArrayList<>();
            }
            bucket[f].add(c);
        }
        int p = 0;
        char[] chars = s.toCharArray();
        for (int i = max; i >= 0; i--) {
            if (bucket[i] != null) {
                for (char c : bucket[i]) {
                    Arrays.fill(chars, p, p+i, c);
                    p += i;
                }
            }
        }
        return new String(chars);
    }
    

4.5 进阶练习

75. 颜色分类(Medium)

  • 思路

    • 遍历数组,对三种颜色出现的次数进行统计,然后填充数组
  • 代码

    public void sortColors(int[] nums) {
        int[] f = new int[3];
        for (int num : nums) {
            f[num]++;
        }
        int p = 0;
        for (int i = 0; i < 3; i++) {
            Arrays.fill(nums, p, p + f[i], i);
            p += f[i];
        }
    }
    

原文地址:https://www.cnblogs.com/primabrucexu/p/14061405.html