【问题标题】:JavaScript build Incomplete Binary Tree from an arrayJavaScript 从数组构建不完整二叉树
【发布时间】:2020-06-06 05:10:16
【问题描述】:

这似乎是一个重复的问题,但我无法在 SOF 或其他地方的任何地方找到我的问题的答案。

我想在 JavaScript 具有 null 值表示 null 子树,而不是具有值 = null 的子树。

预期结果:

TreeNode {
  val: 1,
  left:
   TreeNode {
     val: 2,
     left: TreeNode { val: 4, left: null, right: null },
     right: null },
  right:
   TreeNode {
     val: 3,
     left: null,
     right: TreeNode { val: 5, left: null, right: null } } }

上面的预期输出可以像这样手动完成:

const myTree = new TreeNode(1);
myTree.left = new TreeNode(2);
myTree.left.left = new TreeNode(4);
myTree.right = new TreeNode(3);
myTree.right.right = new TreeNode(5);

树应该是这样的:

   1
   /\
  2  3
 /    \
4      5

这是我的代码:

class TreeNode {
    constructor(value) {
      this.value = value;
      this.left = null;
      this.right = null;
    }
    insert(values, i = 0) {
        if (i >= values.length) return;
        const queue = [this];
        while (queue.length > 0) {
            let current = queue.shift();
            if (current.left === null) {
                current.left = new TreeNode(values[i]);
                break;
            }
            queue.push(current.left);
            if (current.right === null) {
                current.right = new TreeNode(values[i]);
                break;
            }
            queue.push(current.right);
        }
        this.insert(values, i + 1);
        return this;
    }
}
(function() {
    // This builds a tree with null nodes with null values and null children.
    // The goal is to skip updating any null child
    const myTree = new TreeNode(1);
    myTree.insert2([2,3,4,null,null,5]);
    console.log(myTree);
}());

这里的问题是我得到一个 null 子作为 TreeNode ,其 value = null 像这样:

TreeNode {
  value: 1,
  left:
   TreeNode {
     value: 2,
     left: TreeNode { value: 4, left: null, right: null },
     right: TreeNode { value: null, left: null, right: null } },
  right:
   TreeNode {
     value: 3,
     left: TreeNode { value: null, left: null, right: null },
     right: TreeNode { value: 5, left: null, right: null } } }

我可以通过添加来处理空节点的子节点:

constructor(value) {
      this.value = value;
      if (value) this.left = null;
      if (value) this.right = null;
    }

但我无法仅添加 null 值而不是完整的 TreeNode。 我试过了:

if (current.left === null) {
    current.left = values[i] !== null ? new BinaryTree(values[i]) : null;
    break;
}

或者:

if (current.left === null && values[i]) {}

或者:

if (!values[i]) return;

但是没有返回预期的结果。

我还查看了一些使用 Java 的解决方案,但答案使用 Java 中的内置 LinkedList,所以我不确定在 JS 中这样做是否正确。

回答 Scott Sauyet 的问题:

的预期结果
const myTree = new TreeNode (1); 
myTree .insert ([2,null, 4, null, 6, 7])

是:

TreeNode {
    val: 1,
    left: TreeNode {
        val: 2,
        left: TreeNode {
            val: 4,
            left: TreeNode { val: 6, left: null, right: null },
            right: TreeNode { val: 7, left: null, right: null }
        },
        right: null
    },
    right: null
}

【问题讨论】:

  • 你能澄清一下吗?您的意思是:输出是否可以包含 0 作为空节点,或者您是否希望我在代码中添加 0 而不是空值?
  • 不,抱歉。但是如果你没有起始节点应该怎么办?
  • const myTree = new TreeNode (1); myTree .insert ([2,null, 4, null, 6, 7]) 的预期行为是什么?也就是说,如果你不创建应该是其他节点(67)的父节点的节点(3)会发生什么?
  • @ScottSauyet:我已经用你的问题的答案更新了我的问题。
  • @HereticMonkey 我同意。但是,在我必须实现另一个 LinkedList 类来构建 BinaryTree 之前,我想先尝试不使用。

标签: javascript binary-tree


【解决方案1】:

您通过检查values[i] 中的null 是正确的,当它是null 时不创建新节点,但您还需要跳过该节点以获得任何后续值。这在递归解决方案中很难做到,所以我建议让它迭代:

class TreeNode {
    constructor(value) {
      this.value = value;
      this.left = null;
      this.right = null;
    }
    insert(values) {
        const queue = [this];
        let i = 0;
        while (queue.length > 0) {
            let current = queue.shift();
            for (let side of ["left", "right"]) {
                if (!current[side]) {
                    if (values[i] !== null) {
                        current[side] = new TreeNode(values[i]);
                    }
                    i++;
                    if (i >= values.length) return this;
                }
                if (current[side]) queue.push(current[side]);
            }
        }
        return this;
    }
}

(function() {
    const myTree = new TreeNode(1);
    myTree.insert([2,3,4,null,null,5]);
    console.log(myTree);
}());

但请注意,如果您多次调用myTree.insert,则之前的null 节点 将被新数据扩展。

如果这是不希望的,那么您可以采取的一种措施是删除 insert 方法,并仅通过构造函数提供此功能。

或者,否则,您首先需要使用排队机制来找出应该扩展的节点。它们是那些没有 2 个子节点的节点,并且没有(按 BFS 顺序)有子节点的节点。

这是它的外观(以更大的树作为演示):

class TreeNode {
    constructor(value) {
      this.value = value;
      this.left = null;
      this.right = null;
    }
    getInsertionPoints() {
        // find uninterrupted series of leaves in BFS order
        const queue = [this];
        const leaves = [];
        while (queue.length) {
            let current = queue.shift();
            for (let side of ["left", "right"]) {
                if (current[side]) {
                    queue.push(current[side]);
                    leaves.length = 0; // reset
                } else {
                    leaves.push([current, side]);
                }
            }
        }
        return leaves;
    }
    insert(values) {
        let queue = this.getInsertionPoints();
        for (let value of values) {
            let [current, side] = queue.shift();
            if (value !== null) {
                current[side] = new TreeNode(value);
                queue.push([current[side], "left"], [current[side], "right"]);
            }
        }
        return this;
    }
}

(function() {
    const myTree = new TreeNode(1);
    myTree .insert ([2,null, 4, null, 6, 7]);
    myTree .insert ([8, null, 9, 10, 11, 12]);
    console.log(myTree);
}());

【讨论】:

  • 感谢@trincot 的回答。
【解决方案2】:

这是通过计算节点目标的不同方法。

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }

    insert(values) {
        let size = 1,
            number = 0;

        for (let value of values) {
            if (value !== null) {
                [...(number.toString(2).padStart(size, 0))].reduce((node, index, i, { length }) => {
                    let side = ['left', 'right'][index];
                    if (node[side] === null) {
                        node[side] = new TreeNode(i + 1 === length ? value : 'missing');
                    }
                    return node[side];
                }, this);
            }
            number++;
            if (number === 1 << size) {
                size++;
                number = 0;
            }
        }
    }
}

const myTree = new TreeNode(1);
myTree.insert([2, 3, 4, null, null, 5]);

console.log(myTree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

【讨论】:

  • 请注意,对于 OP 添加到他们的问题的第二个示例,这不会产生所需的结果。
【解决方案3】:

我将重新设计 insert 函数,使其递归调用子实例而不是父实例的 insert

如果示例数组包含 15 个元素,则它的值位于索引 0-14。我们在索引和树中位置之间有一个映射。以下是索引如何映射到具有 15 个节点的完整树:

            0
           / \
          /   \
         /     \
        /       \
       /         \
      1           2
     / \         / \
    /   \       /   \
   3     4     5     6
  / \   / \   / \   / \
 7   8 9  10 11 12 13 14

当我们将根节点#0 告诉.insert([ 'a', 'b', 'c', 'd', 'e', 'f', ... ]) 时,我们知道节点#1 获取索引0 处的值('a'),#2 获取索引1 处的值('b')等等. 节点#1 获取索引0,节点#2 获取索引1,节点#3 获取索引2...模式是索引n 是节点#n + 1 的值。这是因为索引 0 不是指根节点,而是指根节点的左子节点。

父节点的索引与其子节点的索引之间存在关系。我们可以称之为getChildIndices(parIndex)

getChildIndices(0) = [ 1, 2 ];
getChildIndices(1) = [ 3, 4 ];
getChildIndices(2) = [ 5, 6 ];
getChildIndices(3) = [ 7, 8 ];
getChildIndices(4) = [ 9, 10 ];
getChildIndices(5) = [ 11, 12 ];
getChildIndices(6) = [ 13, 14 ];
.
.
.

因为你选择的这个映射有一定的优雅,所以功能很简单:

let getChildIndices = parIndex => [ parIndex * 2 + 1, parIndex * 2 + 2 ];

这样我们就可以写出更优雅的插入函数了:

let getChildIndices = parInd => [ parInd * 2 + 1, parInd * 2 + 2 ];

class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
  insert(values, myInd=0) {
    
    let [ lInd, rInd ] = getChildIndices(myInd);
    
    let vals = [
      { ind: lInd, prop: 'left' }, // `lInd` corresponds to our "left" property
      { ind: rInd, prop: 'right'}  // `rInd` corresponds to our "right" property
    ];
    
    for (let { ind, prop } of vals) {
      
      // If we're out-of-bounds in `values`, stop!
      if ((ind - 1) >= values.length) continue;
      
      // A value of `null` in `values` indicates the subtree is null. Otherwise, a child
      // node exists.
      let shouldNodeExist = values[ind - 1] !== null;
      
      if (this[prop] && !shouldNodeExist) { // If the node exists and shouldn't...
        
        // Set the node to null
        this[prop] = null;
        
      } else if (!this[prop] && shouldNodeExist) { // If the node doesn't exist and should...
        
        // Create a new child node
        this[prop] = new TreeNode(null);
        
      }
      
      // Recursively call insert on the new child node, informing it of its index
      if (this[prop]) {
        this[prop].value = values[ind - 1];
        this[prop].insert(values, ind);
      }
      
    }
      
  }
}

let tree1 = new TreeNode(1);
tree1.insert([ 2, 3, 4, null, null, 5 ]);
console.log({ tree1 });

let tree2 = new TreeNode(1); 
tree2.insert([ 2, null, 4, null, 6, 7 ]);
console.log({ tree2 });

请注意,此insert 方法允许从树中添加、修改和删除节点。任何包含null 的索引都会导致其对应的子树被删除。

请注意,如果我们尝试使用此方法指定不连续的子树 - 例如我们在索引 3 和 4 处指定值,但 null 在其父索引 (1) 处,索引 1 处的整个子树将为空,索引 3 和 4 将被忽略。

【讨论】:

  • 虽然这是一种有趣的方法,但它并没有按照请求的方式处理第二种情况。 ==&gt; (1 (2 (4 (6 (null, null)), (7 (null, null)), null), null)
【解决方案4】:

更新

这可以通过专用的广度优先遍历函数 (bft.) 变得更简洁。

const bft = (nodes) => 
  nodes .length == 0
    ? []
    : [
        ...nodes, 
        ...bft(nodes.flatMap (n => n .left ? n.right ? [n.left, n.right] : [n.left] : []))
      ] 

class TreeNode {
  constructor (val) {
    this.val = val
  }
  insert (vals) {
    return vals.reduce ((
      tree, val, _, __,
      parent = bft([tree]) .find (n => n.left === void 0 || n.right === void 0) || {}
    ) => 
      ((parent [parent.left === void 0 ? 'left' : 'right'] = val === null 
        ? null 
        : new TreeNode(val)),
      tree), 
    this)
  }
}

您可以在以下片段中看到这种方法:

const bft = (nodes) => 
  nodes .length == 0
    ? []
    : [
        ...nodes, 
        ...bft(nodes.flatMap (n => n .left ? n.right ? [n.left, n.right] : [n.left] : []))
      ] 

class TreeNode {
  constructor (val) {
    this.val = val
  }
  insert (vals) {
    return vals.reduce ((
      tree, val, _, __,
      parent = bft([tree]) .find (n => n.left === void 0 || n.right === void 0) || {}
    ) => 
      ((parent [parent.left === void 0 ? 'left' : 'right'] = val === null 
        ? null 
        : new TreeNode(val)),
      tree), 
    this)
  }
}


let myTree = new TreeNode (1);
myTree .insert ([2, 3, 4, null, null, 5]);
console .log (myTree);

// Handles subsequent inserts
myTree .insert ([6, null, null, null, 7, 8]);
console .log (myTree);

// Handles blocked nodes
myTree = new TreeNode(1); 
myTree.insert([ 2, null, 4, null, 6, 7 ]);
console.log(myTree);

// Fails (somewhat) gracefully when node cannot be inserted
myTree = new TreeNode (1);
myTree .insert ([null, null, null, 2]);
console .log (myTree);
.as-console-wrapper {min-height: 100% !important; top: 0}

原答案

另一种可能性与这里的大多数答案不同。

大多数其他答案允许单个insert,此时也可以折叠到构造函数中。

这个允许多个inserts。但它不会将leftright 子级设置为null,除非我们实际上在要插入的值中包含null。所以有些节点只有val,有些节点有valleft,还有一些节点有valleftright。通过留下这些漏洞,我们保留了以后添加其他节点的位置。

这种技术也相当优雅地处理了一切都已满的情况,只是拒绝添加剩余的节点。

class TreeNode {
  constructor (val) {
    this .val = val
  }
  insert (vals) {
    return vals .reduce ((t, v) => {
      const queue = [t]
      while (queue .length) {
        let node = queue .shift ()
        if (node .left === void 0) {
          node .left = v === null ? null : new TreeNode(v)
          return t
        } else  if (node .right === void 0) {
          node .right = v === null ? null : new TreeNode(v)
          return t
        } else {
          if (node .left !== null) {
            queue .push (node .left)
          } 
          if (node .right !== null) {
            queue.push (node.right)
          }
        }
      }
      return t // If we hit this point, then our insert failed: there's no room left.      
    }, this)
  }
}

let myTree = new TreeNode (1);
myTree .insert ([2, 3, 4, null, null, 5]);
console .log (myTree);

// Handles subsequent inserts
myTree .insert ([6, null, null, null, 7, 8]);
console .log (myTree);

// Handles blocked nodes
myTree = new TreeNode(1); 
myTree.insert([ 2, null, 4, null, 6, 7 ]);
console.log(myTree);

// Fails (somewhat) gracefully when node cannot be inserted
myTree = new TreeNode (1);
myTree .insert ([null, null, null, 2]);
console .log (myTree);
.as-console-wrapper {min-height: 100% !important; top: 0}

我们使用广度优先遍历使用队列来保持节点仍然要访问,从根开始,如果我们还没有找到插入点,则添加每个非空的左右子节点。

这样做的最大潜在缺点是不能保证任何节点的leftright 子节点都已实际定义。我们在这里将 JS 的两个不同的 nil 值用于不同的目的。 null 表示该子树被阻塞。 undefined 表示虽然它还不存在,但可以插入。有些人真的不喜欢以这种方式区分这两个 nilary 值。我认为它适用于语言的颗粒,但 YMMV。

这不是我特别引以为豪的代码。我宁愿从不改变数据结构,而是总是创建新的。而且我更喜欢尽可能使用表达式而不是语句。但是我已经花了太长时间了,所以我现在不会花时间看看我是否可以清理它。如果不出意外,它提供了与此处的其他答案不同的方法,并解决了他们没有解决的某些问题。

【讨论】:

  • 感谢@Scott 的详细回答!
【解决方案5】:

基于@trincot 的迭代解决方案,下面的代码循环输入而不是队列。

class TreeNode {
    constructor(value) {
      this.value = value;
      this.left = null;
      this.right = null;
    }
    
    insert(values) {
        // clear current children before insert
        this.left = this.right = null;

        const queue = [this];
        for (var i = 0; i < values.length; ) {
          const current = queue.shift();
          for (let side of ["left", "right"]) {
            if (i < values.length && values[i] !== null) {
              current[side] = new TreeNode(values[i]);
              queue.push(current[side]);
            }
            ++i;
          }
        }
        return this;
    }
}


const myTree = new TreeNode(1);
myTree.insert([2,3,4,null,null,5]);
console.log(myTree);

请注意,您将始终从左到右填充节点,并且您将逐级向下遍历树级别(根据您的要求跳过空值)。更深的节点总是新创建的,每个节点只被访问一次。因此,无需检查之前是否设置了 leftright 子级。

添加了额外的检查i &lt; values.length,因为输入数组可能包含奇数个条目(缺少树最右边的底部元素)。

【讨论】:

  • 很好,但是当第二次调用insert 时,这会破坏树中以前的内容。
  • 问题是指构建,而不是扩展。
  • 可能是这样,但第二次调用可能会导致一棵树仍然具有第一次调用的一些节点,而其他节点已被替换。这不是我所说的建筑。
  • 同意,对于顶级节点,感谢您的关注。我已经编辑了代码来解决这个错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 1970-01-01
  • 2020-08-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多