二叉树的概念:
一颗二叉树是节点的有限集合。二叉树由左子树和右子树组成,每一个非空的子树都可以称作一个独立的二叉树。
二叉树的特点:
- 每个节点最多有两颗子树,即树的度最大为2;
- 子树右左右之分,次序不能颠倒。
二叉树的分类:
满二叉树:不存在度为1的节点。只可能度为0和2 (节点个数N=(2^k) -1, k为深度,如下图满二叉树的节点个数为7 = (2 * 2 * 2) - 1)
完全二叉树:具有N个节点的结构与满二叉树的前n个节点的结构相同。称为完全二叉树(永远都有左子树,可能没有右子树)。
二叉树的性质:
(1)若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i - 1)个节点。
(2)若规定只有根节点的二叉树深度为1,则深度为K的二叉树的最大节点数是2^K - 1。
(3)对任何一个二叉树,如果其叶节点个数为n0,度为2的非叶节点个数为n2,则有n0 = n2 + 1,n总 = n0 + n1 + n2;
(4)具有n个节点的完全二叉树的深度k为log2(n + 1)上取整。
(5)对于具有n个节点的完全二叉树,入股按照从上到下,从左到右的顺序对所有节点从0开始编号,则对于序号为i的节点有,
- 若i > 0,双亲序号:(i - 1)/ 2 ;i = 0, 为根节点,无双亲节点
- 若2i + 1 < n,左孩子编号为2i + 1,否则无左孩子。
- 若2i + 1 < n,右孩子编号为2i + 2,否则无右孩子。
二叉树的存储结构:
顺序存储:
缺点:遇到单支树和一般二叉树会浪费大量存储空间。
链式存储:
优点:能很好的利用存储空间。适用于任何二叉树的存储
常见二叉树节点的定义:通常使用左右孩子表示法来描述二叉树
@Data
@NoArgsConstructor
static class Node<T> {
/**
* 左孩子
*/
private Node<T> leftChild;
/**
* 右孩子
*/
private Node<T> rightChild;
/**
* 数据
*/
private T data;
public Node(T data) {
this.data = data;
}
}
二叉树的基本操作:
(1)创建二叉树
(2)先序遍历二叉树
(3)中序遍历二叉树
(4)后序遍历二叉树
(5)拷贝一颗二叉树
(6)层序遍历
(7)二叉树的销毁
(8)二叉树的镜像非递归
(9)求二叉树中节点的个数
(10)求二叉树中叶子节点的个数
(11)求二叉树中第K层节点的个数
(12)求二叉树的高度
(13)检测一颗二叉树是否为完全二叉树
(14)根据数据获取二叉树中的节点
(15)检测一个节点是否在二叉树中
(16)前序遍历的非递归实现
(17)中序遍历的非递归实现
(18)后续遍历的非递归实现
(19)二叉树的镜像递归实现
具体实现:
(1)创建二叉树(必须遵循先序遍历规则)
思路:
- 先将数据按先序遍历的顺序存储在一个数组中。
- 创建根节点,将数据赋值给根节点。
- 创建左子树
- 创建右子树
- 遇到无用的操作符,该节点不被创建,并且函数返回。
代码如下:
private static int index = 0;
public <T> Node<T> create(T[] data, Node<T> node, int i) {
if (i >= data.length) {
return null;
}
if (data[i] == null) {
return null;
}
node.setData(data[index]);
node.setChildLeft(create(data, new Node<>(), ++index));
node.setChildRight(create(data, new Node<>(), ++index));
return node;
}
(2)先序遍历二叉树
思路:先遍历根节点,再遍历左子树,再遍历右子树
public <T> void preIterator(Node<T> node) {
if (node != null && node.getData() != null) {
System.out.print(node.getData() + ",");
preIterator(node.getChildLeft());
preIterator(node.getChildRight());
}
}
(3)中序遍历二叉树
/**
* 中序遍历
*
* @param node
* @param <T>
*/
public <T> void inIterator(Node<T> node) {
if (node != null && node.getData() != null) {
inIterator(node.getChildLeft());
System.out.print(node.getData() + ",");
inIterator(node.getChildRight());
}
}
(4)后序遍历二叉树
/**
* 后序遍历
*
* @param node
* @param <T>
*/
public <T> void postIterator(Node<T> node) {
if (node != null && node.getData() != null) {
postIterator(node.getChildLeft());
postIterator(node.getChildRight());
System.out.print(node.getData() + ",");
}
}
(5)拷贝一颗二叉树(先序遍历规则)
思路:先拷贝根节点,再拷贝左子树,再拷贝右子树。
/**
* 同创建二叉树
* @param src
* @param <T>
* @return
*/
public <T> Node<T> copy(Node<T> src) {
if (src == null || src.getData() == null) {
return null;
}
Node<T> desc = new Node<>();
desc.setData(src.getData());
desc.setChildLeft(copy(src.getChildLeft()));
desc.setChildRight(copy(src.getChildRight()));
return desc;
}
(6)层序遍历
思路:先遍历根节点,每遇到一个节点都先依次判断他的左右孩子是否为空,然后将其左孩子和右孩子加入到队列中。然后将队头节点出队列
/**
* 层序遍历二叉树
*
* @param head
* @param <T>
*/
public <T> void levelIterator(Node<T> head) {
if (head == null) {
return;
}
Queue<Node<T>> queue = new LinkedBlockingQueue<>();
queue.add(head);
while (!queue.isEmpty()) {
Node<T> node = queue.poll();
System.out.print(node.getData());
if (node.getLeftChild() != null) {
queue.add(node.getLeftChild());
}
if (node.getRightChild() != null) {
queue.add(node.getRightChild());
}
}
}
(7)二叉树的销毁(必须遵循后续遍历规则,否则会出现空指针异常)
思路:要销毁一颗二叉树,应该先删除他的左右子树,再删除根节点。
/**
* 销毁一颗二叉树
*
* @param head
* @param <T>
*/
public <T> void destroy(Node<T> head) {
if (head == null) {
return;
}
destroy(head.leftChild);
destroy(head.rightChild);
head.setData(null);
head.leftChild = null;
head.rightChild = null;
// java中这一步没啥用。。函数内部改变引用指向不会影响外部。
head = null;
}
(8)二叉树的镜像非递归
思路:和层序遍历一样,当前节点入队列,左孩子入队列,右孩子入队列,交换队头节点的左右孩子
// 二叉树的镜像---非递归
void MirrorBinTreeNor(PBTNode pRoot) {
Queue* q = (Queue*)malloc(sizeof(Queue));
QueueInit(q);
QueuePush(q, pRoot);
PBTNode tmp = NULL;
while (!QueueEmpty(q)) {
PBTNode cur = QueueFront(q);
if (cur->LChild != NULL)
QueuePush(q, cur->LChild);
if (cur->RChild != NULL)
QueuePush(q, cur->RChild);
swapBTNodeNode(&(cur->LChild), &(cur->RChild));
QueuePop(q, &tmp);
}
}
(9)求二叉树中节点的个数
思路:总节点个数等于 == 左孩子个数 + 右孩子个数 + 根节点个数(1),递归结束条件,当前节点为空,返回0(优化:当左右孩子为空时,直接返回,可以大幅减少递归深度);
/**
* 二叉树节点个数
*
* @param head
* @param <T>
*/
public <T> int nodeCount(Node<T> head) {
if (head == null) {
return 0;
}
if (head.leftChild == null && head.rightChild == null) {
return 1;
}
return nodeCount(head.leftChild) + nodeCount(head.rightChild) + 1;
}
(10)求二叉树中叶子节点的个数
思路:叶子节点个数 == 叶子节点中左孩子的个数 + 叶子节点中右孩子的个数
/**
* 二叉树叶子节点个数
*
* @param head
* @param <T>
*/
public <T> int leafNodeCount(Node<T> head) {
if (head == null) {
return 0;
}
if (head.leftChild == null && head.rightChild == null) {
return 1;
}
return leafNodeCount(head.leftChild) + leafNodeCount(head.rightChild);
}
(11)求二叉树中第K层节点的个数
思路:和求叶子节点个数类似,第K层中左孩子个数 + 第K层中右孩子个数;只是将结束条件变成了K == 1;(K最小为1时,节点为根节点,个数为1)
/**
* 二叉树第k层节点个数
* 第k层节点个数等于第k-1层左孩子个数+第k-1层右孩子个数
*
* @param head
* @param k
* @param <T>
* @return
*/
public <T> int levelKNodeCount(Node<T> head, int k) {
if (head == null) {
return 0;
}
if (k <= 0) {
return 0;
}
if (k == 1) {
return 1;
}
return levelKNodeCount(head.leftChild, k - 1) + levelKNodeCount(head.rightChild, k - 1);
}
(12)求二叉树的高度
思路:判断当前节点是否为空,为空返回0,不为空返回1,将返回值赋值给high,比较左右子树的较大值,取大的。
/**
* 二叉树的高度
*
* @param head
* @param <T>
* @return
*/
public <T> int bTreeHigh(Node<T> head) {
if (head == null) {
return 0;
}
if (head.leftChild == null && head.rightChild == null) {
return 1;
}
int leftHigh = bTreeHigh(head.leftChild);
int rightHigh = bTreeHigh(head.rightChild);
return Math.max(leftHigh, rightHigh) + 1;
}
(13)检测一颗二叉树是否为完全二叉树
思路:完全二叉树的某个节点之前一定是一颗满二叉树。所有的节点度都为2。
找到这个节点。
这个节点的可能情况为:只有左孩子,没有左孩子也没有右孩子。
利用层序遍历,从上到下从左到右,就可以找到这个点。如果在这个点之后出现了任何一个节点有孩子,那这个二叉树就不是完全二叉树。
// 检测一棵树是否为完全二叉树
int IsCompleteBinTree(PBTNode pRoot) {
if (pRoot == NULL)
return 0;
// 标识是否找到了这个特殊的节点。
int flag = 0;
PBTNode cur;
Queue q;
QueueInit(&q);
QueuePush(&q, pRoot);
while (!QueueEmpty(&q)) {
QueuePop(&q, &cur);
if (flag == 1 && (cur->LChild != NULL || cur->RChild != NULL))
return 0;
if (cur->LChild != NULL && cur->RChild != NULL && flag == 0) {
QueuePush(&q, cur->LChild);
QueuePush(&q, cur->RChild);
}
if (cur->LChild == NULL && cur->RChild != NULL)
return 0;
if ((cur->RChild == NULL && cur->LChild == NULL) ||
(cur->RChild == NULL && cur->LChild != NULL))
{
// 找到分界点。
flag = 1;
}
}
return 1;
}
(14)根据数据获取二叉树中的节点
思路:先找左孩子,没找到(node == NULL)才找右孩子。
/**
* 根据数据获取节点,找到则返回
*
* @param head
* @param <T>
* @return
*/
public <T> Node<T> getNodeByData(Node<T> head, T data) {
if (head == null) {
return null;
}
if (head.data.equals(data)) {
return head;
}
if (head.leftChild == null && head.rightChild == null) {
return null;
}
Node<T> node = getNodeByData(head.leftChild, data);
if (node == null) {
node = getNodeByData(head.rightChild, data);
}
return node;
}
(15)检测一个节点是否在二叉树中
思路:和获取二叉树中的节点相同,但是要判断pNode是否为空。
/**
* 判断节点是否在二叉树中
*
* @param head
* @param <T>
* @return
*/
public <T> boolean hasNode(Node<T> head, Node<T> node) {
if (node == null || head == null) {
return false;
}
if (head == node) {
return true;
}
if (head.leftChild == null && head.rightChild == null) {
return false;
}
return hasNode(head.leftChild, node) || hasNode(head.rightChild, node);
}
(16)前序遍历的非递归实现
思路:先将当前节点的右孩子入栈,然后一直往左遍历(左孩子不入栈),如果遇到的节点为空,就出栈。并将当前节点设置为栈中的节点。
// 循环先序遍历二叉树
void PreOrderNor(PBTNode pRoot) {
if (pRoot == NULL)
return;
PBTNode cur = pRoot;
Stack s;
StackInit(&s);
while (1) {
printf("%c ", cur->data);
if (cur->RChild != NULL)
StackPush(&s, cur->RChild);
cur = cur->LChild;
if (cur == NULL) {
int ret = StackPop(&s, &cur);
if (ret == 0)
return;
}
}
}
(17)中序遍历的非递归实现
思路:
(1)一直找到当前树的最左节点,并将遇到的节点入栈。
(2)出栈打印左孩子,判断当前节点有无右孩子,无继续出栈,若有,则继续第一步
//循环中序遍历二叉树
void InOrderNor(PBTNode pRoot) {
if (pRoot == NULL)
return;
PBTNode cur = pRoot;
PBTNode tmp = NULL;
Stack s;
StackInit(&s);
while (1) {
while (cur != NULL) {
StackPush(&s, cur);
cur = cur->LChild;
}
// ret表示pop的状态,0表示空栈
int ret = StackPop(&s, &tmp);
if (!ret)
return;
printf("%c ", tmp->data);
if (tmp->RChild)
cur = tmp->RChild;
}
}
(18)后续遍历的非递归实现
思路:
(1)将当前节点入栈,将当前节点的右孩子入栈,直到找到当前树的最左节点。
(2)看最左节点有无右孩子,有跳到(1),无则打印这个最左节点。
(3)出栈,将出栈的这个节点赋值给当前节点,判断有无左右孩子,有跳到(1)
//循环后续遍历二叉树
void PostOrderNor(PBTNode pRoot) {
if (pRoot == NULL)
return;
Stack s;
StackInit(&s);
PBTNode cur = pRoot;
// 当前节点
PBTNode tmp = NULL;
// 用来记录上一次出栈的节点,以便判断这个节点是否应该继续向下遍历
PBTNode preTmp = NULL;
//放入根节点
StackPush(&s, cur);
while (1) {
// 一直向左,入栈顺序为,cur cur->RChild cur->LChild
while (cur != NULL)
{
if (cur->RChild)
StackPush(&s, cur->RChild);
if (cur->LChild)
StackPush(&s, cur->LChild);
cur = cur->LChild;
}
if (cur == NULL) {
tmp = StackTop(&s);
//栈为空退出
if (tmp == NULL)
return;
//判断当前点的子节点是否是上一次遍历的。如果是则代表这颗树的子节点已经遍历完了,就直接输出当前节点
if (tmp->LChild == preTmp || tmp->RChild == preTmp) {
printf("%c ", tmp->data);
StackPop(&s, &tmp);
preTmp = tmp;
continue;
}
// 不直接遍历则看他的左右孩子是否为空,不为空就入栈
if (tmp->LChild || tmp->RChild) {
cur = tmp;
preTmp = tmp;
continue;
}
//叶子节点,直接打印。
StackPop(&s, &tmp);
preTmp = tmp;
printf("%c ", tmp->data);
}
}
}
(19)二叉树的镜像递归实现
思路:遇到一个非空的节点就交换他们的左右孩子。
// 二叉树的镜像---递归
void MirrorBinTree(PBTNode pRoot) {
if (pRoot) {
swapBTNodeNode(&(pRoot->LChild), &(pRoot->RChild));
MirrorBinTree(pRoot->LChild);
MirrorBinTree(pRoot->RChild);
}
}
以上程序的运行结果: