这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战
题目
315. 计算右侧小于当前元素的个数
给你一个整数数组 nums
**,按要求返回一个新数组 counts
**。数组 counts
有该性质: counts[i]
的值是 nums[i]
右侧小于 nums[i]
的元素的数量。
示例 1:
输入: nums = [5,2,6,1]
输出: [2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
示例 2:
输入: nums = [-1]
输出: [0]
示例 3:
输入: nums = [-1,-1]
输出: [0,0]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
暴力解法
思路
- 我们要求右侧比较小的元素,那么我们可以从右侧开始遍历数组,去给右侧已经遍历的部分做排序;
- 同时我们可以用一个
result
数组来记录每个位置比当前元素小的右侧元素的数量; - 然后用
sortArr
数组来记录已经排序好的右侧数组,从小到大排序; - 每次遍历到当前元素的时候,找到当前元素在已排序数组中的插入位置,当前的插入位置就是数组中比它小的元素的个数;
- 在已排序数组中插入元素,同时记录它的索引值到
result
数组中; - 最终遍历结束返回
result
数组即可。
实现
/**
* @param {number[]} nums
* @return {number[]}
*/
var countSmaller = function(nums) {
const n = nums.length;
let result = new Array(n).fill(0);
let sortArr = [ nums[n - 1] ]; // 已排序的数组
// 逆序遍历,先做排序,每轮插入位置就是比它少的数量
for (let i = n - 2; i >= 0; i--) {
const index = quickFindIndex(sortArr, nums[i]);
if (index === -1) {
result[i] = sortArr.length;
sortArr.push(nums[i]);
} else {
sortArr.splice(index, 0, nums[i]);
result[i] = index;
}
}
return result;
};
// 二分查找
function quickFindIndex(arr, val) {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] >= val) {
if (!mid || arr[mid - 1] < val) {
return mid;
} else {
right = mid - 1;
}
} else {
left = mid + 1;
}
}
return left;
}
性能
优化
这道题我们可以参考之前做过的逆序对题目,一起刷leetcode 剑指 Offer 51. 数组中的逆序对,上次我们说到归并排序可以找到数组中的逆序对,也就是统计出后面元素比前面元素小的数量总和。那么这道题的思路是类似的。
- 我们可以写一个归并排序,然后把数组分成
n
个片段,从1
个片段开始两两进行合并; - 下一轮循环就从
1
变成2
,再到4
...直到n
位置,每次合并相邻的两个片段; - 这道题目我们由于要统计每个元素的具体值,所以我们得加个索引数组
list
来实时记录每个元素的当前位置, 比如4, 2 , 1, 5
第一轮过后2
和4
会交换位置,下次的索引值就不再可靠,所以我们得记录每个元素的当前位置; - 然后每次合并的时候,我们可以只传一个当前的开始位置和间隔,自己构建出两段合并数组的片段,
开始位置 + 间隔
等于片段一, 片段一到开始位置 + 间隔 * 2
等于片段2; - 接着咱们用朴实无华的归并排序来排序这两段数组,用
p1
和p2
来记录两段元素中指针所在的位置,每次比较指针所在的两个元素的大小,小的值先放到结果中去,p1
中的元素统计的时候可以记录结果,判断p2
元素走了多少步,走的步数就是比它小的元素的数量; - 统计完成后返回
result
数组即可。
Talk is cheap
/**
* @param {number[]} nums
* @return {number[]}
*/
var countSmaller = function(nums) {
const n = nums.length;
let result = new Array(n).fill(0);
// 记录它们的位置,实时跟踪最新的位置, 每次不交换值只换位置
let list = new Array(n).fill(0).map((v, i) => i);
// 合并两个有序的数组 -- 直接在原数组做交换
function mergeTwoArray(start, base, temp) {
let p1 = start,
p2 = end1 = start + base,
end2 = Math.min(p2 + base, n);
if (p2 >= n) return;
// 复制节点,因为重新赋值的过程会导致原数组混乱
temp = list.slice(start, end2);
// 两两比较,判断哪个值小哪个放前面
for (let i = start; i < end2; i++) {
let nums1 = nums[temp[p1 - start]],
nums2 = nums[temp[p2 - start]];
if (p1 >= end1 || nums2 < nums1) {
list[i] = temp[p2 - start];
p2++;
} else {
result[temp[p1 - start]] += p2 - end1;
list[i] = temp[p1 - start];
p1++;
}
}
}
// 从一个元素开始两两合并,直到n个元素
let base = 1;
while (base < n) {
const len = Math.ceil(n / base / 2);
// 当前基准值把元素拆成len个片段,相邻的片段两两组合归并排序
for (let i = 0; i < len; i++) {
mergeTwoArray(2 * base * i, base);
}
base *= 2;
}
return result;
};
看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。