节点
根节点
父节点
子节点
叶子节点 (没有子节点的节点)
节点的权(节点值)
路径(从 root 节点找到该节点的路线)
层
子树
树的高度(最大层数)
森林 :多颗子树构成森林
如果该二叉树的所有叶子节点都在最后一层, 并且结点总数= 2^n -1, n 为层数, 则我们称为满二叉树
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层, 而且最后一层的叶子节点在左边连续, 倒数第二层的叶子节点在右边连续, 我们称为完全二叉树
前序遍历递归图解
前序遍历整个路线就两步:1.一直从根节点往左跑到左边的头;2.开始回调到其父亲节点的右分支;
重复此过程
左右跳接时是从左半部分中的最后一个右叶子节点跳到右半部分的第二层的节点上
前序遍历一定是从根节点开始一直往左走,走到左边最后一个叶子节点的时候开始回调,回调到其父节点的右节点,倘若此右节点是单独的一个节点直接输出继续回调到它上一层的右节点上(即它父亲节点的父亲的右分支,刚好比它高一层),倘若此右节点依然是一颗树,那就右继续一直往左边走,走到头往右边回调,最后再回调到比它高一层的右节点上,此回调过程一直会持续到第三层上,第三层的右分支遍历完(即图中7号)回调时会跳到右子树上(即第二层的8号),然后开始又重复此过程,往左边跑到头,再往右边回调
中序遍历递归图解
中序遍历的路线是三角路线(左上右),三个规律:1.从左下开始;2.倘若三角关系最后的右节点不是单独的节点而是一颗树时,先跳到其左子节点再以三角往回走;3.右分支遍历完毕从最后一个右节点往上跳层时是跳到初始三角中的父节点的父节点;
左右跳接时也是以三角形式从左半部分最后一个右叶子节点跳到根节点再跳到右半部分最左下的节点上
中序遍历一定是从最后一个左边的叶子节点开始,以三角的形式开始跑,一定是从最左下的三角开始跑,但是跑回最后的右节点时,如果此时是单个单独的节点直接输出没错,但是倘若是一颗树的话(若6号节点下还有两个节点)则不会跳到此右节点上,会跳到其左子节点上,然后以三角形式会输出6号节点,然后当左半部的右子树部分遍历完后开始回调,回调到初始三角结构中的父节点的父节点(即图示中的2号节点),此时的路线依然是三角,你可以看作是6、2、4构成的三角,当然倘若4号节点有左子节点的话会先跳到这个位置上,然后同样以这样的三角规则遍历到左半部分的最后一个右叶子节点上,然后以三角规则跳到根节点,再跳到右半部分的最左下的叶子节点上,在右半部分重复此操作。
后序遍历递归图解
后序遍历路线是折线路线,三个规律:1.从左下开始;2.倘若折线关系中第二个右关系点不是单独节点而是一颗树时,跳到其左子节点上;3.在从根节点一分为二,左半部分不具有右关系点的节点依然可以走折线路线直接返回到其父节点;
后序遍历从最左下开始,以折线规则(左右上),折线规则中的最后一个节点是父节点,然后从此父节点继续以折线路线走,根中序遍历一样,倘若折线关系中的第二个节点(即偏右的节点)是单独的一个节点可以直接走,如果其是一颗树则跳到它的左子节点上,然后以折线路线返回到该节点,如果此节点没有右边的同伴节点(例如图中的4节点,即在左根右的关系中自己已经是右节点了),依然满足折线规则相当于跨过第二个右关系点直接跳到上关系点,就这样思路会比中序遍历更清晰的跑到左半部分第二层的节点上,倘若右半部分只有一个根节点的右子节点,根据折线关系此时会直接跳到右节点,根节点然后结束,但倘若右半部分依旧是一颗复杂的树,根据折线规则会跳到右半部分最左下的左叶子节点上然后跟左半部分一样折线路线往回走,所以最后一步一定是从第二层的右节点往根节点跳,所以后序遍历中根节点是最后一个输出的
定义二叉树节点的结构
//先创建 HeroNode 结点
class HeroNode {
private int no;
private String name;
private HeroNode left; // 默认null
private HeroNode right; // 默认null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + "]";
}
// 编写前序遍历的方法
public void preOrder() {
System.out.println(this); // 先输出父结点
// 递归向左子树前序遍历
if (this.left != null) {
this.left.preOrder();
}
// 递归向右子树前序遍历
if (this.right != null) {
this.right.preOrder();
}
}
// 中序遍历
public void infixOrder() {
// 递归向左子树中序遍历
if (this.left != null) {
this.left.infixOrder();
}
// 输出父结点
System.out.println(this);
// 递归向右子树中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
// 后序遍历
public void postOrder() {
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
}
定义二叉树:并且指定一个根节点 root 作为整个树的入口
//定义 BinaryTree 二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("二叉树为空,无法遍历");
}
}
}
测试代码
public static void main(String[] args) {
// 先需要创建一颗二叉树
BinaryTree binaryTree = new BinaryTree();
// 创建需要的结点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
// 说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
// 测试
System.out.println("前序遍历"); // 1,2,3,5,4
binaryTree.preOrder();
// 测试
System.out.println("中序遍历");
binaryTree.infixOrder(); // 2,1,5,3,4
System.out.println("后序遍历");
binaryTree.postOrder(); // 2,5,4,3,1
}
这个只写三个遍历方法
//前序遍历
public static void preOrderTraveral(TreeNode node){
if(node == null){
return;
}
System.out.print(node.data+" ");
preOrderTraveral(node.leftChild);
preOrderTraveral(node.rightChild);
}
//中序遍历
public static void inOrderTraveral(TreeNode node){
if(node == null){
return;
}
inOrderTraveral(node.leftChild);
System.out.print(node.data+" ");
inOrderTraveral(node.rightChild);
}
//后序遍历
public static void postOrderTraveral(TreeNode node){
if(node == null){
return;
}
postOrderTraveral(node.leftChild);
postOrderTraveral(node.rightChild);
System.out.print(node.data+" ");
}
其实递归也是用到了栈,只是在系统层面,你的代码中体现不出来,如果我们可以自己去借助栈,设计栈的细节,这就是非递归存在的意义
当完全清晰了三种遍历的路线后用接用栈遍历也很简单,路线还是那样走,用栈的意义无非就是当代码中的节点走到头即treeNode为null无路可走时我可以从栈中弹出它走的上一个节点,并从此节点改变路线
//前序遍历
public static void preOrderTraveralWithStack(TreeNode node){
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = node;
while(treeNode!=null || !stack.isEmpty()){
//迭代访问节点的左孩子,并入栈
while(treeNode != null){
System.out.print(treeNode.data+" ");
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
//如果节点没有左孩子,则弹出栈顶节点,访问节点右孩子
if(!stack.isEmpty()){
treeNode = stack.pop();
treeNode = treeNode.rightChild;
}
}
}
//中序遍历
public static void inOrderTraveralWithStack(TreeNode node){
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = node;
while(treeNode!=null || !stack.isEmpty()){
while(treeNode != null){
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
if(!stack.isEmpty()){
treeNode = stack.pop(); //利用pop会弹出并删除栈顶元素,即使你接下来拿到的右节点也是Null再次回溯上一个节点
System.out.print(treeNode.data+" ");//也可以回溯到其父节点的父节点不必担心把父节点打印两遍
treeNode = treeNode.rightChild;
}
}
}
//后序遍历
public static void postOrderTraveralWithStack(TreeNode node){
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = node;
TreeNode lastVisit = null; //标记每次遍历最后一次访问的节点
while(treeNode!=null || !stack.isEmpty()){//节点不为空,结点入栈,并且指向下一个左孩子
while(treeNode!=null){
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
//栈不为空
if(!stack.isEmpty()){
//出栈
treeNode = stack.pop();
/**
* 这块就是判断treeNode是否有右孩子,
* 如果没有输出treeNode.data,让lastVisit指向treeNode,并让treeNode为空
* 如果有右孩子,将当前节点继续入栈,treeNode指向它的右孩子,继续重复循环
*/
if(treeNode.rightChild == null || treeNode.rightChild == lastVisit) {
System.out.print(treeNode.data + " ");
lastVisit = treeNode;
treeNode = null;
}else{
stack.push(treeNode);
treeNode = treeNode.rightChild;
}
}
}
}
//后序遍历形式二
public static void NoPostOrder(TreeNode node) {
// 创建队列
Stack stack1 = new Stack();
Stack stack2 = new Stack();
stack1.push(node);
while (!stack1.isEmpty()) {
node = stack1.pull();
stack2.push(node);
// 有左孩子就先压入左孩子
if (node.left != null)
stack1.push(node.left);
// 有右孩子就后压入右孩子
if (node.right != null)
stack1.push(node.right);
}
// 逆序打印 根 -> 右 -> 左 的结果,就是后序遍历的结果
while (!stack2.isEmpty())
System.out.print(stack2.pull().value + " ");
}
//用两个栈相当于是逆向思维,从根节点开始用反折线路线想办法把节点放进栈中,打印栈既可以得到折线规则的路线了
public static void levelOrder(TreeNode root){
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
root = queue.pop();
System.out.print(root.data+" ");
if(root.leftChild!=null) queue.add(root.leftChild);
if(root.rightChild!=null) queue.add(root.rightChild);
}
}
将二叉树的前、中、后序遍历改为查找即可
编码思路:
定义二叉树节点
//先创建 HeroNode 结点
class HeroNode {
private int no;
private String name;
private HeroNode left; // 默认null
private HeroNode right; // 默认null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + "]";
}
// 前序遍历查找
public HeroNode preOrderSearch(int no) {
// 比较当前结点是不是
if (this.no == no) {
return this;
}
// 1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
// 2.如果左递归前序查找,找到结点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(no);
}
if (resNode != null) {// 说明我们左子树找到
return resNode;
}
// 1.左递归前序查找,找到结点,则返回,否继续判断,
// 2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
if (this.right != null) {
resNode = this.right.preOrderSearch(no);
}
return resNode;
}
// 中序遍历查找
public HeroNode infixOrderSearch(int no) {
// 判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.infixOrderSearch(no);
}
if (resNode != null) {
return resNode;
}
// 如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
if (this.no == no) {
return this;
}
// 否则继续进行右递归的中序查找
if (this.right != null) {
resNode = this.right.infixOrderSearch(no);
}
return resNode;
}
// 后序遍历查找
public HeroNode postOrderSearch(int no) {
// 判断当前结点的左子节点是否为空,如果不为空,则递归后序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.postOrderSearch(no);
}
if (resNode != null) {// 说明在左子树找到
return resNode;
}
// 如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.postOrderSearch(no);
}
if (resNode != null) {
return resNode;
}
// 如果左右子树都没有找到,就比较当前结点是不是
if (this.no == no) {
return this;
}
return resNode;
}
}
定义二叉树:二叉树需要一个根节点 root 作为整个树的入口
//定义 BinaryTree 二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
// 删除结点
public void delNode(int no) {
if (root != null) {
// 如果只有一个root结点, 这里立即判断root是不是就是要删除结点
if (root.getNo() == no) {
root = null;
} else {
// 递归删除
root.delNode(no);
}
} else {
System.out.println("空树,不能删除~");
}
}
// 前序遍历查找
public HeroNode preOrderSearch(int no) {
if (root != null) {
return root.preOrderSearch(no);
} else {
return null;
}
}
// 中序遍历查找
public HeroNode infixOrderSearch(int no) {
if (root != null) {
return root.infixOrderSearch(no);
} else {
return null;
}
}
// 后序遍历查找
public HeroNode postOrderSearch(int no) {
if (root != null) {
return this.root.postOrderSearch(no);
} else {
return null;
}
}
}
测试代码:测试前序、中序、后序查找
public static void main(String[] args) {
// 先需要创建一颗二叉树
BinaryTree binaryTree = new BinaryTree();
// 创建需要的结点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
// 说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
// 前序遍历
System.out.println("前序遍历方式~~~");
HeroNode resNode = binaryTree.preOrderSearch(4);
if (resNode != null) {
System.out.printf("找到了,信息为 no=%d name=%s\n", resNode.getNo(), resNode.getName());
} else {
System.out.printf("没有找到 no = %d 的英雄\n", 4);
}
System.out.println();
// 中序遍历查找
System.out.println("中序遍历方式~~~");
resNode = binaryTree.infixOrderSearch(5);
if (resNode != null) {
System.out.printf("找到了,信息为 no=%d name=%s\n", resNode.getNo(), resNode.getName());
} else {
System.out.printf("没有找到 no = %d 的英雄\n", 5);
}
System.out.println();
// 后序遍历查找
System.out.println("后序遍历方式~~~");
resNode = binaryTree.postOrderSearch(6);
if (resNode != null) {
System.out.printf("找到了,信息为 no=%d name=%s\n", resNode.getNo(), resNode.getName());
} else {
System.out.printf("没有找到 no = %d 的英雄\n", 6);
}
System.out.println();
}
程序运行结果
前序遍历方式~~~
找到了,信息为 no=4 name=林冲
中序遍历方式~~~
找到了,信息为 no=5 name=关胜
后序遍历方式~~~
没有找到 no = 6 的英雄
如果对二叉树的遍历没有那么熟练,韩老师的代码可能嵌套给你感觉比较乱,具体的实现原理其实就是在单个节点HeroNode类中定义的,但是调用的时候是通过整个二叉树结构BinaryTree类中的同名方法调用,也就是主函数中是用BinaryTree.遍历查找方法,而在BinaryTree中每次又递归root.遍历方法,这个root是节点类型则此递归实际上是在调用HeroNode中的具体实现方法
由于树的本质还是单向链表:
编码思路:
定义二叉树节点
//先创建 HeroNode 结点
class HeroNode {
private int no;
private String name;
private HeroNode left; // 默认null
private HeroNode right; // 默认null
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + "]";
}
// 递归删除结点
// 1.如果删除的节点是叶子节点,则删除该节点
// 2.如果删除的节点是非叶子节点,则删除该子树
public void delNode(int no) {
// 思路
/*
* 1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点. 2.
* 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除) 3.
* 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除) 4.
* 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除 5. 如果第4步也没有删除结点,则应当向右子树进行递归删除.
*
*/
// 2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除)
if (this.left != null && this.left.no == no) {
this.left = null;
return;
}
// 3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除)
if (this.right != null && this.right.no == no) {
this.right = null;
return;
}
// 4.我们就需要向左子树进行递归删除
if (this.left != null) {
this.left.delNode(no);
}
// 5.则应当向右子树进行递归删除
if (this.right != null) {
this.right.delNode(no);
}
}
}
定义二叉树:二叉树需要一个根节点 root 作为整个树的入口
//定义 BinaryTree 二叉树
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
// 删除结点
public void delNode(int no) {
if (root != null) {
// 如果只有一个root结点, 这里立即判断root是不是就是要删除结点
if (root.getNo() == no) {
root = null;
} else {
// 递归删除
root.delNode(no);
}
} else {
System.out.println("空树,不能删除~");
}
}
}
测试代码
public static void main(String[] args) {
// 先需要创建一颗二叉树
BinaryTree binaryTree = new BinaryTree();
// 创建需要的结点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
// 说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
// 测试一把删除结点
System.out.println("删除前,前序遍历");
binaryTree.preOrder(); // 1,2,3,5,4
System.out.println();
binaryTree.delNode(5);
System.out.println("删除节点 5 后,前序遍历");
binaryTree.preOrder(); // 1,2,3,4
System.out.println();
binaryTree.delNode(3);
System.out.println("删除子树 3 后,前序遍历");
binaryTree.preOrder(); // 1,2
}
程序运行结果
删除前,前序遍历
HeroNode [no=1, name=宋江]
HeroNode [no=2, name=吴用]
HeroNode [no=3, name=卢俊义]
HeroNode [no=5, name=关胜]
HeroNode [no=4, name=林冲]
删除节点 5 后,前序遍历
HeroNode [no=1, name=宋江]
HeroNode [no=2, name=吴用]
HeroNode [no=3, name=卢俊义]
HeroNode [no=4, name=林冲]
删除子树 3 后,前序遍历
HeroNode [no=1, name=宋江]
HeroNode [no=2, name=吴用]