前情提要
树的一些基本概念
节点:包含一个数据元素及若干指向子树分支的信息。
节点的度:一个节点拥有子树的数目称为节点的度。
叶子节点:也称为终端节点,没有子树的节点或者度为零的节点。
分支节点:也称为非终端节点,度不为零的节点称为非终端节点。
树的度:树中所有节点的度的最大值。
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
树的深度(高度):树中所有节点的层次最大值称为树的深度。
父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。
子节点:一个节点含有的子树的根节点称为该节点的子节点。
兄弟节点:具有相同父节点的节点互称为兄弟节点。
堂兄弟节点:双亲在同一层的节点互为堂兄弟。
节点的祖先:从根到该节点所经分支上的所有节点。
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林。
1.二叉树的概念及其结构
1. 1 二叉树的概念
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个节点。
从图中可以看出;
1.二叉树不存在度大于2的节点。
2.二叉树有左右树之分,次序不能颠倒,因此二叉树是有序树。
1. 2 特殊的二叉树
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是 2^k-1,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
![满二叉树](https://i-blog.csdnimg.cn/direct/79923bb55af2436e99660cd9107e0d58.png)
1. 3 二叉树的性质
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个节点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 。
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n , 度为2的分支结点个数为m ,则有 n =m+1。
4.若规定根节点的层数为1,具有n个结点的满二叉树的深度log2(n+1)。
1.4 二叉树的存储结构
1.顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空 间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺 序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2.链式结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。
2.二叉树的顺序结构及实现
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。
2. 1 堆的概念
如果有一个关键码的集合K = {k0,k1,k2,…},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:ki <=k2*k+1且 ki<=2*k+2(ki >=k2*k+1且 ki>=2*k+2) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
2. 2 性质
1. 堆中的某个节点的值总不小于或者不大于其父节点
2.堆是一颗完全二叉树。
2. 3 堆的实现
1.堆的向下调整算法
将需要调整的数,与其子节点中较小的数互换位置,然后再依次推类,直到其位于叶节点。(小堆)
void AdjustDown(HPDataType* a, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] < a[child])
//用来判断子节点中哪一个更小
//并且限定child + 1的范围
{
child++;
}
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
2.堆的向上调整
例如插入一个数据,数据肯定位于数组最后一个位置,那么我们需要将其调整到正确位置。
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
child = parent;
parent= (child - 1) / 2;
}
else
{
break;
}
}
}
直接将插入数据与其父节点比较,如果其小于父节点,那么直接换位置即可,然后让其再次与父节点比较,以此类推。
3.堆的创建和初始化
首先我们选择使用顺序结构来实现,那么我们就需要在堆区开辟一个数组来存储数据,并且要标记数组的大小和它的容量,方便我们后面在数组满了的时候扩容。
然后我们进行堆的初始化,初始化中我们先为数组开辟空间,并且给size和capacity初始值。
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;
php->capacity = 4;
}
4.堆的插入
先将数据先放到数组尾部然后不断向上调整到合适位置即可。
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php ->size == php->capacity)
{
HPDataType* temp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * php->capacity * 2);
if (temp == NULL)
{
perror("realloc fail");
return;
}
php->a = temp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
从图中可以看出我们在使用HeapPush后的数据成功建立了一个堆。
5.堆的删除
如果我们数组直接全部向前直接挪动一个数据的位置,那么这很显然大概率可能不再是一个堆了,那么我们应该如何调整才能让它保持是一个堆呢?
我们的思路是将堆顶的数据和最后的一个数据交换位置,然后让size--,来让其不参与调整,最后只需要将堆顶数据往下调整到正确的位置即可。
void Swap(HPDataType* x, HPDataType* y)
{
HPDataType temp = *x;
*x = *y;
*y = temp;
}
void AdjustDown(HPDataType* a, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child+1 < n && 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* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, 0, php->size);
}
6.查看堆顶数据、判断堆是否为空、堆的大小
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
bool HeapEmpty(HP* php)
{
return php->size == 0;
}
int HeapSize(HP* php)
{
return php->size;
}
void HeapDestroy(HP* php)
{
free(php->a);
php->a=NULL;
php->size = php->capacity = 0;
}
2. 4 堆排序实现
如果我们需要排升序,那么我们需要建立大堆,然后将堆顶元素弹出后,调整堆至有序,再次弹出堆顶元素,直至堆为空。最后数组就是升序了。
1.向上建堆及其时间复杂度分析
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
child = parent;
parent= (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(HPDataType* a, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child+1 < n && 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 HeapSort(int* a, int n)
{
//建堆 -- 向上调整建堆 时间复杂度O(N*logN)
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
end--;
}
//时间复杂度O(N*logN)
}
上图 建堆时间复杂度计算
在我们向下调整排序的时候,每次将最大的数拿出去,并且换上小的数到堆顶,在依次向下调整,那么我们可以近似的认为每次调整都是logN次,那么有N个数据,其实数量级也在N*logN这个级别。
2.向下建堆及时间复杂度分析
void AdjustDown(HPDataType* a, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child+1 < n && 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 HeapSort(int* a, int n)
{
//建堆 -- 向下调整建堆 通过从最后非子叶的一层开始向下调整建堆 时间复杂度O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, i, n);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
end--;
}
//时间复杂度O(N*logN)
}
向下调整时间复杂度分析
3. 二叉树的链式结构实现
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入,降低学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树 操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
BTNode* CreateBT()
{
BTNode* Node1 = BuyBTNode(1);
BTNode* Node2 = BuyBTNode(2);
BTNode* Node3 = BuyBTNode(3);
BTNode* Node4 = BuyBTNode(4);
BTNode* Node5 = BuyBTNode(5);
BTNode* Node6 = BuyBTNode(6);
Node1->left = Node2;
Node1->right = Node4;
Node2->left = Node3;
Node4->left = Node5;
Node4->right = Node6;
return Node1;
}
二叉树的逻辑结构图
二叉树是:1.空树 2.非空:根节点,根节点的左子树,根节点的右子树。
3. 1 二叉树前序、中序及后续遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
顺序总是根、左子树、右子树,所以先打印根部数据其次才是左子树,最后才是右子树。
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
图中红色的圆圈一二三是递归的顺序
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
顺序总是左子树、根、右子树,所以先走左子树,一直到左子树为空,然后返回,再打印数据,再递归右子树。
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
顺序总是左子树、右子树、根,所以先走左子树,一直到左子树为空,然后返回,再走右子树,然后为空就返回,最后打印数据。
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
3. 2 二叉树的层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
为了实现层序遍历我们需要借助队列来实现,队列的特点是先进先出(FIFO),那么我们就需要队列来存储二叉树中的左右子树,然后依次打印左右子树中的值即可。
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
3. 3 二叉树的大小
如果root递归到空就返回0,如果不为空就记为1.
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
3. 4 二叉树的高度
二叉树的高度(深度)就是统计最深的即可,那么我们就需要比较左右子树的大小,然后来寻找看谁更大,然后就取较大的这一边即可。因此我们需要先让左子树递归到NULL类似于前序遍历,如果左右子树都为零(root为NULL返回0
),那么我们记该节点为1,再将其与兄弟节点比较即可。
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//return TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
//时间复杂度颇高,比较时候调用TreeHight后面返回值的时候也会调用 O(N^2)
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
3. 5 判断该层的节点数
当前第k层的节点个数 = 左子树的第 k - 1 层的节点个数 + 右子树第 k -1 层的节点个数
当k递归等于1的时候,就是我们根的第k层(选的的参考系不一样)。那么如果指针不为空说明有节点,如果为空则没有节点。
int TreeLevel(BTNode* root, int k)
{
if (root == NULL)
return 0;
if ( k == 1)
return 1;
return TreeLevel(root->left,k-1) + TreeLevel(root->right, k-1);
}
3. 6 二叉树查找
查找相同值类似于前序遍历,先比较相同值然后再遍历左子树,最后遍历右子树。
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* lret = BinaryTreeFind(root->left, x);
//此处用lret保存,避免再次递归返回。
if (lret)
return lret;
BTNode* rret = BinaryTreeFind(root->right, x);
if (rret)
return rret;
return NULL;
}
3. 7 判断两个二叉树是否完全相同
思路:通过比较是否有不同的地方,如果有直接返回false即可,如果整体都没有问题,那么最后返回true。
需要注意的就是,我们其实是和前序遍历的顺序一样,一直递归到NULL,判断相同地方的值是否相同,如果不同直接返回false,那么这个false将在递归中一直返回直到返回给主函数。true只在p、q均为空或者两者的值相同的时候才可以得到。
bool isSameTree(BTNode* p, BTNode* q) {
if (p == NULL && q == NULL)
return true;
if ((p != NULL && q == NULL) || (p == NULL && q != NULL))
return false;
if (p->data != q->data)
return false;
bool lret = isSameTree(p->left, q->left);
if (lret == 0)
return lret;
bool rret = isSameTree(p->right, q->right);
if (rret == 0)
return rret;
return true;
}
3. 8 判断是否为完全二叉树
完全二叉定义:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。
思路:如果我们用队列来装在这个二叉树(遍历),那么数据和数据之间不应该有间隔(NULL就间隔开)。因为我们是从上到下、从左到右依次标记,那么我们就需要用层序遍历来存储BTNode指针。
如果是完全二叉树,那么队列中再判断有无空的时候应该全部都为空,如果不是,那么其中肯定有非空的BTNode指针。
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
break;
}
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//判断是否是完全二叉树,如果NULL后面仍然有非空节点,那么就不是。
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//判断是否有非空,有就返回false
if (front != NULL)
{
QueueDestroy(&q);
return false;
}
}
//我们是使用链表实现,按道理说没有数据不需要在释放queue了
// 但万一其原实现是使用数组,我们就仍然需要destroy
// 因此我们使用destroy可以不去思考实现怎么样的
QueueDestroy(&q);
return true;
}
3. 9 二叉树销毁
类似于二叉树后序遍历,先遍历左右子树,最后销毁自身。
void BTDestroy(BTNode* root)
{
if (root == NULL)
return;
BTDestroy(root->left);
BTDestroy(root->right);
free(root);
}