• 《算法系列》之排序


    简介

      排序算法是每个程序员的算法基本功之一,只有基础够扎实,才能在算法领域有更高的成就。我们学习排序算法时更需要知其然,知其所以然,要不然在做相应题时,拿着死公式是做不好题的。要学习灵活运用,学其排序算法精髓与灵魂,比如归并排序里重要的不是代码模板,而是他的分治思想, 把大的任务分解成更小的任务。往小了讲,平时我们解题时,别人很少拿归并原模板题考你,会变化一下,然后用分治思想解题。往大了说,分治思想在编程开发中也很重要,比如我们的大数据开发也常用到,MapReuce不就是分治思想吗,并发编程不也可以用分治思想吗?所以这些经典排序的内核思想是很值得我们细细品味的

    理论基础

       所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。例如快速排序、归并排序、冒泡排序都在计算机的世界里有着广泛的应用。但需要注意的是,力扣上的很多问题都不会直接考察排序算法,而是考察排序算法体现的思想和细节,例如快速排序的partition操作、计算逆序数、找第k大元素等。
      在算法中基础排序共有十种:冒泡排序选择排序插入排序希尔排序归并排序快速排序基数排序堆排序计数排序桶排序,接下来会给大家介绍一下。

    排序基本概念

    排序基本概念包含以下三点:

    • 时间复杂度:即从序列的初始状态到经过排序算法的变换移位等操作,变到最终排序好的结果状态的过程所花费的时间度量。
    • 空间复杂度:就是从序列的初始状态经过排序移位变换的过程一直到最终的状态所花费的空间开销。
    • 稳定性:算法在排序的过程中不会改变元素彼此的位置的相对次序,反之不稳定的排序算法经常会改变这个次序,这是我们不愿意看到的,有时候甚至比时间复杂度、空间复杂度更重要一些。

    排序算法有很多,不同种类的排序算法适合不同种类的情景,可能有时候需要节省空间对时间要求没那么多,反之,有时候则是希望多考虑一些时间,对空间要求没那么高,总之一般都必须从某一方面做出抉择,不可兼得。

    十种排序算法

      接下来分别介绍十种算法,首先统一看下十种排序算法的情况,如下表。
    在这里插入图片描述

    冒泡排序

    冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
    代码实现

    public class BubbleSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            for (int i = 1; i < arr.length; i++) {
                // 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
                boolean flag = true;
    
                for (int j = 0; j < arr.length - i; j++) {
                    if (arr[j] > arr[j + 1]) {
                        int tmp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = tmp;
    
                        flag = false;
                    }
                }
    
                if (flag) {
                    break;
                }
            }
            return arr;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    选择排序

    选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
    代码实现

    public class SelectionSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            // 总共要经过 N-1 轮比较
            for (int i = 0; i < arr.length - 1; i++) {
                int min = i;
    
                // 每轮需要比较的次数 N-i
                for (int j = i + 1; j < arr.length; j++) {
                    if (arr[j] < arr[min]) {
                        // 记录目前能找到的最小值元素的下标
                        min = j;
                    }
                }
    
                // 将找到的最小值和i位置所在的值进行交换
                if (i != min) {
                    int tmp = arr[i];
                    arr[i] = arr[min];
                    arr[min] = tmp;
                }
    
            }
            return arr;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    插入排序

    插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
    代码实现

    public class InsertSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            // 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
            for (int i = 1; i < arr.length; i++) {
    
                // 记录要插入的数据
                int tmp = arr[i];
    
                // 从已经排序的序列最右边的开始比较,找到比其小的数
                int j = i;
                while (j > 0 && tmp < arr[j - 1]) {
                    arr[j] = arr[j - 1];
                    j--;
                }
    
                // 存在比其小的数,插入
                if (j != i) {
                    arr[j] = tmp;
                }
    
            }
            return arr;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    希尔排序

    希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
    代码实现

    public static void shellSort(int[] arr) {
    
        int length = arr.length;
        int temp;
        for (int step = length / 2; step >= 1; step /= 2) {
            for (int i = step; i < length; i++) {
                temp = arr[i];
                int j = i - step;
                while (j >= 0 && arr[j] > temp) {
                    arr[j + step] = arr[j];
                    j -= step;
                }
                arr[j + step] = temp;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    归并排序

    归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

    • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法)。
    • 自下而上的迭代。

    代码实现

    public class MergeSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            if (arr.length < 2) {
                return arr;
            }
            int middle = (int) Math.floor(arr.length / 2);
    
            int[] left = Arrays.copyOfRange(arr, 0, middle);
            int[] right = Arrays.copyOfRange(arr, middle, arr.length);
    
            return merge(sort(left), sort(right));
        }
    
        protected int[] merge(int[] left, int[] right) {
            int[] result = new int[left.length + right.length];
            int i = 0;
            while (left.length > 0 && right.length > 0) {
                if (left[0] <= right[0]) {
                    result[i++] = left[0];
                    left = Arrays.copyOfRange(left, 1, left.length);
                } else {
                    result[i++] = right[0];
                    right = Arrays.copyOfRange(right, 1, right.length);
                }
            }
    
            while (left.length > 0) {
                result[i++] = left[0];
                left = Arrays.copyOfRange(left, 1, left.length);
            }
    
            while (right.length > 0) {
                result[i++] = right[0];
                right = Arrays.copyOfRange(right, 1, right.length);
            }
    
            return result;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    快速排序

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大量数据最快的排序算法之一了。
    代码实现

    public class QuickSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            return quickSort(arr, 0, arr.length - 1);
        }
    
        private int[] quickSort(int[] arr, int left, int right) {
            if (left < right) {
                int partitionIndex = partition(arr, left, right);
                quickSort(arr, left, partitionIndex - 1);
                quickSort(arr, partitionIndex + 1, right);
            }
            return arr;
        }
    
        private int partition(int[] arr, int left, int right) {
            // 设定基准值(pivot)
            int pivot = left;
            int index = pivot + 1;
            for (int i = index; i <= right; i++) {
                if (arr[i] < arr[pivot]) {
                    swap(arr, i, index);
                    index++;
                }
            }
            swap(arr, pivot, index - 1);
            return index - 1;
        }
    
        private void swap(int[] arr, int i, int j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    堆排序

    堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

    • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列。
    • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列。
      堆排序的平均时间复杂度为 Ο(nlogn)。

    代码实现

    public class HeapSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            int len = arr.length;
    
            buildMaxHeap(arr, len);
    
            for (int i = len - 1; i > 0; i--) {
                swap(arr, 0, i);
                len--;
                heapify(arr, 0, len);
            }
            return arr;
        }
    
        private void buildMaxHeap(int[] arr, int len) {
            for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
                heapify(arr, i, len);
            }
        }
    
        private void heapify(int[] arr, int i, int len) {
            int left = 2 * i + 1;
            int right = 2 * i + 2;
            int largest = i;
    
            if (left < len && arr[left] > arr[largest]) {
                largest = left;
            }
    
            if (right < len && arr[right] > arr[largest]) {
                largest = right;
            }
    
            if (largest != i) {
                swap(arr, i, largest);
                heapify(arr, largest, len);
            }
        }
    
        private void swap(int[] arr, int i, int j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    计数排序

    计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
    代码实现

    public class CountingSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            int maxValue = getMaxValue(arr);
    
            return countingSort(arr, maxValue);
        }
    
        private int[] countingSort(int[] arr, int maxValue) {
            int bucketLen = maxValue + 1;
            int[] bucket = new int[bucketLen];
    
            for (int value : arr) {
                bucket[value]++;
            }
    
            int sortedIndex = 0;
            for (int j = 0; j < bucketLen; j++) {
                while (bucket[j] > 0) {
                    arr[sortedIndex++] = j;
                    bucket[j]--;
                }
            }
            return arr;
        }
    
        private int getMaxValue(int[] arr) {
            int maxValue = arr[0];
            for (int value : arr) {
                if (maxValue < value) {
                    maxValue = value;
                }
            }
            return maxValue;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    桶排序

    桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

    • 在额外空间充足的情况下,尽量增大桶的数量。
    • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。
      同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

    代码实现

    public class BucketSort {
    
        private static final InsertSort insertSort = new InsertSort();
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            return bucketSort(arr, 5);
        }
    
        private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
            if (arr.length == 0) {
                return arr;
            }
    
            int minValue = arr[0];
            int maxValue = arr[0];
            for (int value : arr) {
                if (value < minValue) {
                    minValue = value;
                } else if (value > maxValue) {
                    maxValue = value;
                }
            }
    
            int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
            int[][] buckets = new int[bucketCount][0];
    
            // 利用映射函数将数据分配到各个桶中
            for (int i = 0; i < arr.length; i++) {
                int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
                buckets[index] = arrAppend(buckets[index], arr[i]);
            }
    
            int arrIndex = 0;
            for (int[] bucket : buckets) {
                if (bucket.length <= 0) {
                    continue;
                }
                // 对每个桶进行排序,这里使用了插入排序
                bucket = insertSort.sort(bucket);
                for (int value : bucket) {
                    arr[arrIndex++] = value;
                }
            }
    
            return arr;
        }
    
        /**
         * 自动扩容,并保存数据
         *
         * @param arr
         * @param value
         */
        private int[] arrAppend(int[] arr, int value) {
            arr = Arrays.copyOf(arr, arr.length + 1);
            arr[arr.length - 1] = value;
            return arr;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    基数排序

    基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
    代码实现

    public class RadixSort {
    
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            int maxDigit = getMaxDigit(arr);
            return radixSort(arr, maxDigit);
        }
    
        /**
         * 获取最高位数
         */
        private int getMaxDigit(int[] arr) {
            int maxValue = getMaxValue(arr);
            return getNumLenght(maxValue);
        }
    
        private int getMaxValue(int[] arr) {
            int maxValue = arr[0];
            for (int value : arr) {
                if (maxValue < value) {
                    maxValue = value;
                }
            }
            return maxValue;
        }
    
        protected int getNumLenght(long num) {
            if (num == 0) {
                return 1;
            }
            int lenght = 0;
            for (long temp = num; temp != 0; temp /= 10) {
                lenght++;
            }
            return lenght;
        }
    
        private int[] radixSort(int[] arr, int maxDigit) {
            int mod = 10;
            int dev = 1;
    
            for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
                // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
                int[][] counter = new int[mod * 2][0];
    
                for (int j = 0; j < arr.length; j++) {
                    int bucket = ((arr[j] % mod) / dev) + mod;
                    counter[bucket] = arrayAppend(counter[bucket], arr[j]);
                }
    
                int pos = 0;
                for (int[] bucket : counter) {
                    for (int value : bucket) {
                        arr[pos++] = value;
                    }
                }
            }
    
            return arr;
        }
    
        /**
         * 自动扩容,并保存数据
         *
         * @param arr
         * @param value
         */
        private int[] arrayAppend(int[] arr, int value) {
            arr = Arrays.copyOf(arr, arr.length + 1);
            arr[arr.length - 1] = value;
            return arr;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    解题心得

    • 排序算法很基础也很重要,学好它们是学好算法的基础之一,要知其然,知其所以然。
    • 掌握一定的模板很重要,但掌握算法思想同样重要。
    • 解题中,题目明确考察排序时,不能直接使用API,要不然面试凉凉。
    • 排序题可能有多种选择,我们可以根据需求选择,没说就是时间要求最重要。
    • 有时并不需要写出整个排序算法,就能满足题解要求,要会灵活变通。
    • 很多时候题目的特性与我们排序算法的基础特性一对比,我们就知道用什么算法了。

    算法题目

    21. 合并两个有序链表

    在这里插入图片描述
    题目解析:类似归并排序的合并操作。
    代码如下:

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     * int val;
     * ListNode next;
     * ListNode() {}
     * ListNode(int val) { this.val = val; }
     * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
     * }
     */
    
    /**
     * 归并排序
     */
    class Solution {
        public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            ListNode tmp = new ListNode(0);
            ListNode res = tmp;
            while (l1 != null && l2 != null) {
                // 无论大于,还是等于,都选择l2的值
                if (l1.val < l2.val) {
                    tmp.next = l1;
                    l1 = l1.next;
                    tmp = tmp.next;
                } else {
                    tmp.next = l2;
                    l2 = l2.next;
                    tmp = tmp.next;
                }
            }
            if (l1 != null) {
                tmp.next = l1;
            }
            if (l2 != null) {
                tmp.next = l2;
            }
            return res.next;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    23. 合并K个升序链表

    在这里插入图片描述
    题目解析:类似归并排序合并,再采用递归,分治。
    代码如下:

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     * int val;
     * ListNode next;
     * ListNode() {}
     * ListNode(int val) { this.val = val; }
     * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
     * }
     */
     
    /**
     * 归并排序
     */
    class Solution {
        public ListNode mergeKLists(ListNode[] lists) {
            return merge(lists, 0, lists.length - 1);
        }
    
        // 此方法最为轻便快捷
        public ListNode merge(ListNode[] lists, int l, int r) {
            // 只有一个列表时返回,开始递归合并
            if (l == r) {
                return lists[l];
            }
            if (l > r) {
                return null;
            }
            int mid = l + (r - l) / 2;
            return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
        }
    
        private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            ListNode tmp = new ListNode(0);
            ListNode res = tmp;
            while (l1 != null && l2 != null) {
                // 无论大于,还是等于,都选择l2的值
                if (l1.val < l2.val) {
                    tmp.next = l1;
                    l1 = l1.next;
                    tmp = tmp.next;
                } else {
                    tmp.next = l2;
                    l2 = l2.next;
                    tmp = tmp.next;
                }
            }
            if (l1 != null) {
                tmp.next = l1;
            }
            if (l2 != null) {
                tmp.next = l2;
            }
            return res.next;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    56. 合并区间

    在这里插入图片描述

    题目解析:如果我们按照区间的左端点排序,那么在排完序的列表中,可以合并的区间一定是连续的。

    代码如下:

    /**
     * 排序
     */
    class Solution {
        public int[][] merge(int[][] intervals) {
            List res = new LinkedList<>();
    
            int[] res0 = new int[intervals.length];
            int[] res1 = new int[intervals.length];
    
            // 将区间值左右边界分开
            for (int i = 0; i < intervals.length; i++) {
                res0[i] = intervals[i][0];
                res1[i] = intervals[i][1];
            }
    
            // 排序
            Arrays.sort(res0);
            Arrays.sort(res1);
    
            for (int i = 0, j = 0; i < intervals.length; i++) {
                if (i == (intervals.length - 1) || res0[i + 1] > res1[i]) {
                    res.add(new int[]{res0[j], res1[i]});
                    j = i + 1;
                }
            }
            return res.toArray(new int[res.size()][]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    147. 对链表进行插入排序

    在这里插入图片描述
    在这里插入图片描述
    题目解析:用迭代法实现插入排序即可。
    代码如下:

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     * int val;
     * ListNode next;
     * ListNode() {}
     * ListNode(int val) { this.val = val; }
     * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
     * }
     */
    
    /**
     * 插入排序
     */
    class Solution {
        public ListNode insertionSortList(ListNode head) {
            // 快速排序可能会插入头部,用dummy会让插入头部和其他位置操作一致
            ListNode dummyHead = new ListNode(-1);
            dummyHead.next = head;
            // 需要维护一个至今位置有序的节点。叫做tail
            // 因为需要把当前元素cur插到前面有序链表中,要将最后一个元素与cur后面的元素建立连接
            ListNode tail = head;
            ListNode cur = head.next;
            while (cur != null) {
                if (cur.val >= tail.val) {
                    tail = cur;
                } else {
                    // 一个前驱节点pre来找到需要插入的位置,
                    // 找到第一个满足 pre.next >= cur.val 的前驱节点
                    ListNode pre = dummyHead;
                    while (pre.next.val < cur.val) {
                        pre = pre.next;
                    }
                    tail.next = cur.next;
                    cur.next = pre.next;
                    pre.next = cur;
                }
                cur = tail.next;
            }
            return dummyHead.next;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    148. 排序链表

    在这里插入图片描述
    题目解析:由于题目要求空间复杂度是 O(1),因此不能使用递归,因此这里使用归并排序算法来解决。
    代码如下:

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode() {}
     *     ListNode(int val) { this.val = val; }
     *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
     * }
     */
     
    /**
     * 归并排序
     */
    class Solution {
        public ListNode sortList(ListNode head) {
            return mergeSort(head);
        }
    
        // 归并排序
        private ListNode mergeSort(ListNode head){
            // 如果没有结点,只有一个结点,无需排序,直接返回
            if (head == null || head.next == null) return head;
            // 快慢指针找出中位点
            ListNode slowp = head, fastp = head.next.next, l, r;
            while (fastp != null && fastp.next != null){
                slowp = slowp.next;
                fastp = fastp.next.next;
            }
            // 对右半部分进行归并排序
            r = mergeSort(slowp.next);
            // 链表判断结束的标志:末尾节点.next==null
            slowp.next = null;
            // 对左半部分进行归并排序
            l = mergeSort(head);
            return mergeList(l, r);
        }
        // 合并链表
        private ListNode mergeList(ListNode l, ListNode r){
            // 临时头节点
            ListNode tmpHead = new ListNode(-1);
            ListNode p = tmpHead;
            while (l != null && r != null){
                if (l.val < r.val){
                    p.next = l;
                    l = l.next;
                }else {
                    p.next = r;
                    r = r.next;
                }
                p = p.next;
            }
            p.next =l == null ? r : l;
            return tmpHead.next;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    164. 最大间距

    在这里插入图片描述
    题目解析:一种最简单的思路是将数组排序后再找出最大间距,但传统的基于比较的排序算法(快速排序、归并排序等)均需要 O(NlogN) 的时间复杂度。如果要将时间复杂度降到 O(N),我们就必须使用其他的排序算法。例如,基数排序可以在 O(N) 的时间内完成整数之间的排序。
    代码如下:

    /**
     * 基数排序
     */
    class Solution {
        public int maximumGap(int[] nums) {
            int n = nums.length;
            if (n < 2) {
                return 0;
            }
            long exp = 1;
            int[] buf = new int[n];
            int maxVal = Arrays.stream(nums).max().getAsInt();
    
            while (maxVal >= exp) {
                int[] cnt = new int[10];
                for (int i = 0; i < n; i++) {
                    int digit = (nums[i] / (int) exp) % 10;
                    cnt[digit]++;
                }
                for (int i = 1; i < 10; i++) {
                    cnt[i] += cnt[i - 1];
                }
                for (int i = n - 1; i >= 0; i--) {
                    int digit = (nums[i] / (int) exp) % 10;
                    buf[cnt[digit] - 1] = nums[i];
                    cnt[digit]--;
                }
                System.arraycopy(buf, 0, nums, 0, n);
                exp *= 10;
            }
    
            int ret = 0;
            for (int i = 1; i < n; i++) {
                ret = Math.max(ret, nums[i] - nums[i - 1]);
            }
            return ret;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    215. 数组中的第K个最大元素

    在这里插入图片描述
    题目解析:本题主要考查排序,排序完成后,取出第len - k + 1个值即可。
    代码如下:

    /**
     * 快速排序
     */
    class Solution {
        public int findKthLargest(int[] nums, int k) {
            int len = nums.length;
            int t = len - k + 1;
            return quick_sort(nums, 0, len - 1, t);
        }
    
        private int quick_sort(int[] q, int left, int right, int k) {
            if (left >= right) return q[left];
            int x = q[left + right >> 1];
            int i = left - 1;
            int j = right + 1;
    
            while (i < j) {
                do i++; while (q[i] < x);
                do j--; while (q[j] > x);
                if (i < j) {
                    int smp = q[i];
                    q[i] = q[j];
                    q[j] = smp;
                }
            }
            int sl = j - left + 1;
            if (sl >= k) {
                return quick_sort(q, left, j, k);
            } else {
                return quick_sort(q, j + 1, right, k - sl);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    217. 存在重复元素

    在这里插入图片描述
    题目解析:先排序,再判断相邻两个元素,是否相等。
    代码如下:

    /**
      * 排序
      */
    class Solution {
        public boolean containsDuplicate(int[] nums) {
            Arrays.sort(nums);
            for(int i=0;i
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    274. H 指数

    在这里插入图片描述
    题目解析:首先我们可以将初始的H指数h设为0,然后将引用次数排序,并且对排序后的数组从大到小遍历。
    代码如下:

    /**
     * 排序 
     */
    class Solution {
        public int hIndex(int[] citations) {
            Arrays.sort(citations);
            for (int i = 0; i < citations.length; i++) {
                int h = citations.length - i;
                if (h <= citations[i]) {
                    return h;
                }
            }
            return 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    275. H 指数 II

    在这里插入图片描述
    题目解析:用二分法,降低算法复杂度。
    代码如下:

    /**
     * 二分法
     */
    class Solution {
        public int hIndex(int[] citations) {
            int n = citations.length;
            int left = 0, right = n - 1;
            while (left <= right) {
                int mid = left + (right - left) / 2;
                if (citations[mid] >= n - mid) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            }
            return n - left;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    回到首页

    刷 leetcode 500+ 题的一些感受

    下一篇

    《算法系列》之回溯

  • 相关阅读:
    OpenCV笔记整理【绘制图形文字】
    作为新人,如何快速融入新团队?用好这8个点
    【产品设计】素描最重要的目的是为了好看吗?
    信创环境下使用80端口Nginx无法发送PUT和DELETE请求
    欧拉计划详解第323题:随机整数按位或运算
    神经网络可以解决的问题,神经网络修复老照片
    在springboot中整合mybatis配置流程!
    wPDF v5支持并改进旋转文本的逻辑
    大数据Apache Druid(二):Druid数据结构及架构原理
    Egg 1. 快速开始 Quick Start 1.3 一步步 Step by Step 1.3.5 创建服务
  • 原文地址:https://blog.csdn.net/qq_22136439/article/details/126563714