什么是二叉树

Posted 木辰星

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是二叉树相关的知识,希望对你有一定的参考价值。

什么是树

树这种数据结构就像我们生活中的树一样,树中的元素称之为节点,节点与节点之间的关系称为父子关系;树的三个重要概念

  1. 高度:当前节点到其叶子节点最长路径边的个数;
  2. 深度:根节点到此节点经历的边数;
  3. 层数:节点的深度加1

二叉树

二叉树即每个节点最多可以分两个叉形成两个子节点,但不要求每个节点都有两个子节点;

二叉树中有两个特殊的树:

  1. 满二叉树:叶子节点都在最底层,并且除叶子节点外,每个节点都有两个子节点;
  2. 完全二叉树:最后一层的节点都靠左排列, 并且除最后一层节点外,每个节点都有两个子节点;如果是以数组存储,则从第一个节点到最后一个节点之间每个位置都会有元素,不会出现空的位置;
什么是二叉树

二叉查找树

二叉查找树又称二叉搜索树,它的特点是其任意一个节点,它的左子树都小于这个节点的值,它的右子树都大于这个节点的值,主要用于快速查找数据

二叉查找树的查询操作

先将要查找的数据与根节点的值对比,若根节点的值与要查找的数据相同,则直接返回;若根节点的值大于要查找的数据,则在左子树中递归查找;若根节点的值要小于要查找的数据,则在右子树中递归查找。

二叉查找树的插入操作

插入操作的类似于查找操作,从根节点开始与被插入的数据做比较,如果根节点为空,则将新插入的数据直接作为根节点;如果不为空,则将要插入的数据与当前节点的值比较,如果要插入的数据大于当前节点的值,且当前节点的右子树为空,则将要插入的数据插入到右子节点;如果不为空,则递归右子树,找到要插入的位置;如果要插入的数据比节点数据小,并且节点的左子树为空,则将新数据插入到左子节点的位置;如果不为空,则递归左子树,查找插入位置;如果要插入的数据与当前节点的值相同,可以将这个数据当做大于当前节点的值来插入。

二叉树的删除操作

删除操作可分为三种情况

  1. 删除的节点没有子节点,则直接将要删除的节点设为null;
  2. 删除的节点只有一个子节点,则只需要更新其父节点指向被删除节点的子节点即可;
  3. 删除的节点如果有两个子节点,则需要找到其右子树中最小的节点,将这个节点替换到要删除的节点的位置,然后删除这个最小的节点;替换的节点是右子树中最小的节点,因为这个节点在右子树,所以这个节点一定大于删除节点的左子树所以的节点,也一定小于右子树所有的节点所以替换后的二叉树依然会满足二叉搜索树的条件,而且这个替换的子节点时右子树中最小的节点,它最多只有一个右子树,这样就可以将删除节点含有两个子节点的情况转化为删除节点只有一个子节点的问题。

二叉树实现

/**
* 二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值
*
* @Author cangxiao
* @Desc 二叉查找树
*/

public class BinarySearchTree {
private Node root;
public BinarySearchTree() {
}
/**
* 从根节点开始与被查账的数据比较
* 如果要查找的值小于当前节点,则查找当前节点的左子树
* 如果要查找的数据大于当前节点,则查找当前节点的右子树
* 如果要查找大数据等于当前节点,则返回当前节点
* 直到节点为null ,说明要查找的值不存在
*
* @param value
* @return
*/

public Node find(int value) {
Node p = root;
while (p != null) {
if (value < p.data) {
p = p.left;
} else if (value > p.data) {
p = p.right;
} else {
return p;
}
}
return null;
}
/**
* 1. 如果根节点为空,新数据直接插入到根节点
* 2. 根节点不为空并且根节点的值大于要插入的数据吗,则递归左子树找到插入的位置
* 3. 根节点不为空切根节点的值小于要插入的数据,则递归右子树找到要插入的位置
*
* @param value
* @return
*/

public boolean insert(int value) {
Node newNode = new Node(value);
if (root == null) {
root = newNode;
return true;
}
Node p = root;
while (true) {
if (value < p.data) {
if (p.left == null) {
p.left = newNode;
return true;
}
p = p.left;
} else {
if (p.right == null) {
p.right = newNode;
return true;
}
p = p.right;
}
}
}

/**
* 1. 删除的节点如果没有子节点,则直接将要删除的节点设为null
* 2. 删除的节点如果只有一个子节点,则只需要更新其父节点指向被删除节点的子节点即可;
* 3。删除的节点如果有两个子节点,则需要找到其右子树中最小的节点,将 这个节点替换到要删除的节点的位置,然后删除这个最小的节点;
* 替换的节点是右子树中最小的节点,因为这个节点在右子树,所以这个节点一定大于删除节点的左子树所以的节点,也一定小于右子树所有的节点
* 所以替换后的二叉树依然会满足二叉搜索树的条件;
*
* @param value
* @return
*/

public int delete(int value) {

Node p = root; //p指向要删除的节点
Node pParent = null; //指向被删除节点的父节点
while (p != null && p.data != value) {
pParent = p;
if (value < p.data) {
p = p.left;
} else {
p = p.right;
}
}
if (p == null) return -1;
//当删除的节点有两个子节点时
if (p.left != null && p.right != null) {
//直到删除节点右子树中最小的节点
Node minP = p.right;
Node minPParent = p;//记录删除节点minP的父节点
while (minP.left != null) {
minPParent = minP;
minP = minP.left;
}
//将minP节点数据替换到要删除的p节点,则删除p节点变成了删除minP节点
p.data = minP.data;
p = minP;
pParent = minPParent;
}
/*删除的节点有两个子节点 经过替换操作,删除的节点变成了minP,而minP没有子节点或只有一个右子树,
将删除节点包含两个子节点的情况变成了只有一个自己诶单或没有子节点的状态
*/

//当删除的节点只有一个子节点或没有子节点时,如果有子节点,则这个子节点可能是右子树也可能是左子树
Node child = p;
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null; //p节点的左右没有子节点
/*
* 如果删除节点的父节点为null,则删除节点为根节点
* 如果删除节点为其父节点的左子树,则将删除节点的子子树设为其父节点的左子树
* 如果删除节点为其父节点的右子树,则将删除节点的子节点设为其父节点的右子树;
*/

if (pParent == null) {
root = child;
} else if (pParent.left == p) {
pParent.left = child;
} else {
pParent.right = child;
}
return 1;
}
/**
* 如果树中有多个值需要删除
*
* @param value
* @return
*/

public int delete2(int value) {
int n = 0;
while (true) {
int i = delete(value);
if (i == -1) {
return n;
}
n += i;
}
}
private static class Node {
private int data;
private Node left;
private Node right;

public Node(int data) {
this.data = data;
}
}
}

二叉树的遍历

二叉树有三种经典遍历的方式

什么是二叉树
  1. 前序遍历:对于树中的任意节点,都优先打印当前节点,然后打印它的左子树,最后打印它的右子树,如上图,对于A节点,先打印A节点,然后打印A节点的左子树B、D、E,最后打印A节点的右子树C、F、G,然后对于左子树B、D、E,则先打印B然后打印D,最后打印E,右子树同理,最终打印结果为                     A->B->D->E->C->F->G
  2. 中序遍历:对于树中的任意节点,都优先打印当前节点的左子树,然后打印当前节点,最后打印它的右子树,如上图,对于A节点,先打印A节点的左子树B、D、E,然后打印A节点,最后打印A节点的右子树C、F、G;对于左子树B、D、E,则先打印D然后打印B,最后打印E,右子树同理,最终打印结果为                     D->B->E->A->F->C->G
  3. 后序遍历:对于树中的任意节点,都优先打印当前节点的左子树,然后打印它的右子树,最后打印当前节点,如上图,对于A节点,先打印A节点的左子树B、D、E,然后打印A节点的右子树C、F、G,最后打印A节点;对于左子树B、D、E,则先打印D然后打印E,最后打印B,右子树同理,最终打印结果为                     D->E->B->F->G->C->A

前序遍历

/**
* 前序遍历
*/

public void print(Node node){
if (node!=null){
System.out.println(node.data);
print(node.left);
print(node.right);
}
}

中序遍历

/**
* 中序遍历
*/

public void print(Node node){
if (node!=null){
print(node.left);
System.out.println(node.data);
print(node.right);
}
}

后序遍历

/**
* 后序遍历
*/

public void print(Node node){
if (node!=null){
print(node.left);
print(node.right);
System.out.println(node.data);
}
}

例题

剑指 Offer33. 二叉搜索树的后序遍历序列https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/

后序遍历顺序:左子树 右子树 根节点

递归方式

如果一个数组是后序遍历,则最后一个元素一定是根节点,由于右子树一定大于根节点,所以可以先找到数组中第一个大于根节点的值,这个值之后所有的值一定都大于根节点,并且这个值之前的值都是左子树一定都小于根节点;

如图,它的后序遍历是:1、3、2、6、5;第一个跟大于根节点的值是6,所以6之前的节点都应该小于根节点,6之后的值(除根节点外)都应该大于根节点;然后对于根节点的左子树,根节点为2,左子树1、3、2 也应该满足后序遍历的特点;直到没有节点或只有一个节点递归终止。

public boolean verifyPostorder(int[] postorder) {
return verifyPostorder(postorder, 0, postorder.length-1);
}
private boolean verifyPostorder(int[] array, int left, int right){
//当没有节点或只有一个节点是,递归结束
if(left>=right){
return true;
}
//后序遍历最后一个节点一定为根节点
int root = array[right];
//在数组中找到第一个比根节点大的值, 则这个值之后的节点(除了根节点)都是它的右子树,之前的节点都为它的左子树
int mid = array[left];
while (array[mid]<root){
mid++;
}
int temp = mid;
while (temp<right){
//右子树都应该比根节点大,如果出现小于根节点的择不是后序遍历
if(array[temp++]<root){
return false;
}
}
/*
递归所有的子树
*/

return verifyPostorder(array,left, mid-1) && verifyPostorder(array, mid, right-1);
}

使用栈

参考:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/solution/di-gui-he-zhan-liang-chong-fang-shi-jie-jue-zui-ha/

如图后序遍历的结果是:3、6、5、9、8、11、15、14、13、12、10;

后序遍历倒序为:10、12、13、14、15、11、8、9、5、6、3;

对于二叉树有:左子树 < 根节点 < 右子树

设索引为 i, 从图可以看出如果 arr[i]<arr[i+1],则 arr[i+1] 一定是 arr[i]的右子节点;从索引 i 开始,如果数组一直是升序,则说明当前节点一定是前一个节点的右子节点,比如10、12、13,12一定是10的右子节点,13一定是12的右子节点;如果升序的区间为 [i ,j], 直到出现第一个值不再是升序, 则这个值所在的节点一定是之前  [i ,j] 这些节点中某个节点的左子节点;比如在10、12、13、11中,11 一定是10、12、13中某个节点的左子节点,并且一定是大于11中最小的值,因为如果这个值小于11,则11一定不是它的左子节点,如果这个值不是大于11中最小的值,那么不符合二叉树的特性,比如11小于12和13,那么11插入的时候一定是插入到12的左子节点;经过倒序之后,节点的顺序为:根节点 右子树 左子树,所以第一次出现降序的时候,右子节点最大值已经压入栈中,出栈的时候总最大值开始出栈,所以每次parent一定是剩下节点中的最大节点;如果出现后面的节点大于parent节点,则一定不亅后序遍历。

public boolean verifyPostorder1(int[] postorder){
Stack<Integer> stack = new Stack<>();
int parent = Integer.MAX_VALUE;
for (int i = postorder.length - 1; i >= 0; i--) {
int current = postorder[i];
while (!stack.isEmpty() && current<stack.peek()){
//如果当前值小于栈顶的值,出现了降序,说明当前值的节点是栈中某个元素的左子节点;
//最高一个出栈并且大于当前元素,则出栈的这个元素一定是所有大于当前值中最小的值;
parent = stack.pop();
}
//如果current是左子节点,则parent是current的父节点,如果current是右子节点,则parent 是这个节点的祖先节点
//但是current仍然是parent左子树上的节点,所有current依然应该小于parent节点;
//第一次出现降序的时候,说明右子树的最大值已经压入栈中,出栈是从右子节点开始出栈,所以parent一定是剩下节点中的最大值;
if(current>parent){
return false;
}
//如果当前值大于栈中的元素,说明是升序,则将其压入栈中
stack.push(current);
}
return true;
}


以上是关于什么是二叉树的主要内容,如果未能解决你的问题,请参考以下文章

漫画什么是二叉树?

二叉树怎么建立?

《重学数据结构》之什么是二叉树?

C语言数据结构:什么是树?什么是二叉树?

坐下,这些都是二叉树的基本操作!

二叉树,B树,B+树,红黑树 简介