该部分整体考察频率不高。
384. 打乱数组:洗牌算法,交换思想
洗牌方法如下:
- 初始化原始数组和新数组,原始数组长度为 n;
- 从还没处理的数组(例如还剩 k 个),随机产生一个 [0, k) 之间的数字 p;
- 从剩下的 k 个数中把第 p 个数取出;
- 重复以上步骤。
所以,实现的思路如下:
- 假设 待原地乱序的数组 nums,先做浅拷贝
- 共循环 n 次,在第 i 次循环中( 0 <= i < n):
- 在 [i, n) 中随机抽取一个下标 j;
- 将第 i 个元素与第 j 个元素交换。
const Solution = function(nums) {
this.nums = nums;
};
Solution.prototype.reset = function() {
return this.nums;
};
Solution.prototype.shuffle = function() {
const arr = this.nums.slice(); // 浅拷贝 slice()
let len = arr.length;
for (let i = 0; i < len; i++) {
let randomId = i + Math.floor((len - i) * Math.random()); // [i, len - 1]
// 交换!!!
[arr[randomId], arr[i]] = [arr[i], arr[randomId]]; // 将随机选择的数摘出
}
return arr;
};
时间复杂度:O(n)。
空间复杂度:O(n)。
剑指62. 圆圈中最后剩下的数字:约瑟夫环问题,公式有两个上轮
对于题目中给的示例,最后剩下的 数字3 的下标是 0,我们逆推:
- 第4轮反推,补上 m 个位置,然后模上当时的数组大小 2,那么 3 在这一轮的索引就是 (0 + 3) % 2 = 1.
- 第3轮反推,补上 m 个位置,然后模上当时的数组大小 3,位置就是 (1 + 3) % 3 = 1。
- 依此类推,我们可以得出该 「推导公式」:
(该数字的当前索引 + m) % 上轮剩余数字的个数 = 该数字上一轮的索引。
const lastRemaining = function(n, m) { // 推导一次即可
let ans = 0; // 最后一轮剩下的唯一数字,此时索引为 0
for (let i = 2; i <= n; i++) { // i是上轮剩余数字的个数(初始为2),一直模拟到初始数组长度
ans = (ans + m) % i;
}
return ans; // 实际返回的是 res[ans],因为题中给出的数值等于索引,而且没有给原数组,所以直接返回 ans
};
时间复杂度:O(n)。
空间复杂度:O(1)。
9. 回文数:整数反转
方法一:回文数 => 字符串 => 双指针判定。如果不允许转为字符串,那么思路如下:
「思路」 由题意可知,如果是负数,一定不是回文数;如果是正数,那么将它的倒序值计算出来,然后比较和原数值是否相等。
const isPalindrome = (x) => {
if (x < 0) return false;
let res = 0;
let num = x; // 不能省略,因为x要保留原数,来作对比
while (num) { // 整数反转
res = res * 10 + num % 10;
num = parseInt(num / 10); // 仅限正数时
}
return res == x;
};
或者将整数转换成字符串再反转,
const isPalindrome = (x) => {
if (x < 0) return false;
let s = x.toString().split('').reverse().join('');
let num = Number(s);
return num == x;
};
7. 整数反转:取余带符号 + parseInt
整数反转运算时注意:
1. 取余%是带正负号的 :-12 % 10 等于-2,12 % 10等于2,所以 res = res * 10 + x % 10这个公式本身就和原数值的正负号一致。
2. parseInt 和Math.floor的区别 : 当为正数时两者没区别都是向下取整;但为负数时,parseInt会向上取整,与Math.floor相反。
Math.floor(12 / 10) // 1
Math.floor(-12 / 10) // -2
parseInt(12 / 10) // 1
parseInt(-12 / 10) // -1
const reverse = (x) => {
let res = 0;
while (x) {
res = res * 10 + x % 10; // 取余是带符号的
if (res < Math.pow(-2, 31) || res > Math.pow(2, 31) - 1) {
return 0;
}
x = parseInt(x / 10); // 注意,正负数均存在的情况建议使用~~
}
return res;
}
136. 只出现一次的数字:异或
异或是一个逻辑运算符,数学符号为⊕,js 中用^表示。其运算法则为:a⊕b = (¬a∧b)∨(a∧¬b)。如果a、b两个值不相同,则异或结果为1;如果值相同,则异或结果为0。
「异或运算的几个性质」
- 任何数和
0做异或运算,结果仍是原来的数,a ^ 0 = a。 - 满足交换律,
a ^ b = b ^ a。 - 满足结合律,
a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c。
根据题意可知,数组中含有2m+1个元素,其中m个数各出现2次,只有1个数出现了一次。根据异或运算的交换律和结合律,数组中全部元素的异或运算结果总是可以写成如下形式:
(a1 ^ a1)^(a2 ^ a2)^(a3 ^ a3) ^...^(am ^ am) ^ b = b。
所以 「将数组中全部元素进行异或,结果就是数组中只出现一次的数字」。
解法一:
const singleNumber = (nums) => {
let ans = 0; // 恒等律 a ^ 0 = a
for (let n of nums) { // of遍历值
ans ^= n;
}
return ans;
};
解法二:
const singleNumber = (nums) => {
return nums.reduce((a, b) => a ^ b);
};
时间复杂度:O(n)。
空间复杂度:O(1)。
异或的解法满足线性时间复杂度和常数空间复杂度,如果不考虑复杂度的限制,还可以用哈希表:存储每个数字和该数字出现的次数,遍历数组即可得到数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。
法二:哈希表 (时间、空间复杂度均为 O(n),不如异或法。)
哈希表存储结构{数组元素值:元素出现的次数}。
const singleNumber = function(nums) {
const hashMap = new Map();
for (let i = 0; i < nums.length; i++) {
if (hashMap.has(nums[i])) hashMap.set(nums[i], 2);
else hashMap.set(nums[i], 1);
}
for (let [key, value] of hashMap) {
if (value === 1) return key;
}
return -1;
};
时间复杂度:O(n)。
空间复杂度:O(n)。
1013. 将数组分成和相等的三个部分:count <= 0
注意题中要求三部分的索引是有序的!
如果有和相等的三个部分,那么sum被 3 取余后等于0,而且拆分的3个子部分和都应该等于sum / 3。所以,总体思路 「先判断sum能不能被3取余等于0,然后遍历数组,每找到一个和等于sum / 3的子数组,就令count--,tmp = 0」。
注意,return count <= 0,而不是count == 0。这是因为当arr = [0, 0, 0, 0]时,也符合存在和相等的三个部分,此时count = -1,如果 return count == 0会返回false,与我们的预期不符合。
const canThreePartsEqualSum = (arr) => {
let sum = arr.reduce((a, b) => a + b, 0);
if (sum % 3) return false; // 如果累计和不能被 3 取余,那返回 false
let count = 3; // 3个子部分,判断是否有3个或以上的子数组的累计和满足该条件
let tmp = 0;
for (let num of arr) {
tmp += num;
if (tmp == sum / 3) {
count--;
tmp = 0;
}
}
return count <= 0; // 注意
}
时间复杂度:O(n)。
空间复杂度:O(1)。