目录
1.树的概念
树是一种非线性
的数据结构,它是由
n
(
n>=0
)个有限结点组成一个具有层次关系的集合。
把做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树的根部称之为根节点,根节点之前并没有其它节点,也就是说根节点没有前驱节点
除根节点外,其余结点被分成N(N>0)个互不相交的集合T1、T2、……、Tn,其中每一个集Tn又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
由此可以看出,树是递归定义
的。
我们需要注意的的是树这一结构中,子树不可相交,否则就不是树形结构
树的一些基础概念是需要我们记住的,理解之后不用背也能记得下来
节点的度
:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点
:度为
0
的节点称为叶节点;
(也叫叶子节点)
非终端节点或分支节点
:度不为
0
的节点;
双亲节点或父节点
:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点
:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点
:具有相同父节点的节点互称为兄弟节点;
树的度
:一棵树中,最大的节点的度称为树的度; 如上图:树的度为7;
节点的层次
:从根开始定义起,根为第
1
层,根的子节点为第
2
层,以此类推;
树的高度或深度
:树中节点的最大层次; 如上图:树的高度为
4;
子孙
:以某节点为根的子树中任一节点都称为该节点的子孙;
森林
:由
N
(
N>0
)棵互不相交的树的集合称为森林;
1.1二叉树概念
1.
二叉树不存在度大于
2
的结点
2.
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
1.1.1满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
说,如果一个二叉树的层数为
K
,且结点总数是 2^K-1,则它就是满二叉树。
二叉树的性质
1.
若规定根节点的层数为
1
,则一棵非空二叉树的
第
n
层上最多有 2^n个结点.
2.
若规定根节点的层数为
1
,则
深度为
h
的二叉树的最大结点数是2^h-1
3.
对
任何一棵二叉树
,
如果度为
0
其叶结点个数为N0
,
度为
2
的分支结点个数为N2
,
则有N0=N2+1
4.
若规定根节点的层数为
1
,具有
n
个结点的满二叉树的深度
,
h= log2(n+1) (ps: 是log
以
2为底,n+1
为对数
)
5.
对于具有
n
个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从
0
开始编号,则对于序号为i
的结点有:
(1)
若
i>0
,
i
位置节点的双亲序号(父母节点):
(i-1)/2
;
i=0
,
i
为根节点编号,无双亲节点
由于截断的原因父母节点序号用(i-1)/2即可
(2)
若
2i+1<n
,左孩子序号(
2i+1)
,
2i+1>=n
否则无左孩子
(3)
若
2i+2<n
,右孩子序号(
2i+2)
,
2i+2>=n
否则无右孩子
1.1.2完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为
K 的,有n
个结点的二叉树,当且仅当其每一个结点都与深度为
K
的满二叉树中编号从
1
至
n
的结点一一对应时称之为完全二叉树。 要注意的是
满二叉树是一种特殊的完全二叉树
。
要注意从左到右连续才是完全二叉树
2.二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
2.1 顺序结构
顺序结构存储就是使用
数组来存储
,一般使用数组
只适合表示
完全二叉树
,因为不是完全二叉树会有空间的浪费。而现实使用中
只有堆才会使用数组来存储
。二叉树顺
序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2.1.1 堆的概念
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
大根堆与小根堆
2.1.2 堆的创建
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDatetype;
typedef struct Heap {
HPDatetype* a;
int size;
int capacity;
}HP;
//堆
//初始化
void HeapInit(HP* hp);
//交换数据
void swap(HPDatetype* p1, HPDatetype* p2);
//插入
void HeapPush(HP* hp,HPDatetype x);
//删除
void HeapPop(HP* hp);
//销毁
void HeapDestory(HP* hp);
//打印 方便观察
void HeapPrint(HP* hp);
//查看堆顶数据
HPDatetype HeapTop(HP* hp);
//判空
bool HeapEmpty(HP* hp);
//堆中多少个数据
int HeapSize(HP* hp);
功能实现
堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
向下调整算法有一个前提
:左右子树必须是一个堆,才能调整。
void HeapPrint(HP* hp)
{
assert(hp);
for (int i = 0;i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
void swap(HPDatetype* p1, HPDatetype* p2)
{
HPDatetype tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//插入数据的时候向上调整 建的小根堆,逻辑取反建大根堆
void Adjustup(HPDatetype* a, int child)
{
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;
}
}
}
//插入
void HeapPush(HP* hp, HPDatetype x)
{
assert(hp);
if (hp->size == hp->capacity)
{
int newcapacity = hp->capacity == 0 ? 4:hp->capacity * 2;
HPDatetype* tmp = (HPDatetype*)realloc(hp->a, sizeof(HPDatetype) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail!");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
Adjustup(hp->a, hp->size-1);//插入后需要调整(此处排序方式小根堆)
}
//删除时用向下调整建堆,因为删除数据后不一定是堆了,要重新调整
void Adjustdown(HPDatetype* a, int size,int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child+1< size &&a[child + 1] < a[child])
{
child++;
}
if (a[parent] > a[child])
{
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 输出用(小根堆,输出最小的
void HeapPop(HP* hp)
{
assert(hp);
assert(hp->size>0);
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
Adjustdown(hp->a,hp->size,0);
//向下调整
}
void HeapDestory(HP* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
HPDatetype HeapTop(HP* hp)
{
assert(hp);
assert(hp->size > 0);
return hp->a[0];
}
//判空
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
2.1.3 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.
建堆
升序:建大堆(堆顶数据为最大数,与堆底数据交换后POP,重新建堆)
降序:建小堆(堆顶数据为最小数,与堆底数据交换后POP,重新建堆)
2.
利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
利用我们上面写好的堆
void HeapTest1()
{
int a[] = { 32,15,89,45,35,85,31,74,96,78 };
int n = sizeof(a) / sizeof(a[0]);
//建堆1
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
建堆2 向下调整前提:左右子树必须是堆
for (int i = (n-1-1)/2; i>=0; i--)
{
AdjustDown(a, n, i);//倒着找
}
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);//倒着找
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
int main()
{
//HeapTest();
HeapTest1();
return 0;
}
3. 二叉树遍历
学习二叉树结构,最简单的方式就是遍历。所谓
二叉树遍历
(Traversal)
是按照某种特定的规则,依次对二叉
树中的节点进行相应的操作,并且每个节点只操作一次
。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:
前序
/
中序
/
后序的递归结构遍历
:
1.
前序遍历
(Preorder Traversal
亦称先序遍历
)——
访问根结点的操作发生在遍历其左右子树之前。
2.
中序遍历
(Inorder Traversal)——
访问根结点的操作发生在遍历其左右子树之中(间)。
3.
后序遍历
(Postorder Traversal)——
访问根结点的操作发生在遍历其左右子树之后。
简单的来说就是访问顺序不同,前序遍历:根 左子树 右子树,中序遍历:左子树 根 右子树,后续遍历:左子树 右子树 根
下面举个例子练习一下
程序实现
void preorder(struct TreeNode* root, int* tmp, int* i)//tmp数组地址,*i是0
{
if (root == NULL)
{
return;
}
tmp[(*i)++] = root->val;
preorder(root->left, tmp, i);
preorder(root->right, tmp, i);
}
void Inorder(struct TreeNode* root, int* tmp, int* i)
{
if (root == NULL)
{
return;
}
preorder(root->left, tmp, i);
tmp[(*i)++] = root->val;
preorder(root->right, tmp, i);
}
void preorder(struct TreeNode* root, int* tmp, int* i)
{
if (root == NULL)
{
return;
}
preorder(root->left, tmp, i);
preorder(root->right, tmp, i);
tmp[(*i)++] = root->val;
}
4 .二叉树节点个数、高度、查找等
//节点个数
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return;
}
if (root->data == x)
{
return root;
}
BinaryTreeFind(root->left, x);
BinaryTreeFind(root->right, x);
return NULL;
}
二叉树第K层节点个数
我们的思路可以一层一层往下找,每次k--,直到k=1时停止,也就是到达要找的层数,还是从整棵树向左右子树划分的思想
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)//一层一层往下找
{
assert(k >= 1);
if (root = NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
5.层序遍历
层序遍历
:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1
,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第
2
层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
层序遍历可以使用队列进行遍历(先入先出)
typedef struct BinaryTreeNode* QDataType;
typedef struct QueneNode {
struct QueneNode* next;
QDataType data;
}QNode;
typedef struct Quene {
QNode* head;
QNode* tail;
}Queue;
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
}
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* m = cur->next;
free(cur);
cur = m;
}
pq->head = pq->tail = NULL;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (pq->head == 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 == pq->tail)
{
free(pq->head);
pq->head = pq->tail == 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;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
int QueueSize(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
int size = 0;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
void BinaryTreeLevelOrder(BTNode* root)
{
assert(root);
Queue p;
QueueInit(&p);
QueuePush(&p, root);
while (!QueueEmpty(&p))
{
BTNode* head=QueueFront(&p);//存的是新Root地址
printf("%c ", head->data);
QueuePop(&p);
// 依次插入
if (head->left)
{
QueuePush(&p, head->left);
}
if (head->right)
{
QueuePush(&p, head->right);
}
}
printf("\n");
QueueDestory(&p);
}
6.二叉树的创建和销毁
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
// 以通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树为例
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
BTNode root;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
if (a[*pi] == '#' || *pi >= n)
{
(*pi)++;
return NULL;
}
BTNode* tmp = (BTNode*)malloc(sizeof(BTNode));
assert(tmp);
tmp->data = a[*pi];
(*pi)++;
tmp->left = BinaryTreeCreate(a, n, *pi);
tmp->right = BinaryTreeCreate(a, n, *pi);
return tmp;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root)
{
BinaryTreeDestory((*root)->left);
BinaryTreeDestory((*root)->right);
free(*root);
*root = NULL;
}
}