[前端]_一起刷leetcode 315. 计算右侧小于当前元素的个数

144 阅读4分钟

这是我参与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 个更小的元素 (21)
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

暴力解法

思路

  1. 我们要求右侧比较小的元素,那么我们可以从右侧开始遍历数组,去给右侧已经遍历的部分做排序;
  2. 同时我们可以用一个result数组来记录每个位置比当前元素小的右侧元素的数量;
  3. 然后用sortArr数组来记录已经排序好的右侧数组,从小到大排序;
  4. 每次遍历到当前元素的时候,找到当前元素在已排序数组中的插入位置,当前的插入位置就是数组中比它小的元素的个数;
  5. 在已排序数组中插入元素,同时记录它的索引值到result数组中;
  6. 最终遍历结束返回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;
}

性能

image.png

优化

这道题我们可以参考之前做过的逆序对题目,一起刷leetcode 剑指 Offer 51. 数组中的逆序对,上次我们说到归并排序可以找到数组中的逆序对,也就是统计出后面元素比前面元素小的数量总和。那么这道题的思路是类似的。

  1. 我们可以写一个归并排序,然后把数组分成n个片段,从1个片段开始两两进行合并;
  2. 下一轮循环就从1变成2,再到4...直到n位置,每次合并相邻的两个片段;
  3. 这道题目我们由于要统计每个元素的具体值,所以我们得加个索引数组list来实时记录每个元素的当前位置, 比如4, 2 , 1, 5第一轮过后24会交换位置,下次的索引值就不再可靠,所以我们得记录每个元素的当前位置;
  4. 然后每次合并的时候,我们可以只传一个当前的开始位置和间隔,自己构建出两段合并数组的片段,开始位置 + 间隔等于片段一, 片段一到开始位置 + 间隔 * 2等于片段2;
  5. 接着咱们用朴实无华的归并排序来排序这两段数组,用p1p2来记录两段元素中指针所在的位置,每次比较指针所在的两个元素的大小,小的值先放到结果中去,p1中的元素统计的时候可以记录结果,判断p2元素走了多少步,走的步数就是比它小的元素的数量;
  6. 统计完成后返回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;
};

看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。