树的表达方式
树的定义
树是n个节点的有限集合,满足1.有且只有一个根节点;2.其余节点可以分为m个互不相交的子集,每一个子集也是一颗树,称为根的子树
树的概念
节点:树结构存储的每一个数据元素都被称为“节点”。
根节点:该节点没有前驱。
父节点:子节点和兄弟节点。如图
叶子节点:该节点没有任何子节点
节点的度:节点拥有子树的个数,称为节点的度。
树的度:节点的度的最大值。
树的深度(高度):根节点开始,根为第一层,根孩子为第二层,直到最深的叶子节点
树的存储结构
1.双亲表示法
利用顺序表存储普通树,核心是存储各个节点时,给每个节点添加一个父节点位置的变量。由于根节点没有父节点,通常设置为-1,寻找父亲节点时间复杂度 O(1),由于可以通过孩子节点直接找到父亲节点,寻找孩子节点时间复杂度O(N),因为要遍历整个数组
2.孩子表示法
使用的是“顺序表+链表的结构”,从根节点开始,用顺序表储存树的各个节点,每个节点是一个链表,连接和他的孩子,它查找孩子效率较高,但是不擅长查找父节点,
3.孩子兄弟表示法
从根节点开始,依次存储各个节点的孩子和兄弟节点。内容:孩子指针域|数据域|兄弟指针域
二叉树
每个节点最多两颗子树,不允许存在度大于2的树,以下都可以称为二叉树
满二叉树:分支节点都存在左右子树,而且所有叶子节点在同一层,一颗深度为k的满二叉树节点有(2^k)-1个
完全二叉树:节点数介于2^(k-1)-1到2^k-1之间,若某节点编号为i,那么在满二叉树中节点编号也是i,叶子节点只能在最下层和次下层
二叉树性质
1:第i层最多2^(i-1)个节点
2:深度为k的二叉树至多有2^k - 1个节点。
3:叶节点数目比度为2节点数目多一个。推导:总结点=n0+n1+n2=n1+2*n2+1(这个1是根节点),n0,n1,n2表示度
4:N个节点的完全二叉树深度为[logN]+1
代码实现
首先是定义节点,一般是需要左孩子,右孩子以及本身的值
static class TreeNode {
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val) {
this.val = val;
}
}
二叉树的遍历:
1.中序遍历:
先找左边,再输出自己,最后寻找右树,例如上面树的中序遍历为D,B,E,A,F,C,G,先是采用递归进行实现树的中序遍历:
我们先是采用递归来进行树的中序遍历:
public void inOrder(TreeNode root) {
if(root == null)
return;
System.out.print(root.val+" ");
inOrder(root.left);
inOrder(root.right);
}
我们用栈来进行递归的模拟:
首先定义一个cur指针,指向树的根节点,当这个指针不为空时,我们先将cur压入栈中,然后再向左走,即:
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
但是cur可能向左指向空,因此我们需要找到cur节点的前驱,即top=stack.pop(),因为当cur指向空时,stack栈顶存放的就是cur节点的前驱,打印top的value值,在进行向右寻找,右边遇到空时,还是找前驱:这里分为两种情况,左空+右空,由于左边执行pop时已经弹出了前驱,右边执行时需要找前驱的前驱,正好执行pop弹出前驱的前驱,代码无误,如果实在不理解,看下图:
一个为空只执行一次弹出,前序遍历有写,非递归代码如下:
public void inOrderNor(TreeNode root) {
if(root == null) return;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
System.out.print(top.val + " ");
cur = top.right;
}
}
2.前序遍历:
先输出自己,再向左树寻找,找完左树,在寻找右树,例如上面的树前序遍历为:A,B,D,E,C,F,G,。
public void preOrder(TreeNode root) {
if(root == null) return;
System.out.print(root.val+" ");
preOrder(root.left);
preOrder(root.right);
}
用栈进行递归模拟:
首先定义一个cur指针,初始化指向头节点,当这个指针不指向空的时候,我们将cur指向的节点压入栈中,并打印当前节点的value值,然后cur向左走,遇到一个值执行上面操作,因此我们得到:
while(cur != null) {
stack.push(cur);
System.out.print(cur.val + " ");
cur = cur.left;
}
像上图,如果我们cur指到D节点,cur=cur.left执行后,cur为空时怎么办呢?由于我们向栈中压入元素,栈不为空,我们需要定义一个指针指向该节点的前驱(即他从哪里来的),这里我们定义一个top指向前驱,然后使cur指向前驱的右孩子(这里是前序遍历,先自己,自己已经打印,再左,左为空,因此轮到了右),右边如果不是空,那么按照while(cur != null)这个循环走下去,如果是空,那么我们需要回到cur前驱的前驱(例如:cur为D节点的右,我们需要回到B节点),怎么回?
TreeNode top = stack.pop();
cur = top.right;
已知我们D左边为空时,第一次执行pop,弹出了D,当右边为空时,再次执行pop,pop了B,即top指向了B,那么可能会问,如果左边不空,右边空怎么办?这样子的代码有错吗?好,那我们分析一下;
假设还是上面的完全二叉树,我们这里去掉E节点,考虑B节点,按照刚才的分析,top现在指向了B(且B已经被弹出),cur=top.right,现在cur为空,执行pop,弹出A,那么现在top指向了A,cur指向了A的右孩子,继续按照代码执行。。。总体代码如下: 第一个while中cur!=null,是为了让第一步进来,因为此时栈为空
public void preOrderNor(TreeNode root) {
if(root == null) return;
Stack<TreeNode>stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
while(cur != null) {
stack.push(cur);
System.out.print(cur.val + " ");
cur = cur.left;
}
TreeNode top = stack.pop();
cur = top.right;
}
}
3.后序遍历:
先找左边,再找右边,最后输出自己,例如对于上面树的后序遍历, D,E,B,F,G,C,A先是采用递归进行实现后序遍历:
public void postOrder(TreeNode root) {
if(root == null) return;
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val+" ");
}
用栈进行递归的模拟:
由于是先打印左树,后打印右树,最后才是自己,因此我们还是先定义一个cur指针指向根节点,让他一直左走,当左数为空时寻找右树,当右树为空时以及右树不为空时:有如下代码
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null) {
//左右都是空了这是可以打印弹出打印的元素
System.out.print(top.val + " ");
stack.pop();
} else {
cur = top.right;
}
}
这样子乍一看没什么问题,对吗?不对!!!就算栈里弹出了E,也不代表B的右树为空,不要混淆这两个概念!!!那么一个栈是行不通的。我们可以增加一个指针p,当弹出后记录下top的节点,由于此时弹出的节点为top节点,当他被父节点指到时,即当top.right=p时,表示ok了。如图
代码如下
public void postOrderNor(TreeNode root) {
if(root == null) return;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null || top.right == prev) {
System.out.print(top.val + " ");
stack.pop();
prev = top;
} else {
cur = top.right;
}
}
}
还有另一种方法,申请两个栈:第一个栈储存待处理的节点,按照先左树后右树顺序,弹出栈顶的元素到第二个栈中,这时处理弹出元素的左右节点(孩子),按照先左数后右树顺序。当第一个栈为空时处理完毕,弹出第二个栈的元素,弹出顺序即为后序遍历的顺序。代码如下:
public void postOrderNor(TreeNode root) {
if (root == null) return;
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
stack1.push(root);
while (!stack1.isEmpty()) {
TreeNode cur = stack1.pop();
stack2.push(cur); // 将节点先压入第二个栈中
// 先压左子树,后压右子树
if (cur.left != null) {
stack1.push(cur.left);
}
if (cur.right != null) {
stack1.push(cur.right);
}
}
// 从第二个栈中依次弹出并打印,顺序就是后序遍历的顺序
while (!stack2.isEmpty()) {
System.out.print(stack2.pop().val + " ");
}
}
4.层序遍历
利用队列实现二叉树的层序遍历,先把根节点添加到队列中,先使一个cur指针指向root节点,根节点出队并打印,如果cur指向的不为空,将他的左右节点添加到队列中。代码如下
public void levelOrder(TreeNode root) {
if(root == null) return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode cur = queue.poll();
Stystem.out.print(cur.val+" ");
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
}
一些基础题目:
1.总节点个数:
递归:
法一:遇到节点进行++,递归左右子树
法二:左子树节点+右子树节点+1(根节点)
public void size(TreeNode root) {
if(root == null) {
return;
}
nodeSize++;
size(root.left);
size(root.right);
}
public int size(TreeNode root) {
if(root == null) {
return 0;
}
return size(root.left) + size(root.right)+1;
}
非递归形式:
利用层序遍历,遇到一个节点进行++
public int getCount(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int count = 0;
while(!queue.isEmpty()) {
TreeNode cur = queue.poll();
count++;
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
return count;
}
2.获取叶子节点个数:
递归
法一:遇到叶子节点++,递归左右子树
法二:左叶子节点+右叶子节点
public static int leafSize = 0;
void getLeafCount(TreeNode root) {
if(root == null) return;
if(root.left == null && root.right == null) leafSize++;
getLeafCount(root.left);
getLeafCount(root.right);
}
int getLeafCount(TreeNode root) {
if(root == null) {
return 0;
}
if(root.left == null && root.right == null) {
return 1;
}
return getLeafCount(root.left) +
getLeafCount(root.right);
}
非递归形式
队列实现
public int getCount(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int count = 0;
while(!queue.isEmpty()) {
TreeNode cur = queue.poll();
if(cur.left == null && cur.right == null) {count++};
else {
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
}
return count;
}
栈也是可以的。代码大致结构相同
3.获取第K层节点个数:
找到第k-1层的节点,分别找的左右节点,相加则是第k层节点
public int getK(TreeNode root,int k) {
if(root == null) return 0;
if(k == 1) return 1; // 因为这是第一层
return getK(root.left,k-1)+getK(root.right,k-1)
}
4.获取树的高度:
求出左子树和右子树的高度,它们的最大值+1(根节点)
public int getHeight(TreeNode root) {
if(root == null) {
return 0;
}
int leftHeight = getHeight(root.left);
int rightHeight = getHeight(root.right);
return leftHeight >rightHeight ? leftHeight+1 : rightHeight+1;
}
5.找到树中某个值
TreeNode find(TreeNode root, char val) {
if(root == null) {
return null;
}
if(root.val == val) {
return root;
}
TreeNode leftT = find(root.left,val);
if(leftT != null) {
return leftT;
}
TreeNode rightT = find(root.right,val);
if(rightT != null) {
return rightT;
}
return null;
}
6.判断两棵树是否相同
这里传入的两个节点
1.首先判断结构是否相同,(是否同时为空或者同时不为空)
2.同时为空则为真,同时非空且数值不同则为假。
3.递归左右
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p != null && q == null || p == null && q != null) {
return false;
}
if(p == null && q == null) {
return true;
}
if(p.val != q.val) {
return false;
}
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
7.翻转二叉树
就是普通的交换+递归左右子树,叶子节点的null交换与否无所谓
public TreeNode invertTree(TreeNode root) {
if(root == null) return null;
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
下一篇发文:C语言实现搜索树和平衡树的增删查改