将代码重构为尾递归

时间:2018-08-02 14:39:45

标签: javascript recursion stack tail-recursion

我有一个函数可以通过“节点”,这些节点具有一个键和自己的nodes数组。我也有一组允许的键。目标很简单:如果节点具有允许的密钥,或者子节点具有允许的密钥,则保留该密钥,否则,将其删除。当前的解决方案如下所示:

var allowedKeys = ['x', 'y', 'z'];

var nodes = [{
    key: 'x',
    nodes: []
  },
  {
    key: 'b',
    nodes: [{
        key: 'y',
        nodes: []
      },
      {
        key: 'lol',
        nodes: []
      },
    ]
  },
  {
    key: 'c'
  },
  {
    key: 'd',
    nodes: []
  },
  {
    key: 'e',
    nodes: [{
        key: 't',
        nodes: [{
          key: 'z',
          nodes: []
        }]
      },
      {
        key: 'r',
        nodes: []
      },
    ]
  },
  {
    key: 'f',
    nodes: []
  }

];

function hasChildNodes(node) {
  var hasChildNodes = node.hasOwnProperty('nodes') && node.nodes.length > 0;
  return hasChildNodes;
}

function removeUnnecessaryNodes(nodes, allowedKeys) {
  nodes.forEach(node => {
    if (hasChildNodes(node)) {
      node.nodes = removeUnnecessaryNodes(node.nodes, allowedKeys);
    }
  });

  nodes = nodes.filter(node => allowedKeys.includes(node.key) || hasChildNodes(node));

  return nodes;
}


var filteredNodes = removeUnnecessaryNodes(nodes, allowedKeys);
console.log(filteredNodes);

我想将其重构为使用尾递归,以避免炸毁堆栈。我开始觉得这是不可能的。

P.S。我意识到,大多数JavaScript实现都不使用尾递归优化,这很有趣。

1 个答案:

答案 0 :(得分:1)

在我的评论之后,我想尝试一个堆栈版本。它不像我希望的那样干净,但是我认为无论如何我都会张贴它,以希望它能以某种方式发光。

var nodes = [{
    key: 'x',
    nodes: []
  },
  {
    key: 'b',
    nodes: [{
        key: 'y',
        nodes: []
      },
      {
        key: 'lol',
        nodes: []
      },
    ]
  },
  {
    key: 'c'
  },
  {
    key: 'd',
    nodes: []
  },
  {
    key: 'e',
    nodes: [{
        key: 't',
        nodes: [{
          key: 'z',
          nodes: []
        }]
      },
      {
        key: 'r',
        nodes: []
      },
    ]
  },
  {
    key: 'f',
    nodes: []
  }
];

var allowedKeys = ['x', 'y', 'z'];

const removeUnnecessaryNodes = (nodes, allowedKeys) => {
  const allowed = new Set(allowedKeys);
  const root = {nodes: nodes};
  const stack = [root];
  
  while (stack.length) {
    let curr = stack.pop();
    
    if (curr.nodes && curr.nodes.length) {

      // non-empty node, push children on stack
      curr.nodes.forEach(e => {
        e.parent = curr;
      	stack.push(e);
      });
    }
    else if (!allowed.has(curr.key)) {

      // this is a disallowed leaf; remove it
      let p = curr.parent;

      if (p) {
        p.nodes = p.nodes.filter(e => e !== curr); // O(n)
      }

      // move up the structure, pruning dead ancestors
      while (p && !p.nodes.length && !allowed.has(p.key)) {
        curr = p;
        p = curr.parent;

        if (p) {
          p.nodes = p.nodes.filter(e => e !== curr); // O(n)
        }
      }
    }
  }

  // clean up temporary parent keys
  stack.push(root);
  
  while (stack.length) {
    const curr = stack.pop();
    
    if (curr.nodes) {
      curr.nodes.forEach(e => {
        delete e.parent;
      	stack.push(e);
      });
    }
  }
  
  return root.nodes;
};

var filteredNodes = removeUnnecessaryNodes(nodes, allowedKeys);
console.log(JSON.stringify(filteredNodes, null, 4));

说明

准备工作包括向外部数组添加一个nodes键,并将allowedKeys转换为一组以进行快速查找。然后,DFS结构,为每个键分配新的parent属性,并将子级推入堆栈。如果到达叶节点且不允许使用其键,请从其父节点列表取消链接。如果然后将父级列表设为空,并且不允许父级节点的键,则也要取消链接并重复,直到到达有效的祖先为止。最后,在结构上执行另一个DFS,以删除临时父属性。

需要改进的地方

我试图使用一个对象来存储父级属性,但是遇到比较/复制问题,并求助于链表方法。我确信可以解决此问题,保存第二个清理DFS和潜在的密钥冲突。

此外,节点列表过滤过程为O(n),一旦找到子密钥,就可以通过哈希或至少早期的救援来改进。

我确定还有其他人!我很想听听反馈或听听反馈如何在大输入量下实现。