鸟群离开了森林,整座天空很灰心............................................................................................
目录
前言
今天学习数据结构中二叉树相关的知识,感谢观看!
一、【树的介绍】
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树系列对树的高度提出了更高的要求,此时二叉树已经不能满足要求了,为了降低树的高度,于是衍生出了多叉树,而实际上这些树都是由二叉搜索树演变出来的,它们各有各的特点,适用于不同的场景。
4.42【AVL树】
1、【概念及其介绍】
二叉搜索树在大多数情况下虽已经可以缩短查找的效率,但如果数据有序或接近有序。那么二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
为了避免这个问题,有两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(可以通过对树中的结点进行调整来满足),即可降低树的高度,从而减少平均 搜索长度。 一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1、它的左右子树都是AVL树
2、左右子树高度之差(简称平衡因子:计算规则为节点的右子树高度减去左子树高度)的绝对值不超过1(即取值只能为-1/0/1)。
如下就是一颗AVL树:
如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度也是O(logN)。
注意:
这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之差的绝对值不超过1,因为只有满二叉树才能做到每个结点左右子树高度之差均为0。
2、【AVL树的实现】
1.【节点定义】
由于AVL树是一种相对较平衡的二叉搜索树,所以这里我们可以按照二叉搜索树那样设计为Key类型,但是为了加以区分和提高利用率这里采用pair类型即是KV类型,而为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。
template<class K, class V> struct AVLNode//三叉链结构 { AVLNode<K, V>* _left; AVLNode<K, V>* _right; AVLNode<K, V>* _prev;//三叉链结构,用来记录节点的父亲。 pair<K, V> _kv;//数据为pair类型,包括键值K和值V int _bf;//这里规定某个节点的平衡因子等于该节点的右子树高度减去左子树高度,当各节点的平衡因子取值为1,-1,0时意味着符合AVL树的规则,当出现2,-2就说明AVL树不平衡,需要进行调整。 AVLNode(const pair<K, V>& kv) :_left(nullptr) , _right(nullptr) , _prev(nullptr) , _data(kv) , _bf(0) {} };
注意: 给每个结点增加平衡因子并不是必须的,只是实现AVL树的一种方式,不引入平衡因子也可以实现AVL树,只不过会麻烦一点。
2.【查找模块】
逻辑与二叉搜索树相同,都是根据节点的数据进行大小判断从而进行查找:
- 若树为空树,则查找失败,返回nullptr。
- 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
- 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
- 若key值等于当前结点的值,则查找成功,返回对应结点。
//查找函数 Node* Find(const K& key)//这里按照键值查找,键值比较大小才更符合逻辑,当然可以写成pair类型参数,从而通过val进行查找 { Node* cur = _root; while (cur) { if (key < cur->_data.first) //key值小于该结点的值 { cur = cur->_left; //在该结点的左子树当中查找 } else if (key > cur->_data.first) //key值大于该结点的值 { cur = cur->_right; //在该结点的右子树当中查找 } else //找到了目标结点 { return cur; //返回该结点 } } return nullptr; //查找失败 }
3.【插入模块】
1、插入的步骤
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么 AVL树的插入过程可以分为三步:
- 按照二叉搜索树的插入方法,找到待插入位置。
- 找到待插入位置后,将待插入结点插入到树中。
- 更新平衡因子,如果出现不平衡,则需要进行旋转。
因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:
- 待插入结点的key值比当前结点小就插入到该结点的左子树。
- 待插入结点的key值比当前结点大就插入到该结点的右子树。
- 待插入结点的key值与当前结点的key值相等就插入失败。
如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。
2、平衡因子的更新
但是AVL树插入过程与二叉搜索树不同的是AVL树每次插入新节点都需要更新一下平衡因子,若出现平衡因子大于2的情况就需要进行旋转调整从而确保AVL树的结构正确。
首先这里要明确为什么每插入一个节点就要更新平衡因子,这是由于平衡因子的计算规则是:
右子树高度-左子树高度即平衡因子=右子树高度-左子树高度(bf=右height-左height)。
那么当我们插入的新节点时平衡因子是如何进行更新呢?
新节点如果是父节点的左孩子时那么父节点的平衡因子就需要减一,如果为右孩子时就需要加一。
平衡因子更新规则:
1、新增结点在parent的右边,parent的平衡因子+ +。
2、新增结点在parent的左边,parent的平衡因子− − 。我们需要注意的是每更新完一个结点的平衡因子后,都需要进行以下判断:
1、如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
2、如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
3、如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。那么我们为什么要进行上面3种情况的判断呢?
我们可以发现,在最坏情况下,我们更新平衡因子时会一路更新到根结点。例如下面这种情况:
由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。
3、旋转
那么我们已将将平衡因子更新完了,那么如果出现平衡因子为2或-2就会导致AVL树不平衡了那么对于这种情况我们应该怎么办呢?
首先我们要清楚,我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,才会继续向上更新,直到根节点停止。所以当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
理由如下:
若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。
而当cur是新增结点时,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个叶子节点下。因此在新增结点插入前,其父结点的状态有以下3种可能:
1、其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
2、其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。无论是上面哪种情况都不会出现parent的平衡因子为2或-2的情况。因此综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
所以当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0,而当parent的父节点为2或-2时,就需要进行旋转,而数学家G.M.Adelson-Velskii 和E.M.Landis通过大量的数据发现了下面4种旋转的办法,来解决:
- 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
- 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
- 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
- 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。
下面让我们逐个分析:
1、右单旋
当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。-2说明parent的左子树高度高,需要向右旋转来降低左边的高度,进而达到平衡。
由于这里的情况很多,这里我们看一个通用的例子:
旋转动态图:
动图演示说明:
由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。右单旋的步骤如下:
- 让subL的右子树作为parent的左子树。
- 让parent作为subL的右子树。
- 让subL作为整个子树的根。
- 更新平衡因子。
右单旋后满足二叉搜索树的性质:
- subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
- parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。
平衡因子更新如下:
代码实现:
void RotateR(Node* parent) { Node* SubL = parent->_left; Node* SubLR = SubL->_right; parent->_left = SubLR; if (SubLR) { SubLR->_prev = parent; } Node* parent_prev = parent->_prev; SubL->_right = parent; parent->_prev = SubL; if (parent == _root) { _root = SubL; SubL->_prev = nullptr; } else { if (parent == parent_prev->_left) { parent_prev->_left = SubL; } else { parent_prev->_right = SubL; } SubL->_prev = parent_prev; } parent->_bf = SubL->_bf = 0; }
2、左单旋
当parent的平衡因子为2,cur的平衡因子为1时,说明parent的右子树较高,需要进行左单旋来调节平衡,下面来看几个例子:
同样这里我们看一个通用的例子:
动态图:
左单旋的步骤如下:
- 让subR的左子树作为parent的右子树。
- 让parent作为subR的左子树。
- 让subR作为整个子树的根。
- 更新平衡因子。
左单旋后满足二叉搜索树的性质:
- subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
- parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。
代码实现:
//左旋转 void RotateL(Node* parent) { Node* SubR = parent->_right; Node* SubRL = SubR->_left; parent->_right = SubRL; if (SubRL) { SubRL->_prev = parent; } Node* parent_prev = parent->_prev; SubR->_left = parent; parent->_prev = SubR; if (parent == _root) { _root = SubR; SubR->_prev = nullptr; } else { if (parent_prev->_left == parent) { parent_prev->_left = SubR; } else { parent_prev->_right = SubR; } SubR->_prev = parent_prev; } parent->_bf = SubR->_bf = 0; }
3、右左双旋
先看一个例子:
对于上面的例子使用右旋可以解决问题那如果新插入节点在8的右侧呢?
下面看一个例子:
动态图:
动图演示说明:
- 由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
- 动图中,在b子树当中新增结点,或是在c子树当中新增结点,均会引发右左双旋,动图中以在c子树当中新增结点为例。
右左双旋的步骤如下:
- 以subR为旋转点进行右单旋。
- 以parent为旋转点进行左单旋。
- 更新平衡因子。
右左双旋后满足二叉搜索树的性质:
右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。
经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
subLR的平衡因子取1:
subLR的平衡因子取-1:
subLR的平衡因子取0:
可以看到,经过右左双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右左双旋后无需继续往上更新平衡因子。
代码实现:
void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf; RotateR(parent->_right); RotateL(parent); if (bf == 0) { // subRL自己就是新增 parent->_bf = subR->_bf = subRL->_bf = 0; } else if (bf == -1) { // subRL的左子树新增 parent->_bf = 0; subRL->_bf = 0; subR->_bf = 1; } else if (bf == 1) { // subRL的右子树新增 parent->_bf = -1; subRL->_bf = 0; subR->_bf = 0; } else { assert(false); } }
4、左右双旋
同有左双旋,左单旋无法解决问题,原因是发生了拐弯,需要用左旋讲折线变为直线,再进行右旋。
来看通用的例子:
动态图:
动图演示说明:
- 由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
- 动图中,在b子树当中新增结点,或是在c子树当中新增结点,均会引发左右双旋,动图中以在b子树当中新增结点为例。
左右双旋的步骤如下:
- 以subL为旋转点进行左单旋。
- 以parent为旋转点进行右单旋。
- 更新平衡因子。
左右双旋后满足二叉搜索树的性质:
左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。
subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。
经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。
可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左右双旋后无需继续往上更新平衡因子。
代码实现:
//左右双旋 void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf; RotateL(parent->_left); RotateR(parent); if (bf == 0) { // subRL自己就是新增 parent->_bf = subL->_bf = subLR->_bf = 0; } else if (bf == -1) { // subRL的左子树新增 parent->_bf = 1; subLR->_bf = 0; subL->_bf = 0; } else if (bf == 1) { // subRL的右子树新增 parent->_bf = 0; subLR->_bf = 0; subL->_bf = -1; } else { assert(false); } }
4、插入的整体代码
//插入函数 bool Insert(const pair<K, V> kv) { if (_root == nullptr) { _root = new Node(kv); return true; } else { Node* cur = _root; Node* parent = nullptr; while (cur) { if (kv.first < cur->_kv.first) //key值小于该结点的值 { parent = cur; cur = cur->_left; //在该结点的左子树当中查找 } else if (kv.first > cur->_kv.first) //key值大于该结点的值 { parent = cur; cur = cur->_right; //在该结点的右子树当中查找 } else //找到了目标结点 { return false; } } //树中不存在与kv相等的节点,进行插入即可 cur = new Node(kv); if (cur->_kv.first < parent->_kv.first) { parent->_left = cur; cur->_prev = parent; } else { parent->_right = cur; cur->_prev = parent; } //插入完成不能直接返回true需要判断平衡因子 while (parent) { if (cur == parent->_left) { parent->_bf--; } else { parent->_bf++; } if (parent->_bf == 0) { break; } else if (parent->_bf == 1 || parent->_bf == -1) { cur = parent; parent = parent->_prev; } else if(parent->_bf == 2 || parent->_bf == -2) { //说明此时AVL树不符合规则需要进行调整 if (parent->_bf == 2 && cur->_bf == 1) { //此时右边高,采取左旋 RotateL(parent); } else if (parent->_bf == 2 && cur->_bf == -1) { RotateRL(parent); } else if (parent->_bf == -2 && cur->_bf == 1) { RotateLR(parent); } else if (parent->_bf == -2 && cur->_bf == -1) { RotateR(parent); } else { assert(false); } //符合上面任意一种情况时,经过对应的旋转以后,不平衡的子树也变平衡了,这是由 //于旋转不仅更新了平衡因子,也调节了问题子树的高度,使其高度正常。 break; } else { assert(false); } } //更新完平衡因子才能返回true return true; } }
4.【删除模块】
说完了插入就该说删除了,那么AVL树是如何进行删除的呢?
首先AVL树的删除可以看成插入的反过程,这是因为删除的过程是删除节点会造成高度改变,从而会导致AVL树不平衡,所以我们应该像AVL树的插入一样搞清楚AVL树的删除过程中有哪些情况会导致不平衡,以及我们应该采取怎样的措施。
1、删除的步骤
要进行结点的删除,首先需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同我们可以使用查找函数来实现。
1、通过查找函数找到待删除的节点。
2、判断待删除节点的情况,主要为以下四种情况:
Fi.左右子树均为空——复用情况2或3
Se.左子树为空,右子树不为空——待删结点父节点指向其右节点
Th.右子树为空,左子树不为空——待删结点父节点指向其左节点
Fo.左右子树均不为空——替换法删除
2、平衡因子的更新
我们可以发现AVL的删除过程与二叉搜索树的删除过程相同,但是不同的是我们应该在删除之前进行好每个平衡因子的更新,这样才能保证AVL树的平衡。
那么平衡因子该如何更新呢?
与插入不同当删除为节点为父节点的左孩子时,该父节点的平衡因子应该++,为右孩子时应该--,即平衡因子的更新规则:删除的结点在parent的右边,parent的平衡因子−−。
删除的结点在parent的左边,parent的平衡因子++。并且每更新完一个结点的平衡因子后,都需要进行以下判断:
如果parent的平衡因子等于-1或者1,表明无需继续往上更新平衡因子了。
如果parent的平衡因子等于0,表明还需要继续往上更新平衡因子。
如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。那么我们这样判断的理由是什么呢?
而在最坏情况下,删除结点后更新平衡因子时也会一路更新到根结点。
例如下面这种情况:
注意:
由于节点为三叉链结构,因此在进行调整结点的过程中需要建立两个结点之间的双向关系。
现在让我们具体看以下每个具体情况:
当parent的平衡因子为2,说明parent的右子树高度较高,这时候我们应该关注parent的右(parent_right),如果parent的右孩子的右子树也高即parent_right的平衡因子为1,则需要左旋,parent_right的平衡因子若为0,则说明parent的右子树高度较高,而parent_right的左右子树高度相同,此时仍然进行左单旋,但是唯一不同的是,旋转前后树的高度未发生改变,当parent_right的平衡因子为-1时,说明此时为parent的右子树高,而parent_right的左子树高则需要进行右左双旋。
1、当parent的平衡因子为2,parent_right为1时——左单旋
2、当parent的平衡因子为2,parent_right为0时——左单旋
3、当parent的平衡因子为2,parent_right为-1时——右左双旋
当parent的平衡因子为-2,说明parent的左子树高度较高,这时候我们应该关注parent的左(parent_left),如果parent的左孩子的左子树也高即parent_left的平衡因子为-1,则需要右旋,parent_left的平衡因子若为0,则说明parent的左子树高度较高,而parent_left的左右子树高度相同,此时仍然进行右单旋,但是唯一不同的是,旋转前后树的高度未发生改变,当parent_right的平衡因子为1时,说明此时为parent的左子树高,而parent_leftt的右子树高则需要进行左右双旋。
1、当parent的平衡因子为-2,parent_right为-1时——右单旋
2、当parent的平衡因子为-2,parent_right为0时——右单旋
3、当parent的平衡因子为-2,parent_right为0时——左右双旋
总结
在进行旋转处理时,我们将其分为以下六种情况:
1、当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋。
2、当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋。
3、当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋,但此时平衡因子调整与之前有所不同,具体看代码。
4、当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋。
5、当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋。
6、当parent的平衡因子为2,parent的右孩子的平衡因子为0时,也进行左单旋,但此时平衡因子调整与之前有所不同,具体看代码。与插入结点不同的是,删除结点时若是进行了旋转处理,那么在进行旋转处理后我们必须继续往上更新平衡因子,因为旋转的本质就是降低树的高度,旋转后树的高度降低了,就会影响其父结点的平衡因子,因此我们还需要继续往上更新平衡因子。
3、删除的整体代码
//查找函数 Node* Find(const K& key)//这里按照键值查找,键值比较大小才更符合逻辑,当然可以写成pair类型参数,从而通过val进行查找 { Node* cur = _root; while (cur) { if (key < cur->_kv.first) //key值小于该结点的值 { cur = cur->_left; //在该结点的左子树当中查找 } else if (key > cur->_kv.first) //key值大于该结点的值 { cur = cur->_right; //在该结点的右子树当中查找 } else //找到了目标结点 { return cur; //返回该结点 } } return nullptr; //查找失败 } //更新平衡因子 void AdjustBalance(Node* parent, Node* cur) { while (parent) { if (parent->_left == cur) { parent->_bf++; } else { parent->_bf--; } if (parent->_bf == 1 || parent->_bf == -1) { break; } else if (parent->_bf == 0) { cur = parent; parent = parent->_prev; } else if (parent->_bf == 2 || parent->_bf == -2) { if (parent->_bf == 2 && parent->_right->_bf == 1) { RotateL(parent); } else if (parent->_bf == 2 && parent->_right->_bf == 0) { Node* parent_right = parent->_right; RotateL(parent); parent->_bf = 1; parent_right->_bf = -1; //高度不变 break; } else if (parent->_bf == 2 && parent->_right->_bf == -1) { RotateRL(parent); } else if (parent->_bf == -2 && parent->_left->_bf == 1) { RotateLR(parent); } else if (parent->_bf == -2 && parent->_left->_bf == 0) { Node* parent_left = parent->_left; RotateR(parent); parent->_bf = -1; parent_left->_bf = 1; //此时高度不变 break; } else if (parent->_bf == -2 && parent->_left->_bf == -1) { RotateR(parent); } else { assert(false); } cur = parent->_prev; parent = cur->_prev; } else { assert(false); } } } //删除函数 bool Erase(const K& key) { Node* cur = Find(key); if (cur == nullptr) { return false; } else { Node* delcur = cur;//记录删除的位置 Node* delparent = delcur->_prev;//记录删除位置的父节点 if (cur->_left == nullptr) { if (cur == _root) { _root = cur->_right; if (_root) { _root->_prev = nullptr; } delete delcur; } else { Node* parent = cur->_prev; Node* cur_right = cur->_right; //更新平衡因子 AdjustBalance(parent, cur); //首先改变cur_right的父节点的指向 if (cur_right) { cur_right->_prev = delparent; } //其次改变parent其孩子的指向 if (delparent->_left == delcur) { delparent->_left = cur_right; } else { delparent->_right = cur_right; } //最后删除cur delete delcur; } } else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_left; if (_root) { _root->_prev = nullptr; } delete delcur; } else { Node* parent = cur->_prev; Node* cur_left = cur->_left; //更新平衡因子 AdjustBalance(parent, cur); //首先改变cur_leftt的父节点的指向 if (cur_left) { cur_left->_prev = delparent; } //其次改变parent其孩子的指向 if (delparent->_left == delcur) { delparent->_left = cur_left; } else { delparent->_right = cur_left; } //最后删除cur delete delcur; } } else { Node* MinRightParent = cur; Node* MinRight = cur->_right; while (MinRight->_left) { MinRightParent = MinRight; MinRight = MinRight->_left; } Node* MinRight_right = MinRight->_right; cur->_kv.first = MinRight->_kv.first; cur->_kv.second = MinRight->_kv.second; delcur = MinRight; delparent = MinRightParent; AdjustBalance(MinRightParent, MinRight); if (delcur == delparent->_left) { delparent->_left = MinRight_right; if (MinRight_right) { MinRight_right->_prev = delparent; } } else { delparent->_right = MinRight_right; if (MinRight_right) { MinRight_right->_prev = delparent; } } delete delcur; } return true; } }
5.【修改模块】
修改模块这里没什么好讲的,如果能找到就能进行修改,实现修改AVL树当中指定key值结点的value,我们可以实现一个Modify函数,该函数当中的逻辑如下:
- 调用查找函数获取指定key值的结点。
- 对该结点的value进行修改。
代码如下:
//修改函数 bool Modify(const K& key, const V& value) { Node* ret = Find(key); if (ret == nullptr) //未找到指定key值的结点 { return false; } ret->_kv.second = value; //修改结点的value return true; }
6.【验证是否平衡】
AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。
但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。
我们可以采用后序遍历,计算每棵树的高度,进而判断左右子树高度差是否不超过一,具体步骤如下:
1、从叶子结点开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值+ 1)
2、先判断左子树是否是平衡二叉树。
3、再判断右子树是否是平衡二叉树。
若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)
//判断是否为AVL树 bool IsAVLTree() { int hight = 0; //输出型参数 return _IsBalanced(_root, hight); } //检测二叉树是否平衡 bool _IsBalanced(Node* root, int& hight) { if (root == nullptr) //空树是平衡二叉树 { hight = 0; //空树的高度为0 return true; } //先判断左子树 int leftHight = 0; if (_IsBalanced(root->_left, leftHight) == false) return false; //再判断右子树 int rightHight = 0; if (_IsBalanced(root->_right, rightHight) == false) return false; //检查该结点的平衡因子 if (rightHight - leftHight != root->_bf) { cout << "平衡因子设置异常:" << root->_kv.first << endl; } //把左右子树的高度中的较大值+1作为当前树的高度返回给上一层 hight = max(leftHight, rightHight) + 1; return abs(rightHight - leftHight) < 2; //平衡二叉树的条件 }
3、【AVL树的性能】
AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。4、【AVL树的完整代码】
#include <iostream> using namespace std; #include <assert.h> template<class K, class V> struct AVLNode//三叉链结构 { AVLNode<K, V>* _left; AVLNode<K, V>* _right; AVLNode<K, V>* _prev;//三叉链结构,用来记录节点的父亲。 pair<K, V> _kv;//数据为pair类型,包括键值K和值V int _bf;//这里规定某个节点的平衡因子等于该节点的右子树高度减去左子树高度,当各节点的平衡因子取值为1,-1,0时意味着符合AVL树的规则,当出现2,-2就说明AVL树不平衡,需要进行调整。 AVLNode(const pair<K, V>& kv) :_left(nullptr) , _right(nullptr) , _prev(nullptr) , _kv(kv) , _bf(0) {} }; template<class K, class V> class AVLTree { public: typedef AVLNode<K, V> Node; AVLTree() = default; //遍历 void InOrder() { _InOrder(_root); cout << endl; } //求高度 size_t Height() { return _Height(_root); } //求节点个数 size_t size() { return _size(_root); } //查找函数 Node* Find(const K& key)//这里按照键值查找,键值比较大小才更符合逻辑,当然可以写成pair类型参数,从而通过val进行查找 { Node* cur = _root; while (cur) { if (key < cur->_kv.first) //key值小于该结点的值 { cur = cur->_left; //在该结点的左子树当中查找 } else if (key > cur->_kv.first) //key值大于该结点的值 { cur = cur->_right; //在该结点的右子树当中查找 } else //找到了目标结点 { return cur; //返回该结点 } } return nullptr; //查找失败 } //插入函数 bool Insert(const pair<K, V> kv) { if (_root == nullptr) { _root = new Node(kv); return true; } else { Node* cur = _root; Node* parent = nullptr; while (cur) { if (kv.first < cur->_kv.first) //key值小于该结点的值 { parent = cur; cur = cur->_left; //在该结点的左子树当中查找 } else if (kv.first > cur->_kv.first) //key值大于该结点的值 { parent = cur; cur = cur->_right; //在该结点的右子树当中查找 } else //找到了目标结点 { return false; } } //树中不存在与kv相等的节点,进行插入即可 cur = new Node(kv); if (cur->_kv.first < parent->_kv.first) { parent->_left = cur; cur->_prev = parent; } else { parent->_right = cur; cur->_prev = parent; } //插入完成不能直接返回true需要判断平衡因子 while (parent) { if (cur == parent->_left) { parent->_bf--; } else { parent->_bf++; } if (parent->_bf == 0) { break; } else if (parent->_bf == 1 || parent->_bf == -1) { cur = parent; parent = parent->_prev; } else if(parent->_bf == 2 || parent->_bf == -2) { //说明此时AVL树不符合规则需要进行调整 if (parent->_bf == 2 && cur->_bf == 1) { //此时右边高,采取左旋 RotateL(parent); } else if (parent->_bf == 2 && cur->_bf == -1) { RotateRL(parent); } else if (parent->_bf == -2 && cur->_bf == 1) { RotateLR(parent); } else if (parent->_bf == -2 && cur->_bf == -1) { RotateR(parent); } else { assert(false); } //符合上面任意一种情况时,经过对应的旋转以后,不平衡的子树也变平衡了,这是由于旋转不仅更新了平衡因子,也调节了问题子树的高度,使其高度正常。 break; } else { assert(false); } } //更新完平衡因子才能返回true return true; } } //删除函数 bool Erase(const K& key) { Node* cur = Find(key); if (cur == nullptr) { return false; } else { Node* delcur = cur;//记录删除的位置 Node* delparent = delcur->_prev;//记录删除位置的父节点 if (cur->_left == nullptr) { if (cur == _root) { _root = cur->_right; if (_root) { _root->_prev = nullptr; } delete delcur; } else { Node* parent = cur->_prev; Node* cur_right = cur->_right; //更新平衡因子 AdjustBalance(parent, cur); //首先改变cur_right的父节点的指向 if (cur_right) { cur_right->_prev = delparent; } //其次改变parent其孩子的指向 if (delparent->_left == delcur) { delparent->_left = cur_right; } else { delparent->_right = cur_right; } //最后删除cur delete delcur; } } else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_left; if (_root) { _root->_prev = nullptr; } delete delcur; } else { Node* parent = cur->_prev; Node* cur_left = cur->_left; //更新平衡因子 AdjustBalance(parent, cur); //首先改变cur_leftt的父节点的指向 if (cur_left) { cur_left->_prev = delparent; } //其次改变parent其孩子的指向 if (delparent->_left == delcur) { delparent->_left = cur_left; } else { delparent->_right = cur_left; } //最后删除cur delete delcur; } } else { Node* MinRightParent = cur; Node* MinRight = cur->_right; while (MinRight->_left) { MinRightParent = MinRight; MinRight = MinRight->_left; } Node* MinRight_right = MinRight->_right; cur->_kv.first = MinRight->_kv.first; cur->_kv.second = MinRight->_kv.second; delcur = MinRight; delparent = MinRightParent; AdjustBalance(MinRightParent, MinRight); if (delcur == delparent->_left) { delparent->_left = MinRight_right; if (MinRight_right) { MinRight_right->_prev = delparent; } } else { delparent->_right = MinRight_right; if (MinRight_right) { MinRight_right->_prev = delparent; } } delete delcur; } return true; } } //修改函数 bool Modify(const K& key, const V& value) { Node* ret = Find(key); if (ret == nullptr) //未找到指定key值的结点 { return false; } ret->_kv.second = value; //修改结点的value return true; } //判断是否为AVL树 bool IsAVLTree() { int hight = 0; //输出型参数 return _IsBalanced(_root, hight); } private: void _InOrder(Node* root) { if (root == nullptr) return; _InOrder(root->_left); cout << root->_kv.first << " "; _InOrder(root->_right); } int _Height(Node* root) { if (root == nullptr) return 0; int leftHeight = _Height(root->_left); int rightHeight = _Height(root->_right); return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; } //检测二叉树是否平衡 bool _IsBalanced(Node* root, int& hight) { if (root == nullptr) //空树是平衡二叉树 { hight = 0; //空树的高度为0 return true; } //先判断左子树 int leftHight = 0; if (_IsBalanced(root->_left, leftHight) == false) return false; //再判断右子树 int rightHight = 0; if (_IsBalanced(root->_right, rightHight) == false) return false; //检查该结点的平衡因子 if (rightHight - leftHight != root->_bf) { cout << "平衡因子设置异常:" << root->_kv.first << endl; } //把左右子树的高度中的较大值+1作为当前树的高度返回给上一层 hight = max(leftHight, rightHight) + 1; return abs(rightHight - leftHight) < 2; //平衡二叉树的条件 } size_t _size(Node* root) { if (root == nullptr) { return 0; } return _size(root->_left) + _size(root->_right) + 1; } //左旋转 void RotateL(Node* parent) { Node* SubR = parent->_right; Node* SubRL = SubR->_left; parent->_right = SubRL; if (SubRL) { SubRL->_prev = parent; } Node* parent_prev = parent->_prev; SubR->_left = parent; parent->_prev = SubR; if (parent == _root) { _root = SubR; SubR->_prev = nullptr; } else { if (parent_prev->_left == parent) { parent_prev->_left = SubR; } else { parent_prev->_right = SubR; } SubR->_prev = parent_prev; } parent->_bf = SubR->_bf = 0; } //右旋转 void RotateR(Node* parent) { Node* SubL = parent->_left; Node* SubLR = SubL->_right; parent->_left = SubLR; if (SubLR) { SubLR->_prev = parent; } Node* parent_prev = parent->_prev; SubL->_right = parent; parent->_prev = SubL; if (parent == _root) { _root = SubL; SubL->_prev = nullptr; } else { if (parent == parent_prev->_left) { parent_prev->_left = SubL; } else { parent_prev->_right = SubL; } SubL->_prev = parent_prev; } parent->_bf = SubL->_bf = 0; } //右左双旋 void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf; RotateR(parent->_right); RotateL(parent); if (bf == 0) { // subRL自己就是新增 parent->_bf = subR->_bf = subRL->_bf = 0; } else if (bf == -1) { // subRL的左子树新增 parent->_bf = 0; subRL->_bf = 0; subR->_bf = 1; } else if (bf == 1) { // subRL的右子树新增 parent->_bf = -1; subRL->_bf = 0; subR->_bf = 0; } else { assert(false); } } //左右双旋 void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf; RotateL(parent->_left); RotateR(parent); if (bf == 0) { // subRL自己就是新增 parent->_bf = subL->_bf = subLR->_bf = 0; } else if (bf == -1) { // subRL的左子树新增 parent->_bf = 1; subLR->_bf = 0; subL->_bf = 0; } else if (bf == 1) { // subRL的右子树新增 parent->_bf = 0; subLR->_bf = 0; subL->_bf = -1; } else { assert(false); } } //删除过程中调节平衡因子 void AdjustBalance(Node* parent, Node* cur) { while (parent) { if (parent->_left == cur) { parent->_bf++; } else { parent->_bf--; } if (parent->_bf == 1 || parent->_bf == -1) { break; } else if (parent->_bf == 0) { cur = parent; parent = parent->_prev; } else if (parent->_bf == 2 || parent->_bf == -2) { if (parent->_bf == 2 && parent->_right->_bf == 1) { RotateL(parent); } else if (parent->_bf == 2 && parent->_right->_bf == 0) { Node* parent_right = parent->_right; RotateL(parent); parent->_bf = 1; parent_right->_bf = -1; //高度不变 break; } else if (parent->_bf == 2 && parent->_right->_bf == -1) { RotateRL(parent); } else if (parent->_bf == -2 && parent->_left->_bf == 1) { RotateLR(parent); } else if (parent->_bf == -2 && parent->_left->_bf == 0) { Node* parent_left = parent->_left; RotateR(parent); parent->_bf = -1; parent_left->_bf = 1; //此时高度不变 break; } else if (parent->_bf == -2 && parent->_left->_bf == -1) { RotateR(parent); } else { assert(false); } cur = parent->_prev; parent = cur->_prev; } else { assert(false); } } } Node* _root = nullptr; };
4.43【红黑树】
总结
敬请期待后续.........................................
........................................................给了我坚定的信心,双手弹奏出黎明,原来爱如此的动听
————《我是如此相信》