JS leetcode 有多少小于当前数字的数字 解题分析,你应该了解的桶排序
壹 ❀ 引
刷题其实一直没断,只是这两天懒得整理题目...那么今天来记录一道前天做的题,题目本身不难,不过拓展起来还是有些东西可以讲,题目来自leetcode有多少小于当前数字的数字,题目描述如下:
给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目。
换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i 且 nums[j] < nums[i] 。
以数组形式返回答案。
示例 1:
输入:nums = [8,1,2,2,3] 输出:[4,0,1,1,3] 解释: 对于 nums[0]=8 存在四个比它小的数字:(1,2,2 和 3)。 对于 nums[1]=1 不存在比它小的数字。 对于 nums[2]=2 存在一个比它小的数字:(1)。 对于 nums[3]=2 存在一个比它小的数字:(1)。 对于 nums[4]=3 存在三个比它小的数字:(1,2 和 2)。
示例 2:
输入:nums = [6,5,4,8] 输出:[2,1,0,3]
示例 3:
输入:nums = [7,7,7,7] 输出:[0,0,0,0]
提示:
2 <= nums.length <= 500 0 <= nums[i] <= 100
要求很简单,对于数组中每个元素,统计比它要小的元素的个数,我们来尝试解答它。
贰 ❀ 解题思路
那么我首先想到的比较暴力的做法自然是使用双循环嵌套了,设置一个用于统计个数的变量,外层循环每次取一个元素出来,依次跟内层循环的每个元素对比,只要满足条件,让计数变量自增。
说干就干,实现是这样:
贰 ❀ 壹 使用双循环
/**
* @param {number[]} nums
* @return {number[]}
*/
var smallerNumbersThanCurrent = function (nums) {
let ans = [],
len = nums.length;
// 每次取一位出来作为参照
for (let i = 0; i < len; i++) {
// 计数变量
let sum = 0;
for (let j = 0; j < len; j++) {
if (nums[i] > nums[j]) {
++sum;
};
};
ans.push(sum);
};
return ans;
};
贰 ❀ 贰 排序加indexOf
在实现后,我脑中有了一个想法,能不能把数组排个序,比如[8,1,2,2,3]
排序后是[1,2,2,3,8]
,这样我能知道每个元素前有几个元素,不就是我们想要的答案了。
当然这只是一个模糊的想法,有两个问题需要解决,一是我怎么知道有几个元素比当前元素小,特别是当元素相同的时候,比如[7,7,7,8]
。
比第一个7要小的数为0个,第二个7也是0个,第三个7还是0个,而第四个8是三个,0,0,0,3。这不是就在用indexOf
获取索引吗,管你元素相不相同,相同获取第一个数字的索引就行了。
第二个问题是,一旦数组被我们排序,虽然能得到对应的个数,但这个个数的顺序也被打乱了,怎么还原呢?别忘了我们还可以拷贝一份数组。那么到这里我们来看看第二种实现:
/**
* @param {number[]} nums
* @return {number[]}
*/
var smallerNumbersThanCurrent = function (nums) {
let ans = [],
// 拷贝一份数组,用于维持ans查询结果顺序
nums_ = JSON.parse(JSON.stringify(nums));
//将nums排序好
nums = nums.sort((a, b) => a - b);
nums_.forEach(item => {
ans.push(nums.indexOf(item));
});
return ans;
};
在评论区,我看到有用户在拷贝数组时使用的是[].concat(nums)
,比如:
let arr = [1,2,3];
let arr_ = [].concat(arr);
arr_;//[1,2,3]
那么有个问题,concat
是浅拷贝还是深拷贝?如果我们修改上面例子中的arr,你会发现互不影响:
arr.push(4);
arr_;//[1,2,3]
这能说明concat
是深拷贝吗?其实在MDN中关于concat
描述很清楚,此方法会依次拷贝目标数组中每个元素,而对于元素类型不同做不同决策。如果元素是简单类型,则直接复制元素,但如果元素是对象类型,而是拷贝对象的引用,所以我们可以来看一个对象数组的类型,比如:
let arr = [1,[2,3],4];
let arr_ = [].concat(arr);
arr[1].push(1);
arr_;//[1,[2,3,1],4]
那么关于此方案说到这了。当然除了以上两种方案外,还有一种是官方提到的计数排序+求前缀和的做法。当时相比官方所说的计数排序,我觉得更像桶排序,由于这两种排序十分类似,我暂时未能分清他们,所以这里暂且叫桶排序吧。
我知道第一次听说桶排序的同学一定会很陌生,没事,我们先来简单介绍它,以[3,2,2,1,4]
为例。
由于数组中最大元素是4,所以我们建立五个用于装元素的桶,桶默认值都是0,那为啥是五个?因为数组默认索引是从0开始的,0-1-2-3-4,这不是5个桶吗?
现在我们开始遍历[3,2,2,1,4]
,将数字丢到对应索引的桶中,每丢一个进去让桶的初始值自增1,像这样:
现在我们得到了一个装满数字的桶,因为他也是一个数组,遍历桶,比如桶1的数量为1,输出1个1,桶2数量是2,所以输出2个2,以此类推:
你看,我们不就得到了一个已经排序好的数组了,让我们尝试实现它(注意,这并非官方的桶排序,我只是按照这个思路去实现它)。
// 非正式桶排序
function bucketSort(nums) {
// 找出最大元素
let max = Math.max(...nums),
// 创建桶
bucket = new Array(max + 1).fill(0),
ans = [];
// 统计对应桶位元素数量
nums.forEach(ele => {
bucket[ele]++;
});
for (var i = 0; i < bucket.length; i++) {
while (bucket[i] > 0) {
ans.push(i);
bucket[i]--;
};
};
return ans;
};
bucketSort([3, 2, 2, 1]); //[1,2,2,3]
OK,知道了这个怎么能用于解题呢?仔细想想,当我们桶排序后,对应每个桶的数量,不就是我们希望要的数字吗,比如上方图解中:
比桶1小的元素位0个,也就是bucket[1-1]
个。
比桶2小的元素有1个,也就是bucket[1-1]+bucket[2-1]
个。
比桶3小的元素有3个,也就是bucket[3-1]+bucket[3-2]+bucket[3-3]
个。
比桶4小的元素有4个,也就是bucket[4-1]+bucket[4-2]+bucket[4-3]+bucket[4-4]
个。
当然有个特例,因为桶0是一个,不会有比它小的元素了,所以比它小的一定是0个。
这也是我们在前面说官方用计数排序加求前缀和做法,为何要求前缀和的原因。我知道这有点绕,静下心仔细想想,这只是用了桶排序统计了元素的个数,元素成了桶数组的下标,顺带求了数量的和。
现在让我们实现它,像这样:
/**
* @param {number[]} nums
* @return {number[]}
*/
var smallerNumbersThanCurrent = function(nums) {
let ans = [],
max = Math.max(...nums),
len = nums.length;
// 由于i范围是0 <= nums[i] <= 100,因此创建101个桶
let bucket = new Array(max + 1).fill(0);
// 将元素装到对应的桶
for (let i = 0; i < len; i++) {
// 数字每出现一次,桶的统计数量就加1
bucket[nums[i]]++;
};
// 求n项出现的次数和,考虑越界情况,此处i从1开始
for (let i = 1; i <= 100; i++) {
bucket[i] += bucket[i - 1]
};
// nums中元素对应了桶的索引,索引非0取前一位,索引为0说明没有比它小的,直接赋予0
for (let i = 0; i < len; i++) {
nums[i] ? ans[i] = bucket[nums[i] - 1] : ans[i] = 0
};
return ans;
};
那么关于本题就分析到这里了。