项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
欢迎大家star,留言,一起学习进步
1.序言
在实际工作中,很多业务场景其实也需要一些比较巧妙的算法来支撑,并不是业务逻辑就全是复制粘贴或者说重复的代码写一百遍。越是随着算法研究的深入,越是发现数据结构的重要性。或者说,数据结构中就蕴藏着无数精妙算法的思想,很多算法的思想在数据结构中体现得非常突出。而作为一种非线性的数据结构,二叉树是非常重要非常常见也非常牛逼的一种数据结构,里面包含有递归,栈,队列,dfs等等很多常见的操作。因此,特意写一篇比较长的文章,记录一下二叉树里面的一些常见操作以及里面包含的思想。
2.二叉树节点定义
二叉树内部是由一个一个的Node组成的。因此我们一般定义一个Node类。这个没什么好说的,直接上代码。
static class Node<T> {
T data;
Node left = null;
Node right = null;
Node(T data) {
this.data = data;
}
}
3.初始化二叉树
这个也没太多好说的,直接上代码。
public static Node init() {
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
Node n4 = new Node(4);
Node n5 = new Node(5);
Node n6 = new Node(6);
Node n7 = new Node(7);
Node n8 = new Node(8);
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
n3.left = n6;
n3.right = n7;
n4.left = n8;
return n1;
}
返回的是树的根节点。
4.前序中序后续遍历递归
这个其实也没有太多好说的。但还是稍微说两句。
前序遍历是按根节点->左子树->右子树的顺序访问二叉树。
中序遍历是按左子树->根节点->右子树的顺序访问二叉树。
后序遍历是按左子树->根节点->右子树的顺序访问二叉树。
还是直接上代码
public static void preOrder(Node root) {
if (root != null) {
System.out.print(root.data + " ");
preOrder(root.left);
preOrder(root.right);
}
}
public static void midOrder(Node root) {
if (root != null) {
midOrder(root.left);
System.out.print(root.data + " ");
midOrder(root.right);
}
}
public static void postOrder(Node root) {
if (root != null) {
postOrder(root.left);
postOrder(root.right);
System.out.print(root.data + " ");
}
}
递归的方式简单明了,相信也不用过多解释。
5.前序中序非递归
前序中序遍历非递归的方式较为简单一些。先说说这两种情况。
public static void preOrder2(Node root) {
Stack<Node> stack = new Stack();
if (root == null) {
return;
}
stack.push(root);
while (stack.size() > 0) {
Node tmp = stack.pop();
System.out.print(tmp.data + " ");
if (tmp.right != null) {
stack.push(tmp.right);
}
if (tmp.left != null) {
stack.push(tmp.left);
}
}
}
public static void midOrder2(Node root) {
Stack<Node> stack = new Stack<>();
while (root != null || ! stack.empty()) {
while(root != null) {
stack.push(root);
root = root.left;
}
if (! stack.empty()) {
Node tmp = stack.pop();
System.out.print(tmp.data + " ");
root = tmp.right;
}
}
}
核心思想是利用栈这种数据结构的特点来模拟递归。
在前序遍历中,因为每一次遇到新的节点就要访问,所以直接用一个栈模拟即可。每次pop出一个最后一个压入栈顶端的节点并且访问,然后将该节点的左右子节点压入栈。注意因为栈的特点是后进先出,而前序遍历是先访问左节点再访问右节点,所以压栈的时候应该是先压右子节点再压左子节点,这样保证pop的时候是先pop出的左子节点。
而在中序遍历中,可以想象是先沿着左子树一直遍历直到某个节点的左子树为空,在这个过程中所有的节点自然都被压入栈中。当某个节点的左子树为空时,将该节点pop访问,并开始遍历该节点的右子树。
6.后续遍历的非递归
后续遍历的非递归方式最复杂,先上代码。
private void postorder2(Node node) {
if (node == null) return;
Node cur = node;
Node pre = node;
Stack<Node> stack = new Stack<>();
// cur移动到左子树最下面
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
while (! stack.isEmpty()) {
// pop栈顶
cur = stack.pop();
if (cur.right != null && cur.right != pre) {
// 根节点再入栈
stack.push(cur);
// 处理右子树
cur = cur.right;
while (cur != null) {
// 到右子树最下面
stack.push(cur);
cur = cur.left;
}
} else {
System.out.print(cur.data + " ");
pre = cur;
}
}
}
后续遍历要先将左右子树都访问完毕以后再访问根节点。所以步骤如下:
1.首先肯定是沿着左子树往下搜索,并且一直做压栈操作。
2.当达到左子树为空以后,此时栈顶元素出栈。如果该元素右子树不为空,并且该元素的右子树未被访问,则先将该元素再压栈回去(因为该元素此时未被访问)。
3.将该元素的右子树也压栈,并且沿右子树的的左子树继续搜索。
4.当右子树为空或者已经被访问,此时元素可以被访问,出栈,访问,并且将当前访问节点标记。
7.层次遍历
层次遍历是从根节点开始,沿着二叉树的宽度一层一层往下遍历。由这个特点不难看出,我们可以利用队列先进先出的特点来模拟层次遍历。
更具体地说,我们先让根节点入队列,并且访问根节点。然后让根节点的左节点入队,再让右节点入队。这样左结点就存储在队头的位置,将首先被访问。
访问完根节点以后,左节点出队,同时访问左节点。访问完毕让左节点的左右节点依次入队。此时队列中的头节点为根节点的右节点。
上述过程循环,一直到队列为空即可。
public static void levelOrder(Node root) {
if (root == null) {
return;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node tmp = queue.poll();
System.out.print(tmp.data + " ");
if (tmp.left != null) {
queue.offer(tmp.left);
}
if (tmp.right != null) {
queue.offer(tmp.right);
}
}
}
8.二叉树反转递归与非递归
反转二叉树,因为Homebrew 的作者Max Howell去google面试手写不出来被fxxx off而天下闻名。二话不多说,直接上代码。
public static Node invert(Node root) {
if (root == null) {
return null;
}
Node tmp = root.left;
root.left = root.right;
root.right = tmp;
invert(root.left);
invert(root.right);
return root;
}
递归的方式,是不是跟交换两个数很像有木有。
再看看非递归的方式。
public static Node invert2(Node root) {
if (root == null) {
return null;
}
Deque<Node> deque = new LinkedList<>();
deque.offer(root);
while (! deque.isEmpty()) {
Node parent = deque.poll();
Node tmp = parent.left;
parent.left = parent.right;
parent.right = tmp;
if (parent.left != null) {
deque.offer(parent.left);
}
if (parent.right != null) {
deque.offer(parent.right);
}
}
return root;
}
非递归的方式,则是用一个队列来模拟上述的交换过程,具体过程可以参考层次遍历的分析。
9.求树的深度
如果使用递归的方式求树的深度,那么先求左子树与右子树的深度。而递归的出口为节点为空,此时返回的树深度为0。
public static int findDepth(Node root) {
if (root == null) {
return 0;
}
else {
int leftDepth = findDepth(root.left);
int rightDepth = findDepth(root.right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
}
对于非递归的方式,依旧可以参考层次遍历的思路。假设level为当前节点的层数,last为当前层最后一个节点。当处理完当前层的最后一个节点时level加1。设置变量cur记录当前层已经访问的节点的个数,当cur等于last时,本层访问结束。
public static int findDepth2(Node root) {
if (root == null) {
return 0;
}
Node current;
Deque<Node> deque = new LinkedList<>();
deque.offer(root);
int level = 0;
int cur, last;
while (! deque.isEmpty()) {
cur = 0;
last = deque.size();
while(cur < last) {
current = deque.poll();
cur++;
if (current.left != null) {
deque.offer(current.left);
}
if (current.right != null) {
deque.offer(current.right);
}
}
level++;
}
return level;
}
10. 求树的宽度
求树的宽度与求树的深度比较类似,具体逻辑可以参考代码。
public static int findWidth(Node root) {
if (root == null) {
return 0;
}
Deque<Node> deque = new LinkedList<>();
deque.offer(root);
Node current;
int maxwidth = 0;
int cur, last;
while (! deque.isEmpty()) {
cur = 0;
last = deque.size();
while (cur < last) {
maxwidth = maxwidth > last ? maxwidth : last;
current = deque.poll();
cur++;
if (current.left != null) {
deque.offer(current.left);
}
if (current.right != null) {
deque.offer(current.right);
}
}
}
return maxwidth;
}
11. 求树的节点数递归与非递归
递归的思路
1.如果二叉树为空树,节点为0。
2.如果二叉树不为空,则总节点个数=左子树节点个数+右子树节点个数+1,其中1为自身节点。
public static int countNum(Node root) {
if (root == null) {
return 0;
}
int leftsum = countNum2(root.left);
int rightsum = countNum2(root.right);
return leftsum + rightsum + 1;
}
非递归解法
如果用非递归的方式求解节点个数,思路就比较简单直接了。用任何一种方式遍历整棵树,只需要在遍历的时候加一个count计数即可。
public static int countNum(Node root) {
if (root == null) {
return 0;
}
int count = 0;
Stack<Node> stack = new Stack();
stack.push(root);
while (! stack.isEmpty()) {
Node tmp = stack.pop();
count += 1;
if (tmp.right != null) {
stack.push(tmp.right);
}
if (tmp.left != null) {
stack.push(tmp.left);
}
}
return count;
}
12.求第k层节点数递归与非递归
递归思路:
1.如果二叉树不为空并且k为1,返回1.
2.如果二叉树不为空且k>1,返回左子树中k-1层的节点个数与右子树k-1层节点个数之和
public static int countK(Node root, int k) {
if (root == null || k < 1) {
return 0;
}
if (k == 1) {
return 1;
}
int leftCount = countK(root.left, k - 1);
int rightCount = countK(root.right, k - 1);
return leftCount + rightCount;
}
非递归的思路:
既然是求k层节点数,很自然想到层次遍历的方式。在层次遍历中添加一个level的计数器,当level为k时,此时这一层的节点数即为所求。
public static int countK2(Node root, int k) {
if (root == null || k < 1) {
return 0;
}
Deque<Node> queue = new LinkedList<>();
queue.offer(root);
int level = 1;
int cur, last;
while (! queue.isEmpty() && level < k) {
cur = 0;
last = queue.size();
while (cur < last) {
Node tmp = queue.poll();
if (tmp.left != null) {
queue.offer(tmp.left);
}
if (tmp.right != null) {
queue.offer(tmp.right);
}
cur++;
}
level++;
}
return queue.size();
}
13. 求叶子节点个数递归与非递归
与求节点个数解法很类似,唯一的区别是叶子节点的条件是左右子树均为null。
public static int countLeafNum(Node root) {
if (root == null) {
return 0;
}
if (root.left == null && root.right == null) {
return 1;
}
int leftNum = countLeafNum(root.left);
int rightNum = countLeafNum(root.right);
return leftNum + rightNum;
}
public static int countLeafNum2(Node root) {
if (root == null) {
return 0;
}
Deque<Node> queue = new LinkedList<>();
queue.offer(root);
int count = 0;
while (!queue.isEmpty()) {
Node tmp = queue.poll();
if (tmp.left != null) {
queue.offer(tmp.left);
}
if (tmp.right != null) {
queue.offer(tmp.right);
}
if (tmp.left == null && tmp.right == null) {
count += 1;
}
}
return count;
}