难度标识:
⭐:简单,⭐⭐:中等,⭐⭐⭐:困难
。
tips:这里的难度不是根据LeetCode难度定义的,而是根据我解题之后体验到题目的复杂度定义的。
1.只出现一次的数字 ⭐
思路
要在线性时间复杂度内找到只出现一次的元素并使用常量的额外空间,可以使用 异或 操作。
异或操作的特点如下:
-
任何数字与0进行异或操作结果仍然是那个数字: num ^= 0 = num
-
任何数字与其自身进行异或操作结果是0: num ^= num = 0
基于以上的特点,如果我们对数组中的所有数字进行异或操作,那么成对出现的数字就会相互抵消变为0,而只出现一次的数字则会被保留下来。
代码
var singleNumber = function (nums) {
let res = 0
for (let num of nums) {
res ^= num
}
return res
};
2.多数元素 ⭐
思路
-
Boyer-Moore 投票算法 (核心思路):
-
这是一个在线性时间和常数空间解决问题的方法。
-
设立两个变量,一个是当前候选的多数元素,另一个是计数。
-
遍历数组,对于每个元素:
-
如果计数为0,将当前元素设置为候选多数元素。
-
如果当前元素与候选多数元素相同,计数加1,否则计数减1。
-
-
由于多数元素的数量超过了n/2,所以最后的候选元素就是多数元素。
-
Boyer-Moore 投票算法 是解决此问题的最优方法,因为它只需要 O(n)
的时间复杂度和 O(1)
的空间复杂度。
代码
var majorityElement = function (nums) {
let candidate = nums[0], count = 1
for (let i = 1; i < nums.length; i++) {
if (count === 0) {
candidate = nums[i]
count = 1
} else if (candidate === nums[i]) {
count++
} else {
count--
}
}
return candidate
};
3.颜色分类 ⭐
思路
解这题其实是左右双指针的思路,只不过需要多一个指针去做搜索。这题的官方题解视频讲的也比较清楚,可以看看,便于理解。
-
使用两个指针
left
和right
。left
的左边只有0,right
的右边只有2。 -
使用另一个指针
curr
从头开始遍历。 -
如果
nums[curr]
是0,那么将其与nums[left]
交换,然后left
和curr
都加1。 -
如果
nums[curr]
是2,那么将其与nums[right]
交换,然后right
减1。 -
如果
nums[curr]
是1,只需将curr
加1。
代码
var sortColors = function (nums) {
let left = 0, curr = 0, right = nums.length - 1
while (curr <= right) {
if (nums[curr] === 0) {
[nums[curr], nums[left]] = [nums[left], nums[curr]]
curr++
left++
} else if (nums[curr] === 1) {
curr++
} else {
[nums[curr], nums[right]] = [nums[right], nums[curr]]
right--
}
}
};
4.下一个排列 ⭐⭐
思路
解这题还是有一些难度的,难度在哪呢,不在于解题,而在于看懂题目的意思。那题目具体是什么意思呢?
例如:我们现在有一个数组 [3,2,9,1,5,6,4,3,1]
,它的下一个字典序更大的排列是什么呢,也就是说它符合条件的排列是什么呢?我们知道先从 右往左找
找到一个相邻的左边的值小于右边值的位置,然后再从这个位置的右边找到比这个值大的但是在右边比它大的数中是最小的,[3,2,9,1,5,6,4,3,1]
找到一个相邻的左边的值小于右边值的位置是下标为 4
的位置,这位置的右边找到比这个值大的但是在右边比它大的数中是最小的,那就是下标为 6
的位置,先交换这两个位置,变成 [3,2,9,1,6,5,4,3,1]
,然后将 6
后面的数反转得到结果 [3,2,9,1,6,1,3,4,5]
。
这里我做的时候就有个疑问,为什么要反转?
我们可以这样理解,将这个数组看成一个数:329156431,将这些数重新组合找到比刚那个数大的最小的那个数。这样是不是就理解了,第一步找到:329165431,第二步找到:329161345。这题解起来不难,关键在于理解题目的意思和为什么后面的数需要反转这个点。
解题核心思路:
-
从后向前找递增的边界:
- 从数组的末尾开始,找到第一个不满足“递增”关系的数。即,找到第一个
nums[i] < nums[i+1]
的位置。
- 从数组的末尾开始,找到第一个不满足“递增”关系的数。即,找到第一个
-
交换元素:
- 如果上面的
i
存在,再次从数组末尾开始,找到第一个大于nums[i]
的数。我们将其称为nums[j]
。然后交换nums[i]
和nums[j]
。
- 如果上面的
-
逆序后半部分:
- 无论
i
是否存在(即原数组是否是完全递减的),都需要将位置i+1
到数组末尾的部分逆序排列。这是因为在找到下一个更大排列后,我们希望这一部分是最小的排列,或者在原数组是完全递减的情况下,我们希望整个数组变成最小的排列。
- 无论
具体步骤如下:
-
从后往前查找,找到第一个
nums[i] < nums[i+1]
的位置。 -
如果这样的
i
存在,再从数组末尾开始查找,找到第一个大于nums[i]
的数nums[j]
,然后交换它们。 -
将位置
i+1
到数组末尾的部分逆序排列。
这个方法的原理是在字典序的排列中找到最接近但稍大于原排列的下一个排列。
代码
var nextPermutation = function (nums) {
const n = nums.length
let i = n - 2
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--
}
if (i >= 0) {
let j = n - 1
while (nums[j] <= nums[i]) {
j--
}
[nums[i], nums[j]] = [nums[j], nums[i]]
}
reverse(nums, i + 1, n - 1)
};
function reverse(nums, left, right) {
while (left < right) {
[nums[left], nums[right]] = [nums[right], nums[left]]
left++
right--
}
}
5.寻找重复数 ⭐
思路
解这一题可以先看看我之前写的LeetCode热题100链表题解析里的 环形链表 II这题,思路一模一样的,使用被称为 “龟兔赛跑” 的 快慢指针 的方法来解决,慢指针一次走一步,快指针一次走两步。
注意看题目里的这个条件:其数字都在
[1, n]
范围内(包括1
和n
),这是可以类比解决的关键条件。
核心思路:
-
映射到链表:
- 将
nums
数组看作一个链表,其中数组的值是下一个节点的索引。由于存在重复数字,这将形成一个环。
- 将
-
找到相遇点:
- 使用两个速度不同的指针(一个快,一个慢),它们最终会在某个点相遇,这一点在环内。
-
找到环的入口:
- 从相遇点开始,保持一个指针在相遇点,另一个指针回到起始点。然后,两个指针每次都前进一步,它们再次相遇的地方就是环的入口,即重复的数字。
代码
var findDuplicate = function (nums) {
let slow = nums[0], fast = nums[nums[0]]
while (slow !== fast) {
slow = nums[slow]
fast = nums[nums[fast]]
}
fast = 0
while (slow !== fast) {
slow = nums[slow]
fast = nums[fast]
}
return slow
};
好了,热题100技巧类的题目也已经完结了,技巧类的题目相对而言还是比较简单的。热题100全部解决完了,面试算法应该基本拿下了,完结,撒花,颁勋章。