|
| 1 | +# 题目描述(困难难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +给一个乱序的数组,求出数组排序以后的相邻数字的差最大是多少。 |
| 6 | + |
| 7 | +# 解法一 |
| 8 | + |
| 9 | +先来个直接的,就按题目的意思,先排序,再搜索。 |
| 10 | + |
| 11 | +```java |
| 12 | +public int maximumGap(int[] nums) { |
| 13 | + Arrays.sort(nums); |
| 14 | + int max = 0; |
| 15 | + for (int i = 0; i < nums.length - 1; i++) { |
| 16 | + if (nums[i + 1] - nums[i] > max) { |
| 17 | + max = nums[i + 1] - nums[i]; |
| 18 | + } |
| 19 | + } |
| 20 | + return max; |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +但是正常的排序算法时间复杂度会是 `O(nlog(n))`,题目要求我们在 `O(n)` 的复杂度下去求解。 |
| 25 | + |
| 26 | +自己想了各种思路,但想不出怎么降到 `O(n)`,把 [官方](https://leetcode.com/problems/maximum-gap/solution/) 给的题解分享一下吧。 |
| 27 | + |
| 28 | +我们一般排序算法多用快速排序,平均时间复杂度是 `O(nlog(n))`,其实还有一种排序算法,时间复杂度是 `O(kn)`,`k` 是最大数字的位数,当 `k` 远小于 `n` 的时候,时间复杂度可以近似看成 `O(n)`。这种排序算法就是基数排序,下边讲一下具体思想。 |
| 29 | + |
| 30 | +比如这样一个数列排序: `342 58 576 356`, 我们来看一下怎么排序: |
| 31 | + |
| 32 | +```java |
| 33 | +不足的位数看做是 0 |
| 34 | +342 058 576 356 |
| 35 | +按照个位将数字依次放到不同的位置 |
| 36 | +0: |
| 37 | +1: |
| 38 | +2: 342 |
| 39 | +3: |
| 40 | +4: |
| 41 | +5: |
| 42 | +6: 576, 356 |
| 43 | +7: |
| 44 | +8: 058 |
| 45 | +9: |
| 46 | + |
| 47 | +把上边的数字依次拿出来,组成新的序列 342 576 356 058,然后按十位继续放到不同的位置。 |
| 48 | +0: |
| 49 | +1: |
| 50 | +2: |
| 51 | +3: |
| 52 | +4: 342 |
| 53 | +5: 356 058 |
| 54 | +6: |
| 55 | +7: 576 |
| 56 | +8: |
| 57 | +9: |
| 58 | + |
| 59 | +把上边的数字依次拿出来,组成新的序列 342 356 058 576,然后按百位继续装到不同的位置。 |
| 60 | +0: 058 |
| 61 | +1: |
| 62 | +2: |
| 63 | +3: 342 356 |
| 64 | +4: |
| 65 | +5: 576 |
| 66 | +6: |
| 67 | +7: |
| 68 | +8: |
| 69 | +9: |
| 70 | + |
| 71 | +把数字依次拿出来,最终结果就是 58 342 356 576 |
| 72 | +``` |
| 73 | + |
| 74 | +为了代码更好理解, 我们可以直接用 `10` 个 `list` 去存放每一组的数字,官方题解是直接用一维数组实现的。 |
| 75 | + |
| 76 | +对于取各个位的的数字,我们通过对数字除以 `1`, `10`, `100`... 然后再对 `10` 取余来实现。 |
| 77 | + |
| 78 | +```java |
| 79 | +public int maximumGap(int[] nums) { |
| 80 | + if (nums.length <= 1) { |
| 81 | + return 0; |
| 82 | + } |
| 83 | + List<ArrayList<Integer>> lists = new ArrayList<>(); |
| 84 | + for (int i = 0; i < 10; i++) { |
| 85 | + lists.add(new ArrayList<>()); |
| 86 | + } |
| 87 | + int n = nums.length; |
| 88 | + int max = nums[0]; |
| 89 | + //找出最大的数字 |
| 90 | + for (int i = 1; i < n; i++) { |
| 91 | + if (max < nums[i]) { |
| 92 | + max = nums[i]; |
| 93 | + } |
| 94 | + } |
| 95 | + int m = max; |
| 96 | + int exp = 1; |
| 97 | + //一位一位的进行 |
| 98 | + while (max > 0) { |
| 99 | + //将之前的元素清空 |
| 100 | + for (int i = 0; i < 10; i++) { |
| 101 | + lists.set(i, new ArrayList<>()); |
| 102 | + } |
| 103 | + //将数字放入对应的位置 |
| 104 | + for (int i = 0; i < n; i++) { |
| 105 | + lists.get(nums[i] / exp % 10).add(nums[i]); |
| 106 | + } |
| 107 | + |
| 108 | + //将数字依次拿出来 |
| 109 | + int index = 0; |
| 110 | + for (int i = 0; i < 10; i++) { |
| 111 | + for (int j = 0; j < lists.get(i).size(); j++) { |
| 112 | + nums[index] = lists.get(i).get(j); |
| 113 | + index++; |
| 114 | + } |
| 115 | + |
| 116 | + } |
| 117 | + max /= 10; |
| 118 | + exp *= 10; |
| 119 | + } |
| 120 | + |
| 121 | + int maxGap = 0; |
| 122 | + for (int i = 0; i < nums.length - 1; i++) { |
| 123 | + if (nums[i + 1] - nums[i] > maxGap) { |
| 124 | + maxGap = nums[i + 1] - nums[i]; |
| 125 | + } |
| 126 | + } |
| 127 | + return maxGap; |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +# 解法二 |
| 132 | + |
| 133 | +上边的解法还不是真正的 `O(n)`,下边继续介绍另一种解法,参考了 [这里](https://leetcode.com/problems/maximum-gap/discuss/50643/bucket-sort-JAVA-solution-with-explanation-O(N)-time-and-space) ,评论区对自己帮助很多。 |
| 134 | + |
| 135 | +我们知道如果是有序数组的话,我们就可以通过计算两两数字之间差即可解决问题。 |
| 136 | + |
| 137 | +那么如果是更宏观上的有序呢? |
| 138 | + |
| 139 | +```java |
| 140 | +我们把 0 3 4 6 23 28 29 33 38 依次装到三个箱子中 |
| 141 | + 0 1 2 3 |
| 142 | + ------- ------- ------- ------- |
| 143 | +| 3 4 | | | | 29 | | 33 | |
| 144 | +| 6 | | | | 23 | | | |
| 145 | +| 0 | | | | 28 | | 38 | |
| 146 | + ------- ------- ------- ------- |
| 147 | + 0 - 9 10 - 19 20 - 29 30 - 39 |
| 148 | +我们把每个箱子的最大值和最小值表示出来 |
| 149 | + min max min max min max min max |
| 150 | + 0 6 - - 23 29 33 38 |
| 151 | +``` |
| 152 | + |
| 153 | +我们可以只计算相邻箱子 `min` 和 `max` 的差值来解决问题吗?空箱子直接跳过。 |
| 154 | + |
| 155 | +第 `2` 个箱子的 `min` 减第 `0` 个箱子的 `max`, `23 - 6 = 17` |
| 156 | + |
| 157 | +第 `3` 个箱子的 `min` 减第 `2` 个箱子的 `max`, `33 - 29 = 4` |
| 158 | + |
| 159 | +看起来没什么问题,但这样做一定需要一个前提,因为我们只计算了相邻箱子的差值,没有计算箱子内数字的情况,所以我们需要保证每个箱子里边的数字一定不会产生最大 `gap`。 |
| 160 | + |
| 161 | +我们把箱子能放的的数字个数记为 `interval`,给定的数字中最小的是 `min`,最大的是 `max`。那么箱子划分的范围就是 `min ~ (min + 1 * interval - 1)`、`(min + 1 * interval) ~ (min + 2 * interval - 1)`、`(min + 2 * interval) ~ (min + 3 * interval - 1)`...,上边举的例子中, `interval` 我们其实取了 `10`。 |
| 162 | + |
| 163 | +划定了箱子范围后,我们其实很容易把数字放到箱子中,通过 `(nums[i] - min) / interval` 即可得到当前数字应该放到的箱子编号。那么最主要的问题其实就是怎么去确定 `interval`。 |
| 164 | + |
| 165 | +`interval` 过小的话,需要更多的箱子去存储,很费空间,此外箱子增多了,比较的次数也会增多,不划算。 |
| 166 | + |
| 167 | +`interval` 过大的话,箱子内部的数字可能产生题目要求的最大 `gap`,所以肯定不行。 |
| 168 | + |
| 169 | +所以我们要找到那个保证箱子内部的数字不会产生最大 `gap`,并且尽量大的 `interval`。 |
| 170 | + |
| 171 | +继续看上边的例子,`0 3 4 6 23 28 29 33 38`,数组中的最小值 `0` 和最大值 `38` ,并没有参与到 `interval` 的计算中,所以它俩可以不放到箱子中,还剩下 `n - 2` 个数字。 |
| 172 | + |
| 173 | +像上边的例子,如果我们保证至少有一个空箱子,那么我们就可以断言,箱子内部一定不会产生最大 `gap`。 |
| 174 | + |
| 175 | +因为在我们的某次计算中,会跳过一个空箱子,那么得到的 `gap` 一定会大于 `interval`,而箱子中的数字最大的 `gap` 是 `interval - 1`。 |
| 176 | + |
| 177 | +接下来的问题,怎么保证至少有一个空箱子呢? |
| 178 | + |
| 179 | +鸽巢原理的变形,有 `n - 2` 个数字,如果箱子数多于 `n - 2` ,那么一定会出现空箱子。总范围是 `max - min`,那么 `interval = (max - min) / 箱子数`,为了使得 `interval` 尽量大,箱子数取最小即可,也就是 `n - 1`。 |
| 180 | + |
| 181 | +所以 `interval = (max - min) / n - 1` 。这里如果除不尽的话,我们 `interval` 可以向上取整。因为我们给定的数字都是整数,这里向上取整的话对于最大 `gap` 是没有影响的。比如原来范围是 `[0,5.5)`,那么内部产生的最大 `gap` 是 `5 - 0 = 5`。现在向上取整,范围变成`[0,6)`,但是内部产生的最大 `gap` 依旧是 `5 - 0 = 5`。 |
| 182 | + |
| 183 | +所有问题都解决了,可以安心写代码了。 |
| 184 | + |
| 185 | +```java |
| 186 | +public int maximumGap(int[] nums) { |
| 187 | + if (nums.length <= 1) { |
| 188 | + return 0; |
| 189 | + } |
| 190 | + int n = nums.length; |
| 191 | + int min = nums[0]; |
| 192 | + int max = nums[0]; |
| 193 | + //找出最大值、最小值 |
| 194 | + for (int i = 1; i < n; i++) { |
| 195 | + min = Math.min(nums[i], min); |
| 196 | + max = Math.max(nums[i], max); |
| 197 | + } |
| 198 | + if(max - min == 0) { |
| 199 | + return 0; |
| 200 | + } |
| 201 | + |
| 202 | + //算出每个箱子的范围 |
| 203 | + int interval = (int) Math.ceil((double)(max - min) / (n - 1)); |
| 204 | + |
| 205 | + //每个箱子里数字的最小值和最大值 |
| 206 | + int[] bucketMin = new int[n - 1]; |
| 207 | + int[] bucketMax = new int[n - 1]; |
| 208 | + |
| 209 | + //最小值初始为 Integer.MAX_VALUE |
| 210 | + Arrays.fill(bucketMin, Integer.MAX_VALUE); |
| 211 | + //最小值初始化为 -1,因为题目告诉我们所有数字是非负数 |
| 212 | + Arrays.fill(bucketMax, -1); |
| 213 | + |
| 214 | + //考虑每个数字 |
| 215 | + for (int i = 0; i < nums.length; i++) { |
| 216 | + //当前数字所在箱子编号 |
| 217 | + int index = (nums[i] - min) / interval; |
| 218 | + //最大数和最小数不需要考虑 |
| 219 | + if(nums[i] == min || nums[i] == max) { |
| 220 | + continue; |
| 221 | + } |
| 222 | + //更新当前数字所在箱子的最小值和最大值 |
| 223 | + bucketMin[index] = Math.min(nums[i], bucketMin[index]); |
| 224 | + bucketMax[index] = Math.max(nums[i], bucketMax[index]); |
| 225 | + } |
| 226 | + |
| 227 | + int maxGap = 0; |
| 228 | + //min 看做第 -1 个箱子的最大值 |
| 229 | + int previousMax = min; |
| 230 | + //从第 0 个箱子开始计算 |
| 231 | + for (int i = 0; i < n - 1; i++) { |
| 232 | + //最大值是 -1 说明箱子中没有数字,直接跳过 |
| 233 | + if (bucketMax[i] == -1) { |
| 234 | + continue; |
| 235 | + } |
| 236 | + |
| 237 | + //当前箱子的最小值减去前一个箱子的最大值 |
| 238 | + maxGap = Math.max(bucketMin[i] - previousMax, maxGap); |
| 239 | + previousMax = bucketMax[i]; |
| 240 | + } |
| 241 | + //最大值可能处于边界,不在箱子中,需要单独考虑 |
| 242 | + maxGap = Math.max(max - previousMax, maxGap); |
| 243 | + return maxGap; |
| 244 | + |
| 245 | +} |
| 246 | +``` |
| 247 | + |
| 248 | +# 总 |
| 249 | + |
| 250 | +这道题主要是对排序算法的了解,第一次见到了基数排序的应用,解法三其实是桶排序的步骤。 |
0 commit comments