赞
踩
树的概念
- 现实中的树
- 数据结构中的树
树是一种非线性的数据结构,它是由n(n>=0)个有限节点组成一个具有层次关系的集合。把它叫做>树树是因为它看起来像一颗倒挂的树,也就是
说,它的根朝上,叶朝下。
- 树的特点
1.有一个特殊的节点,称为根节点,根节点没有前驱节点。
2.除根结点外,其余节点被分成M个互不相交的集合,其中每一个集合又是一颗结构与树类似的子树。每颗子树的根节点有且只有一个前驱3,可以有0个或多个后继
3.树是递归定义的
- 注意
- 树型结构中,子树之间不能有交集,否则就不是树形结构
- 除根点外,每个节点有且仅有一个父节点
- 节点的度:一个节点含有的子树的个数称为该节点的度;如:A为6。
- 叶节点或终端节点:度为0的节点称为叶节点;如:B,C,H,I。
- 非终端节点或分支节点:度不为0的节点;如:D,E,F,G。
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点父节点;如:A是B的父节点。
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;如:B是A的子节点。
- 兄弟节点:具有相同父节点的节点称为兄弟节点。如:B,C是兄弟节点。
- 树的度:一棵树中,最大的节点的度称为树的度;如上图:树的度为6
- 节点的层次:从跟开始定义起,根为第一层,根的子节点为第二层,以此类推;
- 树的高度或深度:树中节点的最大层次;如上图:树的高度为4。
- 唐兄弟节点:双亲在同一层互为堂兄弟;如:H,I互为兄弟节点
- 节点的祖先:从根到该节点所经分支上的所有节点;如:A是所有节点的祖先。
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>0)棵互不相交的树的集合称为森林。
树结构既要保存值域,也要保存节点和节点之间的关系
树结构比线性表复杂,因为不知道孩子的数量,但如果知道树的度,那就很好定义了
- 例如
#define N struct TreeNode { int data; struct TreeNode* subs[N];//指针数组 }
- 1
- 2
- 3
- 4
- 5
- 6
这种方法会导致每个节点的度均为5,实际用不了这么多,这种方法会导致空间浪费,而且在不知道树的度的情况下,采用这种方法就比较难了
常见的有:双亲表示法,孩子表示法,孩子双亲表示法及孩子兄弟表示法.
- 孩子兄弟表示法:
原理:
直接让父节点指向第一个孩子,剩下的孩子用孩子的兄弟指针链接起来typedef int DataType; struct Node { struct Node* _firstChild1; //第一个孩子节点 struct Node* _pNextBrother; //指向其下一个兄弟节点 DataType _data; //节点中的数据域 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 画图演示
- 可以用于表示文件系统的目录树结构
- 双亲表示法
二叉树: 度为2的树,是节点的一个有限集合,该集合:
1.或者为空
2. 由一个节点加上两颗别称为左子树和右子树的二叉树组成
3. 二叉树不存在度大于2的节点
4. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
5. 任意的二叉树都是由以下几种情况复合而成的:
- 若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个节点
- 若规定根节点的层数为1,则深度为h的二叉树的最大节点树2^h-1
- 对任何一颗非空二叉树,如果度为0其叶节点个数为n0,度为2的分支节点个数为n2,则有n0=n2+1
- 若规定根节点的层数为1,具有n个节点的满二叉树的深度,h=log2(N+1)
- 对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的节点有:
1.若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2.若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3.若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
二叉树一般可以使用两种结构存储,一种顺序存储,一种链式存储.
- 顺序存储
顺序存储就是使用数组存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树,会有空间浪费.
如图:
而现实中只有堆才会使用数组来存储.二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树.
如图:
将逻辑结构中树的数据存储在数组里,用下标代表不同的数据,以便于访问.
假设双亲节点下标为parent,左孩子的下表为leftchild,右孩子的下标为rightchild,则他们的下标关系为:
- leftchild=parent2+1 左子节点的索引等于双亲节点2+1
- rightchild=parent2+2 右子节点的索引等于双亲节点2+2
- parent=(child-1)/2 双亲节点的索引等于子节点-1后的二分之一(向下取整)
左孩子的下标均为奇数,右孩子的下标均为偶数,但是在求双亲节点的式子中,只是(child-1)/2,并没有区分左右孩子,这是因为在计算时是向下取整,无需区分左右孩子.
- 链式存储
用链来指示元素中的逻辑关系.通常链表中的每个节点由三个域组成,数据域,做指针域,右指针域.左右指针域,分别用来存储该节点的左孩子和右孩子所在节点的存储地址.链式结构又分为二叉链和三叉链.
如图:
完全二叉树和满二叉树是可以通过数组来进行存储的,它们间的父子关系可以通过下标来表.
普通的二叉树不适合用数组存储,因为可能存在大量的空间浪费。而完全二叉树更适合用顺序结构存储。
完全二叉树更适合用顺序结构存储,因为它的空间利用率高,不会造成浪费.
通常把堆(一种二叉树)使用顺序结构的数组存储,这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段
- 分类:
- 小堆:每一个双亲结点的值均小于等于其对应的子节点的值,而根节点的值就是最小的
- 大堆:每一个双亲结点的值均大于其对应的子节点的值,而根节点的值就是最大的
- 性质
- 堆中的某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一颗完全二叉树
- 堆的结构:
堆结构的基本结构是数组,创建堆结构的时直接动态开辟即可,
typedef int HPDataType;//堆中存储的数据的类型 typedef struct Heap { HPDataType* a;//存储数据 int size;//记录堆中有效元素的个数 int capacity;//记录堆的容量 }HP;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
将堆初始化,首先传过来的结构体和指针不能为空,要进行断言.
void HeapInit(HP*php) { assert(php); php->a=NULL; php->size=php->capacity=0; }
- 1
- 2
- 3
- 4
- 5
- 6
堆的物理地址就是数组,依次访问下标打印即可
void HeapPrint(HP* php) { assert(php); for (size_t i = 0; i < php->size; i++) { printf("%d ", php->a[i]); } printf("\n"); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
对于动态开辟的内存在使用完毕后要销毁
void HeapDestory(HP* php) { assert(php); free(php->a); php->a=NULL; php->capacity=php->size=0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
交换时要传地址
void Swap(HPDataType* pa, HPDataType* pb) { HPDataType tmp = *pa; *pa = *pb; *pb = tmp; }
- 1
- 2
- 3
- 4
- 5
- 6
此算法是为了确保插入数据后的堆依然是符合堆的性质而单独封装出来的函数,就好比如我们后续要插入的数字10,以小根堆举例:
逻辑结构:
物理结构:
void AdjustUp(HPDataType* a, size_t child) { int parent=(child-1)/2; while(child>0) { if(a[child]<a[parent])//小根堆 //if (a[child] > a[parent]) 大根堆 { Swap(&a[child],&a[parent]); child=parent; parent=(child-1)/2; } else { break; } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
如果一个二叉树整体不符合堆的性质,就要进行向下调整,
步骤:
- 找出左右孩子中最小的那个
- 根双亲节点比较,如果比双亲节点小,就交换
- 递归,从孩子的位置继续向下调整,直到结束
void AdjustDown(HPDataType* a,int n,int parent) { int minChild=parent*2+1; while(minChild<n)//确保右孩子存在 if (child + 1 < size && a[child + 1] < a[child]) { //1.确保child的下标对应的值最小,即取左右孩子较小那个 if(minChild+1<n&&a[minChild+1]<a[minChild]) { //右孩子较小 minChild++; } //2、如果孩子小于父亲则交换,并继续往下调整 if(a[minChild]<a[parent]) { Swap(&a[minChild,&a[parent]]); parent=minChild; minChild=parent*2+1; } else { break;//如果中途满足堆的性质,直接返回 } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
堆元素的插入不可以任意位置插入,因为要符合大根堆或小根堆的性质,不能改变堆原本的结构,所以尾插是最合适的,并且,尾插后还要检查是否符合堆的性质。
- 例如:
- 思路:
这颗树在没插入数字10之前是个小堆,在插入后就不是了,改变了小根堆的性质了。因为子结点10小于其父结点28,那该怎么办呢?
首先最基本的,在插入之前就要先判断该堆的容量是否还够插入数据,先检查要不要扩容,扩容完毕后。我们可以发现,插入的10只会影响到从自己本身开始到根,也就是祖先,只要这条路上符合堆的性质,插入即成功。
此时要用到向上调整算法:当我们看到插入的10比父亲28小时,此时交换数字,但是此时10又要比18小,再次交换,最终发现10比15还小,再次交换,此时结束,到根部了。当然这是最坏的情况,如果在中间换的过程中满足了堆的性质,那么就不需要再换了,直接返回即可。void HeapPush(HP* php,HPDataType x)//在堆php中插入x { assert(php); // 判断是否扩容 if(php->size==php->capacity) { int newCapacity=php->capacity==0?4:php->capacity*2;//如果初始容量为0就扩容为4,否则扩容为原来的2倍 HPDataType* tmp=(HPDataType*)realloc(php-a,newCapacity*sizeof(HPDataType));//先将新空间的指针暂时赋给tmp(可能会出现开辟失败的情况,此时tmp指向NULL) if(tmp==NULL)//判断是否开辟成功 { perror("realloc fail"); exit(-1); } php->a=tmp;//开辟成功,再将新空间的指针赋给a php->capacity=newCapaciy;//更新容量 } php->a[php->size]=x; php->size++; AdjustUp(php->a,); //向上调整 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 测试函数:
void TestHeap() { HP hp; HeapInit(&hp); //插入数据 HeapPush(&hp, 1); HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); //打印 HeapPrint(&hp); //销毁 HeapDestroy(&hp); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
堆的删除同样也要确保删除后依旧是堆.
这里堆的删除是删除堆顶的数据。以小根堆为例,删除堆顶的数据,也就是把最小的数据删掉,要保证依旧是堆:
- 思路:
- 首先,把第一个数据和最后一个数据进行交换
交换后,此时的堆就不符合其性质了,因为原先最后一个数据肯定是比第一个数据大的,现在最后一个数据到了堆顶,就不是堆了,但是根结点的左子树和右子树不受影响,单独看它们依旧是堆。- 接着,–size,确保删除堆顶数据
因为此时堆顶的数据已经到了堆尾,只需要像顺序表那样–size,确保有效数据减1也就是确保了堆顶的删除- 最后,运用向下调整算法,确保其是堆结构
流程图:
时间复杂度分析:
第一个数据和最后一个数据交换是O(1),而向下调整算法的时间复杂度为O(logN),因为向下调整是调整高度次,根据结点个数N可以推出高度约为logN。void HeapPop(HP* php) { void HeapPop(HP* php) { assert(php); assert(php->size > 0);//确保size>0 Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾 php->size--; //向下调整 AdjustDown(php->a, php->size, 0); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 测试函数:
TestHeap2() { HeapInit(&hp); //插入数据 HeapPush(&hp, 5); HeapPush(&hp, 3); HeapPush(&hp, 0); HeapPush(&hp, 8); HeapPush(&hp, 9); HeapPrint(&hp);//打印 //删除堆顶数据 HeapPop(&hp); HeapPrint(&hp);//打印 //销毁 HeapDestroy(&hp); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
直接返回堆顶即可。前提是得断言size>0。
void HeapTop(HP* php) { assert(php); assert(!HeapEmpty(php)); return php->a[0]; }
- 1
- 2
- 3
- 4
- 5
- 6
若size为0,直接返回即可。
bool HeapEmpty(HP* php) assert(php); return php->size==0; }
- 1
- 2
- 3
- 4
直接返回size即可。
int HeapSize(HP* php) { assert(php); return php->size;//size为0即为空 }
- 1
- 2
- 3
- 4
- 5
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);
}
代码实现: void HeapSort(int *a,int n) { //建堆——向上调整 —— O(N*logN) //for(int i=1;i<n;++i) //{ //AdjustUp(a,i); //} //建堆——向下调整 —— O(N) for(int i=(n-1-1)/2;i>=0;--i) { AdjustDown(a,n,i); } } int main() { int a[]={65,100,70,32,50,60}; HeapSort(a,sizeof(a)/sizeof(int)); return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。