树的介绍及二叉树的C++实现
- 一 . 树的概念
- 二 . 相关术语
- 三 . 树的表示
- 四 . 什么是二叉树
- 1> 二叉树的特性
- 2> 特殊的二叉树
- 3> 二叉树的性质
- 五 . 二叉树的存储结构
- 1> 二叉树的顺序存储
- 2> 二叉树的链式存储
- 六 . 堆
- 1> 什么是堆
- 2> 用堆存储数据
- 3> 堆的Cpp代码实现
- 1) 堆的初始化
- 2) 堆的销毁
- 3) 堆的遍历
- 4) 向上(或向下)调整建大堆(或小堆)
- 小堆中向上调整
- 小堆中向下调整
- 大堆中向上调整
- 大堆中向下调整
- 5) 堆的插入
- 6) 堆删除
- 7) 返回堆顶元素
- 8) 返回堆中元素个数
- 9) 判断堆是否为空
- 4> 堆排序
- 1) 排升序
- 2) 排降序
- 5> TOP - K问题
- 七 . 二叉树的链式存储结构
- 1> 链式二叉树的代码实现
- 1) 链式二叉树的四种遍历方式
- 前序遍历
- 中序遍历
- 后序遍历
- 层序遍历
- 2) 返回二叉树中节点的个数
- 返回二叉树中节点的总个数
- 返回二叉树中叶子节点的个数
- 返回二叉树中第k层节点的总个数
- 3) 二叉树中的查找
- 4) 返回二叉树的高度
- 5) 判断是否为完全二叉树
- 6) 二叉树的销毁
一 . 树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一 个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树, 也就是说它是根朝上,而叶朝下的。
- 根结点: 树中的从上开始的第一个节点, 是树中的特殊节点
- 根节点没有前驱结点
- 有至少一个或者n个后继节点
- 其余结点: 除根节点之外的节点 。 它们被分成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是兄弟节点
- 树的度:
- 一棵树中,最大的节点的度称为树的度
- 如上图:A节点的度为6,是最大的度 因此树的度为 6
- 节点的层次:
- 从根开始定义起,根为第1层,根的子节点为第2层,以此类推
- 树的高度(也称深度):
- 树中节点的最大层次
- 如上图:树的高度为4
- 堂兄弟节点:
- 双亲在同一层的节点互为堂兄弟
- 如上图:H、I互为兄弟节点
- 节点的祖先:
- 从根到该节点所经分支上的所有节点
- 如上图:A是所有节点的祖先
- 子孙:
- 以某节点为根的子树中任一节点都称为该节点的子孙
- 如上图:所有节点都是A的子孙
- 森林:
- 由m 加粗样式(m>0) 棵互不相交的树的集合称为森林
三 . 树的表示
- 树有很多种表示方式, 例如:
- 双亲表示法
- 孩子表示法
- 孩子双亲表示法
- 孩子兄弟表示法等
- 孩子兄弟表示法是最常用的一种表示法, 介绍如下:
- 树中的任意一个节点的组成
- 值域 : 存储数据
- 孩子节点 : 指向其第一个孩子节点
- 兄弟节点 : 指向它的下一个兄弟节点
typedef int DataType
struct TreeNode
{
struct TreeNode* firstChild1; // 指向其第一个孩子节点
struct TreeNode* nextBrother; // 指向其下一个兄弟节点
DataType _data; // 节点中的数据域
}
左孩子右兄弟表示法 : 指向左边第一个孩子节点(子节点), 指向右边第一个兄弟节点
高度为h的完全二叉树 : 前h-1层为满的, 第h层不一定满, 但是第h层一定是从左到右连续的
四 . 什么是二叉树
一棵二叉树是节点的有限集合
- 节点可以为空, 此时为空树
- 可以只有一个节点, 该节点被称为根节点
- 也可以由根与子树构成(左子树和右子树, 子树可为空)
1> 二叉树的特性
- 二叉树的度最大为2
- 每个父节点所拥有子节点的个数为 1或2
- 不存在子节点的节点是二叉树中的叶节点
- 二叉树的子树有左右之分, 不可以颠倒。 因此二叉树也被称为有序树
对于任意的二叉树都是由以下情况复合而成的
2> 特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对
应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树
3> 二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1个结点.
- **若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h - 1)
- 对任何一棵二叉树, 如果其叶结点个数为 n0 , 度为2的分支结点个数为 n1, 则有 n0= n1 +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> 二叉树的顺序存储
- 顺序结构存储就是使用数组来存储
- 一般使用数组只适合表示完全二叉树,因为非完全二叉树会有空间的浪费
- 在现实中使用中 只有数据结构 - 堆(完全二叉树)才会使用数组来存储
- 二叉树的顺序存储在物理上通过数组来实现,在逻辑上是一颗二叉树的结构
- 完全二叉树的顺序存储:
- 非完全二叉树的顺序存储:
2> 二叉树的链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是:
- 链表中每个结点由三个域组成,数据域和左右指针域
- 左右指针分别用来指向该结点的左孩子节点和右孩子节点
- 链式结构又分为二叉链和三叉链,在本篇文章中讲述的是二叉链
- 二叉链:
- 三叉链:
六 . 堆
1> 什么是堆
完全二叉树的顺序存储结构被称之为堆
堆分为大堆和小堆:
- 大堆 : 父节点 >= 子节点 (指的是节点中存储的值)
- 小堆 : 父节点 <= 子节点 (指的是节点中存储的值)
2> 用堆存储数据
堆是一种数据结构, 可以看作是一种容器, 用来存放数据, 若要将数据存入堆中, 那么要通过以下步骤:
- 将数据按照完全二叉树的顺序存储方式存入到一维数组中,
- 2 . 调整数组中元素的关系, 必须满足 父节点 >=(<=) 子节点
- 3 . 当数组中的元素满足在二叉树中 父节点 >= 子节点时, 这是相当于将数据存入大堆中
- 4 . 如果满足 父节点 <= 子节点, 这是相当于将数据存入小堆中。
- 堆示例图:
3> 堆的Cpp代码实现
1) 堆的初始化
// 堆的初始化
void HPInit(HP* php)
{
assert(php);
php->_arr = new HPDataType[4];
php->capacity = 4;
php->size = 0;
}
2) 堆的销毁
// 堆的销毁
void HPDestroy(HP* php)
{
assert(php);
if (!php)
{
return;
}
delete[] php->_arr;
php->_arr = NULL;
php->capacity = php->size = 0;
}
3) 堆的遍历
// 堆的遍历
void HPPrint(HP* php)
{
for (int i = 0; i < php->size; i++)
{
cout << php->_arr[i] << " ";
}
cout << endl;
}
4) 向上(或向下)调整建大堆(或小堆)
小堆中向上调整
// 堆向上调整 (堆中插入或互换元素向上调整) (小堆)
void AdjustUpS(HPDataType* a, int child)
{
while (child)
{
int parent = (child - 1) / 2;
if (a[parent] < a[child])
return;
else
{
HPSwap(a[parent], a[child]);
child = parent;
}
}
}
小堆中向下调整
// 堆向下调整 (堆中插入或互换元素向下调整) (小堆)
void AdjustDownS(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
HPSwap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
大堆中向上调整
// 堆向上调整 (堆中插入或互换元素向上调整) (大堆)
void AdjustUpB(HPDataType* a, int child)
{
while (child)
{
int parent = (child - 1) / 2;
if (a[parent] > a[child])
return;
else
{
HPSwap(a[parent], a[child]);
child = parent;
}
}
}
大堆中向下调整
// 堆向下调整 (堆中插入或互换元素向下调整) (大堆)
void AdjustDownB(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
HPSwap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
5) 堆的插入
// 堆的插入 (小堆 插入元素向上调整)
void HPPush(HP* php, HPDataType val)
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* newHP = new HPDataType[php->capacity * 2];
for (int i = 0; i < php->size; i++)
{
newHP[i] = php->_arr[i];
}
delete[] php->_arr;
php->_arr = newHP;
}
php->_arr[php->size] = val;
++php->size;
AdjustUpS(php->_arr, php->size - 1); // 对新插入堆中的值 val 进行比较排序, 直至符合小堆为止
}
6) 堆删除
// 堆删除 (小堆, 删除 向上调整)
void HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
HPSwap(php->_arr[0], php->_arr[php->size - 1]);
--php->size;
AdjustDownS(php->_arr, php->size, 0); // 再进行向下调整
}
7) 返回堆顶元素
// 返回堆顶元素
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->_arr[0];
}
8) 返回堆中元素个数
// 返回堆中元素的个数
int HPSize(HP* php)
{
assert(php);
return php->size;
}
9) 判断堆是否为空
// 判断堆是否为空
bool HPEmpty(HP* php)
{
return php->size == 0;
}
4> 堆排序
1) 排升序
排升序 : 将待排序序列排为升序, 步骤如下:
- 将待排序序列构造为一个大堆
- 利用堆顶元素, 也就是根元素是堆中最大值的特点, 将堆顶元素换至堆末尾
- 然后将新的堆顶元素进行向下调整
注意:此时向下调整时, 原堆顶元素(被换至末尾的堆顶元素)不参与向下调整
- 之后再次进行如上步骤
- 直至待排序序列完全为升序
2) 排降序
派降序: 将待排序序列排为降序, 步骤如下:
- 将待排序序列构造为一个小堆
- 利用堆顶元素, 也就是根元素是堆中最小值的特点, 将堆顶元素换至堆末尾
- 然后将新的堆顶元素进行向下调整
注意:此时向下调整时, 原堆顶元素(被换至末尾的堆顶元素)不参与向下调整
- 之后再次进行如上步骤
- 直至待排序序列完全为降序
5> TOP - K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
- 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
- 对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
- 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
七 . 二叉树的链式存储结构
1> 链式二叉树的代码实现
1) 链式二叉树的四种遍历方式
前序遍历
// 链式二叉树的前序遍历
void PreOrder(BTNode* root)
{
if (!root)
{
cout << " ";
return;
}
cout << root->data << " ";
PreOrder(root->left);
PreOrder(root->right);
}
中序遍历
// 链式二叉树的中序遍历
void InOrder(BTNode* root)
{
if (!root)
{
//cout << " ";
return;
}
PreOrder(root->left);
cout << root->data << " ";
PreOrder(root->right);
}
后序遍历
// 链式二叉树的后序遍历
void EpilOgue(BTNode* root)
{
if (!root)
{
//cout << " ";
return;
}
PreOrder(root->left);
PreOrder(root->right);
cout << root->data << " ";
}
层序遍历
链式二叉树的层序遍历需要依靠队列这一种数据结构
// 链式二叉树的层序遍历
void Sequence(BTNode* root)
{
queue<BTNode*> que;
if (root)
{
que.push(root);
}
while (!que.empty())
{
BTNode* front = que.front();
cout << front->data << " ";
que.pop();
if(front->left)
que.push(front->left);
if(front->right)
que.push(front->right);
}
cout << endl;
}
2) 返回二叉树中节点的个数
返回二叉树中节点的总个数
// 返回链式二叉树中节点的总个数 (后序递归遍历)
int TreeSize(BTNode* root)
{
// return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
if (!root)
{
return 0;
}
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
返回二叉树中叶子节点的个数
// 返回链式二叉树中叶子节点的个数
int TreeLeafSize(BTNode* root)
{
if (!root)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
返回二叉树中第k层节点的总个数
// 返回链式二叉树第k层所有节点的个数 (非完全二叉树)
int TreeKLevel(BTNode* root, int k)
{
assert(k > 0);
if (!root)
return 0;
if (k == 1)
return 1;
return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);
}
3) 二叉树中的查找
// 链式二叉树中查找值为val的节点 并返回其地址
BTNode* TreeFind(BTNode* root, BTDataType val)
{
if (!root)
return NULL;
if (root->data == val)
return root;
BTNode* ret = TreeFind(root->left, val);
if (ret)
return ret;
ret = TreeFind(root->right, val);
if (ret)
return ret;
return NULL;
}
4) 返回二叉树的高度
// 返回二叉树的高度(或者说 最大深度)
int TreeHight(BTNode* root)
{
if (!root)
return 0;
int left = TreeHight(root->left);
int right = TreeHight(root->right);
return left >= right ? left + 1 : right + 1;
}
5) 判断是否为完全二叉树
// 判断是否为完全二叉树
int TreeComplete(BTNode* root)
{
queue<BTNode*> q1;
if (root)
q1.push(root);
while (!q1.empty())
{
BTNode* front = q1.front();
if (!front)
break;
q1.push(front->left);
q1.push(front->right);
q1.pop();
}
while (!q1.empty())
{
if (q1.front())
return false;
q1.pop();
}
return true;
}
6) 二叉树的销毁
// 链式二叉树的销毁
void TreeDestory(BTNode** root)
{
if (!(*root))
return;
TreeDestory(&((*root)->left));
TreeDestory(&((*root)->right));
delete *root;
*root = NULL;
}