打怪升级:第8天 |
---|
![]() |
朋友们大家好啊,前段时间我们结束了线性存储结构的学习,经过之前的刷级打怪,今天我们就要开启新的地图、挑战更强大的怪兽 - - 树。
树,顾名思义就是像现实生活中的树一样的,不过这棵树和我们平常见到的树不太一样:它是一颗倒立的树。
如下图:
树:树是由n(n>=0)个有限节点组成带有层次结构的集合。由图我们可以看出树是一种非线性数据结构,把它叫做树是因为他看起来像是一个倒挂的树。
我们在前面学到的线性存储结构包含:顺序表和链表两种,而线性表中数据的对应关系为:1对1,
也就是说一个数据最多只能和一个数据存在关联,换句话说就是一个人最多只能认识一个其他人。
那么显而易见,这种存储结构是不能满足我们现实生活使用的,而今天要介绍的树则可以将多个数据关联在一起,
就比如:我们的族谱、公司或学校的领导管理层次划分等。
有一个特殊节点:根节点,根节点没有前驱节点
除根节点外,其他节点被分成m个互不相交的集合T1,T2,…Tn,其中每一个集合Ti(0<=i<=m)又是一颗结构与树类似的子树,每一个根节点有唯一一个前驱,可以有0个、或多个后继,因此树是递归定义的。
需要注意的是:树形结构中子树之间不能存在交集,否则就不是树了(属于图)。
如下图:
节点的度:一个节点含有的子树的个数叫做节点的度,比如上图中:A的度为3,B的度为2;
叶子节点或终端节点:度为0的节点称为叶子结点,比如上图中:J, F, K, L, H, I;
非终端节点或分支节点:度不为0的节点,如上图的:B, C, D, E等;
双亲节点或父节点:若一个节点含有子节点,那这个节点就称为它子节点的父节点,如上图中:A是B的父节点;
孩子节点或子节点:若一个节点含有子节点,那这个节点的子节点就是它的孩子节点,如上图中:B是A的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点,如上图中:B, C, D互称为兄弟节点;
树的度:一个树中最大的节点的度称为树的度,如上图中:树的度为3;
节点的层次:从根节点开始计为1(或0)的话,第二层就是2(或1),以此类推;
如图:
树的高度或深度: 树中节点的最大层次,上图中为:4;
堂兄弟节点:双亲在同一层次的节点互为堂兄弟节点,如上图中:F和G,F和H;
节点的祖先:从根节点出发到达该节点所经过的所以节点都是它的祖先节点(父节点也是它的祖先节点),如上图中A是所有节点的祖先;
节点的子孙:以某节点为根的子树的所有节点都是它的子孙节点,如上图中:所以节点都是A的子孙节点;
森林:由m(m>0)棵互不相交的树的集合称为森林。
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间
的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法
等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{struct Node* _firstChild1; // 第一个孩子结点struct Node* _pNextBrother; // 指向其下一个兄弟结点DataType _data; // 结点中的数据域
};
一棵二叉树是结点的一个有限集合,该集合:
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
typedef int BTDataType;// 二叉链
struct BinaryTreeNode
{BTDataType data; // 当前节点值域BinaryTreeNode* left; // 指向当前节点左孩子BinaryTreeNode* right; // 指向当前节点右孩子
};// 三叉链
struct BinaryTreeNode
{BTDataType data; // 当前节点值域BinaryTreeNode* parent; // 指向当前节点双亲BinaryTreeNode* left; // 指向当前节点左孩子BinaryTreeNode* right; // 指向当前节点右孩子
};
普通二叉树是不适合顺序存储的,那可能会造成大量的空间浪费,而完全二叉树更适合顺序存储。
在现实生活中我们把**堆(一种二叉树)**使用顺序结构来存储。
需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
如果有一个关键码的集合K = { k0,k1 ,k2 ,…,k n-1 },把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足: k i<= k 2i+1且 k i<= k 2i+2( k i>= k 2i+1且 k i>= k 2i+2) i = 0,1,
2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆的实现可以使用向下调整和向上调整两种方法,不过由于向上调整比向下调整时间复杂度大很多(向上调整:O(nlogn),向下O(n) ),因此在实际使用是我们都使用向下调整,下面我们使用的方法也是向下调整。
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
int array[] = {27,15,19,18,28,34,65,49,25,37};
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算
法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的
子树开始调整,一直调整到根节点的树,就可以调整成堆。
int a[] = {1,5,3,8,7,6};
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个节点不影响最终结果):
因此建堆的时间复杂度位O(n)。
插入数据到数组的尾上,然后通过向上调整算法,直到满足堆。
删除堆是删除堆顶数据,将最后堆顶数据与数组最后一个数据交换,删除最后一个数据,再对堆顶数据进行向下调整。
// 堆 -- 数组
typedef int HPDataType;
typedef struct Heap
{HPDataType* _a;int _size;int _capacity;
}Heap;// 堆的构建
void HeapCreate(Heap* php, HPDataType* a, int n)
{assert(php);php->_a = a;php->_size = php->_capacity = n;for (int i = (php->_size - 2) / 2; i >= 0; i--) // 从第一个非叶子节点开始向下调整{AdjustDown(php->_a, n, i);}
}
//堆的销毁
void HeapDestroy(Heap* php)
{assert(php);php->_a = NULL;php->_size = php->_capacity = 0;
}static void Swap(HPDataType*x, HPDataType*y)
{HPDataType z = *x;*x = *y;*y = z;
}
// 向上调整 -- 大堆存储
static void AdjustUp(Heap* php)
{assert(php);int child = php->_size - 1;while (child > 0){int parent = (child - 1) / 2;if (php->_a[child] > php->_a[parent]){Swap(&php->_a[child], &php->_a[parent]);child = parent;}else{break;}}
}
//堆的插入,同时保持堆的形态
void HeapPush(Heap* php, HPDataType data)
{assert(php);if (php->_size == php->_capacity){int newcapacity = php->_capacity == 0 ? 4 : php->_capacity * 2;HPDataType* newnode = (HPDataType*)realloc(php->_a, sizeof(HPDataType) * newcapacity);if (newnode == NULL){perror("HeapPush::malloc");exit(-1);}php->_a = newnode;php->_capacity = newcapacity;}php->_a[php->_size++] = data;AdjustUp(php);
}
// 堆的打印
void HeapPrint(Heap* php)
{assert(php);for (int i = 0; i < php->_size; i++){printf("%d ", php->_a[i]);}printf("\n");
}
// 向下调整
static void AdjustDown(HPDataType* arr, int n, int parent)
{assert(arr);int child = parent * 2 + 1; // 左孩子下标while (child < n){if (child + 1 < n && arr[child + 1] < arr[child]) // 右孩子存在,并且值更大child++;if (arr[parent] > arr[child]){Swap(&arr[parent], &arr[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}
// 堆的判空 为空返回 1
bool HeapEmpty(Heap* php)
{assert(php);return php->_size == 0;
}
//堆的删除,保持堆的形态
void HeapPop(Heap* php)
{assert(php);assert(!HeapEmpty(php));Swap(&php->_a[php->_size - 1], &php->_a[0]); // 将最后一个数据放到第一个php->_size--;// 将交换到最上面的数据进行向下调整AdjustDown(php->_a, php->_size, 0);
}
//获取堆中数据个数
int HeapSize(Heap* php)
{assert(php);return php->_size;
}
//获取堆顶数据
HPDataType HeapTop(Heap* php)
{assert(php);assert(!HeapEmpty(php));return php->_a[0];
}
堆排序是利用堆的思想来排序
- 建堆:
升序:建大堆
降序:建小堆- 利用堆删除的特性来进行排序
建堆和堆删除过程中都用到了向下调整,因此掌握了向下调整就可以实现堆排序。
升序排序思想介绍:
首先建大堆,此时堆顶数据就是最大值,我们在每次删除的过程中会将当前堆顶元素换到最后,后重新调整堆,那么我们每次删除数据时都会将当前堆中的最大值调整到最后,直到堆中只剩下一个数据就结束,此时获得的数据就是按升序排列好的。
如图:
代码如下:
void HeapSort(int* a, int n)
{Heap hp;HeapCreate(&hp, a, n);while(HeapSize(&hp) > 1){HeapPop(&hp);}HeapDestory(&hp);
}
Top-k问题:求数据集合中前k个最大元素或最小元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能
数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
示例:
void PrintTopK(int* a, int n, int k)
{// 1. 建堆--用a中前k个元素建堆Heap hp;HeapCreate(&hp, a, k);int i=k;while(iif(hp._a[0]Swap(&hp._a[0], a[i]);AdjustDown(hp._a, k, 0);}i++;}// 2. 将剩余n-k个元素依次与堆顶元素交换,不满足则替换for(i=0;iprintf("%d ", hp._a[i]);}printf("\n");HeapDestroy(&hp);
}void TestTopk()
{int n = 100000;int* a = (int*)malloc(sizeof(int)*n);srand((unsigned int)time(NULL));for (size_t i = 0; i < n; ++i){a[i] = rand() % 1000000;}a[5] = 1000000 + 1; // 额外创建k个数据,便于判断结果的有效性a[1231] = 1000000 + 2;a[531] = 1000000 + 3;a[5121] = 1000000 + 4;a[115] = 1000000 + 5;a[2335] = 1000000 + 6;a[9999] = 1000000 + 7;a[76] = 1000000 + 8;a[423] = 1000000 + 9;a[3144] = 1000000 + 10;PrintTopK(a, n, 10);
}
在学习二叉树的实现之前先来分析一下二叉树的结构:
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉
树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历
是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为
根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
根的前序遍历图解:
代码实现:
void BinaryTreePrevOrder(BTNode* root)
{if (root){printf("%c->", root->_data);BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);}else{printf("NULL->");}
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{if (root){printf("%c->", root->_data);BinaryTreeInOrder(root->_left);BinaryTreeInOrder(root->_right);}else{printf("NULL->");}
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{if (root){printf("%c->", root->_data);BinaryTreePostOrder(root->_left);BinaryTreePostOrder(root->_right);}else{printf("NULL->");}
}
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在
层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层
上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历
typedef BTNode* QDataType;
// 链式结构:表示队列
typedef struct QListNode
{QDataType _data;struct QListNode* _next;
}QNode;// 队列的结构
typedef struct Queue
{int len;QNode* _front;QNode* _rear;
}Queue;// 初始化队列
void QueueInit(Queue* q)
{assert(q);q->len = 0;q->_front = q->_rear = NULL;
}
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{QNode* tmp = (QNode*)malloc(sizeof(QNode));if (tmp == NULL)exit(-1);tmp->_data = data;tmp->_next = NULL;if (q->_front == NULL) // 空队列{q->_front = q->_rear = tmp;}else{q->_rear->_next = tmp;q->_rear = q->_rear->_next;}q->len++;
}
// 检测队列是否为空,为空返回1
bool QueueEmpty(Queue* q)
{assert(q);return q->len == 0;
}
// 队头出队列
void QueuePop(Queue* q)
{assert(q);if (QueueEmpty(q))exit(-1);QNode* next = q->_front->_next;free(q->_front);q->_front = next;if (q->_front == NULL)q->_rear = NULL;q->len--;
}
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{assert(q);if (QueueEmpty(q))exit(-1);return q->_front->_data;
}
// 销毁队列
void QueueDestroy(Queue* q)
{assert(q);QNode* cur = q->_front;while (cur){QNode* next = cur->_next;free(cur);cur = next;}
}// 层序遍历 -- 借助队列,出一个根节点,进两个孩子
void LevelOrder(BTNode* root)
{Queue q;QueueInit(&q);QueuePush(&q, root);BTNode* cur = NULL;while (cur || !QueueEmpty(&q)){cur = QueueFront(&q); // 出一个 ,进两个QueuePop(&q);printf("%c->", cur->_data);if (cur->_left)QueuePush(&q, cur->_left);if (cur->_right)QueuePush(&q, cur->_right);cur = NULL;}QueueDestroy(&q);
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode* root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root);// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{assert(a);if (a[*pi] == '#'){return NULL;}else{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));newnode->_data = a[*pi];(*pi)++;newnode->_left = BinaryTreeCreate(a, n, pi);(*pi)++;newnode->_right = BinaryTreeCreate(a, n, pi);return newnode;}
}
// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{if (root){BinaryTreeDestory(root->_left);BinaryTreeDestory(root->_right);free(root);}
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{if (root)return 1 + BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right);elsereturn 0;
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{if (root){if (root->_left == NULL && root->_right == NULL)return 1;elsereturn BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);}elsereturn 0;
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{if (root){if (k == 1)return 1;elsereturn BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);}elsereturn 0;
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root){if (root->_data == x)return root;else{BTNode* target = BinaryTreeFind(root->_left, x);if (target)return target;elsereturn BinaryTreeFind(root->_right, x);}}elsereturn NULL;
}// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{if (root){if (root->_left == NULL && root->_right != NULL) // 此情况则不是完全二叉树return false;return BinaryTreeComplete(root->_left)&& BinaryTreeComplete(root->_right);}elsereturn true;
}
文章内容汇总:
树的介绍、
二叉树的顺序存储–堆,
以及链式存储,
堆的应用–堆排序、top-k问题,
以及二叉树链式存储相关的基本操作,
如果有什么疑问或者建议都可以在评论区留言,感谢大家对的支持。