前文
今天来看看作者是怎么被前端笔试题目吊打的,估计已经凉凉,等来年春招!
微众银行(一)
题目描述: 随机播放器 时间限制:3000MS 内存限制:589824KB
喜欢听歌的小明觉得系统的随机播放总是不能让他满意,于是小明自己想了一种全新的随机播放歌曲方法。
小明会播放歌单中的第一首曲子,然后将播放的这首曲子移除出歌单,如果歌单里还有曲子,就把剩余曲子中的第一首拿到最后一首去。小明会如此播放直到所有曲子都被移除出歌单 (也就是都被播放了一遍) 。
小明播了几次就累了,他不想一遍又一遍操作,于是向你求助: 他把初始的歌单给你,希望你能直接把按照小明规则下的真实播放顺序告诉给他。
输入描述
第一行 1 个整数 n,表示歌单的长度
第二行 n 个整数 a1 a2...an,表示从第一首到最后一首顺序的初始歌单中各歌曲的ID。对于100%的数据,1 <= n <= 50000 ,1 <= ai <= 10^9
输出描述
输出一行n个整数表示按照规则后真正的放歌顺序
样例输入:
6
2 3 2 1 4 5
样例输出:
2 2 4 3 5 1
提示:开始播放歌单中第一首,即歌曲2。将其移除出歌单后为[3 2 1 4 5],接着将剩余歌单中第一首移动到最后一首,得到[2 1 4 5 3]。 播放剩余歌单的第一首,即歌曲2。将其移除出歌单后为[1 4 5 3],接着将剩余歌单中第一首移动到最后一首,得到[4 5 3 1]。 播放剩余歌单的第一首,即歌曲4。将其移除出歌单后为[5 3 1],接着将剩余歌单中第一首移动到最后一首,得到[3 1 5]。 播放剩余歌单的第一首,即歌曲3。将其移除出歌单后为[1 5],接着将剩余歌单中第一首移动到最后一首,得到[5 1]。 播放剩余歌单的第一首,即歌曲5。将其移除出歌单后为[1],接着将剩余歌单中第一首移动到最后一首,得到[1]。 播放最后一首,即歌曲1。 实际播放顺序为: 2 2 4 3 5 1
解题思路:
思路很简单,使用数组的模仿队列方法,设置一定条件,使得队列数组始终隔代删除头部,添加尾部
代码实现:
function a(n, songs) {
let result = []
while(songs.length > 0) {
const song1 = songs.shift()
result.push(song1)
if (result.length > 0) {
const song2 = songs.shift()
songs.push(song2)
}
}
return result
}
代码分析
初始化一个空数组 `result`,用来存储最终处理后的歌曲列表。
进入一个循环,循环的条件是 `songs` 数组的长度大于 0,也就是还有未处理的歌曲。
在每次循环迭代中,从 `songs` 数组中取出第一首歌曲 `song1`,然后将其添加到 `result` 数组中。这表示每次处理都会将一个歌曲放入 `result`。
接下来,检查 `result` 数组的长度是否大于 0。如果大于 0,说明 `result` 中至少有一首歌曲。在这种情况下,再次从 `songs` 数组中取出一首歌曲 `song2`,然后将其添加回到 `songs` 数组的末尾。这一步的目的是将处理过的歌曲重新放回到 `songs` 数组的末尾,以便在下一次循环中继续处理。
循环继续,重复步骤 3 和步骤 4,直到 `songs` 数组为空,即所有歌曲都被处理完毕。
最后,函数返回存储在 `result` 数组中的歌曲列表,这个列表的顺序是按照一定规则处理后的结果。
时间与空间复杂度分析:
时间复杂度:
- 在
while循环中,我们不断地从songs数组中取出歌曲,然后再添加回去。每次循环迭代,我们从songs中取出两个歌曲(song1 和 song2),然后将 song2 添加回数组的末尾。 - 这意味着每个循环迭代都需要执行常数次操作,具体来说,每个循环迭代执行了两次
shift操作和一次push操作。 - 因此,
while循环的总迭代次数取决于songs数组的长度,假设songs的长度为 n,则循环迭代的次数约为 n/2。 - 所以,整个代码的时间复杂度为 O(n)。
空间复杂度:
- 代码中使用了一个额外的数组
result来存储重新排列后的歌曲。 - 此外,还有一些常数级的额外空间开销,如变量
song1和song2。 - 因此,整个代码的空间复杂度主要由
result数组决定,其空间复杂度为 O(n)。
58同城(一)
题目描述:
举例说明:现在有一个整数数组nums,找到其中的最长严格递增 (即两个相等的项不算递增)的子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除) 数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例1:输入: nums = [3,6,4,7,0,7,4,5,8,9]输出: 5
解释: 最长的递增子序列有两个[3,4,5,8,9]、[0,4,5,8,9]和[3,6,7,8,9],因此长度为5
示例2
输入:nums = [5,4,3,2,1]
输出:1
解释:最长的递增子序列为[5],[4],[3],[2],[1],因此长度为1
示例3
输入:nums = [2,2,2,2,2]
输出::1
解释:最长递增子序列为[2],因此长度为1
示例4
输入:nums = [10,9,2,5,3,7,101,18]
输出: 4
解题思路:
通过动态维护一个递增的数组
arr,使用二分查找来找到合适的位置插入或更新元素,以求得最长递增子序列。由于需要每迭代一次就要给出一个最优解,其次需要查找每次递增数组中的元素,我们可以考虑二分查找结合贪心算法的思想去解决本题
代码实现
function lengthOfLIS(nums) {
let arr = [],
left,
right
for (const num of nums) {
left = 0
right = arr.length
while (left < right) {
const mid = Math.floor((left + right) / 2)
if (arr[mid] < num) {
left = mid + 1
} else {
right = mid
}
}
if (left === arr.length) {
arr.push(num)
} else {
arr[right] = num
}
}
return arr
}
代码分析
一、原理解析
创建一个空数组 `arr`,用来存储最长递增子序列。
遍历输入数组 `nums` 中的每个元素 `num`。
对于每个元素 `num`,使用二分查找来确定它在数组 `arr` 中的插入位置。这个二分查找的目标是在 `arr` 中找到第一个大于或等于 `num` 的元素的位置。
如果找到的位置 `left` 等于 `arr` 的长度,说明 `num` 大于 `arr` 中的所有元素,因此将 `num` 添加到 `arr` 的末尾,扩展了最长递增子序列。
否则,如果找到的位置 `left` 不等于 `arr` 的长度,说明 `num` 应该插入到 `arr` 中某个位置,这个位置是 `left`,然后将 `arr` 中的该位置的元素更新为 `num`。这样,不会增加 `arr` 的长度,但可能改变了其中的某个元素,从而构成更长的递增子序列。
最终,函数返回数组 `arr`,该数组包含了最长递增子序列。
二、核心思想:贪心算法
体现在以下几个方面:
- 逐步构建最优解: 代码中通过迭代遍历输入数组
nums,逐步构建最长递增子序列arr。每次迭代都选择当前元素num,然后通过二分查找找到num应该插入的位置,并将其插入arr中。这个过程是逐步构建最优解的典型特征,每一步都选择当前最佳的元素来扩展子序列。 - 贪心选择策略: 代码中采用了贪心的选择策略,即始终选择能够使得当前递增子序列更长的元素。如果
num大于当前子序列的最后一个元素,那么它被添加到子序列的末尾,从而使子序列更长。如果num不大于最后一个元素,那么通过二分查找找到合适的插入位置,以便后续的元素有更多的机会加入。 - 无后效性: 贪心法通常具有无后效性,即当前的选择不会影响以后的选择。在这段代码中,每次插入一个元素后,都不会影响后续元素的插入位置,因为后续元素仍然会根据自身的值和当前子序列的状态进行插入。
时间与空间复杂度分析:
时间复杂度:
- 遍历输入数组
nums中的每个元素需要 O(n) 的时间,其中 n 是输入数组的长度。 - 在内部的
while循环中,进行二分查找,每次查找需要 O(log(n)) 的时间,最多执行 n 次查找。 - 因此,整个算法的时间复杂度为 O(n * log(n))。
空间复杂度:
- 声明了一个数组
arr用于存储最长递增子序列,其长度最多为 n。 - 此外,还有一些辅助变量
left和right,它们的空间复杂度可以忽略不计。 - 因此,整个算法的空间复杂度为 O(n)。