文章目录
树的概念
树是一种非线性的数据结构,它是由n (n >= 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)棵互不相交的树的集合称为森林;
树的表示
普通表示:
双亲表示法:
有可能用数组去表示树,一个节点可能有多个孩子,但是只有一个父亲,我们就用叶子往上走这样表示,双亲表示法就是只存储父亲的下标。
A没有父亲它的下标就是-1,B的父亲下标是0,C的父亲下标是0,G的父亲下标是2。
这种方式倒着往上走就是可以从孩子找祖先。
树实际应用(Linux树状目录)
二叉树的概念及结构
概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
二叉树度结点不大于2。
二叉树是有序树, 二叉树的子树有左右之分,次序不能颠倒。
特殊的二叉树
斜树
左斜树是所有的结点都只有左子树的二叉树,所有结点都只有右子树的二叉树叫右斜树。这两者统称为斜树。斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。
满二叉树
从第一层开始,每一层都是满的,若层数是k,则节点总数是2的k次幂减1。
完全二叉树
前k-1层都是满的,最后一层不一定满(满或者不满),但是从左到右必须是连续的,它的节点范围:
【2(k-1) ,2k - 1】
二叉树性质
-
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2(i-1)个结点.
-
若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h-1 .
-
对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2,则有n0 = n2+1
-
若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log(n+1) . (ps: 是log以2为底,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则无右孩子。
二叉树顺序结构及实现
堆的实现
堆是属于操作系统进程地址空间内存区域的划分。
我们下面实现数据结构中的堆。
堆是一个完全二叉树:分为小根堆和大根堆。
小根堆:任何一个节点的值都<=孩子的值
大根堆:任何一个节点的值都>=孩子的值
应用:
1.堆排序,第一个时间复杂度达到–O(N*log N)的排序。
2.topK问题:找一堆数据前K大或者前K小。
数组下标计算父子关系公式:
左孩子:leftchild = parent*2 + 1
右孩子:rightchild = parent*2 + 2
孩子算父亲:parent = (child - 1) / 2
堆向下调整算法
给出一个数组,逻辑上看做一颗完全二叉树。通过从根节点开始的向下调整算法可以把它调整成一个小堆。
前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
堆的创建
给出一个数组,逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们把它构建成一个堆。根节点左右子树不是堆,这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
采用向下调整建堆。
int a[] = {1,5,3,8,7,6};
堆的插入
先插入一个数组的尾上,再进行向上调整算法,直到满足堆。
1.先将元素插入到对的末尾,即最后一个孩子之后。
2.插入之后如果堆的性质遭到了破坏,将新插入节点顺着双亲往上调整到合适位置即可。
AdjustUp
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
1.将堆顶元素与堆中追后一个元素进行交换。
2.删除堆中最后一个元素
3.将堆顶元素向下调整到满足堆特性为止。
堆的代码实现
heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapPrint(HP* php);
void Swap(HPDataType* p1, HPDataType* p2);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
void HeapInit(HP* php);
void HeapDestroy(HP* php);
// xֶ̬
void HeapPush(HP* php, HPDataType x);
// ɾѶԪ
void HeapPop(HP* php);
// ضѶԪ
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
Heap.c
#include "Heap.h"
void HeapPrint(HP* php)
{
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 插入x继续保持堆形态 -- logN
void HeapPush(HP* php, HPDataType x)
{
assert(php);
// 扩容
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity*sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
// 找出小的那个孩子
if (minChild+1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
// 删除堆顶元素 -- 找次大或者次小 -- logN
// O(logN)
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
// 返回堆顶的元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
堆的应用
堆排序:
1.建堆:
升序:建大堆
降序:建小堆
2.利用堆删除思想来进行排序
int a[] = {20,17,4,16,5,3};
建堆和堆删除中都用到了向下调整,因此必须掌握了向下调整。
二叉树链式结构及实现
快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
二叉树:
- 空树
- 非空:根节点,根节点的左子树、根节点的右子树组成的。
二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
二叉树的遍历
前序、中序以及后序遍历
- 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
根 左子树 右子树
遇到根先访问,访问1,然后访问左子树的根,访问2,再访问2的左子树 右子树,访问3,访问3的左子树 右子树都为NULL,然后访问2的右子树,右子树为NULL。再访问1的右子树,右子树的根是4,然后访问4的左子树,访问5,然后访问5的左子树,左子树为NULL,然后访问5的右子树,右子树为NULL,然后访问4的右子树,根为6,再访问6的左子树,左子树为NULL,在访问右子树也为NULL,4的右子树结束,4这棵树作为1的右子树,到此整棵树访问结束。
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
根不是空,打印第一个值就是1,打印以后要去走左子树右子树,左子树右子树别分为规模类似的子问题,从栈帧的角度,往下走建立第二个栈帧,1打印了就往1的左树递归,1的左是2,2不是空打印2,再紧接着访问2的左子树,再往下递归,从物理上来说这里建立了三层栈帧,第一层栈帧root存的是1的地址,第二层栈帧root存的是2的地址,第三层栈帧root存的是3的地址,3打印了,3不是空,再往3的左子树去走,3的左边是空,所以就打印出了123NULL,所有的函数结束就回到调用它的地方,所以就回到了3,3走完了就走3的右,3的右还是空继续打印NULL,然后return回到调用的地方。3这个函数结束了,3是作为2的左递归下来的,3结束了回到它调用的地方,就回到2,2的左完了,就去2的右,2的右是NULL,打印NULL然后回到调用的地方,2的左、右都结束了,2作为1的左递归下来的,现在回到1的左,1的左结束,再去递归1的右,1的右是4,4不为NULL打印4,然后就递归4的左,4的左是5,打印5,递归5的左,5的左是空,打印NULL,然后回来在递归5的右,5的右也是空,打印NULL,然后继续回去4的左,继续递归走,走的是4的右,4的右是6,打印6,6不是NULL,继续6的左,6的左是NULL,打印NULL然后回到6的左,继续递归6的右,6的右也是空,打印完也回到6,此时6结束了,6是作为4的右下来的,结束后6回到4,对于4而言,4的左、右都结束了,4作为1的右下来的,所以回到1,1的左、右也结束了,1再出去,函数的调用全部结束。
从底层的角度整个过程在建立栈帧,保存数据。右数建立的栈帧,和左树递归调用建立的栈帧是重叠的,递归右边的时候,左边栈帧已经销毁了。
- 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
左子树 根 右子树
// 二叉树中序遍历
void InOrder(BTNode * root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
我们只看看中序的左边,第一个是根,根1不是空,先要去递归它的左子树,然后就遇到2,2不是空,2不能访问,继续递归2的左子树,左子树是3,遇到3不能访问,先访问3的左子树,3的左子树是NULL,打印NULL,NULL结束了回到3,此时打印3,然后访问3的右子树,右子树是空,打印NULL继续返回,3递归完了就会回到它递归调用的地方,回到2,然后就打印2,然后再递归2的右子树,2的右子树也是一个NULL,打印NULL,回到2的右,2递归结束就回到2最初递归调用的地方,也就是1的左。然后就访问1,打印1,然后再访问1的右,不能访问4,就访问4的左…不断往下走,走到空就是尽头,然后再不断返回。
- 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
左子树 右子树 根
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
InOrder(root->right);
printf("%d ", root->data);
}
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
// 二叉树中序遍历
void InOrder(BTNode * root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
InOrder(root->right);
printf("%d ", root->data);
}
BTNode* CreateTree()
{
BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
assert(n1);
BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
assert(n2);
BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
assert(n3);
BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
assert(n4);
BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
assert(n5);
BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
assert(n6);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n5->data = 5;
n6->data = 6;
n1->left = n2;
n1->right = n4;
n2->left = n3;
n2->right = NULL;
n4->left = n5;
n4->right = n6;
n3->left = NULL;
n3->right = NULL;
n5->left = NULL;
n5->right = NULL;
n6->left = NULL;
n6->right = NULL;
return n1;
}
int main()
{
BTNode* root = CreateTree();
PreOrder(root);
printf("\n");
InOrder(root);
printf("\n");
return 0;
}
节点个数以及高度等
求结点个数
求一棵树结点的个数:遍历计数
//求一棵树节点的个数
int TreeSize(BTNode* root)
{
static int count = 0;
if (root == NULL)
return count;
++count;
TreeSize(root->left);
TreeSize(root->right);
return count;
}
采用静态变量进行计数,我们发现调用一次可以,调用两次就出现了问题,局部的静态成员变量只有在第一次调用的时候会初始化。所以采用局部静态变量的这种方法是不可取的。我们要把静态变量放到全局去。
//求一棵树节点的个数
static int count = 0;
void TreeSize(BTNode* root)
{
if (root == NULL)
return count;
++count;
TreeSize(root->left);
TreeSize(root->right);
return ;
}
还有一种方式:切分子问题
int TreeSize(BTNode* root)
{
return root == NULL ? 0 :
TreeSize(root->left) + TreeSize(root->right) + 1;
}
叶子结点的个数
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL
&& root->right == NULL)
return 1;
return TreeLeafSize(root->left)
+ TreeLeafSize(root->right);
}
深度(高度)
父亲的高度 = 左右子树大的那个 + 1
//深度高度
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int lh = TreeHeight(root->left);
int rh = TreeHeight(root->right);
return lh > rh ? lh + 1 : rh + 1;
}
求k层节点数
假如k是3,求第三层就转换成求左子树和右子树的第二层。即求左右子树的k-1层。
递归过程:
从1开始递归,求第三层结点数,然后就求左子树的第二层,左子树的第二层就转换为求它左子树的第一层,然后就返回1,再求2为根节点的右子树的第一层,为NULL,返回0,左树加起来返回1。然后去找右树…
//求k层节点数
int TreeKLevel(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
//转换求子树的k-1层
return TreeKLevel(root->left, k - 1)
+ TreeKLevel(root->right, k - 1);
}
返回x所在的节点
找到节点后是层层往回返回的过程。比如说找6,从1进入,递归左子树,没找到,再去递归右子树。
//返回x所在的节点
BTNode* TreeFind(BTNode* root,BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
//先去左树找
BTNode* lret = TreeFind(root->left, x);
if (lret)
return lret;
//左树没找到,再去右树找
BTNode* rret = TreeFind(root->right, x);
if (rret)
return rret;
return NULL;
}