10. 二叉树集合【LC102】

372 阅读5分钟

题目: 【LC102】层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

image.png

关键思路: BFS实现,采用队列即可,每次把当前层的所有节点放到队列中,按层遍历即可。

var levelOrder = function (root) {
  let res = [];
  if (!root) {
    return res;
  }
  const queue = [root]; //初始队列
  while (queue.length) {
    let len = queue.length; //当前层节点的数量
    const tempArr = []; //新的层数组
    for (let i = 0; i < len; i++) {
      const node = queue.shift();
      tempArr.push(node.val);
      if (node.left) q.push(node.left); //检查左节点,存在左节点就继续加入队列
      if (node.right) q.push(node.right); //检查左右节点,存在右节点就继续加入队列
    }
    res.push(tempArr) //推入当前层的数组
  }
  return res;
}

知识拓展:

(1)JS实现一棵BST

二叉树的一个节点Node由val,left,right组成,val保存本节点的值,left指向左节点,right指向右节点。

二叉树有一个特殊性:相对本节点较小的值保存在左节点,相对本节点较大的值保存在右节点

节点数据

function Node(val,left,right){ 
    this.val = val; 
    this.left = left; 
    this.right = right; 
}

定义BST

function BST(){ 
    this.root = null; 
    this.insert = insert; 
    this.find = find; 
    this.remove = remove; 
}

定义插入方法

function insert(data) {
    const node = new Node(data, null, null);
    if(!this.root) {
        this.root = node;
        return;
    }
    let curNode = this.root;
    while(true) {
        if(curNode.val > data) { // 往左侧插入
            if(!curNode.left) {
                curNode.left = node;
                break;
            } 
            curNode = curNode.left;// 继续往左找
        } else {
            if(!curNode.right) {
                curNode.right = node;
                break;
            } 
            curNode = curNode.right;
        }
    }
}

定义二叉树查询方法

function find(data) {
    let curNode = this.root;
    while(true) {
        if(curNode.val === data) {
            return curNode;
        }
        curNode = curNode.val > data ? curNode.left : curNode.right;
        if(!curNode) { //找不到的情况
            return null;
        }
    }
}

定义二叉树删除:

二叉树删除节点有两个子节点的情况,比较复杂,重点来说下:

最难的操作就是下面四点,只要找到了下面四个节点,删除操作就游刃而解了

1、找到要删除的节点

2、找到要删除节点的父节点

3、找到要删除节点的前驱或后继节点(如下以找后继为例)

4、找到要删除的节点的前驱或后继节点的父节点(如下以找后继的父节点为例)

其中,1和2,只需要对树进行遍历,定义一个delparent来每次存储遍历到得节点的前一个节点,这样便能在找到要删除节点的同时,找到要删除节点的父节点。

这里找后继相对比较简单, 因为是找后继,所以要从删除节点的右孩子开始寻找,找出以右孩子为根节点的树中最小的那个节点 (同理找前驱则是以左孩子为根节点的树中最大的那个节点), 由于是找最小,所以一直遍历.left,就可以找出来了。

    var backsuccussor = delNode.right;
    var backsuccussorParent = delNode.right;
    var backsuccussorRight = backsuccussor;
    //寻找后继节点 & 后继节点父节点
    while (backsuccussor.left != null) {
        backsuccussorParent = backsuccussor;
        backsuccussor = backsuccussor.left;
    }

当然,前驱、后继都是按中序遍历。后继节点父节点(因为是寻找后继节点,所以后继节点一定为其父节点的左孩子,而后继节点如果有孩子就一定是有孩子)

image.png

11是10的后继节点(右树的最左侧)

将11的父节点的left,指向11的right。

image.png

把10节点删除,重新替换为11节点(其后继节点)

11节点的left指向10节点的left 11节点的right指向10节点的right

image.png

最后一步,把原来10节点的父节点(8节点)的left或者right指向11节点 image.png

整体回顾下有两个子节点的二叉树删除思路:

image.png

整体来看下删除方法:

function remove(data) {
  let cur = this.root;
  let delNode = null;
  let delParent = null;
  let isLeft = true; // delNode 在 delParent 的左支还是右支;

  // 找出删除节点及其父节点
  while (cur && cur.val !== data) {
    delParent = cur;
    if (cur.val > data) {
      isLeft = true;
      cur = cur.left;
    } else {
      isLeft = false;
      cur = cur.right;
    }
  }
  let isRoot = this.root == cur;
  if (cur.val !== data) {
    return false; //没找到当前值的节点,删除失败
  }
  delNode = cur;
  // 缺少找不到的情况处理方案

  // 1)删除节点没有子节点的情况
  if (!delNode.left && !delNode.right) {
    if (isRoot) {
      this.root = null
    } else {
      if (isLeft) {
        delParent.left = null
      } else {
        delParent.right = null
      }
    }
  }
  // 2)删除节点只有一个子节点
  if (delNode.left ^ delNode.right) {
    if (isLeft) {
      delParent.left = delNode.left || delNode.right || null;
    } else {
      delParent.right = delNode.left || delNode.right || null;
    }
  }
  // 3)删除节点有两个子节点
  if (delNode.left && delNode.right) {
    // 找其后继节点
    let backsuccussor = delNode.right;
    let backsuccussorParent = delNode.right;
    while (backsuccussor.left) {
      backsuccussorParent = backsuccussor;
      backsuccussor = backsuccussor.left
    };
    //后继节点,其父一定有left,本身一定无左支。
    backsuccussorParent.left = backsuccussor.right;
    backsuccussor.left = delNode.left;
    backsuccussor.right = delNode.right;
    if (isRoot) {
      this.root = backsuccussor;
    } else if (isLeft) {
      delParent.left = backsuccussor;
    } else {
      delParent.right = backsuccussor
    }
  }
  return true;
}

二叉树的缺点:如果节点要频繁删除的话,不适合使用二叉树,特别是被删除的节点有两个子节点的;

二叉树的优点:查找无规律的数字或者字母的数据结构适合二叉树的使用场景

(2)二叉树的遍历

二叉树的先序,中序,后序遍历:(无非是什么时机输出root)

先序遍历表示先访问根节点,然后访问左节点,最后访问右节点。

中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。

后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。

//递归实现
function traversal(root) {
  if (root) {
    console.log(root.val) // 先序
    traversal(root.left)
    console.log(root.val) // 中序
    traversal(root.right)
    console.log(root.val) // 后序
  }
}

非递归实现

非递归实现使用了栈的结构,通过栈的先进后出模拟递归实现。 以下是先序遍历代码实现。

//非递归实现,先序遍历
function preOrder(root) {
  if (root) {
    const stack = [root];
    while (stack.length) {
      const node = stack.pop();
      console.log(node.val);
      if (node.right) { //注意,先入右,再入左,出栈就是中左右的顺序了
        stack.push(node.right)
      } else if (node.left) {
        stack.push(node.left)
      }
    }
  }
}


image.png

中序遍历非递归实现思路:

注意,理解上,第一步可以先把树补全,即把叶子节点的左右都指向null

中序遍历是先左再根最后右

  • 首先应该先把最左边节点遍历到底依次 push 进栈
  • 当左边没有节点时,就打印栈顶元素,然后寻找右节点
  • 对于最左边的叶节点来说,可以把它看成是两个 null 节点的父节点
  • 左边打印不出东西就把父节点拿出来打印,然后再看右节点
function inOrder(root) {
  if (root) {
    let stack = [root];
    while (stack.length) {
      if (root.left) {
        stack.push(root.left) // 左侧全部压栈
        root = root.left;
      } else {
        //左侧栈顶元素没有再左的了,弹出栈顶元素
        const node = stack.pop();
        console.log(node.val)
        // 如果当前栈顶元素有右侧分支
        if (node.right) {
          stack.push(node.right);
          root = node.right; // 右侧分支的左分支全部压栈
        }
        // 没有的话会继续弹出栈顶节点
      }
    }
  }
}

非递归形式实现后序遍历:

// 后序遍历是先左再右最后根

// 所以对于一个栈来说,应该先 push 根节点

// 然后 push 右节点,最后 push 左节点

function backOrder(root) {
  if (root) {
    let stack1 = [root];
    let stack2 = [];
    // 后序遍历是先左再右最后根
    // 所以对于一个栈来说,应该先 push 根节点
    // 然后 push 右节点,最后 push 左节点
    while (stack1.length) {
      let node = stack1.pop();
      stack2.push(node.val);
      if (node.left) { // 先放左再方右
        stack1.push(node.left)
      }
      if (node.right) {
        stack1.push(node.right)
      }
    }
    while (stack2.length > 0) {
      console.log(stack2.pop());
    }
  }
}

———— 前端、Javascript实现、算法、刷题、leetcode