一、树的概念以及结构
1.树的概念
树是一种非线性的数据结构,它是由n(n>0)个有限节点组成的一个具有层次关系的集合。把它叫做树是因为其看起来像一棵倒挂的树,根在上,叶子在下。
1.1 有一个特殊的节点,称为根节点,根节点没有前驱节点。
1.2 除根节点外,其余节点被分为M(M>0)个互不相交的集合Tree1、Tree2、Tree3...Treem,其中每一个集合Treei(1<=i<=m)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以又0个或多个后继。
1.3 因此树是递归定义的。


注:树形结构中子树之间不能有交集,否则就不再是树形结构了,而是图。树形结构中任何节点都只能只有一个箭头指向该节点,或者说任何节点都只能只有一个双亲节点。

2.树的相关概念

节点的度:一个节点含有子树的个数被称为该节点的度。例如:A节点的度为6,F节点的度为3,H节点的度为0。
叶子节点(终端节点) :度为0的节点被称为叶子节点。例如:B、C、H、I...都是叶子节点。
分支节点(非终端节点) :度不为0的节点。例如:A、D、E、F、G...都是分支节点。
父节点(双亲节点) :若一个节点有子节点,则称这个节点为该子节点的父节点。例如:A是B的父节点,D是H的父节点,E是I的父节点。
子节点(孩子节点) :一个节点的子树的根节点,该子树的根节点被称为该节点的子节点。例如:E是A的子节点,J是E的子节点,P是J的子节点。
兄弟节点 :具有相同父节点的节点互称为兄弟节点。例如:D和E的父节点都是A,因此D和E是兄弟节点;I和J的父节点都是E,因此I和J是兄弟节点。
树的度:一棵树中最大的节点的度为该树的度。例如:该树的度为6。
节点的层次:从根节点开始,一般来说根节点处于第1层,根的子节点处于第2层,依次类推。
树的高度(深度) :树中节点的最大层次。例如:该树的高度为4。
堂兄弟节点:父节点在同一层,但父节点不同的节点互称为堂兄弟节点。例如:H和I互为堂兄弟节点,J和K互为堂兄弟节点。
节点的祖先 :从根节点到该节点所经历的所有分支节点都是该节点的祖先节点。例如:J节点的祖先节点有E节点和A节点,L节点的祖先节点有F节点和A节点,并且根节点是所有节点的祖先。
子孙 :以该节点为根,其所有子树上的所有节点都是该节点的子孙。例如:对于E节点而言,其子孙节点有I、J、P、Q节点,对应F节点,其子孙有K、L、M节点。
森林:由m(m>0)棵互不相交的树的集合称为森林,一棵树也可以是森林。
注:每个节点都可能会有多个身份,既可以是父节点,也可以是子节点等。一棵树上,除了叶子节点就是分支节点。只有根节点没有父节点。任意一棵树都可以由两个部分组成:1.根节点2.根节点下的N棵子树,每棵子树也可以以同样的方法表示。
3.树的结构表示
树的结构有四种表示方法:
3.1 如果已知树的度,可以直接定义。
cpp
typedef int TreeDataType;
//例如树的度为3
typedef struct TreeNode
{
TreeDataType data;
struct TreeNode* child1;
struct TreeNode* child2;
struct TreeNode* child3;
}TreeNode;
3.2 用一个顺序表存储孩子。
cpp
typedef int TreeDataType;
//用顺序表存储孩子
typedef struct SeqList
{
struct Tree* child;
//...
}SeqList;
typedef struct TreeNode
{
TreeDataType data;
SeqList child; //顺序表,顺序表内数据类型为struct Tree*
}TreeNode;
3.3 双亲表示法,每个节点都会存储双亲节点的指针或下标。
cpp
typedef int TreeDataType;
typedef struct TreeNode
{
TreeDataType data;
int parenti; //父节点的下标
}TreeNode;

3.4左孩子右兄弟表示法。
cpp
typedef int TreeDataType;
//左孩子右兄弟表示法
typedef struct TreeNode
{
TreeDataType data;
struct TreeNode* leftchild; //左边的第一个孩子
struct TreeNode* rightbrother; //右边的第一个兄弟
}TreeNode;

4.树的实际应用
文件系统就是一棵树。Linux的文件系统被称为目录树,/代表根目录。

二、二叉树的概念以及结构
1.概念
一棵二叉树是节点的有限集合该集合:
1.1有可能为空。
1.2 由一个根节点加上两棵子树构成,这两棵子树分别是左子树和右子树,同样也是二叉树。

从上图可以看出:1.二叉树不存在度大于2的节点。2.二叉树的子树有左右之分,顺序不能颠倒,因此二叉树是有序树。
注:对于任意的二叉树都是由以下几种情况复合而成的:

2.特殊的二叉树
2.1 满二叉树:一个二叉树,如果每一层的节点数都达到了最大数目,那么这个二叉树就是一棵满二叉树。如果该满二叉树的层数为K,且该二叉树的节点数为2^(K)-1个,那么该二叉树就是一棵满二叉树。

2.2 完全二叉树:前K-1层每一层的节点数都是满的,最后一层的节点是连续的。K层完全二叉树的节点数范围为:2^(K-1)~2^(K)-1个节点。

注:满二叉树是一种特殊的完全二叉树。
3.二叉树的性质
3.1 若规定根节点的层数为1,则一棵非空二叉树的第i层的节点数最多为2^(i-1)个。
3.2 若规定根节点的层数为1,则一棵高度为h的二叉树最大节点数为2^h-1个。
3.3 对于任意一棵非空二叉树,若其度为0的节点数为n0,度为2的节点数为n2,则有n0=n2+1。
3.4 若规定根节点的层数为1,有n个节点的满二叉树,其高度h=log2(n+1)。
3.5 对于具有n个节点的完全二叉树,如果按照从上到下,从左到右的数组顺序,从0开始依次编号,则对于下标为i的节点来说:
cpp
1. 若i>0,i位置的双亲节点的下标为:(i-1)/2,若i=0,则无双亲节点。
2. 若2*i+1<n,左孩子下标:2*i+1,若2*i+1>n,则无左孩子。
3. 若2*i+2<n,右孩子下标:2*i+2,若2*i+2>n,则无右孩子。
3.6 完全二叉树中度为1的节点要么只有1个要么只有0个。
4.二叉树的存储结构
二叉树一般可以用两种结构来存储:顺序结构和链式结构。
4.1 顺序结构:顺序结构就是用数组来进行存储,并且只有完全二叉树才适合用数组存储,非完全二叉树用数组存储会有很大的空间浪费。并且完全二叉树在物理结构上是一个数组,在逻辑结构上是一棵二叉树。

4.2 链式结构:链式结构有二叉链和三叉链,通常会在高阶数据结构和红黑树上会用到链式结构。


cpp
typedef int TreeDataType;
//二叉链
typedef struct BinTreeNode
{
TreeDataType data;
struct BinTreeNode* leftchild; //指向左孩子
struct BinTreeNode* rightchild; //指向右孩子
}BinTreeNode;
//三叉链
typedef struct BinTreeNode
{
TreeDataType data;
struct BinTreeNode* leftchild; //指向左孩子
struct BinTreeNode* rightchild; //指向右孩子
struct BinTreeNode* parent; //指向双亲节点
}BinTreeNode;
三、二叉树的顺序结构及实现
3.1.二叉树的顺序结构
普通的二叉树并不适合用顺序结构存储,只有完全二叉树更适合用顺序结构存储。现实中的堆(一种完全二叉树)就是用顺序结构进行存储的。

3.2 堆的概念及结构
堆的概念:
cpp
1.堆是一棵完全二叉树。
2.堆分为大堆和小堆。
大堆:任何一个父节点的值都大于等于子节点的值。
小堆:任何一个父节点的值都小于等于子节点的值。


注:堆并不代表有序,不满足以上两个条件的二叉树不是堆。
3.3 堆的接口实现
结构:
cpp
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a; //动态数组,用于存储数据
int size; //数组内有效数据个数
int capacity; //数组的容量
}Heap;
初始化:
cpp
void HeapInit(Heap* php);
cpp
//初始化
void HeapInit(Heap* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
销毁:
cpp
void HeapDestroy(Heap* php);
cpp
//销毁
void HeapDestroy(Heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
交换:
cpp
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
插入:

cpp
void HeapPush(Heap* php, HPDataType x);
cpp
//插入
void HeapPush(Heap* php, HPDataType x)
{
assert(php);
//扩容
if (php->size == php->capacity)
{
//如果容量为0则扩到4,不为0则扩为2倍
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
//将数据尾插到数组
php->a[php->size++] = x;
//向上调整,保证插入数据后依旧符合堆的特性
AdjustUp(php->a, php->size - 1);
}
向上调整:
父节点与子节点下标的关系:
cpp
父亲找孩子:
leftchild=parent*2+1;
rightchild=parent*2+2;
孩子找父亲:
parent=(child-1)/2

cpp
void AdjustUp(HPDataType* a, int child);
cpp
//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
//已知孩子下标,计算父亲的下标
int parent = (child - 1) / 2;
//结束条件1:孩子下标为0
while (child != 0)
{
//小堆中如果孩子的值小于父亲就交换,大堆中如果孩子的值大于父亲就交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
//重新计算孩子和父亲的下标
child = parent;
parent = (child - 1) / 2;
}
else
{
//结束条件2:小堆中孩子的值大于等于父亲,大堆中孩子的值小于等于父亲
break;
}
}
}
判空:
cpp
bool HeapEmpty(Heap* php);
cpp
//判空
bool HeapEmpty(Heap* php)
{
assert(php);
return (php->size == 0);
}
删除:堆删除一般删除的都是堆顶的元素

cpp
void HeapPop(Heap* php);
cpp
//删除
void HeapPop(Heap* php)
{
assert(php);
//如果堆为空就不能继续删除
assert(!HeapEmpty(php));
//交换堆顶和堆尾的数据
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
//向下调整
AdjustDown(php->a, php->size, 0);
}
向下调整:

cpp
void AdjustDown(HPDataType* a, int sz, int parent);
cpp
//向下调整
void AdjustDown(HPDataType* a, int sz, int parent)
{
//计算孩子的下标,且默认左孩子是较小的那个
int child = parent * 2 + 1;
//结束条件1:父亲是叶子节点,孩子不存在
while (child<sz)
{
//如果右孩子存在,且右孩子的值小于左孩子
if ((child+1<sz)&&a[child+1]<a[child])
{
//那么较小的孩子就是右孩子
child++;
}
//小堆中如果孩子小于父亲,交换
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
//迭代继续求孩子和父亲的下标
parent = child;
child = parent * 2 + 1;
}
else
{
//结束条件2:孩子大于等于父亲
break;
}
}
}
取堆顶元素:
cpp
HPDataType HeapTop(Heap* php);
cpp
//取堆顶元素
HPDataType HeapTop(Heap* php)
{
assert(php);
//且堆不能为空
assert(!HeapEmpty(php));
return php->a[0];
}
获取堆的大小:
cpp
int HeapSize(Heap* php);
cpp
//获取堆的大小
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
3.4 堆的应用
3.4.1 堆排序
堆排序分为两个步骤:建堆和利用堆的删除来排序
cpp
void HeapSort(int* a,int sz);
建堆:建堆又分为两种,分别是向上调整建堆和向下调整建堆
方法1:向上调整建堆

cpp
int i = 1;
for (i = 1; i < sz; i++)
{
AdjustUp(a, i);
}
方法2:向下调整建堆

cpp
//向下调整建堆
//最后一个节点的下标
int end = sz - 1;
//从最后一个非叶子节点开始,到根调整完后结束
for (end = (sz - 1 - 1) / 2; end >= 0; end--)
{
AdjustDown(a, sz, end);
}
利用堆的删除排序:
如果要排升序,需要建大堆;如果要排降序,需要建小堆。

cpp
//排序
int i = 0;
for (i = sz-1; i > 0; i--)
{
//交换堆顶和堆尾的数据
Swap(&a[0], &a[i]);
//向下调整
AdjustDown(a, i, 0);
}
3.4.2 建堆的时间复杂度分析
向上调整建堆的时间复杂度为O(N*log(N)):

向下调整建堆的时间复杂度为O(N):

3.4.3 TopK
TopK问题:求大量数据中前K个最大或最小的元素,比如世界500强,专业前10名富豪榜等等。
例如:求N个数据中最大的前K个数据,N非常大。
正常思路:将这N个数向下调整建成大堆,然后PopK次即可找出最大的前K个数。
简单思路:
1.取这N个数中的前K个数建一个小堆。
2.后N-K个数依次与堆顶元素进行比较,如果比堆顶元素大,那么就将堆顶元素替换覆盖,然后向下调整。
3.最后这个K个数的小堆就是这N个数据中最大的前K个数了。
生成N个小于10000的随机数并写入文件中:
cpp
//生成N个小于10000的随机数
void CreatData(int N)
{
srand(time(NULL));
FILE* pf = fopen("text.txt", "w");
if (pf == NULL)
{
perror("fopen fail");
return;
}
//生成N个随机数,并写入文件中
int random = 0;
int i = 0;
for (i = 0; i < N; i++)
{
random = rand();
random %= 10000;
fprintf(pf, "%d\n",random);
}
fclose(pf);
}
TopK:
cpp
//TopK
void TopK(int K,int N)
{
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen fail");
return;
}
//取前K个数
int i = 0;
int* a = (int*)malloc(sizeof(int) * K);
if (a == NULL)
{
perror("malloc fail");
return;
}
for (i = 0; i < K; i++)
{
int tmp = 0;
fscanf(pf, "%d", &tmp);
a[i] = tmp;
}
//向下调整建堆
int end = K - 1;
for (end = (K - 1 - 1) / 2; end >= 0; end--)
{
AdjustDown(a, K, end);
}
//将后面N-K个数与堆顶元素进行比较
for (i = K; i < N; i++)
{
int tmp = 0;
fscanf(pf, "%d", &tmp);
if (tmp > a[0])
{
a[0] = tmp;
AdjustDown(a, K, 0);
}
}
for (i = 0; i < K; i++)
{
printf("%d ", a[i]);
}
free(a);
fclose(pf);
}
TopK的时间复杂度分析:
1.取前K个数并用向下调整的方法建成小堆:K
2.后N-K个数依次与堆顶元素进行比较,比堆顶元素大就进行替换,替换后向下调整:(N-K)*log(K)
四、链式二叉树
4.1 前置说明
二叉树可以是空树也可以是非空树。每一棵非空二叉树都可以分解为根、左子树、右子树。并且二叉树的非空子树也可以继续分为根、左子树、右子树。

链式二叉树的结构:
cpp
typedef int BTDataType;
//结构
typedef struct BinaryTreeNode
{
BTDataType data; //节点的值
struct BinaryTreeNode* left; //指向左子树
struct BinaryTreeNode* right; //指向右子树
}Node;
4.2 二叉树的遍历
4.2.1 二叉树的前序、中序、后序遍历
前序遍历(Preorder Traversal):以根、左子树、右子树的访问顺序进行遍历。
中序遍历(Inorder Traversal):以左子树、根、右子树的访问顺序进行遍历。
后序遍历(Postorder Traversal):以左子树、右子树、根的访问顺序进行遍历。
二叉树的遍历是一个递归过程,且递归的结束条件为遇到空子树就结束并返回。以下面这棵二叉树为例。

生成节点:
cpp
//生成节点
Node* CreatNode(int x)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
构造二叉树:
cpp
//建立链式二叉树
Node* CreatTree()
{
Node* node1 = CreatNode(1);
Node* node2 = CreatNode(2);
Node* node3 = CreatNode(3);
Node* node4 = CreatNode(4);
Node* node5 = CreatNode(5);
Node* node6 = CreatNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
前序遍历:
cpp
//前序遍历
void PrevOrder(Node* root)
{
//结束条件:当前节点为NULL
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}

中序遍历:
cpp
//中序遍历二叉树
void InOrder(Node* root)
{
//结束条件:当前节点为NULL
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}

后序遍历:
cpp
//后序遍历
void PostOrder(Node* root)
{
//结束条件:遇到空子树
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}

4.2.2 计算二叉树节点个数、叶子节点个数、高度以及第k层节点个数
以该二叉树为例

1.计算二叉树节点个数:
思路:分治算法,分别求出根节点的左子树的节点个数和右子树的节点个数,然后加上自身就是整棵树的节点个数,同时左子树和右子树又可以分为根和子树的形式,递归求解。
结束条件:当根为NULL时结束,返回0。
继续条件:根不为NULL,则继续递归求出根的左子树和右子树的节点数,然后加根节点自身就是这棵树的节点个数。
cpp
//求二叉树的节点个数
int CountNode(Node* root)
{
//如果是空则返回0
if (root == NULL)
{
return 0;
}
//不为空则计算左子树节点个数和右子树节点个数,然后加1
int left = CountNode(root->left);
int right = CountNode(root->right);
return left + right + 1;
}

2.计算二叉树叶子节点个数
结束条件1:当前的树是一棵空树,没有叶子节点,返回0。
结束条件2:当根的左右子树都为NULL时为叶子节点返回1。
继续条件:根的左右子树不全为NULL,继续计算左右子树中叶子节点个数,并求和返回。
cpp
//求叶子节点的个数
int Countleaf(Node* root)
{
//结束条件1:当前树是一棵空树,没有叶子节点,返回0
if (root == NULL)
{
return 0;
}
//结束条件2当前节点没有子树,为叶子节点,返回1
if (root->left == NULL && root->right == NULL)
{
return 1;
}
int left = Countleaf(root->left);
int right = Countleaf(root->right);
return left + right;
}

3.求二叉树的高度
结束条件:遇到空树时,空树的高度是0,返回0。
继续条件:求出左子树和右子树的高度,然后取其中较高的子树的高度,然后加1。因为子树和根之间的高度差1,因此求出了子树的高度后树的高度为子树的高度加1。
cpp
//求二叉树的高度
int TreeHight(Node* root)
{
//结束条件:如果根为NULL则返回0
if (root == NULL)
{
return 0;
}
//如果根不为空则返回左子树和右子树中较高的子树的高度再加1
int leftheight = TreeHight(root->left);
int rightheight = TreeHight(root->right);
return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

4.求第k层节点的个数
当k为3时:
结束条件1:当根为NULL时,返回0。
结束条件2:当k==1且当前节点不为BULL时,则返回1。
继续条件:求出左子树第k-1层的节点个数和右子树第k-1层的节点个数,然后求和返回。
cpp
//求第K层节点数量
int LevelKNode(Node* root, int k)
{
//结束条件1:根为空则返回0
if (root == NULL)
{
return 0;
}
//结束条件2:k为1时返回1
if (k == 1)
{
return 1;
}
//继续条件:k不为1,且根不为NULL
//求子树的第k-1层的节点个数并统计
int left = LevelKNode(root->left, k - 1);
int right = LevelKNode(root->right, k - 1);
return left + right;
}

4.2.3 二叉树查找值为x的节点
结束条件1:当前节点为NULL,则直接返回NULL。
结束条件2:当前节点的值为x,则直接返回当前节点的地址。
继续条件1:先求左子树中有没有值为x的节点,如果有,就直接返回,没有就继续求右子树。
继续条件2:如果左子树没有存在值为x的,则遍历右子树查找有没有值为x的节点,有就直接返回。
最后的结束条件3:如果左子树和右子树中都没有值为x的节点,则返回NULL。
cpp
//二叉树查找值为x的节点
Node* FindxNode(Node* root, int x)
{
//结束条件1:当前节点为NULL,则直接返回NULL
if (root == NULL)
{
return NULL;
}
//结束条件2:当前节点的值为x,则直接返回当前节点的地址
if (root->data == x)
{
return root;
}
//继续条件1:先求左子树中有没有值为x的节点,如果有,就直接返回,没有就继续求右子树
Node* node = FindxNode(root->left, x);
if (node != NULL)
{
return node;
}
//继续条件2:如果左子树没有存在值为x的,则遍历右子树查找有没有值为x的节点,有就直接返回
node = FindxNode(root->right, x);
if (node != NULL)
{
return node;
}
//结束条件3:如果左子树和右子树中都没有值为x的节点,则返回NULL
return NULL;
}

4.2.4 层序遍历
层序遍历需要用队列的先进先出解决问题。
核心思路:上一层出的时候将其左右孩子节点带入队列,如果孩子节点为NULL节点则不带入。

需要改变的队列结构:
cpp
//此处对队列节点内数据类型的重定义需要用树节点的指针
//并且不能提前用对树节点重命名的名称来重命名队列内节点数据的类型
typedef struct BinaryTreeNode* QDataType;
//链式队列的每个节点的结构
typedef struct QueueNode
{
QDataType data; //节点的数据
struct QueueNode* next; //指向下一个节点的指针
}QNode;
//管理队列所需要的结构
typedef struct Queue
{
QNode* phead; //头结点的指针
QNode* tail; //尾节点的指针
int size; //队列有效数据个数
}Queue;
层序遍历:
cpp
//层序遍历
void LevelOrder(Node* root)
{
Queue q;
QueueInit(&q);
//如果根节点不为NULL,入队列
if (root != NULL)
{
QueuePush(&q, root);
}
//循环出入队列,直到队列为NULL
while (!QueueEmpty(&q))
{
Node* front = QueueFront(&q);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
printf("%d ", front->data);
}
printf("\n");
QueueDestroy(&q);
}
注:队列内节点的数据类型是树节点的指针。
二叉树的销毁:
二叉树的销毁一般是通过后序遍历,先销毁左右子树的节点,最后销毁根节点。
cpp
//二叉树的销毁
void BTDestroy(Node* root)
{
if (root == NULL)
{
return;
}
BTDestroy(root->left);
BTDestroy(root->right);
free(root);
}
判断二叉树是否为完全二叉树:
完全二叉树有一个性质,节点是连续的,因此可以使用层序遍历来判断二叉树的节点是否连续,来判断二叉树是不是完全二叉树。
cpp
//判断二叉树是否为完全二叉树
bool TreeComplete(Node* root)
{
Queue q;
QueueInit(&q);
//root不为NULL入队
if (root)
{
QueuePush(&q, root);
}
//层序遍历,并且NULL也需要入队
while (!QueueEmpty(&q))
{
Node* front = QueueFront(&q);
QueuePop(&q);
//如果遇到NULL就跳出循环
if (front == NULL)
{
break;
}
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
while (!QueueEmpty(&q))
{
Node* front = QueueFront(&q);
//如果队列里面还有非空元素,说明这棵二叉树不连续
if (front != NULL)
{
QueueDestroy(&q);
return false;
}
QueuePop(&q);
}
return true;
QueueDestroy(&q);
}
