前 K 个高频元素、寻找峰值和合并区间

前 K 个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

说明:

你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。

方法一

思路

这一题属于比较简单的算法题,整体的思路是需要开辟一个新的储存空间,对数组中数字的出现的次数进行记录,再对这个存储记录进行读取,进而得出前 K 个高频元素

详解

第一步,记录每个数出现的次数

let map = new Map();
for (let num of nums) { // 记录每个数出现的次数
map.set(num, (map.get(num) || 0) + 1);
}

第二步,设置一个数组 countOfNum,其第 i 个元素表示出现 i 次的元素数组

let countOfNum = Array(nums.length);
for (let key of map.keys()) {
let value = map.get(key);
if (countOfNum[value] === undefined) {
countOfNum[value] = [key]
} else {
countOfNum[value].push(key)
}
}

第三步,从 countOfNum 末尾开始往头遍历,将非空元素都加入结果数组中,直到结果数组的大小等于K。

let res = [];
for (let i = countOfNum.length - 1; i >= 0 && res.length < k; i--) {
if (countOfNum[i] !== undefined) {
res = res.concat(countOfNum[i])
}
}
return res;

代码

/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
const topKFrequent = function(nums, k) {
let map = new Map();
for (let num of nums) {
map.set(num, (map.get(num) || 0) + 1);
}
let countOfNum = Array(nums.length);
for (let key of map.keys()) {
let value = map.get(key);
if (countOfNum[value] === undefined) {
countOfNum[value] = [key]
} else {
countOfNum[value].push(key)
}
}
let res = [];
for (let i = countOfNum.length - 1; i >= 0 && res.length < k; i--) {
if (countOfNum[i] !== undefined) {
res = res.concat(countOfNum[i])
}
}
return res;
};

复杂度分析

  • 时间复杂度:O(n)O(n)

    • 上述解法中,我们使用了一层 for 循环,里面的代码会执行 n 遍,它消耗的时间是随着 n 的变化而变化的,因此时间复杂度是 O(n)O(n)

  • 空间复杂度: O(n)O(n)

    • 上述解法中,我们额外声明的空间大小和输入规模成正比,所以空间复杂度为 O(n)O(n)

方法二

思路

整体思路同方法一,这一种方法我们使用 forEach 和 sort 简化了实现方案

详解

第一步,构建一个对象作为储存空间

const hashTable = {}
nums.forEach(item => {
if (hashTable[item] === undefined) {
hashTable[item] = 1
} else {
hashTable[item]++
}
})

第二步,根据存储的出现次数,对数组进行排序

hashTableArray = Object.keys(hashTable)
hashTableArray.sort((prev, next) => {
return hashTable[next] - hashTable[prev]
})

第三步,截取数去的前K项,得到想要的结果

return hashTableArray.slice(0, k)

代码

/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
const hashTable = {}
nums.forEach(item => {
if (hashTable[item] === undefined) {
hashTable[item] = 1
} else {
hashTable[item]++
}
})
hashTableArray = Object.keys(hashTable)
hashTableArray.sort((prev, next) => {
return hashTable[next] - hashTable[prev]
})
return hashTableArray.slice(0, k)
};

复杂度分析

  • 时间复杂度:O(n)O(n)

  • 空间复杂度: O(n)O(n)

寻找峰值

峰值元素是指其值大于左右相邻值的元素。 给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。 数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。 你可以假设 nums[-1] = nums[n] = -∞。

示例

输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。

方法一 取最大值

思路

取最大值法思路最简单,最大值必然就是其峰值。因为这是算法题,我们就不用 Math.max 了。直接一次循环解决问题。

详解

  1. 使用数组的 reduce 方法遍历数组,设其初始值为 0。

  2. 当遍历到的值大于上一次返回的下标所对应的值时返回当前下标。

  3. 遍历结束后所获取的值为最终答案。

代码

const findPeakElement = function (nums) {
return nums.reduce((index, cur, i) => {
if (nums[index] < cur) {
return i;
}
return index;
}, 0);
};

复杂度分析

  • 时间复杂度:O(n)O(n) 对于每个元素,通过遍历数组进行比较值的大小来寻找它所对应的目标元素,这将耗费 O(n)O(n) 的时间。

  • 空间复杂度:O(1)O(1)

    算法执行的过程中,声明的变量并不与数组的长度 nn 相关。故而空间复杂度为 O(1)O(1)

方法二 暴力法

思路

暴力法很简单,遍历每个元素,并寻找到元素 n 大于元素 n + 1。则 n 就是我们要找的峰值。如果没有这个值,则数组的最后一个元素就是峰值。

详解

  1. 遍历数组找出第 i 个元素大于 i+1 个元素的元素的下标为结果。

  2. 如果遍历完成没有找到,则取元素最后一个值的下标为结果。

代码

const findPeakElement = function (nums) {
// 如果全部从小到大排列,则峰值为最后一个元素
let result = nums.length - 1;
// 否则只要找出第 i 个元素大于 i+1 个元素,则第 i 个元素就是峰值
for (let i = 0; i < nums.length - 1; i += 1) {
if (nums[i] > nums[i + 1]) {
result = i;
break;
}
}
return result;
};

复杂度分析

  • 时间复杂度:O(n)O(n) 对于每个元素,通过遍历数组进行比较值的大小来寻找它所对应的目标元素,这将耗费 O(n)O(n) 的时间。

  • 空间复杂度:O(1)O(1)

    算法执行的过程中,声明的变量并不与数组的长度 nn 相关。故而空间复杂度为 O(1)O(1)

方法三 二分法

思路

将数组从中间分成两个数组 L 和 R。比较 L 最后一个值 l 与 R 的第一个值 r 的大小。如果 l > r 则数组 L 必有一个及以上原数组的峰值,取数组 L 再做上一步。反之则数组 R 必有一个及以上原数组的峰值,取数组 R 再做上一步。当数组长度为 1 时。其值为峰值。

详解

  1. 将目标数组从中间分成两个数组 L 和 R。

  2. 比较 L 最后一个值 l 与 R 的第一个值 r 的大小。

  3. 如果 l > r 则数组 L 必有一个及以上原数组的峰值,取数组 L 作为目标数组。

  4. 反之则数组 R 必有一个及以上原数组的峰值,取数组 R 作为目标数组。

  5. 目标数组长度为 1 则其值为峰值。

  6. 目标数组长度大于 1 则重复 1,2,3,4 步骤。直至目标数组长度为 1 为止。

代码

const findPeakElement = function (nums) {
let lIndex = 0; // 虚拟数组第一个元素下标
let rIndex = nums.length - 1; // 虚拟数组最后一个元素下标
let mid; // 数组中间元素下标。
while (lIndex < rIndex) { // 当数组长度不为 1 时。
mid = Math.floor((lIndex + rIndex) / 2); // 取当前虚拟数组的中间元素的下标(当数组长度为偶数时取小的那个),可将虚拟元素隔开成两个数组。
if (nums[mid] > nums[mid + 1]) { // 比较左边数组最后一个元素与右边数组的第一个元素的大小
rIndex = mid; // 左边数组最后一个元素大、则取左边数组
} else {
lIndex = mid + 1; // 右边数组第一个元素大、则取右边数组
}
}
return lIndex;
};

复杂度分析:

  • 时间复杂度:O(logn)O(logn)

    每次循环数组的长度都除以 2 。设 x 次循环之后 lIndex===rIndexlIndex === rIndex。也就是说 2 的 x 次方等于 n,那么 x=log2nx = log2^n 。也就是说当循环 log2^n 次以后,代码运行结束。因此这个代码的时间复杂度为:O(logn)O(logn)

  • 空间复杂度:O(1)O(1)

    算法执行的过程中,声明的变量并不与数组的长度 n 相关。故而空间复杂度为 O(1)。

合并区间

给出一个区间的集合,请合并所有重叠的区间。

示例

输入: [[1,3],[2,6],[8,10],[15,18]],
输出: [[1,6],[8,10],[15,18]]。
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

方法一 合并重叠

思路:

从示例入手: 1.[1, 3] [2, 6] 是否可以合并只要对比 [1, 3]的最大值 3,[2, 6]的最小值 2,3 <= 2 ,则说明可以合并,否则不能。 2.[1, 3] [2, 6] [8,10],从3个来看,如果 [2, 6]的最大值6 <= [8,10]的最小值 8,则说明[2, 6] 不能和 [8,10],因此 [1, 3] [2, 6] 执行合并操作,[8,10] 继续与下一个数组执行步骤一。

详解:

1.对区间进行排序(升序)
2.从第一区间起取当前拟合并区间为a,
3.取下一区间为b(如果没有b了则输出a,退出)
4.如果a的尾 > b 的头 ,则合并为 a,否则输出a,把b作为a。
const merge = intervals => {
intervals.sort((a, b) => {
if (a[0] !== b[0]){
return a[0] - b[0];
}
return a[1] - b[1];
});
let len = intervals.length,
ans = [], // 定义新数组
start, end; // 遍历当前区间的最小值与最大值
for (let i = 0; i < len; i++) {
let s = intervals[i][0],
e = intervals[i][1];
if (start === undefined){
start = s, end = e;
} else if (s <= end){
end = Math.max(e, end);
} else {
let part = [start, end];
ans.push(part);
start = s;
end = e;
}
}
if (start !== undefined) {
let part = [start, end];
ans.push(part);
}
return ans;
};

复杂度分析

  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(1)O(1)

方法二 合并重叠

思路

从示例入手: 1.申明一个变量用于存储合并的结果 res = [] 2.第一个数组直接放入res中,res = [[1, 3]] 3.对比 res中的 [1, 3] 和 下一个数组 [2 ,6],[1,3] 的最大值 3 <= [2, 6] 的最小值2,则 res = [[1, 6]] 4.继续对比 res中的 [1, 6] 和下一个数组 [8, 10],则 res = [[1, 6], [8, 10]] 5.然后 从 [8, 10] 开始 继续执行步骤三

详解

  1. 对区间进行排序(升序)

  2. 定义一个新的数组,用于存储新的数组区间。

  3. 从第二个值开始遍历原数组,比较当前区间的最小值是否大于新数组最后一个区间的最大值,如果满足则push进入新的数组;又或者比较当前区间的最大值是否大于新新数组的随后一个区间的最大值,若满足则将新数组的最后一个区间的最大值替换成当前区间的最大值。

const merge = function(intervals) {
if (!intervals || intervals.length === 0) {
return [];
}
let input = intervals.sort((a, b) => a[0] - b[0]);
let res = [];
res.push(input[0]);
for(let i = 1, len = input.length; i < len; i++) {
if (input[i][0] > res[res.length - 1][1]) {
res.push(input[i]);
} else if (input[i][1] > res[res.length - 1][1]){
res[res.length - 1][1] = input[i][1];
}
}
return res;
};

复杂度分析

  • 时间复杂度:O(n)O(n)

    该算法对含有 n 个结点的列表进行了一次遍历。因此时间复杂度为 O(n)O(n)

  • 空间复杂度:O(n)O(n)

    该算法只开辟了额外空间用于存储结果,空间大小与数组长度n成正比。故空间复杂度为O(n)O(n)