例如,我们有系列 1、2、3、4、5。我们取每 3 个元素 => 3、1、5、2、4(选择的元素不应该保留,当系列不为空时我们可以取)。通过循环双向链表的幼稚实现并不是性能的好主意。你能给我一个建议,哪些数据结构和算法更适用吗?
4 回答
如果您只需要最后一个数字,则称为约瑟夫斯问题,并且有众所周知的公式可以O(N)
及时计算答案。
我不知道是否可以调整它以运行完整的模拟,所以我将O(N log N)
在这里描述一个简单的解决方案:
让我们使用隐式键将所有数字保存在一个陷阱中。我们需要k
在每一步找到第-个元素并删除它(实际上,可以有一个移位,所以它更像是(cur_shift + k) % cur_size
,但这并不重要)。一个陷阱可以做到。我们只需要将其拆分为 3 部分[0, k - 1]
,[k, k]
然后[k + 1, cur_size - 1]
打印与第二部分对应的节点中的数字,然后将第一部分和最后一部分重新合并在一起。每一步都需要O(log N)
时间,因此对于给定的约束应该足够好。
构建一个包含数字 1 到 n 的完整二叉树,例如对于 n=15,它将是:
在每个分支中,存储其左侧的节点数;这将使我们能够快速找到第 i 个节点。(您会看到这棵树具有非常可预测的结构和值,并且生成它比构建具有随机排序值的相同大小的二叉树要高效得多。它也是树中树的理想候选者。大批。)
然后,要找到第 i 个数字,从根节点开始,在每个节点上,如果 i 比左边的节点数大 1,则找到第 i 个数字,否则向左走(如果i 不大于左侧的节点数)或右侧(如果 i 比左侧的节点数大 1 多)。
每当您向左走时,减少该节点左侧的节点数(因为我们将删除一个)。
每当您向右走时,将要查找的数字减去节点左侧的节点数,再加 1(如果节点中的值已被删除,则加 0)。
当您找到第 i 个节点时,读取其值(以添加到移除顺序列表),然后将其值设置为 0。此后,如果我们正在寻找的第 i 个节点的值已被删除,我们将向右走,然后取最左边的节点。
我们从值 i = k 开始,然后每次我们删除第 i 个节点中的数字时,我们都会减少节点的总数并设置i = (i + k - 1) % total
(或者如果它为零:)i = total
。
这给出了 log 2 N 的查找时间和 N×LogN 的总复杂度。
示例演练:在n=15(如上图)和k=6的情况下,第一步是 6、12、3、10、2。此时的情况是:
我们刚刚删除了第二个数字,现在i = 2 + 6 - 1 = 7
. 我们从根节点开始,它的左边有 4 个节点,并且仍然有它的值,所以我们向右走,从我们正在寻找的 7 中减去 5,得到 2。我们到达节点 12(已经擦除)并发现它的左边有2个节点,所以我们减少它左边的节点数,然后向左走。我们来到节点 10(已被擦除),发现它的左边有 1 个节点,并且 1 = 2 - 1 所以这就是我们要寻找的节点;然而,由于它的值已被删除,我们向右走并从我们正在寻找的 2 中减去 1 并得到 1。我们到达节点 11,它的左侧有 0 个节点(因为它是叶子),并且0 = 1 - 1,所以这是我们正在寻找的节点。
然后我们将节点的总数从 10 减少到 9,并将 i 从 7 更新到(7 + 6 - 1) % 9 = 3
并继续找到第三个节点(现在是值为 5 的节点)。
这是 JavaScript 中的一个简单实现。它在不到一秒的时间内解决了 100,000 个数字的序列,并且通过使用数组中的树结构,它可能会变得更快、更节省空间。
(与上面的解释不同,数字的索引是从零开始的,为了简化代码;所以索引 0 是树中的第一个数字,我们寻找具有多个左连接子节点的节点,等于目标指数。)
function Tree(size) { // CONSTRUCTOR
var height = Math.floor(Math.log(size) / Math.log(2));
this.root = addNode(height, 1 << height, size);
this.size = size;
function addNode(height, value, max) { // RECURSIVE TREE-BUILDER
var node = {value: value > max ? 0 : value, lower: (1 << height) - 1};
if (height--) {
node.left = addNode(height, value - (1 << height), max);
if (value < max) { // DON'T ADD UNNECESSARY RIGHT NODES
node.right = addNode(height, value + (1 << height), max);
}
}
return node;
}
}
Tree.prototype.cut = function(step) { // SEE ANSWER FOR DETAILS
var sequence = [], index = (step - 1) % this.size;
while (this.size) {
var node = this.root, target = index;
while (node.lower != target || node.value == 0) {
if (target < node.lower) {
--node.lower;
node = node.left;
} else {
target -= node.lower + (node.value ? 1 : 0);
node = node.right;
}
}
sequence.push(node.value);
node.value = 0;
index = (index + step - 1) % --this.size;
}
return sequence;
}
var tree = new Tree(15);
var sequence = tree.cut(6);
document.write("15/6→" + sequence + "<BR>");
tree = new Tree(100000);
sequence = tree.cut(123456);
document.write("100000/123456→" + sequence);
笔记:
如果查看 n=10 的树,您会看到根右侧的节点有一棵不完整的树,左侧有 2 个节点,但是上面代码示例中实现的算法给出了不正确的左侧-节点数为 3 而不是 2:
但是,左侧有不完整树的节点本身永远不会保存值,右侧也永远不会有节点。所以不管怎样,你总是往左边走,他们的左节点数量太高这一事实并不重要。
这是一个用数组表示二叉树的实现,只将左子树的大小存储为节点值。输入数组实际上并没有被存储,而是默默地假设为底层的叶子,在二叉树下面:
function josephusPermutation(size, step) {
var len = 1 << 32 - Math.clz32(size-1), // Smallest power of 2 >= size
tree = Array(len).fill(0), // Create tree in array representation
current = 0,
skip = step - 1,
result = Array(size).fill(0),
goRight, leftSize, order, i, j;
// Initialise tree with sizes of left subtrees as node values
(function init(i) {
if (i >= len) return +(i - len < size); // Only count when within size
var left = tree[i] = init(i*2); // recursive, only store left-size
return left + (left ? init(i*2+1) : 0); // return sum of left and right
})(1);
for (j = 0; j < result.length; j++, size--) {
current = (current + skip) % size; // keep within range
order = current;
for (i = 1; i < len; i = i*2+goRight) {
leftSize = tree[i];
goRight = order >= leftSize;
if (goRight) {
order -= leftSize; // Moving rightward, counting what is at left side.
} else {
tree[i]--; // we will remove value at left side
}
}
result[j] = 1 + i - len;
}
return result;
}
var sequence = josephusPermutation(100000, 123456);
console.log(sequence.join(','));
下面是 Lei Wang 和 Xiaodong Wang (2013) 1 O(n log k)
算法的实现(如果不是基于 Errol Lloyd 于 1983 年发表的算法,则非常相似)。这个想法是将原始序列划分n/m
为高度的二叉树log k
。该算法实际上是为“猫科动物”约瑟夫斯问题设计的,其中参与者可以拥有不止一个生命(在下面的数组变量中列出global.l
)。
我也喜欢O(1)
Knuth、Ahrens 和 Kaplansky 的空间算法(在 Gregory Wilson 的硕士论文中进行了概述,加利福尼亚州立大学,海沃德,1979 2),虽然处理速度可能很快,但取决于参数。
Knuth 的算法J(n,d,t)
(t
是ith
命中),一个降序:
Let x1 = d * t and for k = 2,3,...,
let x_k = ⌊(d * x_(k−1) − d * n − 1) / (d − 1)⌋
Then J(n,d,t) = x_p where x_p is the first term in the sequence <= n.
Ahrens 算法J(n,d,t)
, 一个升序:
Let a1 = 1 and for k = 2,3,...
let a_k = ⌈(n − t + a_(k−1)) * d / (d − 1)⌉
If a_r is the first term in the sequence such that a_r + 1 ≥ d * t + 1
then J(n,d,t) = d * t + 1 − a_r.
卡普兰斯基算法J(n,d,t)
:
Let Z+ be the set of positive integers and for k =1,2,...,t
define a mapping P_k : Z+ → Z+ by P_k(m) = (m+d−1)−(n−k+1)(m−k+d−1)/(n−k+1)
Then, J(n,d,t) = P1 ◦ P2 ◦···◦Pt(t).
JavaScript 代码:
var global = {
n: 100000,
k: 123456,
l: new Array(5).fill(1),
m: null,
b: null,
a: [],
next: [],
prev: [],
i: 0,
limit: 5,
r: null,
t: null
}
function init(params){
global.m = Math.pow(2, Math.ceil(Math.log2(params.k)));
params.b = Math.ceil(params.n / global.m);
for (let i=0; i<params.b; i++){
let s = i * global.m,
t = (i + 1) * global.m,
u = [];
for (let j=0; j<global.m; j++)
u[j] = 0;
for (let j=s; j<=Math.min(t-1,params.n-1); j++)
u[j-s] = -(j + 1);
global.a[i] = [];
build(u, global.a[i]);
t = (i + 1) % params.b;
params.next[i] = t;
params.prev[t] = i;
}
}
function build(u,v){
function count(_v, i){
if (global.m < i + 2){
if (_v[i] < 0)
return 1;
else
return 0;
} else {
_v[i] = count(_v, 2*i + 1);
_v[i] = _v[i] + count(_v, 2*i + 2);
return _v[i];
}
}
for (let i=0; i<global.m; i++)
v[global.m + i - 1] = u[i];
count(v, 0);
}
function algorithmL(n, b){
global.r = 0;
global.t = b - 1;
while (global.i < global.limit){
tree(global, global);
let j = leaf(global, global);
hit(global.i,j,global,global);
global.i = global.i + 1;
}
}
function tree(params_r,params_t){
if (params_t.t === global.next[params_t.t] && params_r.r < global.k){
params_r.r = global.k + global.a[params_t.t][0] - 1 - (global.k - params_r.r - 1) % global.a[params_t.t][0];
} else {
while (params_r.r < global.k){
params_t.t = global.next[params_t.t];
params_r.r = params_r.r + global.a[params_t.t][0];
}
}
}
function size(t,j){
if (global.a[t][j] < 0)
return 1
return global.a[t][j];
}
function leaf(params_r,params_t){
let j = 0,
nxt = params_r.r - global.k;
while (j + 1 < global.m){
let rs = size(params_t.t, 2*j + 2);
if (params_r.r - rs < global.k){
j = 2*j + 2;
} else {
j = 2*j + 1;
params_r.r = params_r.r - rs;
}
}
params_r.r = nxt;
return j;
}
function hit(i,j,params_r,params_t){
let h = -global.a[params_t.t][j];
console.log(h);
if (global.l[h-1] > 1)
global.l[h-1] = global.l[h-1] - 1;
else
kill(i,j,params_r,params_t);
}
function kill(i,j,params_r,params_t){
global.a[params_t.t][j] = 0;
while (j > 0){
j = Math.floor((j - 1) / 2);
global.a[params_t.t][j] = global.a[params_t.t][j] - 1;
}
if (params_t.t !== global.next[params_t.t]){
if (global.a[params_t.t][0] + global.a[global.next[params_t.t]][0] === global.m){
params_r.r = params_r.r + global.a[global.next[params_t.t]][0];
combine(params_t);
} else if (global.a[params_t.t][0] + global.a[global.prev[params_t.t]][0] === global.m){
t = global.prev[params_t.t];
combine(params_t);
}
}
}
function combine(params_t){
let x = global.next[params_t.t],
i = 0,
u = [];
for (let j=0; j<global.m; j++)
if (global.a[params_t.t][global.m + j - 1] < 0){
u[i] = global.a[params_t.t][global.m + j - 1];
i = i + 1;
}
for (let j=0; j<global.m; j++)
if (global.a[x][global.m + j - 1] < 0){
u[i] = global.a[x][global.m + j - 1];
i = i + 1;
}
build(u,global.a[params_t.t]);
global.next[params_t.t] = global.next[global.next[params_t.t]];
global.prev[global.next[params_t.t]] = params_t.t;
}
init(global);
algorithmL(global.n, global.b);
(1) L. Wang 和 X. Wang。广义约瑟夫斯问题算法的比较研究。应用数学与信息科学, 7, No. 4, 1451-1457 (2013)。
(2) Wilson (1979) 的参考文献:
Knuth, DE, The Art of Computer Programming , Addison-Wesley, Reading Mass., Vol I Fundal Algorithms, 1968, Ex. 22, p158; 卷。III,排序和搜索,例如。2,第 18-19 页;卷。I,第 2 版,第 181 页。
Ahrens, W., Mathematische Unterhaltungen und Spiele , Teubner: Leipzig, 1901, Chapter 15, 286-301。
Kaplansky, I. 和 Herstein IN,Matters Mathematical,切尔西,纽约,1978 年,第 121-128 页。