数的概念与结构:
什么是树:
树是一种非线性的数据结构,它是由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)棵互不相交的树的集合称为森林;
树的表示:树的表示相比线性结构就要复杂一些,因为其是层次结构的,其的下一个结点可能有多个,也可能只有一个,因此十分难以在逻辑结构上进行表示。于是伟大的人类提出了一种方法可以很简单的表示任意一棵二叉树,这种表示方法称之为孩子兄弟表示法。
typedef int DataType; struct Node { struct Node* _firstChild1; // 第一个孩子结点 struct Node* _pNextBrother; // 指向其下一个兄弟结点 DataType _data; // 结点中的数据域 };
所谓孩子兄弟表示法就是让任何一个结点仅指向它的第一个孩子结点和右边相邻的兄弟结点(如果没有孩子或兄弟指向空),由此任意一棵树上的任何一个节点都可以变成一个度最大仅为2结点,由此我们可以将任何一棵树都变成一棵二叉树。
二叉树:
什么是二叉树:
二叉树是一棵所有节点的度最大为2的树。任何一棵树都可以通过孩子兄弟表示法转换为二叉树,因此我们接下来的所有研究都是围绕二叉树进行的。下图就是一棵二叉树。
二叉树的特点:
1. 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
2. 二叉树的子树有左右之分,其子树的次序不能颠倒。
3.若二叉树的层次从0开始,则在二叉树的第i层至多有2^i个结点(i>=0)。
4.高度为k的二叉树最多有2^(k+1) - 1个结点(k>=-1)。 (空树的高度为-1)
5.对任何一棵二叉树,如果其叶子结点(度为0)数为m, 度为2的结点数为n, 则m = n + 1。
特殊的二叉树:
满二叉树:
一个深度为k(>=-1)且有2^(k+1) - 1个结点的二叉树称为完美二叉树。意思是说这棵深度为k的二叉树任意一行的结点都已经是满的了。
完全二叉树:
完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。
二叉树的存储:
顺序存储:顺序存储指的是利用链表在连续的地址空间上进行存储。但是顺序存储只适用于完全二叉树,因为这样才不会有空间的浪费,所以顺序的存储方式一般用来存储堆。
链式存储:链式的二叉树存储方式是最为常用的二叉树存储形式,用一个二叉链(或三叉链)的形式来构成二叉树。
二叉链:如下是一个二叉链的存储形式
二叉链结构:
typedef int BTDataType; // 二叉链 struct BinaryTreeNode { struct BinTreeNode* _Left; // 指向当前节点左孩子 struct BinTreeNode* _Right; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 }
三叉链:
// 三叉链 struct BinaryTreeNode { struct BinTreeNode* _Parent; // 多了一个指向父亲的指针 struct BinTreeNode* _Left; // 指向当前节点左孩子 struct BinTreeNode* _Right; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 };
堆:
堆的概念:
堆是典型的通过使用顺序方式存储二叉树来实现的数据结构,因为堆可以保证其必然是一棵完全二叉树。
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆有两点性质:
1、其保证其一定是一棵完全二叉树。
2、堆中任意一个孩子结点必不大于(大根堆)或者不小于(小根堆)其父结点。
小堆:
大堆:
堆的实现:
堆的实现中有三个基础操作,向上调整,向下调整以及建堆,他们三个共同构成了堆的所有基础操作。
堆的向下调整:
现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
但是,使用向下调整算法需要满足一个前提:
若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。
向下调整算法的基本思想:
1.从根结点处开始,选出左右孩子中值较小的孩子。
2.让小的孩子与其父亲进行比较。
若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。
typedef int DataType; void swap(DataType* p1, DataType*p2) { DataType tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustDown(DataType* a, int child, int parent) { int minchild = parent * 2 + 1; while (minchild < child) { if (a[minchild]>a[minchild + 1] && minchild + 1 < child) { minchild++; } if (a[minchild] < a[parent]) { swap(&a[minchild], &a[parent]); parent = minchild; minchild = parent * 2 + 1; } else { break; } } }
使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN)
上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?
答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。
for (int i = (n - 2) / 2; i >= 0; i--) { AdjustDown(a, n, i); }
那么建堆的时间复杂度又是多少呢?
当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。
我们计算建堆过程中总共交换的次数: T(n)=1×(h−1)+2×(h−2)+...+2h−3×2+2h−2×1
两边同时乘2得:2T(n)=2×(h−1)+22×(h−2)+...+2h−2×2+2h−1×1
两式相减得:T(n)=1−h+21+22+...+2h−2+2h−1
运用等比数列求和得:T(n)=2h−h−1
由二叉树的性质,有N=2h−1和h=log2(N+1),于是T(n)=N−log2(N+1)
用大O的渐进表示法:T(n)=O(N)
堆的向下调整算法的时间复杂度:T(n)=O(logN)。
建堆的时间复杂度:T(n)=O(N)。
堆的向上调整算法:
当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。
1.将目标结点与其父结点比较。
2.若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。
typedef int HPDataType //交换函数 void Swap(HPDataType* x, HPDataType* y) { HPDataType tmp = *x; *x = *y; *y = tmp; } //堆的向上调整(小堆) void AdjustUp(HPDataType* 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; } } }
堆的思想就只有两种,一个向下和向上调整;
堆的实现和基本结构:
创建堆的结构体类型:
typedef int DataType; typedef struct Heap { DataType* a;//用于存储数据 int size;//记录个数 int capacity;//容量 }Heap;
然后初始化堆:
//初始化堆 void Init(Heap* ps) { assert(ps); ps->a = NULL; ps->capacity = ps->size = 0; }
销毁堆:
//堆的销毁 void Destory(Heap *ps) { assert(ps); free(ps->a); ps->a = NULL; ps->capacity = ps->size = 0; }
打印堆:
void Print(Heap* ps) { assert(ps); for (int i = 0; i < ps->size; ++i) { printf("%d ", ps->a[i]); } printf("\n"); }
堆的插入:
void Push(Heap* ps, int x) { assert(ps); if (ps->capacity == ps->size) { int newnode= ps->capacity == 0 ? 4 : ps->capacity * 2; DataType* tmp = (DataType*)realloc(ps->a, sizeof(DataType)*newnode); ps->a = tmp; ps->capacity = newnode; } ps->a[ps->size] = x; ps->size++; //向上调整 AdjustUp(ps->a, ps->size - 1); }
堆的删除:
void Pop(Heap* ps)
{
assert(ps);
assert(!Empty(ps));
swap(&ps->a[0], &ps->a[ps->size - 1]);
ps->size--;
HeapDonw(ps->a, ps->size - 1, 0);
}
获取堆顶的数据:
//堆顶的数据
DataType Top(Heap* ps)
{
assert(ps);
assert(!Empty(ps));
return ps->a[0];
}
获取堆的数据个数:
//获取堆中数据个数 int Size(Heap* ps) { assert(ps); return ps->size;//返回堆中数据个数 }
堆的判空:
bool Empty(Heap *ps) { assert(ps); return ps->size == 0; }
二叉树和堆内容到此介绍结束了,感谢您的阅读!!!
如果内容对你有帮助的话,记得给我点个赞——做个手有余香的人。感谢大家的支持!!!