一、树
- 节点的度:一个节点含有的字数的个数就叫做该节点的度
- 叶节点/终端节点:度为0的节点
- 非终端节点/分支接点:度不为0的几点
- 双亲结点/父节点:含有子节点的节点
- 孩子节点/子节点:一个节点含有的子树的节点
- 兄弟节点:具有相同父节点的节点
- 树的度:一棵树中,最大的节点称为数的度
- 节点的层次:从根节点开始,根为第一层,根的子节点为第二层,以此类推
- 树的高度或深度:树的最大层次
- 堂兄弟节点:父节点在同一层的节点
- 节点的祖先:从根节点到该节点所经过的所有节点
- 子孙:以某节点为根的子树中任意节点都称为该节点的子孙
- 森林:m颗互不相交的数的集合
树的表示方法:
在上述代码中,firstChild指向左边的第一个子节点,而pNextBro指向右边的第二个子节点,若没有子节点,则pNextBro指向NULL。如下图所示:
二、二叉树
概念
如上图所示,二叉树不存在度大于2的节点,且二叉树的子树有左右之分,分别为左子树和右子树。二叉树还有以下几种情况:
有两种特殊的二叉树:
- 满二叉树:该二叉树中的每层的节点数都达到最大值;或者说如二叉树的层数为k,且总结点数的和为2^k - 1;
- 完全二叉树:前k-1层是满的,最后一层是不满的;
二叉树还具有以下几种性质:
- 若根节点的层数为1,则第i层上最多有2^( i - 1 )个节点;
- 若根节点的层数为1,则深度为h的二叉树的最大节点数是2^h - 1;
- 对任意一个二叉树,如果度为0的节点数为a,则度为2的节点数为a-1;
- 若根结点的的层数为1,具有n个节点的满二叉树的深度h = log(n+1);
- 在完全二叉树中,度为0的节点个数比度为1的节点个数多一;度为1的节点数为1或0;
- 对于有n个节点的完全二叉树,按从上至下,从左至右的数组顺序对所有节点从0开始编号:
在上图中,若父节点的下标是2,对应的值是12,则我们可以得到其左孩子下标为 2 i + 1 = 5,则对应的值是25;右孩子下标为 2 i + 2 = 6,对应的值是10;据图可以看出是这样的。
如果说父节点的下标是3,对应的值是35,左孩子下标为7,对应的值是21,右孩子下标是8而下标8数据不存在,故不存在右孩子。
通过父节点 i 下标推子节点下标,n表示节点总数:
- 右节点下标:2i+2 < n;
- 左孩子下标:2i+1 < n;
通过子节点下标推父节点下标:
- 右节点下标R为偶数:(R - 1) / 2或者(R - 2) / 2;
- 左节点下标L为奇数:(L - 1) / 2。
在上图中,10是右节点对应下标6,无论下标减1或是2,其都能得到父节点下标;25是左节点,下标减1得到父节点下标。
实现
对于二叉树不选择数组而是链式存储的原因:
对于完全二叉树选择数组是没问题的,而对于非完全二叉树来说,数组有空间的浪费。
三、堆的实现
堆分为大堆(父节点比子节点大)和小堆(父节点比子节点小)。堆有两个性质:
- 堆中某个节点的值总是不大于(或不小于)其父节点的值;
- 堆总是一颗完全二叉树;
堆有以下接口:
typedef int HPDataType; typedef struct Heap { HPDataType* arr; int size;//堆的数据个数 int capacity;//堆的容量 }HP; //堆的初始化 void HeapInit(HP* hp); //堆的销毁 void HeapDestory(HP* hp); //堆的插入 void HeapPush(HP* hp, HPDataType val); //堆的数据删除 void HeapPop(HP* hp); //获取堆顶数据 int HeapTop(HP* hp); //打印 void HeapPrint(HP* hp); //判空 int HeapEmpty(HP* hp); //堆的数据个数 int HeapSize(HP* hp);
//堆的初始化 void HeapInit(HP* hp) { assert(hp); hp->arr = NULL; hp->capacity = hp->size = 0; } //堆的销毁 void HeapDestory(HP* hp) { assert(hp); free(hp->arr); hp->arr = NULL; hp->capacity = hp->size = 0; } //交换数据 void Swap(HPDataType* x1, HPDataType* x2) { HPDataType tmp = *x2; *x2 = *x1; *x1 = tmp; } //向上调整算法 void AdjustUp(HP* hp, int child) { assert(hp); //这里的child和parent的下标关系是:parent * 2 + 1 = child; int parent = (child - 1) / 2; while (child > 0) { //当前堆是小堆,就是说子节点比父节点大,父节点比子节点小 // 这里的if条件判断就是说:child比parent小,子字节比父节点小,因此交换 if (hp->arr[child] < hp->arr[parent]) { //交换两值 Swap(&hp->arr[child], &hp->arr[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } //向下调整算法 void AdjustDown(HPDataType* arr, int root, int size) { assert(arr); int child = root * 2 + 1; while (child < size) { //1、找到左右孩子的较小值 if (arr[child] > arr[child + 1]) { child++; } //2、交换 if (child+1 < size && arr[child] < arr[root]) { Swap(&arr[child], &arr[root]); root = child; child = root * 2 + 1; } else { break; } } } //堆的插入 void HeapPush(HP* hp, HPDataType val) { assert(hp); //检查容量 if (hp->size == hp->capacity) { int newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2; HPDataType* newArr = realloc(hp->arr, sizeof(HPDataType) * newCapacity); if (newArr == NULL) { printf("realloc failed\n"); exit(-1); } hp->capacity = newCapacity; hp->arr = newArr; } //插入数据 hp->arr[hp->size] = val; hp->size++; //向上调整算法 AdjustUp(hp, hp->size - 1); } //堆的删除:删除堆顶的数据 void Delete(HP* hp) { assert(php); Swap(&php->_arr[0], &php->_arr[php->_size - 1]); php->_size--; AdjustDown(php->_arr, php->_size, 0); } //获取堆顶数据 int HeapTop(HP* hp) { assert(hp); assert(hp->size > 0); return hp->arr[0]; } //打印 void HeapPrint(HP* hp) { assert(hp); for (int i = 0; i < hp->size; i++){ printf("%d ", hp->arr[i]); } printf("\n"); } //判空 int HeapEmpty(HP* hp) { assert(hp); return hp->size == 0; } //堆的数据个数 int HeapSize(HP* hp) { assert(hp); return hp->size; }
堆排序
将无序数组排成有序数组
- 将数组建成堆
- 对堆进行排序
建堆既可以使用向上调整算法,也可以使用向下调整算法:
//向上调整算法 void AdjustUp(int* arr, int childPos) { int parentPos = (childPos - 1) / 2; while (childPos > 0) { if (arr[childPos] > arr[parentPos]) { Swap(&arr[childPos], &arr[parentPos]); childPos = parentPos; parentPos = (childPos - 1) / 2; } else { break; } } } //向下调整算法 void AdjustDown(int* arr, int size, int root) { int parentPos = root; int childPos = parentPos * 2 + 1; while (childPos < size) { if (childPos+1<size && arr[childPos] < arr[childPos + 1]) { childPos++; } if (arr[parentPos] < arr[childPos]) { Swap(&arr[parentPos], &arr[childPos]); parentPos = childPos; childPos = parentPos * 2 + 1; } else { break; } } } void HeapSort(int* arr, int size) { //向上调整算法: for (int i = 0; i < size; i++){ AdjustUp(arr, i); } //1、数组转换成堆 //向下调整算法:从最后一个非叶子节点(最后一个节点的父亲)向前遍历 //这里的i表示要调整的节点下标,size表示数组大小。 int end = (size - 1 - 1) / 2; for (int i = end; i >= 0; i--) { AdjustDown(arr, size, i); } //2、数组堆排序: int end = size - 1; while (end > 0) { Swap(&arr[0], &arr[end]); AdjustDown(arr, end--, 0); } }
在数组转换成大堆后,将首元素与尾元素交换,此时数组末尾元素就是最大的数据,再通过向下调整算法将数据个数减一(即end--,因为此时末尾数据是最大的,我们接下来就是找到次大数据并将其归位),从下标为0的位置开始调整。以此类推即可将数组排序。