顺序与链式二叉树的原理、应用与实现(万字长文)
一、树概念及结构
树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
-
有一个特殊的结点,称为根结点,根节点没有前驱结点
-
除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i
<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
树的相关概念
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
- 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
- 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
- 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
- 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
- 森林:由m(m>0)棵互不相交的树的集合称为森林;
树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间
的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法
等。简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
树在实际中的运用(表示文件系统的目录树结构)
二、二叉树概念及结构
概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
对于任意的二叉树都是由以下几种情况复合而成的:
特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
说,如果一个二叉树的层数为K,且结点总数是2k -1,则它就是满二叉树。 - 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K
的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对
应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
二叉树的性质
-
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个结点.
-
若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h-1.
层数 最大节点数 h = 1 2^0 == 1 h = 2 2^1 == 2 h = 3 2^2 == 4 ... h = h 2^(h-1) = x个 性质总结: 1.即每一层的最大节点数为x = 2^(h-1) 2.每一层的最大节点数都是之前所有层的和+1. 即所有节点的和 = 最大层的下一层的最大节点书-1 即n=2^h -1 3.对n=2^h-1 同时取对数,得到: log₂(n+1) = h 即高度h等于log₂(n+1)
-
对任何一棵二叉树, 如果叶结点(度为0)个数为n0, 度为2的分支结点个数为n2,则有n0=n2 +1
-
若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1).
-
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对
于序号为i的结点有:-
若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
-
若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
-
若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
-
二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
- 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空
间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺
序存储在物理上是一个数组,在逻辑上是一颗二叉树。
链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是
链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所
在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前简单二叉树一般都是二叉链,红黑树等会用到三叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
三、二叉树的顺序结构及实现
二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结
构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统
虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆的概念及结构
将根节点是最大的堆叫做最大堆或大根堆,根节点是最小的堆叫做最小堆或小根堆。
堆的作用:
- 选数
- 排序
- topK问题(N个数中,找最大或最小的前k个)
堆的性质:
-
堆中某个节点的值总是不大于或不小于其父节点的值;
-
堆总是一棵完全二叉树。
-
大堆:树中所有父亲都大于等于孩子
小堆:树中所有父亲都小于等于孩子
计算孩子与父亲的下标关系
1. 通过父亲得到左右孩子的下标
-
leftchild = parent*2+1
-
rightchild = parent*2+2
2. 通过孩子找到父亲的下标
前提,需要知道自己是左孩子还是右孩子
如何得知自己是左孩子还是右孩子?
下标的逻辑关系:
-
左孩子一定是奇数位
-
右孩子一定是偶数位
可以得到
-
parent = (leftchild - 1)/2
-
parent = (rightchild - 2)/2
根据计算机取整规则(向下取整):(左右孩子下标-1)/2的值是一样的
因此计算父亲下标的统一规则
parent = (child - 1) / 2
堆的逻辑结构是一颗完全二叉树
堆的物理结构是数组,
逻辑结构感性说明:想象的结构,图形
物理结构:实际存储结构,内存中的存储方式
判断数组是不是堆
-
方法:(画图)转成完全二叉树,看满不满足堆的条件
依据:任何一个数组都可以看作是完全二叉树,但完全二叉树不一定是堆
堆的代码实现
1. 向上调整算法
将子节点从下到上调整到符合堆的合适位置.
用途
- 插入(在堆的前提下插入新元素)
时间复杂度:O(log2(n))
分析:最坏要走到顶点,即高度次log2(n+1),最好是插入就满足,不用调整,即O(1)次
代码实现
大堆版本
void AdjustUp(HeapDataType* a, int child)
{
//向上调整算法-大堆:父亲小于最大儿就换
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
2. 向下调整算法
用于将一棵二叉树从顶点开始调整成堆.
用途:
- 删除堆的某个元素
- 无序数组建堆(最快方法)
- 堆排序
给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
时间复杂度:O(log2n)
分析:最坏情况是从堆顶调整到堆底.即高度次
代码实现
前提:左右子树是堆
适合建堆:
//大堆版本
void AdjustDown(HeapDataType *a,int size, int parent)
{
//假设左孩子大
int child = parent * 2 + 1;
//调整到叶子就结束
while (child < size) //左孩子不存在,右孩子一定不存在
{
//向下调整算法-大堆:选出最大孩子
if (child + 1 < size && a[child] < a[child + 1]){
child++;
}
//我小于我最大的孩子,就要交换,让他来当父亲,我当儿子 --- 因为大堆要选出最大的,只要最大的
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
3. 堆的创建
1. 向下调整算法建堆
原理:从最小父亲开始向上依次执行调整算法(虽然可以从最小孩子,不过没有意义,可以优化掉) --- 从最小的父亲开始,调整成堆,然后依次往前调整其他父亲节点.
-- 即从最小子树开始,倒着走,依次让最小子树成为堆,走完一层后使父节点的左右子树都为堆,始终满足向下调整算法,继续循环,直到调整到堆顶,建堆完成.
可以从数组末尾(都是叶子节点)开始,但是没有意义,因为:
- 叶子节点本身就满足堆.不需要调整
- 数组末尾部分位于二叉树最后一层,最后一层节点数量可能非常大(满二叉树为所有节点的一半-1),遍历对效率有影响
综上,可以优化掉叶子节点,避免浪费,提高效率
int a[] = {1,5,3,8,7,6};
代码实现
//接收一个堆指针(输出型参数),一个数组,一个数组大小
void HeapCreate(Heap *ps, HeapDataType *a, int size)
{
assert(ps);
//1. 开辟堆中数组空间
ps->a = (HeapDataType *)malloc(size * sizeof(HeapDataType));
if (ps->a == NULL)
{
perror("malloc fail");
exit(1);
}
memcpy(ps->a, a, size *sizeof(HeapDataType));//拷贝数组过去
ps->size = size;
ps->capacity = 2 * size;
//2. 建堆
//从最小父亲开始向下调整
//循环,直到父亲(堆顶)
int parent = (size - 1 - 1) / 2; //找最小的父亲,即可
while (parent >= 0)
{
AdjustDown(ps->a, ps->size, parent);
parent--;
}
}
建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个节点不影响最终结果):
因此:建堆的时间复杂度为O(N)。
向下调整算法建堆时间复杂度计算
假设满二叉树树高度h
各层的节点数为
第一层 2 ^ 0 ------向下调整h-1次
第二层 2 ^ 1 ------向下调整h-2次
第三层 2 ^ 2 ------向下调整h-3次
... ...
第h - 1层 2 ^ (h - 2) ------向下调整1次
第h层 2 ^ (h - 1)
向下调整算法建堆是从最小父亲开始,即第h-1层的最后一个节点 parent = (size-1-1)/2
最坏情况下所有节点需要执行的次数为
f(h) = 2^(h-2)*1 + 2^(h-3)*2 + ... + 2^1*(h-2) + 2^0*(h-1) 错位相减
2*f(h) = 2^(h-1)*1 + 2^(h-2)*2 + ... + 2^2*(h-2) + 2^1*(h-1)
作差、合并得f(h) = 2^h -h-1
其中 满二叉树节点数N = 2^h-1,即h = log(N+1) 代入得
f(N) = N - 1 - log(N+1) , 舍去logN(数量级)
所以O(n) = n
2. 向上调整算法建堆(简)
向上调整算法也可以建堆,但是效率十分低下,不推荐使用
代码实现
void HeapCreate(Heap *ps, HeapDataType *a, int size)
{
assert(ps);
//1. 开辟堆中数组空间
ps->a = (HeapDataType *)malloc(size * sizeof(HeapDataType));
if (ps->a == NULL)
{
perror("malloc fail");
exit(1);
}
memcpy(ps->a, a, size *sizeof(HeapDataType));//拷贝数组过去
ps->size = size;
ps->capacity = 2 * size;
//插入式建堆
for(int i = 1; i<size;i++){
AdjustUp(ps,i);
}
}
时间复杂度:
Tn = 1*20+2*21+3*22+...+h*2h-1;
Tn = S1+S2+...+Sn;
Sn = n*2(n-1); //同增关系
只看最后一项: 根据 h=log2(n+1),代换得到 h*2h-1=log2(n+1)*n
至少最后一项来看,就已达到O(n*log2n)级别,显然向上调整建堆的Sn的量级比向下调整建堆的量级高出很多
假设满二叉树树高度h
各层的节点数为
第一层 2 ^ 0
第二层 2 ^ 1 ------向上调整1次
第三层 2 ^ 2 ------向上调整2次
... ...
第h - 1层 2 ^ (h - 2) ------向上调整h-2次
第h层 2 ^ (h - 1) ------向上调整h-1次
计算方法还是错位相减,由图显然可发现向上调整算法执行次数数量级明显提高
不再计算
O(n) = n*logN
4. 堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
代码实现
void HeapPush(Heap *ps,HeapDataType x)
{
assert(ps);
//考虑扩容
if (ps->size == ps->capacity){
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
HeapDataType *tmp = (HeapDataType *)realloc(ps->a, newCapacity * sizeof(HeapDataType));
if (tmp == NULL){
perror("realloc fail!");
exit(-1);
}
ps->a = tmp;
ps->capacity = newCapacity;
}
ps->a[ps->size] = x;
ps->size++;
AdjustUp(ps->a, ps->size - 1);
}
5. 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
代码实现
void HeapPop(Heap *ps)
{
assert(ps);
assert(ps->size > 0); // 保证堆中存在数据
Swap(&ps->a[ps->size-1], &ps->a[0]);
ps->size--;
AdjustDown(ps->a,ps->size, 0);
}
6. 堆的其他功能 -- 代码实现
Heap.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<string.h>
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType *a;
int size;
int capacity;
//顺序表思想
}Heap;
void HeapPrint(Heap *ps);
void HeapInit(Heap *ps);
void HeapDestroy(Heap *ps);
void HeapPush(Heap *ps,HeapDataType x);
void HeapPop(Heap*ps);
int HeapSize(Heap *ps);
bool HeapEmpty(Heap *ps);
//接受一个堆,数组, 数组大小。建一个堆
void HeapCreate(Heap*ps, HeapDataType *a, int size);
void HeapSort(HeapDataType *a, int size);
void AdjustDown(HeapDataType *a, int size, int parent);
void AdjustUp(HeapDataType *a, int child);
void Swap(HeapDataType *p1, HeapDataType *p2);
Heap.c
#include"Heap.h"
void Swap(HeapDataType *p1, HeapDataType *p2){
HeapDataType tmp = 0;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void HeapPrint(Heap *ps){
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
}
void HeapInit(Heap *ps){
assert(ps);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
void HeapDestroy(Heap *ps){
assert(ps);
free(ps->a);
ps->capacity = ps->size = 0;
}
//选数相当快,只需要logN次
HeapDataType HeapTop(Heap *ps){
assert(ps);
assert(ps->size > 0);
return ps->a[0];
}
int HeapSize(Heap *ps){
assert(ps);
return ps->size;
}
bool HeapEmpty(Heap *ps){
assert(ps);
return ps->size == 0;
}
堆的应用
1. 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
升序:建大堆
降序:建小堆 - 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
堆排序代码实现
HeapSort.c
#include"Heap.h"
//建大堆:升序
//建小堆:降序
// 原理:
// 1. 建好堆,堆顶一定是最大/最小
// 2. 首位交换,堆顶到末尾(等价于堆删除操作) ---> 且最后一定是最大/最小的
// 3. 调整堆
void HeapSort(HeapDataType *a, int size)
{
assert(a);
//向上调整建堆(测试用)
//int parent = (size - 1 - 1) / 2;
//while (parent >= 0)
//{
// AdjustDown(a, size, parent);
// parent--;
//}
// ---- 1. 向下调整算法建堆 ----
//时间复杂度O(n)
int parent = (size - 1 - 1) / 2;
while (parent >= 0)
{
AdjustDown(a, size, parent);
parent--;
}
// ---- 2. 选数 ----
//时间复杂度O(n*logN),n个数都要高度次调整
//int end = size - 1; //下标版本
//元素个数版本,能够复用删除写法
while (size > 1) //元素个数大于1
{
Swap(&a[0], &a[size - 1]); //交换
size--;
AdjustDown(a, size, 0); //调整堆 -- 注意,此处end为元素个数
}
}
时间复杂度分析
//建堆时间复杂度为N
//"删除"过程和向上调整算法建堆几乎一样(镜像操作),约等于n*logN
//合计时间复杂度 O(n + n*logN) = O(n*logN)
2. TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆 - 用剩余的N-K个元素依次与堆顶元素来比较,若是小堆则大于堆顶元素时替换,然后向下调整.最后沉淀下来的就是最大的前K个;若是大堆则相反;
时间复杂度:
O(K+(N-K)*log2K) == O(N*log2K) //K远小于N
K:前K个先建大小为K的堆
后N-K个,最坏情况每次都比上一个大,每次都要向下调整到堆底.
四、二叉树链式结构与实现
二叉树的遍历
前序、中序以及后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉
树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历
是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
- 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
- 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
- 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为
根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在
层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层
上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
代码实现
Tree.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
//树的遍历
//根据不同的递归遍历方法 访问 每个树(结构体)的data和左右孩子 进行操作。
//每次递归到一颗新树(每个节点都是一颗树),都要先判断是否空树 ---- 以防止野指针错误
//---- 创建二叉树 ----
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode *left; //left sub tree
struct BinaryTreeNode *right; //right sub tree
}BTNode;
/*
node->data = x;
node->left = NULL;
node->right = NULL;
*/
BTNode* BuyBTNode(BTDataType x);//构建值为x的节点
/*
printf("%d", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
*/
void PrevOrder(BTNode *root);//先根遍历
/*
PrevOrder(root->left);s
printf("%d", root->data);
PrevOrder(root->right);
*/
void InOrder(BTNode *root); //中根遍历
/*
PrevOrder(root->left);
PrevOrder(root->right);
printf("%d", root->data);
*/
void PostOrder(BTNode *root); //后根遍历
void LevelOrder(BTNode *root); //层序遍历
/*
return BTSize(root->left) + BTSize(root->right) + 1;
*/
int BTSize(BTNode* root); //求二叉树节点个数
/**/
int BTLeafSize(BTNode* root); //求二叉树叶子节点个数
int BTHeight(BTNode *root);//求二叉树高度
int BTLevelKSize(BTNode *root, int k);//计算二叉树第k层节点
BTNode *BTFind(BTNode* root, BTDataType x); //二叉树查找某节点地址
void BTDestroy(BTNode*root); //二叉树销毁
BTNode * rebuildBinaryTree(BTDataType* str, int *pi); //构建一颗二叉树, pi为数组的下标地址
bool isBTComplete(BTNode *root);
构建二叉树与基本功能
tree.c
#include"Tree.h"
/*创建一个值为x的二叉树节点*/
BTNode* BuyBTNode(BTDataType x) //创建值为x的节点
{
BTNode *node = (BTNode*)malloc(sizeof(BTNode));//使用指针要指向实际空间
if (!node)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
/*
遍历命名
N(Node)、L(Left subtree)和R(Right subtree)
根据访问结点操作发生位置命名:
① NLR:前序遍历(Preorder Traversal 亦称(先序遍历))
——访问根结点的操作发生在遍历其左右子树之前。
② LNR:中序遍历(Inorder Traversal)
——访问根结点的操作发生在遍历其左右子树之中(间)。
③ LRN:后序遍历(Postorder Traversal)
——访问根结点的操作发生在遍历其左右子树之后。
前三种次序与后三种次序对称
*/
构建二叉树
/*构建一个二叉树*/
BTNode *rebuildBinaryTree(BTDataType* str, int *pi) // pi为数组的下标地址
{
if (str[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode*root = (BTNode*)malloc(sizeof(BTNode));
root->data = str[(*pi)++];
root->left = rebuildBinaryTree(str, pi);
root->right = rebuildBinaryTree(str, pi);
return root;
}
二叉树销毁
/*二叉树销毁*/
void BTDestroy(BTNode*root) /*一级指针传值,调用后需要在函数外置空root*/
{
if (!root)
{
return;
}
BTDestroy(root->left);
BTDestroy(root->right);
free(root);
root->left = root->right = NULL;
}
前序遍历
/*前序遍历*/
void PrevOrder(BTNode *root) //previous order顺序 NLR:前序遍历(Preorder Traversal 亦称
{
if (!root )
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
中序遍历
/*中序遍历*/
void InOrder(BTNode *root) // ..中
{
if (!root )
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
后序遍历
/*后续遍历*/
void PostOrder(BTNode *root) // post- 前缀 ..之后 ..后面 ..后
{
if (!root )
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
中序遍历(非递归)
vector<int> inorderTraversal(TreeNode* root) {
vector<int> v;
stack<TreeNode*> st;
TreeNode* cur = root;
while(cur || !st.empty())
{
while(cur)
{
st.push(cur);
cur = cur->left;
}
TreeNode* top = st.top();
v.push_back(top->val);
st.pop();
cur = top->right;
}
return v;
}
层序遍历
#define _CRT_SECURE_NO_WARNINGS 1
#include"Tree.h"
#include"Queue.h"
//无层级
void LevelOrder(BTNode *root)
{
if (!root) return;
//通过队列实现
Queue queue;
QueueInit(&queue);
if (root)
QueuePush(&queue, root);
//通过队列缓存子树滚动数据实现遍历
while (!QueueEmpty(&queue))
{
BTNode* front = QueueFront(&queue);//把数据从队列中取出
printf("%d ", front->data);
QueuePop(&queue);
if (front->left)
{
QueuePush(&queue, front->left);
}
if (front->right)
{
QueuePush(&queue, front->right);
}
}
QueueDestroy(&queue);
}
//有层级
void LevelOrder2(BTNode *root)
{
if (!root)
{
return;
}
Queue queue;
QueueInit(&queue);
int levelSize = 0;
QueuePush(&queue, root);
levelSize = 1;
while (!QueueEmpty(&queue))
{
while (levelSize--)
{
BTNode* front = QueueFront(&queue);
printf("%d ", front->data);
QueuePop(&queue);
if (front->left)
QueuePush(&queue, front->left);
if (front->right)
QueuePush(&queue, front->right);
}
printf("\n");
levelSize = QueueSize(&queue);
}
QueueDestroy(&queue);
}
计算二叉树第k层节点个数
/*求二叉树第k层节点个数*/
int BTLevelKSize(BTNode *root , int k)
{
/*空*/
if (!root)
{
return 0;
}
//遇到第k层
if (k == 1) //最后一层,第k层
{
return 1;
}
return BTLevelKSize(root->left,k-1) + BTLevelKSize(root->right,k-1) ;
}
计算树的节点个数(全部节点和)
int TreeSize(struct BinaryTreeNode *root)
{
return !root ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
前序遍历保存二叉树节点的值
#define _CRT_SECURE_NO_WARNINGS 1
#include"Tree.h"
int TreeSize(struct BinaryTreeNode *root)
{
return !root ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
void _preorderTraversal(struct BinaryTreeNode *root, int *str, int* pi)
{
if (!root)
{
return ;
}
str[(*pi)++] = root->data;
_preorderTraversal(root->left, str, pi);
_preorderTraversal(root->right, str, pi);
}
int* preorderTraversal(struct BinaryTreeNode* root, int* returnSize){
*returnSize = TreeSize(root);
int *a = (int*)malloc(*returnSize * sizeof(int));
int i = 0;
_preorderTraversal(root, a, &i);
return a;
}
计算叶子节点个数
/*求二叉树叶子节点个数*/
int BTLeafSize(BTNode* root)
{
if (!root)
{
return 0;
}
//思想:左右节点为空即为叶子
if(!root->left && !root->right)
{
return 1;
}
return BTLeafSize(root->left) + BTLeafSize(root->right);
}
二叉树查找
/*二叉树查找*/
BTNode *BTFind(BTNode* root, BTDataType x)
{
if (!root ) return NULL;
//自己就是,找到了
if (root->data == x) return root;
//左子树找到了
BTNode*left = BTFind(root->left ,x);
if (left) return left;
//右子树找到了
BTNode*right = BTFind(root->right,x);
if (right) return right;
//没找到
return NULL; //root !=NULL , 值不为x ,左右子树找不到,返回空
}
计算二叉树高度
-
方法一
思路:比较左右子树高度,不要小的 只有关系运算符能够实现:比较两边选一边舍弃另一边
存在问题:递归之后栈帧会销毁,数据不再保存,每次递归都需要重新计算以往计算过的子树高度,有性能浪费;且每个子树高度计算都是指数级
/*求二叉树高度*/
int BTHeight(BTNode *root)
{
if (!root) return 0;
return BTHeight(root->left) > BTHeight(root->right)
? BTHeight(root->left) + 1
: BTHeight(root->right) + 1;
}
- 方法二
通过记下当前栈帧数据,避免重复计算
int BTHeight(BTNode *root)
{
if (!root) return 0;
int leftHeight = BTHeight(root->left);
int rightHeight = BTHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
判断是否完全二叉树
#include"Tree.h"
#include"Queue.h"
bool isBTComplete(BTNode *root)
{
if (!root)
{
return false;
}
Queue queue;
QueueInit(&queue);
QueuePush(&queue, root);
while (!QueueEmpty(&queue))
{
BTNode* front = QueueFront(&queue);
QueuePop(&queue);
if (!front) //一定存在下一层节点,所以直接跳出去,不用插入所有节点
{
QueuePop(&queue);
break;
}
QueuePush(&queue, front->left);
QueuePush(&queue, front->right);
}
while (!QueueEmpty(&queue))
{
if (QueueFront(&queue))
{
QueueDestroy(&queue);
return false;
}
QueuePop(&queue);
}
QueueDestroy(&queue);
return true;
}
判断是否是子树
#include<stdio.h>
struct TreeNode
{
int val;
struct TreeNode* left; //left sub tree
struct TreeNode* right; //right sub tree
};
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
if (subRoot == NULL)
{
return true;
}
if (root == NULL)
{
return false;
}
if (root->val == subRoot->val) //如果相等,且是相同子树,返回真
{
if (isSameTree(root, subRoot))
{
return true;
}
}
return isSubtree(root->left, subRoot)
|| isSubtree(root->right, subRoot);
}
判断两棵树是否相同相同
#include<stdio.h>
struct TreeNode
{
int val;
struct TreeNode* left; //left sub tree
struct TreeNode* right; //right sub tree
};
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
if (p == NULL && q == NULL)
{
return 1;
}
else if (p == NULL&&p != q || q == NULL && p != q)
{ /*注意逻辑运算先后顺序,整个if-else是判断有1个NULL以上为前提,运算符要求先有空*/
//可以优化成 p == NULL && q == NULL,因为已有前提;
return 0;
}
if (p->val != q->val)
{
return 0;
}
else
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
两个树是否对称
#include "Tree.h"
bool _isSymmetric(BTNode *root1, BTNode *root2)
{
if (!root1 &&!root2)
{
return true;
}
if (!root1 || !root2)
{
return false;
}
if (root1->data != root2->data)
{
return false;
}
return _isSymmetric(root1->left, root2->right) && _isSymmetric(root1->right, root2->left);
}
bool isSymmetric(BTNode *root)
{
return !root || _isSymmetric(root->left, root->right);
}
两棵树是否对称2
//检查左右子树是否对称
//检查方法:
// 1.两个节点都为空为对称
// 2.两节点的值相等 && a节点左与b节点右对称 && a节点右与b节点左
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == nullptr)
return true;
return check(root->left, root->right);
}
bool check(TreeNode* p, TreeNode* q) {
if (!q && !p)
return true; // 两个都是空
if (!q || !p)
return false; // 两个都不是空的前提,其中一个为空,pass
return check(p->left, q->right) && check(p->right, q->left) &&
p->val == q->val;
}
};
二叉树的直径
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
/*
递归分解:
分别计算root左右节点的最大直径,然后求和与flag作比较+置换
(每个节点都要这么做)
flag是最长路径的节点个数,flag-1就是路径数
(先计算节点数,最后再-1得到路径)
*/
class Solution {
public:
int answer = 0; //最长经过的节点数
int diameterOfBinaryTree(TreeNode* root) {
answer = 1; //为空时满足return条件
depth(root);
return answer-1;
}
int depth(TreeNode * root ){
if(root == nullptr) return 0;
int L = depth(root->left); //root的左子树深度 = 左子树最长路径节点数
int R = depth(root->right);//root的右子树节点数 = ...
answer = max(L+R+1,answer);//左右连起来最长路径节点数,与旧值比较
return max(L,R)+1; //返回root的最长子树的深度
}
};
单值二叉树
如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树
#include<stdio.h>
struct TreeNode
{
int val;
struct TreeNode* left; //left sub tree
struct TreeNode* right; //right sub tree
};
bool isUnivalTree(struct TreeNode* root){
if (root == NULL)
{
return true;
}
if (root->left && root->left->val != root->val)
{
return false;
}
if (root->right && root->right->val != root->val)
{
return false;
}
return isUnivalTree(root->left) && isUnivalTree(root->right);
}
翻转二叉树
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) return nullptr;
TreeNode* left = invertTree(root->left);
TreeNode* right = invertTree(root->right);
root->left = right;
root->right =left;
return root;
}
最大深度
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
return max(maxDepth(root->left),maxDepth(root->right))+1;
}
用到的队列
Quque.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//#include"tree.h"
struct BinaryTreeNode; //类型声明 : 类型是可以声明的,只要变量名就可以---原理,搜索整个源码
typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
struct QueueNode *next;
QDataType data;
}QNode;
//控制变量结构体
//只有一个值,就不用定义结构体,有多个就定义结构题。
typedef struct Queue
{
struct QueueNode *head; //队头,出队,头删
struct QueueNode *tail; //队尾,入队,尾插
}Queue;
//是指针变量就传二级指针,是普通变量就传一级
void QueueInit(Queue *pq);
void QueueDestroy(Queue *pq);
void QueuePush(Queue *pq, QDataType x);
void QueuePop(Queue *pq);
QDataType QueueFront(Queue *pq);
QDataType QueueBack(Queue *pq);
int QueueSize(Queue *pq);
bool QueueEmpty(Queue *pq);
Queue.c
#include "Queue.h"
void QueueInit(Queue *pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode * next = NULL;
QNode *cur = pq->head;
while (cur)
{
next = cur->next;
free(cur);
cur = next;
}
pq->head=pq->tail = NULL;
}
void QueuePush(Queue *pq,QDataType x)
{
assert(pq);
QNode *newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail ");
exit(-1);
}
//先初始化再使用
newnode->data = x;
newnode->next = NULL;
//单链表队列-尾插头删。
if (pq->tail == NULL)//头尾都行,判断一个就可以了
{
pq->head = pq->tail = newnode;
}
else //尾插
{
pq->tail->next = newnode; //队尾指向的节点链接上新节点
pq->tail = newnode; //队尾指向新节点
}
}
void QueuePop(Queue *pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->head->next == NULL)//只有一个节点
{
free(pq->head);//先释放
pq->tail = pq->head = NULL;//后置空
}
else
{
QNode *next = pq->head->next;//记住下一个
free(pq->head);//释放头节点
pq->head = next;//下个节点成为新节点
}
}
QDataType QueueFront(Queue *pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue *pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
int QueueSize(Queue *pq)
{
assert(pq);
QNode *cur = pq->head;
int size = 0;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
bool QueueEmpty(Queue *pq)
{
assert(pq);
//return QueueSize(pq) == 0;
return pq->head == NULL ;//只要有一个就可以了
//head为空tail也为空
}
本文来自博客园,作者:HJfjfK,原文链接:https://www.cnblogs.com/DSCL-ing/p/18344162