JavaScript 构建不完全二叉树的数组

5

这个问题可能看起来像是一个重复的问题,但我在SO和其他地方都没能找到答案。

我想要用 JavaScript 从一个数组和一个非null根节点构建一个不完整二叉树,用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 作为其子节点,代码如下: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
}

不好意思,但是如果没有起始节点会发生什么呢? - Nina Scholz
我总是需要从一个非空的根节点开始。这就是为什么我用一个数字而不是一个空值来开始新的TreeNode()。因此,数组的第一个数字实际上是根节点的左子节点,第二个数字是右子节点。 - Viet
2
const myTree = new TreeNode(1); myTree.insert([2,null,4,null,6,7])的预期行为是什么?也就是说,如果您不创建应该是其他节点(67)的父节点的节点(3),会发生什么? - Scott Sauyet
1
@ScottSauyet:我已经更新了我的问题,并回答了你的问题。 - Viet
1
@HereticMonkey 我同意。但是,在我不得不实现另一个LinkedList类来构建BinaryTree之前,我想先尝试一下没有它的方法。 - Viet
显示剩余13条评论
5个回答

3
你做得很正确,检查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);
}());


1
感谢@trincot的回答。 - Viet

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; }


请注意,这不会为问题的第二个示例生成所需的结果。 - trincot

1
我会重新设计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处指定值,但在它们的父索引(1)处指定了null,则索引1处的整个子树将为null,并且将忽略索引3和4。

虽然这是一种有趣的方法,但它没有按照请求的方式处理第二种情况。 ==> (1 (2 (4 (6 (null, null)), (7 (null, null)), null), null) - Scott Sauyet

1

更新

使用专门的广度优先遍历函数(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。但是,除非我们实际上包含一个null在要插入的值中,否则它不会将leftright子节点设置为null。因此,有些节点只有一个val,有些节点有一个val和一个left,其他节点有一个val,一个left和一个right。通过留下这些空洞,我们保留了添加其他节点的位置。

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

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}

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

这样做的一个潜在问题是,不能保证任何节点的左或右子节点实际上已定义。在这里,我们使用JS的两个不同的nil值来完成不同的目的。null表示此子树被阻止。undefined表示虽然它还不存在,但可用于插入。有些人真的不喜欢以这种方式区分这两个零元值。我认为它符合语言的规则,但你的想法可能不同。

这不是我特别自豪的代码。我更喜欢永远不要改变数据结构,而是总是创建新的数据结构。尽可能地使用表达式而不是语句。但我已经花了太长时间在这上面,所以我现在不会花时间去看看是否可以整理它。如果没有其他办法,它提供了一种与此处其他答案不同的方法,并解决了它们无法解决的某些问题。

感谢@Scott提供详细的答案! - Viet

0
在 @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 < values.length,因为输入数组可能包含奇数个条目(缺少树的最右下角元素)。

不错,但当第二次调用 insert 时,这将破坏树中的先前内容。 - trincot
该问题涉及构建,而非扩展。 - kmoerman
可能确实如此,但第二次调用可能会导致一棵仍具有第一次调用的某些节点,而其他节点已被替换的树。这不是我所谓的构建。 - trincot
同意,针对顶级节点,感谢您分享您的关注。我已经编辑了代码以解决这个错误。 - kmoerman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接