|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1608. 特殊数组的特征值](https://leetcode.cn/problems/special-array-with-x-elements-greater-than-or-equal-x/solution/by-ac_oier-z525/)** ,难度为 **简单**。 |
| 4 | + |
| 5 | +Tag : 「排序」、「二分」、「枚举」、「模拟」、「计数」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个非负整数数组 `nums`。如果存在一个数 `x` ,使得 `nums` 中恰好有 `x` 个元素 大于或者等于 `x` ,那么就称 `nums` 是一个 特殊数组 ,而 `x` 是该数组的 特征值 。 |
| 10 | + |
| 11 | +注意: `x` 不必 是 `nums` 的中的元素。 |
| 12 | + |
| 13 | +如果数组 `nums` 是一个 特殊数组 ,请返回它的特征值 `x` 。否则,返回 `-1` 。可以证明的是,如果 `nums` 是特殊数组,那么其特征值 `x` 是 唯一的 。 |
| 14 | + |
| 15 | +示例 1: |
| 16 | +``` |
| 17 | +输入:nums = [3,5] |
| 18 | +
|
| 19 | +输出:2 |
| 20 | +
|
| 21 | +解释:有 2 个元素(3 和 5)大于或等于 2 。 |
| 22 | +``` |
| 23 | +示例 2: |
| 24 | +``` |
| 25 | +输入:nums = [0,0] |
| 26 | +
|
| 27 | +输出:-1 |
| 28 | +
|
| 29 | +解释:没有满足题目要求的特殊数组,故而也不存在特征值 x 。 |
| 30 | +如果 x = 0,应该有 0 个元素 >= x,但实际有 2 个。 |
| 31 | +如果 x = 1,应该有 1 个元素 >= x,但实际有 0 个。 |
| 32 | +如果 x = 2,应该有 2 个元素 >= x,但实际有 0 个。 |
| 33 | +x 不能取更大的值,因为 nums 中只有两个元素。 |
| 34 | +``` |
| 35 | +示例 3: |
| 36 | +``` |
| 37 | +输入:nums = [0,4,3,0,4] |
| 38 | +
|
| 39 | +输出:3 |
| 40 | +
|
| 41 | +解释:有 3 个元素大于或等于 3 。 |
| 42 | +``` |
| 43 | +示例 4: |
| 44 | +``` |
| 45 | +输入:nums = [3,6,7,7,0] |
| 46 | +
|
| 47 | +输出:-1 |
| 48 | +``` |
| 49 | + |
| 50 | +提示: |
| 51 | +* $1 <= nums.length <= 100$ |
| 52 | +* $0 <= nums[i] <= 1000$ |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +### 排序 + 枚举 + 二分 |
| 57 | + |
| 58 | +根据题意并结合 $nums[i]$ 的数据范围为 $1e3$,我们可以通过「枚举」的方式找到 `x`,而对于每个 `x` 的合法性检查,我们需要快速知道 `nums` 中比 `x` 大的数的个数,这可以通过「排序 + 二分」来做。 |
| 59 | + |
| 60 | +Java 代码: |
| 61 | +```Java |
| 62 | +class Solution { |
| 63 | + public int specialArray(int[] nums) { |
| 64 | + Arrays.sort(nums); |
| 65 | + int n = nums.length; |
| 66 | + for (int x = 0; x < 1010; x++) { |
| 67 | + int l = 0, r = n - 1; |
| 68 | + while (l < r) { |
| 69 | + int mid = l + r >> 1; |
| 70 | + if (nums[mid] >= x) r = mid; |
| 71 | + else l = mid + 1; |
| 72 | + } |
| 73 | + if (nums[r] >= x && x == n - r) return x; |
| 74 | + } |
| 75 | + return -1; |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | +TypeScript 代码: |
| 80 | +```TypeScript |
| 81 | +function specialArray(nums: number[]): number { |
| 82 | + const n = nums.length |
| 83 | + nums.sort((a,b)=>a-b) |
| 84 | + for (let x = 0; x < 1010; x++) { |
| 85 | + let l = 0, r = n - 1 |
| 86 | + while (l < r) { |
| 87 | + const mid = l + r >> 1 |
| 88 | + if (nums[mid] >= x) r = mid |
| 89 | + else l = mid + 1 |
| 90 | + } |
| 91 | + if (nums[r] >= x && x == n - r) return x |
| 92 | + } |
| 93 | + return -1 |
| 94 | +}; |
| 95 | +``` |
| 96 | +* 时间复杂度:排序的复杂度为 $O(n\log{n})$;枚举找 `x` 的复杂度为 $O(C\log{n})$,其中 $C = 1e3$ 为 $nums[i]$ 的值域大小 |
| 97 | +* 空间复杂度:$O(\log{n})$ |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +### 排序 + 二分 |
| 102 | + |
| 103 | +若不利用 $nums[i]$ 的值域偏小(即值域放大到 $1e9$),我们可以通过「排序 + 两次二分」的操作来做:第一次二分找分割点 `x`(二分范围为 $[0, 1e9]$);第二次则是在 `getCnt` 函数内部中使用二分来对 `x` 进行合法性检查。 |
| 104 | + |
| 105 | +我们知道若真实的特殊值为 `x`,那么在以 `x` 为分割点的数轴上具有「二段性」,假设当前值为 `k`: |
| 106 | +* 小于真实特殊值 `x` 的 `k` 值满足「`nums` 中大于等于 `k` 的元素个数超过 `k` 个」 |
| 107 | +* 大于等于真实特殊值 `x` 的 `k` 值满足「`nums` 中大于等于 `k` 的元素个数不超过 `k` 个」 |
| 108 | + |
| 109 | +因此可以通过「二分」来找真实特殊值为 `x`,至于 `x` 的合法检查则和解法一相同,先通过排序确保数组有序,再通过二分的方式来统计大于等于 `x` 的数的个数。 |
| 110 | + |
| 111 | +Java 代码: |
| 112 | +```Java |
| 113 | +class Solution { |
| 114 | + int[] nums; |
| 115 | + public int specialArray(int[] _nums) { |
| 116 | + nums = _nums; |
| 117 | + Arrays.sort(nums); |
| 118 | + int l = 0, r = (int) 1e9; |
| 119 | + while (l < r) { |
| 120 | + int mid = l + r >> 1; |
| 121 | + if (getCnt(mid) <= mid) r = mid; |
| 122 | + else l = mid + 1; |
| 123 | + } |
| 124 | + return getCnt(r) == r ? r : -1; |
| 125 | + } |
| 126 | + int getCnt(int x) { |
| 127 | + int n = nums.length, l = 0, r = n - 1; |
| 128 | + while (l < r) { |
| 129 | + int mid = l + r >> 1; |
| 130 | + if (nums[mid] >= x) r = mid; |
| 131 | + else l = mid + 1; |
| 132 | + } |
| 133 | + return nums[r] >= x ? n - r : 0; |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | +TypeScript 代码: |
| 138 | +```TypeScript |
| 139 | +let nums: number[] |
| 140 | +function specialArray(_nums: number[]): number { |
| 141 | + nums = _nums |
| 142 | + nums.sort((a,b)=>a-b) |
| 143 | + let l = 0, r = 1e9 |
| 144 | + while (l < r) { |
| 145 | + const mid = l + r >> 1 |
| 146 | + if (getCnt(mid) <= mid) r = mid |
| 147 | + else l = mid + 1 |
| 148 | + } |
| 149 | + return getCnt(r) == r ? r : -1 |
| 150 | +}; |
| 151 | +function getCnt(x: number): number { |
| 152 | + let n = nums.length, l = 0, r = n - 1 |
| 153 | + while (l < r) { |
| 154 | + const mid = l + r >> 1 |
| 155 | + if (nums[mid] >= x) r = mid |
| 156 | + else l = mid + 1 |
| 157 | + } |
| 158 | + return nums[r] >= x ? n - r : 0 |
| 159 | +} |
| 160 | +``` |
| 161 | +* 时间复杂度:排序复杂度为 $O(n\log{n})$,二分找答案复杂度为 $O(\log{C} \times \log{n})$ |
| 162 | +* 空间复杂度:$O(\log{n})$ |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +### 模拟(计数 + 枚举) |
| 167 | + |
| 168 | +另外一种除非过大缩放 $nums$ 的长度 $n$,否则仅有代码量优势的做法是使用「计数 + 枚举」的模拟做法。 |
| 169 | + |
| 170 | +先使用静态数组对 $nums$ 进行词频统计,随后通过逆序枚举的方式找特殊值 `x`,同时使用 `tot` 统计遍历过的桶的总元素个数,当满足 `tot = x` 时,返回结果。 |
| 171 | + |
| 172 | +Java 代码: |
| 173 | +```Java |
| 174 | +class Solution { |
| 175 | + public int specialArray(int[] nums) { |
| 176 | + int[] cnts = new int[1010]; |
| 177 | + for (int x : nums) cnts[x]++; |
| 178 | + for (int i = 1009, tot = 0; i >= 0; i--) { |
| 179 | + tot += cnts[i]; |
| 180 | + if (i == tot) return i; |
| 181 | + } |
| 182 | + return -1; |
| 183 | + } |
| 184 | +} |
| 185 | +``` |
| 186 | +TypeScript 代码: |
| 187 | +```TypeScript |
| 188 | +function specialArray(nums: number[]): number { |
| 189 | + const cnts = new Array<number>(1010).fill(0) |
| 190 | + for (let x of nums) cnts[x]++ |
| 191 | + for (let i = 1009, tot = 0; i >= 0; i--) { |
| 192 | + tot += cnts[i] |
| 193 | + if (tot == i) return i |
| 194 | + } |
| 195 | + return -1 |
| 196 | +}; |
| 197 | +``` |
| 198 | +* 时间复杂度:$O(n + C)$,其中 $C = 1e3$ 为 $nums[i]$ 的值域大小 |
| 199 | +* 空间复杂度:$O(C)$ |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +### 最后 |
| 204 | + |
| 205 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1608` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 206 | + |
| 207 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 208 | + |
| 209 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 210 | + |
| 211 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 212 | + |
0 commit comments