There is an array generated by a rule:
- The first item is
1
. - If
k
is in the array, thenk*3+1
andk*2+1
are in the array. - The array is sorted. There are no duplicate values.
Please write a function that accepts an input n
. It should return the index n
of the array.
For example [1, 3, 4, 7, 9, 10, 13, 15, 19, 21, 22, 27, ....]
n = 10
, return 22
n = 100
, return 447
// 起始数字为1,之后每个数字都是裂变成 两倍加1 和 三倍加1 两个数字。
const arr = [1];
let cache = [1]; // 减少裂变计算
const generate = (index) => {
let stable = arr[index] && ((2 * cache[0] + 1) > arr[index]);
while (arr.length < index + 1 || !stable) {
console.log('计算');
const newer = [];
cache.forEach((n) => {
const k2 = 2 * n + 1;
const k3 = 3 * n + 1;
// 不能有重复
if (!arr.includes(k2) && !newer.includes(k2)) {
newer.push(k2);
}
if (!arr.includes(k3) && !newer.includes(k3)) {
newer.push(k3);
}
})
arr.push(...newer); // 存入裂变出来的新数
cache = newer.sort((a, b) => a - b); // 新数也是下一次裂变的基数
arr.sort((a, b) => a - b); // 排序取目标值
stable = !!arr[index] && ((2 * cache[0] + 1) > arr[index]); // 下一次裂变有可能产生比目标值更小的树,导致目标值改变
}
return arr[index];
}
console.log(generate(10));
console.log(generate(100));
console.log(generate(20));
console.log(arr);
暂记
2025.04.25 更新
现在AI越来越强了,看看AI写的 (DeepSeek)
1、使用 优先队列
function getNthElement(n) {
const heap = [1]; // 维护一个优先队列(最小堆),用数组模拟
const seen = new Set([1]); // 防止重复
for (let i = 0; i < n; i++) {
const current = heap[0]; // 当前最小值
const candidates = [current * 3 + 1, current * 2 + 1];
// 生成新元素并去重
candidates.forEach(num => {
if (seen.has(num)) return; // 去重
seen.add(num);
// 二分查找插入位置
let left = 0, right = heap.length;
while (left < right) {
const mid = (left + right) >> 1;
heap[mid] < num ? left = mid + 1 : right = mid;
}
heap.splice(left, 0, num);
});
heap.shift(); // 移除已处理的元素
}
return heap[0]; // 第n+1个元素是新的堆顶
}
用数组模拟了一个最小堆,用二分查找降低时间复杂度,挺好。
不过没有保留整个数组,处理完的元素都扔了,也就没有缓存,每次调用都重新生成一遍。
2、使用 双指针双序列
function findNthElement(n) {
if (n === 0) return 1;
const arr2 = [3]; // 存储2k+1的序列
const arr3 = [4]; // 存储3k+1的序列
let i2 = 0, i3 = 0; // 双指针
for (let i = 1; i < n; i++) {
// 选择当前最小候选值
let current;
if (arr2[i2] < arr3[i3]) {
current = arr2[i2];
i2++;
} else if (arr3[i3] < arr2[i2]) {
current = arr3[i3];
i3++;
} else { // 处理相等的情况
current = arr2[i2];
i2++;
i3++;
}
// 生成新候选值(保证严格递增)
arr2.push(current * 2 + 1);
arr3.push(current * 3 + 1);
}
// 返回第n个元素
return Math.min(arr2[i2], arr3[i3]);
}
console.log(findNthElement(10)); // 22
console.log(findNthElement(100)); // 447
这应该是最快的实现方式了,时间复杂度只有 O(n)
不过,通用性就不太行了,如果每个数是裂变成3个或更多,就得准备更多序列和指针,比较大小时也麻烦了。
通用性还是 最小堆的算法最通用,如果是裂变成多个数也能用,就算不是 2k+1 这种简单裂变规则也都能用。
继续优化
双指针法的实现里,指针的移动,以及 两个序列数组的 push
都可以优化
function findNthElement(n) {
if (n === 0) return 1;
const arr2 = Array(n); // 预留空间 避免频繁的push数组扩容 消耗性能
const arr3 = Array(n);
arr2[0] = 3;
arr3[0] = 4;
let i2 = 0, i3 = 0; // 双指针
let index = 1; // 当前处理到了第几个元素
while (index < n) {
// 选择当前最小候选值 current就是每一个满足要求的元素
const curr2 = arr2[i2], curr3 = arr3[i3];
const current = curr2 <= curr3 ? curr2 : curr3;
// 生成新候选值(保证严格递增) 可见arr2 arr3 两数组长度永远是相等的
arr2[index] = current * 2 + 1; // 直接索引赋值,比push更快
arr3[index] = current * 3 + 1;
index++;
// 移动指针 如果相等的情况,两个指针都要移动
i2 += (current === curr2);
i3 += (current === curr3);
}
// 返回第n个元素
return Math.min(arr2[i2], arr3[i3]);
}
如果要做缓存,避免每次重新生成,也只需要把每次的 current
收集好就行
// 最小堆算法的 加缓存
const getNthElement = (() => {
const res = [1]; // 结果数组 闭包 缓存
const heap = [1]; // 维护一个优先队列(最小堆),用数组模拟
let index = 0; // 当前处理到了第几个元素
return (n) => {
if (res.length > n) return res[n]; // 直接返回缓存
res.length = n + 1; // 预留空间 以避免频繁的push数组扩容 消耗性能
while (index < n) {
const current = heap.shift(); // 弹出最小值,用来裂变
const candidates = [current * 3 + 1, current * 2 + 1]; // 生成新元素
// 插入新元素,维护最小堆,保证递增且不重复
candidates.forEach((num) => {
let left = 0; // 二分查找插入位置
let right = heap.length;
while (left < right) {
const mid = (left + right) >> 1;
heap[mid] < num ? (left = mid + 1) : (right = mid);
}
if (heap[left] === num) return; // 去重 不需要用Set
heap.splice(left, 0, num);
});
index++; // 索引同步递增
res[index] = heap[0]; // 此时堆顶元素已确定是下一个结果,放入结果数组
}
return res[n];
};
})();