鸟群离开了森林,整座天空很灰心............................................................................................
目录
前言
今天学习数据结构中二叉树相关的知识,感谢观看!
一、【树的介绍】
1.1【树的概念及其结构】
我们普遍认为的树,是一种植物,有各种种类和形状,而数据结构中的树则指的是一种数据存储方式他表现出来就类似于一种树状图,树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。下面是其特点:
1.有一个特殊的结点,称为根结点,根节点没有前驱结点
2.除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。3.每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
因此,树是递归定义的。注意:树形结构中,子树之间不能有交集,否则就不是树形结构
1.2【树的结构组成】
树由多个节点共同构成,本质上可以看作两部分根节点和子树。
下面是一些相关的概念:
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林;
1.3【树的表示】
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
孩子兄弟表示法,也称为二叉树表示法或父亲表示法,是一种用于树状数据的存储方式。这种表示法的每个结点由三个部分组成:
- 结点值
- 一个指向结点第一个孩子的指针(FirstChild)
- 一个指向结点下一个兄弟节点的指针(pNextBrother)
通过这种方式,可以沿着这些指针遍历整个树,从而访问到结点的所有子节点以及它的兄弟节点。这种表示法利用了链表的特性,将树的结构以链式形式存储,这样可以有效地压缩存储空间并便于处理大量数据。
typedef int DataType; struct Node { struct Node* _firstChild; // 第一个孩子结点 struct Node* _pNextBrother; // 指向其下一个兄弟结点 DataType _data; // 结点中的数据域 };
实际当中的应用:
二、【二叉树】
2.1【二叉树的概念及其结构】
二叉树是树的一种,其规定一个根节点只有两个子树(左子树和右子树),一棵二叉树是结点的一个有限集合,该集合有下面两种结构:
1. 空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
2.2【二叉树的性质】
1. 若规定根节点的层数为第1层,则一棵非空二叉树的第i层上最多有2^(i-1) 个结点.
2. 若规定根节点的层数为第1层,则深度为h的二叉树的最多有2^h-1(对应为满二叉树)
3. 对任何一棵二叉树, 叶子节点个数=双分支节点个数+1,完全二叉树最对有一个单分支节点
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log以2为底的n+16. 二叉树不存在度大于2的结点
7. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树8. 对于具有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否则无右孩子也就是:
左节点下标=父节点*2+1
右节点下标=父节点*2
在堆中会用到。
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.3【特殊的二叉树】
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.5【二叉树的存储结构】
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。typedef int BTDataType; // 二叉链 struct BinaryTreeNode { struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 } // 三叉链 struct BinaryTreeNode { struct BinTreeNode* _pParent; // 指向当前节点的双亲 struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 };
三、【二叉树的顺序结构——堆】
3.1【堆的概念及其结构】
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
【大堆,小堆的介绍】
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
【堆的性质】:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
3.2【堆的实现】
3.2.1.【思路模块】
我们知道堆有3类分别是,完全二叉树,大堆,小堆。下面我们着重讲解大小堆的实现,我们清楚大堆是要保证父节点在任何情况下都大于其子节点,小堆则相反那么我们应该怎样实现呢?这里就需要介绍两个方法:向上调整法和向下调整法
1.【向上调整法】
这里假设使要建大堆向上调整法就是从上向下调整也就是在插入数据时从上向下先插入父节点,后插入对应子节点,每插入一次子节点均需与其父节点比较如果出现子节点大于父节点,则交换子节点和父节点这里需要注意的点是这里应该循环判断才能确保从跟节点到某个叶子节点的路径上均满足大堆的要求。从根节点的下一个节点开始向下迭代,每次迭代进行一次向上调整。
向上体现在通过孩子去向上调整父亲这里举个例子:
【代码实现】:
void Swap(HpDataType* p1, HpDataType* p2)//交换函数 { HpDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustUp(HpDataType* hd, int child)//向上调整 { int parent = (child - 1) / 2;//先定义父节点,下标位置为(左子节点-1)/2 //while (parent >= 0) while (child > 0)//子节点为0时来到根节点的位置,此时没有父节点 { if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]);//如果孩子大于父亲就交换 child = parent;//注意下标也要交换也就是说要把孩子变为父亲,父亲变为孩子 parent = (child - 1) / 2;//交换后的父亲继续与其父亲调整 } else { break;//符合大堆就停止 } } }
建立小堆只需将“ > ”改为“ < ”即可。
【时间复杂度】
2.【向下调整法】
这里假设建立的堆是小堆,与向上调整相反,向下调整则是通过父节点去调整子节点,需要注意的是,子节点有两个,要确保子节点存在,且子节点不能超过数组的范围。同时注意应从第一个非叶子结点开始向前迭代,每次迭代都进行一次向下调整。
比如我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
【代码实现】
void Swap(HpDataType* p1, HpDataType* p2) { HpDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustDown(HpDataType* hd, int size, int parent) { int child = 2 * parent + 1; while (child < size) { if (child + 1 < size && hd[child+1] < hd[child])//找出最小的子节点 { child += 1; } if (hd[child] < hd[parent])//将最小的子节点与父节点进行交换 { Swap(&hd[child], &hd[parent]); parent = child; child = 2 * parent + 1; } else { break; } } }
3.【时间复杂度】
4.【堆的插入和删除】
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
void AdjustUp(HpDataType* hd, int child) { int parent = (child - 1) / 2; //while (parent >= 0) while (child > 0) { if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void HeapPush(HP* php, HpDataType x) { assert(php); if (php->size == php->capacity) { int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2; HpDataType* tmp = (HpDataType*)realloc(php->hd, newCapacity * sizeof(HpDataType)); if (tmp == NULL) { perror("realloc fail"); return; } php->hd = tmp; php->capacity = newCapacity; } php->hd[php->size] = x; php->size++; //AdjustUp(php->hd, php->size-1); AdjustDown(php->hd, php->size, 0); }
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
void HeapPop(HP* php) { assert(php); assert(!HeapEmpty(php)); Swap(&php->hd[0], &php->hd[php->size - 1]); php->size--; AdjustDown(php->hd, php->size, 0); } void AdjustDown(HpDataType* hd ,int size ,int parent) { int child = 2 * parent + 1; while (child < size) { if (child + 1<size && hd[child] < hd[child + 1]) { child += 1; } if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); parent = child; child = 2 * parent + 1; } else { break; } } }
【时间复杂度】
O(N)+O(N+log以2为底的N)=N+log以2为底的N
3.2.2【代码模块】
Heap.h:
#pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> typedef int HpDataType; typedef struct Heap { HpDataType* hd; int capacity; int size; }HP; void HeapInit(HP* php); void HeapDestroy(HP* php); void HeapPush(HP* php, HpDataType x); void HeapPop(HP* php); HpDataType HeapTop(HP* php); void AdjustUp(HpDataType* hd, int child); void AdjustDown(HpDataType* parent, HpDataType* child); bool HeapEmpty(HP* php); int HeapSize(HP* php); void Swap(HpDataType* parent, HpDataType* child);
Heap.c
#define _CRT_SECURE_NO_WARNINGS #include "Heap.h" void Swap(HpDataType* p1, HpDataType* p2) { HpDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } void HeapInit(HP* php) { assert(php); php->hd = NULL; php->capacity = php->size = 0; } void HeapDestroy(HP* php) { assert(php); free(php->hd); php->hd = NULL; php->capacity = php->size = 0; } void HeapPush(HP* php, HpDataType x) { assert(php); if (php->size == php->capacity) { int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2; HpDataType* tmp = (HpDataType*)realloc(php->hd, newCapacity * sizeof(HpDataType)); if (tmp == NULL) { perror("realloc fail"); return; } php->hd = tmp; php->capacity = newCapacity; } php->hd[php->size] = x; php->size++; AdjustUp(php->hd, php->size-1); //AdjustDown(php->hd, php->size, 0); } void AdjustUp(HpDataType* hd, int child) { int parent = (child - 1) / 2; //while (parent >= 0) while (child > 0) { if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void HeapPop(HP* php) { assert(php); assert(!HeapEmpty(php)); Swap(&php->hd[0], &php->hd[php->size - 1]); php->size--; AdjustDown(php->hd, php->size, 0); } void AdjustDown(HpDataType* hd ,int size ,int parent) { int child = 2 * parent + 1; while (child < size) { if (child + 1<size && hd[child] < hd[child + 1]) { child += 1; } if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); parent = child; child = 2 * parent + 1; } else { break; } } } HpDataType HeapTop(HP* php) { assert(php); assert(!HeapEmpty(php)); return php->hd[0]; } bool HeapEmpty(HP* php) { assert(php); return (php->size == 0); } int HeapSize(HP* php) { assert(php); return php->size; }
test.c
#define _CRT_SECURE_NO_WARNINGS #include "Heap.h" int main() { HP hp; HeapInit(&hp); int a[] = { 65,10,7,32,50,60 }; for (int i = 0; i < sizeof(a) / sizeof(int); ++i) { HeapPush(&hp, a[i]); } while (!HeapEmpty(&hp)) { int top = HeapTop(&hp); printf("%d\n", top); HeapPop(&hp); } return 0; }
3.3【堆的应用】
3.3.1【堆排序】
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
升序:建大堆
降序:建小堆2.利用堆删除思想来进行排序
每次建堆第一个位置总是最大或最小的数,升序就把最大的数与最后一个元素交换位置对应的是大堆,相反则是小堆。
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。【代码部分】
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> typedef int HpDataType; void Swap(HpDataType* p1, HpDataType* p2) { HpDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustUp(HpDataType* hd, int child) { int parent = (child - 1) / 2; //while (parent >= 0) while (child > 0) { if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void AdjustDown(HpDataType* hd, int size, int parent) { int child = 2 * parent + 1; while (child < size) { if (child + 1 < size && hd[child] < hd[child + 1]) { child += 1; } if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); parent = child; child = 2 * parent + 1; } else { break; } } } void HeapSort(HpDataType* arr, int size) { /*for (int i = 1; i <= size; i++) { AdjustUp(arr, i); }*/ for (int i = (size - 1 - 1) / 2; i > 0; i--) { AdjustDown(arr, size, i); } int end = size - 1; while (end) { Swap(&arr[0], &arr[end]); AdjustDown(arr, end, 0); end--; } } int main() { HpDataType arr[] = { 9,4,2,1,7,3,0,8,5,6 }; int size = sizeof(arr) / sizeof(int); HeapSort(arr, size); for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } return 0; }
3.3.2【Top-K问题】
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下用于处理数据量比较大的问题。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 把数据集合中前K个元素拿来建堆
前k个最大的元素,则建小堆(这样才能确保第一个元素较小,后续元素才能正常入堆)
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
【代码部分】
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <time.h> typedef int HpDataType; void Swap(HpDataType* p1, HpDataType* p2) { HpDataType tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustDown(HpDataType* hd, int size, int parent) { int child = 2 * parent + 1; while (child < size) { if (child + 1 < size && hd[child] < hd[child + 1]) { child += 1; } if (hd[child] > hd[parent]) { Swap(&hd[child], &hd[parent]); parent = child; child = 2 * parent + 1; } else { break; } } } void CreateNDate() { // 造数据 int n = 10000; srand(time(0)); const char* file = "data.txt"; FILE* fin = fopen(file, "w"); if (fin == NULL) { perror("fopen error"); return; } for (size_t i = 0; i < n; ++i) { int x = rand() % 1000000; fprintf(fin, "%d\n", x); } fclose(fin); } void PrintTopK(int k) { const char* file = "data.txt"; FILE* fout = fopen(file, "r"); if (fout == NULL) { perror("fopen error"); return; } int* kminheap = (int*)malloc(sizeof(int) * k); if (kminheap == NULL) { perror("malloc error"); return; } for (int i = 0; i < k; i++) { fscanf(fout, "%d", &kminheap[i]); } // 建小堆 for (int i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDown(kminheap, k, i); } int val = 0; while (!feof(fout)) { fscanf(fout, "%d", &val); if (val > kminheap[0]) { kminheap[0] = val; AdjustDown(kminheap, k, 0); } } for (int i = 0; i < k; i++) { printf("%d ", kminheap[i]); } printf("\n"); } int main() { //CreateNDate();//这两句需要分开侧式,因为每调用一次该函数,都会将文件内容重新覆盖 PrintTopK(5); return 0; }
人为干预数字结果,将前5个都加上3个1
验证程序:
四、【二叉树的链式结构——《左右孩子表示法》】
4.1【左右孩子表示法的介绍】
在学习二叉树的基本操作前,我们需先要创建一棵二叉树,然后才能学习其相关的基本操作。这里我们使用左右孩子表示地形式来构建二叉树,为了便于实现和调试,这里我直接通过二叉树的结构对其进行构建。
要点:
1.这里并没有通过单独的函数来构建树。
2.二叉树包括:
空树
非空:根节点,根节点的左子树、根节点的右子树组成的。3.采用递归形式实现,将二叉树看成三部分:
根节点左孩子
右孩子
同时对于其左孩子和右孩子也可以看成这三部分。
注意:也就是说二叉树本质上可以看成3部分,根,左子树,右子树。
4.2【左右孩子表示法对应二叉树的实现】
4.2.1.【思路模块】
1.【三种遍历方式】
前序遍历:根——>左——>右
中序遍历:左——>根——>右
后序遍历:左——>右——>根
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
比如前序遍历:
代码部分:
前序遍历:
void PrevOrder(BTNode* root) { if (root == NULL) { printf(" #"); return; } printf(" %d", root->data); PrevOrder(root->left); PrevOrder(root->right); }
中序遍历:
void InOrder(BTNode* root) { if (root == NULL) { printf(" #"); return; } InOrder(root->left); printf(" %d", root->data); InOrder(root->right); }
后序遍历:
void PostOrder(BTNode* root) { if (root == NULL) { printf(" #"); return; } PostOrder(root->left); PostOrder(root->right); printf(" %d", root->data); }
2.【二叉树的高度】
什么是二叉树的高度?其是指从二叉树的根节点开始到二叉树叶子节点的最长路径上的节点个数。
比如下图中的红线表示的路径就表示从根节点到叶子节点的最长路径,其路径上的节点个数为4,则该二叉树的高度为4
由此可见,只要将左子树和右子树的高度进行比较取较大者再加上1(根节点),就能得到二叉树的高度。
这里需要注意的是:每次在遍历得到二叉树的高度时需要将其记录下来,不然每次在递归调用时都会重复调用,大大降低效率,具体我会在代码中进行解释。
代码部分:
int BTreeHeight(BTNode* root) { if (root == NULL)//如果是空树和不存在的节点就返回0 { return 0; } int leftheight = BTreeHeight(root->left);//定义leftheight来存放左子树的高度 int rightheight = BTreeHeight(root->right);//定义rightheight来存放右子树的高度 return (leftheight > rightheight) ? leftheight + 1 : rightheight + 1; //return BTreeHeight(root->left) > BTreeHeight(root->right) ? BTreeHeight(root->left) + 1 : BTreeHeight(root->right) + 1; //不采用上面的写法是由于上面的写法会导致重复调用, }
3.【K层的节点数】
对于任意一棵二叉树,它的K层节点数的范围是从1~2^(k-1),那么我们应如何确定呢?
实际上对于一棵非空的二叉树而言,它的第一层的节点树一定是1(根节点)也就是说,我们可以通过递归将第K层节点总数转化为求k-1层........一直到第一层的节点数。
代码部分:
int Count_K_Node(BTNode* root, int k) { assert(k > 0); if (root == NULL)//如果是空树和不存在的节点就返回0 { return 0; } if (k == 1)//第一层有一个节点 { return 1;//返回1 } int Left = Count_K_Node(root->left, k - 1);//递归左树 int Right = Count_K_Node(root->right, k - 1);//递归右数 return Left + Right; //这里的k==1就返回1也可以这样理解,当k能取到1时,说明第k层有节点存在, //由于是左右子树分别递归所以当任意子树递归到k==1时,只可能有一个节点,并且是对应子树的分支节点 }
4.【查找节点】
这里的查找结点是通过,节点的对应的数据来进行遍历查找,采用的是前序遍历的形式,先遍历根节点其次是左右子树,出现与目标值相等的节点就返回该节点。
代码部分:
BTNode* Find_BTNode(BTNode* root, BinaryTReeData x) { if (root == NULL)//空树直接返回空 { return NULL; } if (x == root->data)//如果当前遍历的节点对应的数据与目标值相同 { return root;//返回该节点 } BTNode* LeftRet = Find_BTNode(root->left, x);//递归遍历左子树 if (LeftRet)//判断左子树的返回值是否为空,不为空就是找到了,为空就递归遍历右子树 { return LeftRet; } BTNode* RightRet = Find_BTNode(root->right, x); if (RightRet) { return RightRet; } return NULL;//左右子树均遍历完全都没有返回值,说明书中不存在该值,返回空表示找不到。
5.【二叉树的构造和销毁】
二叉树的构建和销毁,没放在开头的原因是因为用函数的方式构建树,并不是十分自由,反而利用其结构直接将各个节点进行连接更加方便。
销毁就是释放各个节点对应的内存,最后将根节点置空即可
代码部分:
//这里是通过数组的方式来实现树,空节点用‘#’来表示。 //使用时可以输入各节点对应的值,默认是先序序列构建树。 BTNode* CreateBTree(char* arr, int* pi)//这里pi代表整型i的地址,这样就能在每次递归调用中使用和改变的均是同一个i的值 { if (arr[*pi] == '#') { (*pi)++;//迭代数组 return NULL; } BTNode* root = CreateNode(arr[*pi]);//先构建根节点,按照先序序列 (*pi)++; root->left = CreateBTree(arr, pi);//先序序列构建左子树 root->right = CreateBTree(arr, pi);//先序序列构建右子树 return root;//最后返回构建二叉树的根节点 } void BTreeDestroy(BTNode* root) { if (root == NULL)//如果是空树或者是空节点就不用销毁直接返回 { return; } BTreeDestroy(root->left);//递归销毁左子树和右子树 BTreeDestroy(root->right); free(root);//最后释放根节点,使用者需要在外部进行置空。 } int main() { char arr[100]={0}; scanf("%s",arr); int i=0; BTNode* root=CreateBTree(arr,&i); BTreeDestroy(root); root=NULL; return 0; }
4.2.2【代码模块】
BinaryTree.h:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> typedef int BinaryTReeData; typedef struct BinaryTReeNode { BinaryTReeData data; struct BinaryTReeNode* right; struct BinaryTReeNode* left; }BTNode; BTNode* BTreeInit(); void BTreeDestroy(BTNode* root); BTNode* CreateNode(BinaryTReeData x); void PrevOrder(BTNode* root); void InOrder(BTNode* root); void PostOrder(BTNode* root); int BTreeSize(BTNode* root); int CountLeaves(BTNode* root); int BTreeHeight(BTNode* root); int Count_K_Node(BTNode* root,int k); BTNode* Find_BTNode(BTNode* root, BinaryTReeData x); bool IsCompleteBTree(BTNode* root); BTNode* CreateBTree(char* arr,int* pi);
BinaryTree.c:
#define _CRT_SECURE_NO_WARNINGS #include "BinaryTree.h" BTNode* BTreeInit() { BTNode* node1 = CreateNode(1); BTNode* node2 = CreateNode(2); BTNode* node3 = CreateNode(3); BTNode* node4 = CreateNode(4); BTNode* node5 = CreateNode(5); BTNode* node6 = CreateNode(6); BTNode* node7 = CreateNode(7); BTNode* node8 = CreateNode(8); node1->left = node2; node1->right = node4; node2->left = node3; node4->left = node5; node4->right = node6; //node5->left = node7; node2->right = node7; node3->right = node8; return node1; } BTNode* CreateNode(BinaryTReeData x) { BTNode* newnode = (BTNode*)malloc(sizeof(BTNode)); if (newnode == NULL) { perror("malloc failed"); return NULL; } else { newnode->data = x; newnode->right = NULL; newnode->left = NULL; return newnode; } } void PrevOrder(BTNode* root) { if (root == NULL) { printf(" #"); return; } printf(" %d", root->data); PrevOrder(root->left); PrevOrder(root->right); } void InOrder(BTNode* root) { if (root == NULL) { printf(" #"); return; } InOrder(root->left); printf(" %d", root->data); InOrder(root->right); } void PostOrder(BTNode* root) { if (root == NULL) { printf(" #"); return; } PostOrder(root->left); PostOrder(root->right); printf(" %d", root->data); } int BTreeSize(BTNode* root) { if (root == NULL) { return 0; } int LeftTree = BTreeSize(root->left); int RightTree = BTreeSize(root->right); return LeftTree + RightTree + 1; } int CountLeaves(BTNode* root) { if (root == NULL) { return 0; } if (root->left == NULL && root->right == NULL) { return 1; } int leftleaf = CountLeaves(root->left); int rightleaf = CountLeaves(root->right); return leftleaf + rightleaf; } int BTreeHeight(BTNode* root) { if (root == NULL)//如果是空树和不存在的节点就返回0 { return 0; } int leftheight = BTreeHeight(root->left);//定义leftheight来存放左子树的高度 int rightheight = BTreeHeight(root->right);//定义rightheight来存放右子树的高度 return (leftheight > rightheight) ? leftheight + 1 : rightheight + 1; //return BTreeHeight(root->left) > BTreeHeight(root->right) ? BTreeHeight(root->left) + 1 : BTreeHeight(root->right) + 1; //不采用上面的写法是由于上面的写法会导致重复调用, } int Count_K_Node(BTNode* root, int k) { assert(k > 0); if (root == NULL)//如果是空树和不存在的节点就返回0 { return 0; } if (k == 1)//第一层有一个节点 { return 1;//返回1 } int Left = Count_K_Node(root->left, k - 1);//递归左树 int Right = Count_K_Node(root->right, k - 1);//递归右数 return Left + Right; //这里的k==1就返回1也可以这样理解,当k能取到1时,说明第k层有节点存在, //由于是左右子树分别递归所以当任意子树递归到k==1时,只可能有一个节点,并且是对应子树的分支节点 } BTNode* Find_BTNode(BTNode* root, BinaryTReeData x) { if (root == NULL)//空树直接返回空 { return NULL; } if (x == root->data)//如果当前遍历的节点对应的数据与目标值相同 { return root;//返回该节点 } BTNode* LeftRet = Find_BTNode(root->left, x);//递归遍历左子树 if (LeftRet)//判断左子树的返回值是否为空,不为空就是找到了,为空就递归遍历右子树 { return LeftRet; } BTNode* RightRet = Find_BTNode(root->right, x); if (RightRet) { return RightRet; } return NULL;//左右子树均遍历完全都没有返回值,说明书中不存在该值,返回空表示找不到。 } //这里是通过数组的方式来实现树,空节点用‘#’来表示。 //使用时可以输入各节点对应的值,默认是先序序列构建树。 BTNode* CreateBTree(char* arr, int* pi)//这里pi代表整型i的地址,这样就能在每次递归调用中使用和改变的均是同一个i的值 { if (arr[*pi] == '#') { (*pi)++;//迭代数组 return NULL; } BTNode* root = CreateNode(arr[*pi]);//先构建根节点,按照先序序列 (*pi)++; root->left = CreateBTree(arr, pi);//先序序列构建左子树 root->right = CreateBTree(arr, pi);//先序序列构建右子树 return root;//最后返回构建二叉树的根节点 } void BTreeDestroy(BTNode* root) { if (root == NULL)//如果是空树或者是空节点就不用销毁直接返回 { return; } BTreeDestroy(root->left);//递归销毁左子树和右子树 BTreeDestroy(root->right); free(root);//最后释放根节点,使用者需要在外部进行置空。 }
test.c:
#define _CRT_SECURE_NO_WARNINGS #include "BinaryTree.h" int main() { BTNode* root = BTreeInit(); PrevOrder(root); printf("\n"); int size = BTreeSize(root); printf("%d\n", size); int leaves = CountLeaves(root); printf("%d\n", leaves); int K_Count = Count_K_Node(root, 3); printf("%d\n", K_Count); BTreeDestroy(root); root = NULL; return 0; }
4.3 【二叉树基础oj练习】
1.题目链接: 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题解:
bool isUnivalTree(struct TreeNode* root) { if(root==NULL)//题目规定空树也符合单值二叉树 { return true; } if(root->left&&root->left->val!=root->val)//这里注意判断不符合的情况,因为符合的情况只能说明 { //某一组值符合,其他值仍要继续判断,所以不能根据其 return false; //得到返回值 } if(root->right&&root->right->val!=root->val) { return false; } return isUnivalTree(root->left)&&isUnivalTree(root->right);//递归判断所有子树 }
2.题目链接: 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题解:
bool isSameTree(struct TreeNode* p, struct TreeNode* q) { if(p==NULL&&q==NULL)//如果两棵树均为空,符合条件 { return true; } if(p==NULL||q==NULL)//第一个if语句没有进入,说明两棵树有一个不为空,则两棵树不相同 { return false; } if(p->val!=q->val)//到这里说明两棵树均不为空,则递归判断两棵树各个节点的值是否相同 { return false; } return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right); }
3.题目链接: 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题解:
bool _isSymmetric(struct TreeNode* LeftRoot,struct TreeNode* RightRoot) { if(LeftRoot==NULL&&RightRoot==NULL) { return true; } if(LeftRoot==NULL||RightRoot==NULL) { return false; } if(LeftRoot->val!=RightRoot->val) { return false; } return _isSymmetric(LeftRoot->left,RightRoot->right)&&_isSymmetric(LeftRoot->right,RightRoot->left); } bool isSymmetric(struct TreeNode* root) { return _isSymmetric(root->left,root->right); }
4.题目链接: 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题解:
int TreeSize(struct TreeNode* root) { return root == NULL ? 0 : TreeSize(root->left)+ TreeSize(root->right) + 1; } void _preorder(struct TreeNode* root, int* arr,int* pi) { if(root==NULL) { return; } arr[(*pi)++]=root->val; _preorder(root->left,arr,pi); _preorder(root->right,arr,pi); } int* preorderTraversal(struct TreeNode* root, int* returnSize) { *returnSize=TreeSize(root); int* arr=(int*)malloc(*returnSize*sizeof(int)); int i=0; _preorder(root,arr,&i); return arr; }
5.题目链接: 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题解:
bool isSameTree(struct TreeNode* p, struct TreeNode* q) { if(p==NULL&&q==NULL) { return true; } if(p==NULL||q==NULL) { return false; } if(p->val!=q->val) { return false; } return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right); } bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot) { if(root==NULL) { return false; } if(isSameTree(root,subRoot)) { return true; } return isSubtree(root->left,subRoot)||isSubtree(root->right,subRoot); }
6.题目链接:226. 翻转二叉树 - 力扣(LeetCode)
题解:
struct TreeNode* invertTree(struct TreeNode* root) { if(root==NULL) { return NULL; } struct TreeNode* temp; temp = root->left; root->left = root->right; root->right = temp; invertTree(root->left); invertTree(root->right); return root; }
7.题目链接:110. 平衡二叉树 - 力扣(LeetCode)
题解:
int BTreeHeight(struct TreeNode* root) { if (root == NULL)//如果是空树和不存在的节点就返回0 { return 0; } int leftheight = BTreeHeight(root->left);//定义leftheight来存放左子树的高度 int rightheight = BTreeHeight(root->right);//定义rightheight来存放右子树的高度 return (leftheight > rightheight) ? leftheight + 1 : rightheight + 1; //return BTreeHeight(root->left) > BTreeHeight(root->right) ? BTreeHeight(root->left) + 1 : BTreeHeight(root->right) + 1; //不采用上面的写法是由于上面的写法会导致重复调用, } bool isBalanced(struct TreeNode* root) { if(root==NULL) { return true; } int Left=BTreeHeight(root->left); int Right=BTreeHeight(root->right); if(abs(Left-Right)>1) { return false; } return isBalanced(root->left)&&isBalanced(root->right); }
4.4【二叉树的应用】
4.4.1【二叉搜索树】
1、【概念及其介绍】
二叉搜索树又称二叉排序树,它有两种形式,空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
它的左右子树也分别为二叉搜索树。
例如下面就是一颗二叉搜索树:
由于二叉搜索树中,每个结点左子树上所有结点的值都小于该结点的值,右子树上所有结点的值都大于该结点的值,因此对二叉搜索树进行中序遍历后,得到的是升序序列也就不难理解了。
2、 【二叉搜索树的实现】
对于以下二叉搜索树,对应数据分布为:
int a[] = {0,1,2,3,4,5,6,7,8,9};
1.【查找模块】
根据二叉搜索树的特性,我们在二叉搜索树当中查找指定值的结点的方式如下:
a、若树为空树,则查找失败,返回nullptr。
b、若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
c、若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
d、若key值等于当前结点的值,则查找成功,返回对应结点的地址。这里有两种实现方式分别是非递归形式和递归形式,我们先来看非递归形式:
bool Find(const K& key) { BSNode* cur = _root; while (cur) { if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return true; } } return false; }
再来看递归形式:
bool _Find(BSNode* root, const K& key) { if (root == nullptr) { return false; } if (root->_key > key) { return _Find(root->_left, key); } else if (root->_key < key) { return _Find(root->_right, key); } else { return true; } } bool Find(const K& key) { return _Find(_root, key); }
2.【插入模块】
根据二叉搜索树的性质,其插入操作非常简单:
1、如果是空树,则直接将插入结点作为二叉搜索树的根结点。
2、如果不是空树,则按照二叉搜索树的性质进行结点的插入,若不是空树,插入结点的具体操作如下:a、若待插入结点的值小于根结点的值,则需要将结点插入到左子树当中。
b、若待插入结点的值大于根结点的值,则需要将结点插入到右子树当中。
c、若待插入结点的值等于根结点的值,则插入结点失败。
如此进行下去,直到找到与待插入结点的值相同的结点判定为插入失败,或者最终插入到某叶子结点的左右子树当中(即空树当中)。这里同样也有两种实现方式,先来看非递归的:
bool Insert(const K& key) { if (_root == nullptr) { _root = new BSNode(key); return true; } BSNode* cur = _root; BSNode* parent = nullptr; while (cur) { parent = cur; if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return false; } } cur = new BSNode(key); if (parent->_key < key) { parent->_right = cur; } else { parent->_left = cur; } return true; }
再来看递归的:
bool _Insert(BSNode*& root, const K& key) { if (root == nullptr) { root = new BSNode(key); return true; } if (root->_key > key) { return _Insert(root->_left, key); } else if (root->_key < key) { return _Insert(root->_right, key); } else { return false; } } bool Insert(const K& key) { return _Insert(_root, key); }
注意当我们插入之后,可以通过前序遍历来进行打印,从而查明二叉树中的值。
3.【删除模块】
二叉搜索树的删除过程相对来说比较复杂,我们可以想象以下在我们删除某一结点后该二叉搜索树还满足二叉搜索树的性质吗?
首先查找要删除的元素是否在二叉搜索树中,如果不存在,则返回。而如果存在,那么要删除的结点可能存在下面四种情况:
a. 待删除的结点无孩子直接将该节点进行删除——直接删除
由于无孩子节点可以归类到,只有左孩子节点或者只有右孩子节点里,所以在实现时只需考虑3种情况。重点看下面的情况。b. 待删除结点只有左孩子
删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点——直接删除
注:当要删除的节点为根节点时,且根节点只包含左孩子时为了保证搜索二叉树的结构需要将根节点的左孩子变为新的根节点。
动图演示:
c. 待删除结点只有右孩子
删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点——直接删除
动图演示:
d. 待删除结点有左、右孩子
若待删除结点的左右子树均不为空,那么当我们在二叉搜索树当中找到该结点后,可以使用替换法进行删除——使用替换法删除。
由于待删除结点具有左右孩子,那么如果我们直接将其删除或者直接将其孩子连接到待删除节点的父亲下,都会对搜索二叉树的结构产生影响,那么应该怎么做呢?
可以将待删除结点左子树当中值最大的结点(左子树中的最右节点),或是待删除结点右子树当中值最小的结点(右子树中的最左节点)代替待删除结点被删除。(请注意无论是左子树当中的最大节点还是右子树当中的最小节点,它们都是最接近待删除节点的,下面都以后者为(使用右子树中的左节点为例))
然后将待删除结点的值改为代替其被删除的结点的值即可。而代替待删除结点被删除的结点,必然左右子树当中至少有一个为空树(因为他们是最右或最左的节点,一般为叶子节点),因此删除该结点的方法与前面说到的情况一和情况二的方法相同。
同样也是两种实现方式,非递归和递归:
非递归:
bool Erase(const K& key) { BSNode* parent = nullptr;//定义父亲用来记录待删结点的父亲 BSNode* cur = _root; while (cur)//遍历树,直到找到待删节点 { if (cur->_key > key) { parent = cur; cur = cur->_left;//查找逻辑 } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else//这里就是找到了 { //准备删除 if (cur->_left==nullptr) {//只有右孩子 if (cur == _root)//如果待删节点为根节点 { _root = cur->_right;//由于只有右孩子,右孩子作为新的根节点 } else//待删节点不是根节点 {//查明待删节点是其父节点的做孩子还是右孩子 if (cur == parent->_left)//是左孩子就直接连接 { parent->_left = cur->_right; } else//右孩子同样 { parent->_right = cur->_right; } } delete cur;//查明以后删除待删结点 } else if (cur->_right == nullptr) {//只有左孩子,处理步骤同上面的右孩子 if (cur == _root) { _root = cur->_left; } else { if (cur == parent->_left) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } } delete cur; } else {//左右孩子均不为空 BSNode* MinRight_Parent = cur;//定义变量用来找到右子树最左节点的父亲 BSNode* MinRight = cur->_right;//定义变量用来找到右子树最左节点 while (MinRight->_left) { MinRight_Parent = MinRight; MinRight = MinRight->_left; } swap(cur->_key, MinRight->_key);//找到以后与待删结点进行交换 if (MinRight == MinRight_Parent->_left)//由于为最左节点,仅有右孩子 {//判断待删结点是其父节点的哪个孩子。 MinRight_Parent->_left = MinRight->_right; } else { MinRight_Parent->_right = MinRight->_right; } delete MinRight; } return true;//遍历完全就返回true } } return false;//遍历完就返回false }
递归:
bool _Erase(BSNode*& root, const K& key) { if (root == nullptr) { return false; } if (root->_key > key) { return _Erase(root->_left, key); } else if (root->_key < key) { return _Erase(root->_right, key); } else { if (root->_left == nullptr) { BSNode* del = root; root = root->_right; delete del; return true; } else if (root->_right == nullptr) { BSNode* del = root; root = root->_left; delete del; return true; } else { BSNode* MinRight = root->_right; while (MinRight->_left) { MinRight = MinRight->_left; } swap(root->_key, MinRight->_key); _Erase(root->_right, key); } } } bool Erase(const K& key) { return _Erase(_root, key); }
注意:
只能是待删除结点左子树当中值最大的结点,或是待删除结点右子树当中值最小的结点代替待删除结点被删除,因为只有这样才能使得进行删除操作后的二叉树仍保持二叉搜索树的特性。
4.【构造函数】
构造函数非常简单,构造一个空树即可。
BSTree() :_root(nullptr)//_root为根节点 {}
5.【拷贝构造及赋值重载】
拷贝构造函数也并不难,拷贝一棵和所给二叉搜索树相同的树即可。
BSNode* _Copy( BSNode* root) { if (root == nullptr) { return nullptr; } BSNode* newroot = new BSNode(root->_key); newroot->_left = _Copy(root->_left); newroot->_right = _Copy(root->_right); return newroot; } BSTree(const BSTree<K>& cp)//完成深拷贝,创建需要构造的对象,并对其进行copy { _root = _Copy(cp._root); }
这里的赋值重载为现代写法,在讲解现代写法之前先来看一下传统写法:
这里传引用的目的是为了防止无限拷贝。
//传统写法 const BSTree<K>& operator=(const BSTree<K>& t) { if (this != &t) //防止自己给自己赋值 { _Destory(_root); //先将当前的二叉搜索树中的结点释放 _root = _Copy(t._root); //拷贝t对象的二叉搜索树 } return *this; //支持连续赋值 }
我们再来看一下,现代写法:
赋值运算符重载函数的现代写法非常精辟,函数在接收右值时并没有使用引用进行接收,因为这样可以间接调用BSTree的拷贝构造函数完成拷贝构造。我们只需将这个拷贝构造出来的对象的二叉搜索树与this对象的二叉搜索树进行交换,就相当于完成了赋值操作,而拷贝构造出来的对象t会在该赋值运算符重载函数调用结束时自动析构。
BSTree<K>& operator=(BSTree<K> t) { swap(_root, t._root); return *this; }
6.【析构函数】
析构函数完成对象中二叉搜索树结点的释放,注意释放时采用后序释放,当二叉搜索树中的结点被释放完后,将对象当中指向二叉搜索树的指针及时置空即可。
采用后续的原因是需要确保左右子树都被释放完再释放根节点。
void _Destroy(BSNode*& root) { if (root == nullptr) return; _Destroy(root->_left); _Destroy(root->_right); delete root; root = nullptr; } ~BSTree() { _Destroy(_root); }
7.【完整代码】
这里附上完整代码:
非递归实现:
template<class K> struct Binary_Search_Node { K _key; Binary_Search_Node<K>* _left; Binary_Search_Node<K>* _right; Binary_Search_Node(const K& key=K()) :_key(key), _left(nullptr), _right(nullptr) {} }; template<class K> class BSTree { public: typedef struct Binary_Search_Node<K> BSNode; BSTree() :_root(nullptr) {} //cbt(bt),cp BSTree<K>& operator=(BSTree<K> t) { swap(_root, t._root); return *this; } ~BSTree() { _Destroy(_root); } BSTree(const BSTree<K>& cp)//完成深拷贝,创建需要构造的对象,并对其进行copy { _root = _Copy(cp._root); } bool Find(const K& key) { BSNode* cur = _root; while (cur) { if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return true; } } return false; } bool Insert(const K& key) { if (_root == nullptr) { _root = new BSNode(key); return true; } BSNode* cur = _root; BSNode* parent = nullptr; while (cur) { parent = cur; if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return false; } } cur = new BSNode(key); if (parent->_key < key) { parent->_right = cur; } else { parent->_left = cur; } return true; } bool Erase(const K& key) { BSNode* parent = nullptr; BSNode* cur = _root; while (cur) { if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else { //准备删除 if (cur->_left==nullptr) {//只有右孩子 if (cur == _root) { _root = cur->_right; } else { if (cur == parent->_left) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } } delete cur; } else if (cur->_right == nullptr) {//只有左孩子 if (cur == _root) { _root = cur->_left; } else { if (cur == parent->_left) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } } delete cur; } else {//左右孩子均不为空 BSNode* MinRight_Parent = cur; BSNode* MinRight = cur->_right; while (MinRight->_left) { MinRight_Parent = MinRight; MinRight = MinRight->_left; } swap(cur->_key, MinRight->_key); if (MinRight == MinRight_Parent->_left) { MinRight_Parent->_left = MinRight->_right; } else { MinRight_Parent->_right = MinRight->_right; } delete MinRight; } return true; } } return false; } void InOrder() { _InOrder(_root); cout << endl; } private: void _InOrder(const BSNode* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_key << " "; _InOrder(root->_right); } BSNode* _Copy( BSNode* root) { if (root == nullptr) { return nullptr; } BSNode* newroot = new BSNode(root->_key); newroot->_left = _Copy(root->_left); newroot->_right = _Copy(root->_right); return newroot; } void _Destroy(BSNode*& root) { if (root == nullptr) return; _Destroy(root->_left); _Destroy(root->_right); delete root; root = nullptr; } BSNode* _root; };
递归实现:
using namespace std; template<class K> struct Binary_Search_Node { K _key; Binary_Search_Node<K>* _left; Binary_Search_Node<K>* _right; Binary_Search_Node(const K& key = K()) :_key(key), _left(nullptr), _right(nullptr) {} }; template<class K> class R_BSTree { public: typedef struct Binary_Search_Node<K> BSNode; bool Find(const K& key) { return _Find(_root, key); } bool Insert(const K& key) { return _Insert(_root, key); } void InOrder() { _InOrder(_root); cout << endl; } bool Erase(const K& key) { return _Erase(_root, key); } R_BSTree() = default; //cbt(bt),cp R_BSTree(const R_BSTree<K>& cp)//完成深拷贝,创建需要构造的对象,并对其进行copy { _root = _Copy(cp._root); } R_BSTree<K>& operator=(R_BSTree<K> t) { swap(_root, t._root); return *this; } ~R_BSTree() { _Destroy(_root); } private: BSNode* _Copy(BSNode* root) { if (root == nullptr) { return nullptr; } BSNode* newroot = new BSNode(root->_key); newroot->_left = _Copy(root->_left); newroot->_right = _Copy(root->_right); return newroot; } void _Destroy(BSNode*& root) { if (root == nullptr) return; _Destroy(root->_left); _Destroy(root->_right); delete root; root = nullptr; } void _InOrder(const BSNode* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_key << " "; _InOrder(root->_right); } bool _Find(BSNode* root, const K& key) { if (root == nullptr) { return false; } if (root->_key > key) { return _Find(root->_left, key); } else if (root->_key < key) { return _Find(root->_right, key); } else { return true; } } bool _Insert(BSNode*& root, const K& key) { if (root == nullptr) { root = new BSNode(key); return true; } if (root->_key > key) { return _Insert(root->_left, key); } else if (root->_key < key) { return _Insert(root->_right, key); } else { return false; } } bool _Erase(BSNode*& root, const K& key) { if (root == nullptr) { return false; } if (root->_key > key) { return _Erase(root->_left, key); } else if (root->_key < key) { return _Erase(root->_right, key); } else { if (root->_left == nullptr) { BSNode* del = root; root = root->_right; delete del; return true; } else if (root->_right == nullptr) { BSNode* del = root; root = root->_left; delete del; return true; } else { BSNode* MinRight = root->_right; while (MinRight->_left) { MinRight = MinRight->_left; } swap(root->_key, MinRight->_key); _Erase(root->_right, key); } } } BSNode* _root=nullptr; };
3、【二叉搜索树的应用】
1、【Key问题模型】
Key问题模型又叫K模型:
K模型,即只有key作为关键码,结构中只需存储key即可,关键码即为需要搜索到的值。比如:给定一个单词,判断该单词是否拼写正确。具体方式如下:
以单词集合中的每个单词作为key,构建一棵二叉搜索树。
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。直接用搜索树即可。
2、【KV问题模型】
KV模型,对于每一个关键码key,都有与之对应的值value,即<key, value>的键值对。
举个例子
英汉词典就是英文与中文的对应关系,即<word, Chinese>就构成一种键值对。具体方式如下:
1、以<单词, 中文含义>为键值对,构建一棵二叉搜索树。注意:二叉搜索树需要进行比较,键值对比较时只比较key。
2、查询英文单词时,只需给出英文单词就可以快速找到与其对应的中文含义。【模型代码】
#include <string> namespace KV { template<class K,class V> struct Binary_Search_Node { K _key; V _val; Binary_Search_Node<K,V>* _left; Binary_Search_Node<K,V>* _right; Binary_Search_Node(const K& key = K(),const V& val=V()) :_key(key), _val(val), _left(nullptr), _right(nullptr) {} }; template<class K,class V> class BSTree { typedef struct Binary_Search_Node<K, V> BSNode; public: BSTree() :_root(nullptr) {} //cbt(bt),cp BSTree<K,V>& operator=(BSTree<K,V> t) { swap(_root, t._root); return *this; } ~BSTree() { _Destroy(_root); } BSTree(const BSTree<K,V>& cp)//完成深拷贝,创建需要构造的对象,并对其进行copy { _root = _Copy(cp._root); } BSNode* Find(const K& key) { BSNode* cur = _root; while (cur) { if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return cur; } } return nullptr; } bool Insert(const K& key,const V& val) { if (_root == nullptr) { _root = new BSNode(key,val); return true; } BSNode* cur = _root; BSNode* parent = nullptr; while (cur) { parent = cur; if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return false; } } cur = new BSNode(key,val); if (parent->_key < key) { parent->_right = cur; } else { parent->_left = cur; } return true; } bool Erase(const K& key) { BSNode* parent = nullptr; BSNode* cur = _root; while (cur) { if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else { //准备删除 if (cur->_left == nullptr) {//只有右孩子 if (cur == _root) { _root = cur->_right; } else { if (cur == parent->_left) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } } delete cur; } else if (cur->_right == nullptr) {//只有左孩子 if (cur == _root) { _root = cur->_left; } else { if (cur == parent->_left) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } } delete cur; } else {//左右孩子均不为空 BSNode* MinRight_Parent = cur; BSNode* MinRight = cur->_right; while (MinRight->_left) { MinRight_Parent = MinRight; MinRight = MinRight->_left; } swap(cur->_key, MinRight->_key); if (MinRight == MinRight_Parent->_left) { MinRight_Parent->_left = MinRight->_right; } else { MinRight_Parent->_right = MinRight->_right; } delete MinRight; } return true; } } return false; } void InOrder() { _InOrder(_root); cout << endl; } private: BSNode* _Copy(BSNode* root) { if (root == nullptr) { return nullptr; } BSNode* newroot = new BSNode(root->_key); newroot->_left = _Copy(root->_left); newroot->_right = _Copy(root->_right); return newroot; } void _Destroy(BSNode*& root) { if (root == nullptr) return; _Destroy(root->_left); _Destroy(root->_right); delete root; root = nullptr; } void _InOrder(const BSNode* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_key << ":" << root->_val << endl; _InOrder(root->_right); } BSNode* _root=nullptr; }; }
测试代码:
void Test_KV_model() { KV::BSTree<string, string> dict; dict.Insert("I", "我"); dict.Insert("Love","爱"); dict.Insert("You", "你"); //dict.InOrder(); string str; while (cin >> str) { KV::Binary_Search_Node<string, string>* ret = dict.Find(str); if (ret) { cout << ret->_val << endl; } else { cout << " NO " << endl; } } } int main() { Test_KV_model(); return 0; }
4、【二叉搜索树的性能分析】
对于二叉搜索树来说,无论的插入还是删除操作,都需要先进行查找,因此查找的效率代表了二叉搜索树中各个操作的性能。
对于二叉搜索树这棵特殊的二叉树,我们每进行一次查找,若未查找到目标结点,则还需查找的树的层数就减少了一层,所以我们最坏情况下需要查找的次数就是二叉搜索树的深度,深度越深的二叉搜索树,比较的次数就越多。
对于有n个结点的二叉搜索树:
最优的情况下,二叉搜索树为完全二叉树,其平均比较次数为:logN
最差的情况下,二叉搜索树退化为单支树,其平均比较次数为:N而时间复杂度描述的是最坏情况下算法的效率,因此普通二叉搜索树各个操作的时间复杂度都是O ( N )。
所以实际上,二叉搜索树在极端情况下是没办法保证效率的,因此由二叉搜索树又衍生出来了AVL树、红黑树等,它们对二叉搜索树的高度进行了优化,使得二叉搜索树非常接近完全二叉树,因此对于这些树来说,它们的效率是可以达到O ( logN ) 的。
除了二叉搜索树,AVL树,红黑树,这些结构为二叉树的结构之外,还有像B树和B+树这样结构为多叉树的树形结构。
B树和B+树是查找存储在磁盘当中的数据时经常用到的数据结构,B树系列对树的高度提出了更高的要求,此时二叉树已经不能满足要求了,为了降低树的高度,于是衍生出了多叉树,而实际上这些树都是由二叉搜索树演变出来的,它们各有各的特点,适用于不同的场景。
总结
敬请期待后续.........................................
........................................................给了我坚定的信心,双手弹奏出黎明,原来爱如此的动听
————《我是如此相信》