玩转层次遍历

555 阅读4分钟

2021/02/13

我是星,亲手点燃黑暗森林的火星

层次遍历是树的遍历方式之一,树的很多相关操作都可以使用层次遍历的思想解决,比如求树的高度求树的宽度判断是否是完全二叉树等。

树的层次遍历

层次遍历是指按照树的层级顺序遍历节点。如下面这颗二叉树一共有三层,层次遍历的结果为:1,2,3,4,5,6二叉树的层次遍历

层次遍历的核心是使用一个队列,首先将根节点入队。循环如下操作,直到队列为空:

  • 出队,记为p
  • 如果p的左孩子不为空,左孩子入队
  • 如果p的右孩子不为空,右孩子入队

以上面的二叉树为例,层次遍历的过程如下图所示:

层次遍历的Java实现如下:

public void levelOrder() {
    if (isEmpty()) {
        return;
    }
    Queue<BinaryNode> queue = new LinkedList<>();
    queue.offer(this.root);
    BinaryNode<E> node = null;
    while (!queue.isEmpty()) {
        node = queue.poll();
        System.out.print(node.element + " ");
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
    }
}

树的高度

递归法求高度

树的高度很容易想到使用递归的方式求解,一棵树的高度=左右子树中较高的子树的高度+1,左右子树的高度又可以继续使用这个公式求解,这就是递归的方法。递归的实现也非常容易:

public int heightWithRecursion(BinaryNode root) {
    if (root == null) {
        return 0;
    }
    int leftHeight = heightWithRecursion(root.left);
    int rightHeight = heightWithRecursion(root.right);
    return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}

利用层次遍历求高度

由于层次遍历是按层遍历,每遍历完一层的节点,高度就增加1,因此可以用来求树的高度。

在层次遍历的基础上,为了记录高度,需要一个变量(我定义为int levelSize)来记录某一层还未遍历的节点个数,默认初始为1(第一层只有一个根节点),层次遍历队列每出栈一个元素,则将levelSize1。当levelSize==0时,代表某一层遍历完成了,此时高度增加1,并将当前队列的元素个数赋给levelSize,继续层次遍历。

求树的高度只需要在层次遍历的代码基础上稍作修改:

public int height(BinaryNode<E> root) {
    if (isEmpty()) {
        return 0;
    }
    int levelSize = 1;
    int height = 0;
    Queue<BinaryNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        BinaryNode<E> node = queue.poll();
        levelSize--;
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
        if (levelSize == 0) {
            height++;
            levelSize = queue.size();
        }
    }
    return height;
}

树的宽度

树的宽度是指:节点个数最多的那一层的节点个数。和求高度类似,可以利用层次遍历来解决,并且和求高度的过程很相似。

在求高度的代码中,我们增加了一个变量levelSize来记录某一层还未遍历的节点个数,在树不为空的情况下,令默认宽度width=1,在levelSize==0时,此时队列的元素个数即为下一层的节点总数,这时可以和width比较,如果大于width,则修改新的宽度。

只需要在修改levelSize==0时的逻辑即可:

public int width() {
    if (isEmpty()) {
        return 0;
    }
    int width = 1;
    int levelSize = 1;
    Queue<BinaryNode> queue = new LinkedList<>();
    queue.offer(this.root);
    BinaryNode<E> node = null;
    while (!queue.isEmpty()) {
        node = queue.poll();
        levelSize--;
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
        if (levelSize == 0) {
            // 这时的levelSize是下一层的节点个数
            levelSize = queue.size();
            if (levelSize > width) {
                width = levelSize;
            }
        }
    }
    return width;
}

判断一棵树是否位完全二叉树

判断一棵树是否是完全二叉树,同样可以利用层次遍历的思想,只不过某个节点如果没有左或右孩子时,也要进行相应的操作,具体规则如下:

  • 如果节点有左、右孩子,正常加入队列
  • 如果一个节点没有左孩子,有右孩子,那么一定不是完全二叉树
  • 如果一个节点的左孩子为空,那么所有在此之后遍历的节点必须全是叶子节点,才是完全二叉树

上面的规则中,最后一个规则需要判断剩余的节点是否全是叶子节点,可以增加一个变量boolean mustAllLeaf,默认为false

在层次遍历的基础上,修改代码:

public boolean isCompleteBinaryTree() {
  if (this.root == null) {
      return false;
  }
  Queue<BinaryNode> nodeQueue = new LinkedList<>();
  nodeQueue.offer(root);
  boolean mustAllLeaf = false;
  while (!nodeQueue.isEmpty()) {
      BinaryNode<E> currentNode = nodeQueue.poll();
      // 如果必须全为叶子节点,而存在非叶子节点,直接判定非完全二叉树
      if (mustAllLeaf && !isLeaf(currentNode)) {
          return false;
      }
      if (currentNode.left != null) {
          nodeQueue.offer(currentNode.left);
      } else if (currentNode.right != null) {
          // 无左孩子有右孩子直接判定非完全二叉树
          return false;
      }
      if (currentNode.right != null) {
          nodeQueue.offer(currentNode.right);
      } else {
          mustAllLeaf = true;
      }
  }
  return false;
}