文章目录
- 前 K 个高频元素
- 方法一
- 方法二
- 代码
- 寻找峰值
- 方法一 取最大值
- 方法二 暴力法
- 方法三 二分法
- 合并区间
- 方法一 合并重叠
- 方法二 合并重叠
- 搜索二维矩阵 II
- 方法一 暴力法
- 方法二 相邻比较法
- 计算右侧小于当前元素的个数
- 方法一 暴力法
- 方法二 排序法
前 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 了。直接一次循环解决问题。
详解
- 使用数组的 reduce 方法遍历数组,设其初始值为 0。
- 当遍历到的值大于上一次返回的下标所对应的值时返回当前下标。
- 遍历结束后所获取的值为最终答案。
代码
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 就是我们要找的峰值。如果没有这个值,则数组的最后一个元素就是峰值。
详解
- 遍历数组找出第 i 个元素大于 i+1 个元素的元素的下标为结果。
- 如果遍历完成没有找到,则取元素最后一个值的下标为结果。
代码
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 时。其值为峰值。
详解
- 将目标数组从中间分成两个数组 L 和 R。
- 比较 L 最后一个值 l 与 R 的第一个值 r 的大小。
- 如果 l > r 则数组 L 必有一个及以上原数组的峰值,取数组 L 作为目标数组。
- 反之则数组 R 必有一个及以上原数组的峰值,取数组 R 作为目标数组。
- 目标数组长度为 1 则其值为峰值。
- 目标数组长度大于 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 = log2^nx=log2n 。也就是说当循环 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] 开始 继续执行步骤三
详解
- 对区间进行排序(升序)
- 定义一个新的数组,用于存储新的数组区间。
- 从第二个值开始遍历原数组,比较当前区间的最小值是否大于新数组最后一个区间的最大值,如果满足则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)。
搜索二维矩阵 II
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
每行的元素从左到右升序排列。 每列的元素从上到下升序排列。
示例
现有矩阵 matrix 如下:[[1, 4, 7, 11, 15],[2, 5, 8, 12, 19],[3, 6, 9, 16, 22],[10, 13, 14, 17, 24],[18, 21, 23, 26, 30]
]给定 target = 5,返回 true。给定 target = 20,返回 false。
方法一 暴力法
思路
题目只需找出二维数组中是否包含目标值,因此直接用两个 for 循环遍历二维矩阵的所有元素,找到目标元素则返回 true,遍历完仍未找到则返回 false。
详解
- 第一层 for 循环取到二维数组中所有的一维数组
- 第二层 for 循环中用两个索引值取到二维数组中每一个具体的值
- 将取到的每一个值和目标值作比较,若相同则返回 true
- 若直到所有循环结束还未找到与目标值相同的值则返回 false
代码
const searchMatrix = function (matrix, target) {for (let i = 0; i < matrix.length; i++) {for (let j = 0; j < matrix[i].length; j++) {if (matrix[i][j] === target) {return true;}}}return false;
};
复杂度分析
- 时间复杂度:O(n^2)O(n2) 通过遍历二维矩阵的所有元素来寻找它所对应的目标元素,这将耗费 O(n^2)O(n2) 的时间。
- 空间复杂度:O(1)O(1)
方法二 相邻比较法
思路
由于矩阵的行和列是排序的,从左到右递增,从上到下递增,所以对任意元素和目标值比较大小时,总可以去找相对较小(往左往上)或相对较大(往右往下)的值继续比较,直到找到目标值或找不到。
详解
- 直接取二维数组中左下角的值和目标值比较
- 若该值比目标值大,则向上查找(第一个索引减 1),若该值比目标值小,则向右查找(第二个索引加 1)
- 重复该操作,当查询值等于目标值时则返回 true
- 若直到查询值离开二维数组时还未找到目标值则返回 false
代码
const searchMatrix = function (matrix, target) {let j = matrix.length - 1;let i = 0;while (j >= 0 && i < matrix[0].length) {if (matrix[j][i] > target) {j--;} else if (matrix[j][i] < target) {i++;} else {return true;}}return false;
};
复杂度分析
- 时间复杂度:O(n + m)O(n+m) 由于行只能运算 m 次,列只能运算 n 次,是两个变量之和,这将耗费 O(n + m)O(n+m) 的时间。
- 空间复杂度:O(1)O(1) 用到了两个变量,其空间复杂度为 O(1)O(1)
计算右侧小于当前元素的个数
给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i]
的值是 nums[i]
右侧小于 nums[i]
的元素的数量。
示例
输入: [5,2,6,1]
输出: [2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1).
2 的右侧仅有 1 个更小的元素 (1).
6 的右侧有 1 个更小的元素 (1).
1 的右侧有 0 个更小的元素.
方法一 暴力法
思路
暴力法很简单,遍历每个元素 x,并遍历查找在其后边并且比它小的元素,进行累加,最后将统计出来的个数 push 到新开辟的数组中。
详解
算法流程:
- 声明一个数组用来存储最后的结果;
- 第一层循环用来遍历所有的元素;
- 循环体内先声明一个变量,初始值为 0,用来记录当前元素右边并且比当前元素小的个数;
- 接下来通过第二个循环来计算当前元素右边比它小的数,由于只需要计算它右边的数,所以第二个循环从第 i + 1 开始,遍历到数组的最后一个值即可。第二层循环体内通过 if 语句来判断比当前元素小的数,然后执行 num ++;
代码
const countSmaller = (nums) => {const newArr = [];for (let i = 0, len = nums.length; i < len; i += 1) {let num = 0;for (let j = i + 1; j < nums.length; j += 1) {if (nums[j] < nums[i]) {num += 1;}}newArr.push(num);}return newArr;
};
复杂度分析
- 时间复杂度:O(n^2)O(n2) 对于每个元素,通过遍历数组的其余部分来寻找它所对应的目标元素,所以时间复杂度为 O(n^2)O(n2)。
- 空间复杂度:O(n)O(n) 额外开辟了一个数组来存放结果,所以空间复杂度为 O(n)O(n)。
方法二 排序法
思路
首先将数组元素进行从小到大的排序,排序完成后,遍历原数组,找出原数组中当前元素在排序后的数组中的数组下标,即为该元素右侧比它小的个数,然后将排序后的数组中的这个元素删除。
例如: 原数组 nums = [5,2,6,1]; 排序后的数组为 newArr = [1,2,5,6]; 对原数组 nums 进行遍历,首先是元素 5,在 newArr 中其数组下标为 2,其右侧比它小的元素有2个;然后将 newArr 中的5这个元素删除,newArr = [1,2,6];接下来是元素2,在newArr中的下标为1,然后将 newArr 中的2这个元素删除,其右侧比它小的元素有1个;以此类推…
详解
算法流程:
- 将原数组从小到大排序,并声明一个新数组来存储;
- 声明一个数组用来存储最后的结果;
- 对原数组进行遍历;
- 通过数组的方法 indexOf 找到遍历的当前元素在排序后的数组中的位置,并赋值给变量index,该变量的值即为当前元素右侧比它小的个数;
- 通过数组的方法 splice ,把当前元素从原数组中删除;
代码
const countSmaller = (nums) => {const newArr = [...nums].sort((a, b) => a - b);const result = [];nums.forEach((n) => {const index = newArr.indexOf(n);result.push(index);newArr.splice(index, 1);});return result;
};
复杂度分析
- 时间复杂度:O(nlogn)O(nlogn) 代码中 sort 函数的时间复杂度为 O(nlogn)O(nlogn),for 循环的时间复杂度为 O(n)O(n),因此整体的时间复杂度为 O(nlogn)O(nlogn)。
- 空间复杂度:O(n)O(n) 申请了 2 个大小为 n 的数组空间,因此空间复杂度为 O(n)O(n)。