科林算法_2 树型结构

目录

​编辑

一、树

二、二叉树

2.1 满二叉树

2.2 完全二叉树(CBT)

2.2.1 性质

2.2.2 线性存储结构 

2.2.3 链式存储结构

2.2.3 遍历

2.2.3.1 深度遍历:先父子

2.2.3.1.1 前序遍历:根左右

2.2.3.1.2 中序遍历:左根右

2.2.3.1.3 后序遍历:左右根

2.2.3.2 广度遍历:先兄弟(层序遍历)

2.2.4 构造二叉树

2.2.4.1 前序序列反向构造二叉树

2.2.4.2 创建一棵完全二叉树(空间连续,使用顺序结构)

2.3 二叉搜索树/二叉查找树/排序二叉树(BST)

2.3.1 添加节点

2.3.2 删除节点

2.3.3 将一棵BST转变为有序的双向链表

三、 平衡树

3.1 二叉平衡搜索树(AVL)

3.1.1 平衡因子(BF)

二叉树上某结点平衡因子定义为:该结点的左子树的深度减去它的右子树的深度

3.1.2 旋转

3.1.2.1 LL型平衡旋转 右旋

3.1.2.2 RR型平衡旋转 左旋

3.1.2.3 LR型平衡旋转 先左旋再右旋

3.1.2.4 RL型平衡旋转 先右旋再左旋

3.2 红黑树(RBT)

3.2.1 红黑树的添加

3.2.2 红黑树的删除

3.3 多路平衡搜索树(B/B+)

3.3.1 B-Tree(B树)

3.3.1.1 B-Tree的添加

3.3.1.2 B-Tree的删除

3.3.2 B+-Tree(B+树)

3.3.2.1 B+-Tree的添加

3.3.2.2 B+-Tree的删除

3.3.2.3 B-Tree和B+-Tree的区别

四、哈夫曼树

4.1 构建哈夫曼树

4.2 哈夫曼编码

五、字典树 TrieTree


一、树

相比哈希表的好处:可以一个节点一个节点加入

数据结构:树(Tree)【详解】_数据结构 树-CSDN博客

二、二叉树

树中每个节点最多两个孩子

A的左子树:以A的左孩子为根节点的子树

2.1 满二叉树

每一层都是满节点

对于一棵高度为k的满二叉树

(1) 总结点数 2^k-1

(2) 总叶子节点数 2^(k-1)

2.2 完全二叉树(CBT)

除了最后一层都是满的,且最后一层的节点都连续集中在最左边

2.2.1 性质

对于任意二叉树,总节点个数(s)=度为0的节点(n0)+度为1的节点(n1)+度为2的节点(n2)

同时,总节点个数(s)=根节点数(1)+叶子节点数(n0*0+n1*1+n2*2)   所以,n0=n2+1

(3) 度为0的节点永远比度为2的节点多1,n0=n2+1

若一棵完全二叉树,叶子节点为124,求总结点数最多有多少个?

叶子节点n0=124,n2=123,在一颗完全二叉树中度为1的节点只有1个或者0个。

所以答案为:124+123+1=248

(4) 对于一棵节点数为n的完全二叉树,有2^(k-1)-1<n<2^k-1,即k=floor(log2n)+1

2.2.2 线性存储结构 

(5)将一棵完全二叉树,按照从上到下从左到右的顺序编号1~n,父节点(非叶子节点)为1~n/2

节点i的左孩子为2i,右孩子为2i+1

将一棵完全二叉树,按照从上到下从左到右的顺序编号0~n-1,父节点为0~n/2-1

节点i的左孩子为2i+1,右孩子为2i+2

2.2.3 链式存储结构

二叉链表

typedef struct node {
	int nVlaue;
	struct node* pLeft;
	struct node* pRight;
}BinaryTree;

2.2.3 遍历

2.2.3.1 深度遍历:先父子
2.2.3.1.1 前序遍历:根左右
void PreOrderTraversal(BinaryTree* pTree) {
	if (pTree) {
		printf("%d ", pTree->nVlaue);
		PreOrderTraversal(pTree->pLeft);
		PreOrderTraversal(pTree->pRight);
	}
}

循环遍历方式的优点:

1. 便于保存完整路径

2. 减少了函数调用的时间

void PreOrderTraversal1(BinaryTree* pTree) {
	stack<BinaryTree*> s;
	while (pTree || !s.empty()) {
		while (pTree) {
			printf("%d ", pTree->nVlaue);
			s.push(pTree);
			pTree = pTree->pLeft;
		}
		if (!s.empty()) {
			pTree = s.top();
			s.pop();
			pTree = pTree->pRight;
		}
	}
	printf("\n");
}
2.2.3.1.2 中序遍历:左根右
void InOrderTraversal(BinaryTree* pTree) {
	if (pTree) {
		InOrderTraversal(pTree->pLeft);
		printf("%d ", pTree->nVlaue);
		InOrderTraversal(pTree->pRight);
	}
}

void InOrderTraversal1(BinaryTree* pTree) {
	stack<BinaryTree*> s;
	while (pTree || !s.empty()) {
		while (pTree) {
			s.push(pTree);
			pTree = pTree->pLeft;
		}
		if (!s.empty()) {
			pTree = s.top();
			s.pop();
			printf("%d ", pTree->nVlaue);
			pTree = pTree->pRight;
		}
	}
	printf("\n");
}
2.2.3.1.3 后序遍历:左右根
void PostOrderTraversal(BinaryTree* pTree) {
	if (pTree) {
		PostOrderTraversal(pTree->pLeft);
		PostOrderTraversal(pTree->pRight);
		printf("%d ", pTree->nVlaue);
	}
}

可以看到,前序遍历和中序遍历的非递归算法很相似,原因在于都是先处理根后再寻找其右孩子。而当后序遍历时则会遇到一些问题:按照左右根顺序处理某节点时,并不知道此时栈顶节点是否输出(右子树是否完成),所以此时应当添加标记,记录上一个处理的节点。若此时栈顶节点右子树为空或为标记节点,即可以说明该节点的右子树已经处理完了。

void PostOrderTraversal1(BinaryTree* pTree) {
	BinaryTree* last = NULL;
	stack<BinaryTree*> s;
	while (pTree || !s.empty()) {
		while (pTree != NULL) {
			s.push(pTree);
			pTree = pTree->pLeft;
		}
		if (s.top()->pRight == NULL || s.top()->pRight == last) {
			printf("%d ", s.top()->nVlaue);
			last = s.top();
			s.pop();
		}
		else {
			pTree = s.top()->pRight;
		}
	}
	printf("\n");
}

能否通过任意两种遍历还原一棵二叉树?

通过中序遍历将左右子树区分开,这样就能能通过前序或者后序还原二叉树。

步骤:先根据前序或后序找到根,再通过中序找到根的位置分开左右子树。

2.2.3.2 广度遍历:先兄弟(层序遍历)
void LevelOrderTraversal(BinaryTree* pTree) {
	queue<BinaryTree*> q;
	q.push(pTree);
	while (!q.empty()) {
		pTree = q.front();
		q.pop();
		printf("%d ", pTree->nVlaue);
		if (pTree->pLeft)
			q.push(pTree->pLeft);
		if (pTree->pRight)
			q.push(pTree->pRight);
	}
}

※按层换行打印?(高度?每层最左?最右?每层平均值?Z字形,螺旋形打印?)

1.标记末尾节点

1.1 q.back

1.2 双标记

2.双队列

2.1 队列交替,队列空则换行

2.2 新队列记录节点层数。当输出元素与队首层数不同,换行

3.计数

3.1 q.size

输出计数器-1,当计数器归0换行,重新获取为q.size

3.2 双计数器

记录当前层和下一层

4.后序

后序中根和最右侧节点总在最后面

6.按前序遍历,+【】

1【2【4【】5【7【【】9【】】8【】】】3【【】6【】】】

【加一层,】减一层,根据括号关系确定节点深度,需要额外空间存储结果

7.入队特殊标记分割每一层

root ▲ left right ▲ll lr rl rr▲

出队特殊标记时,换行并把标记压入队尾

8.补空

1 23 45▲6 ▲▲78▲▲▲▲ 

子节点为空压入特殊字符,这样就可以根据2的次方换行

103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)

2.2.4 构造二叉树

2.2.4.1 前序序列反向构造二叉树
//前序序列反向构造二叉树
BinaryTree* PreCreate(int* a, int& cur) {
	if (a[cur] != 0) {
		BinaryTree* p = (BinaryTree*)malloc(sizeof(BinaryTree));
		p->nVlaue = a[cur];
		cur++;
		p->pLeft = PreCreate(a, cur);

		p->pRight = PreCreate(a, cur);
		return p;
	}
	else {
		cur++;
		return NULL;
	}
}

void PreCreate1(BinaryTree** tree) {
	int num;
	scanf_s("%d", &num);
	if (num == 0) {
		(*tree) = NULL;
		return;
	}
	*tree = (BinaryTree*)malloc(sizeof(BinaryTree));
	(*tree)->nVlaue = num;
	PreCreate1(&(*tree)->pLeft);
	PreCreate1(&(*tree)->pRight);

}
2.2.4.2 创建一棵完全二叉树(空间连续,使用顺序结构)

1.节点数组

2.结点赋值

3.左右关联

2.3 二叉搜索树/二叉查找树/排序二叉树(BST)

树中任意一个父节点的值,大于左子树,小于右子树。

查找效率与树的高度有关,理想情况下为O(logn)

核心需求:增删对树的影响越小越好

2.3.1 添加节点

1. 先将数据放入节点中

2. 看树的状态(空|非空)

3. 小于找左子树,大于找右子树;一般情况下BST要求树上的值唯一

typedef struct node{
	int nVlaue;
	node* pLeft;
	node* pRight;
}BinaryTree;

void addNode(BinaryTree** pTree, int num) {
	BinaryTree* pTmp = (BinaryTree*)malloc(sizeof(BinaryTree));
	pTmp->nVlaue = num;
	pTmp->pLeft = nullptr;
	pTmp->pRight = nullptr;
	if (*pTree == nullptr) {
		*pTree = pTmp;
		return;
	}

	BinaryTree* pNode = *pTree;
	while (pNode) {
		if (pNode->nVlaue == num) {
			free(pTmp);
			pTmp = nullptr;
			return;
		}
		else if (pNode->nVlaue < num) {
			if (pNode->pRight == nullptr) {
				pNode->pRight = pTmp;
				return;
			}	
			else pNode = pNode->pRight;
		}
		else if (pNode->nVlaue > num) {
			if (pNode->pLeft == nullptr) {
				pNode->pLeft = pTmp;
				return;
			}
			else pNode = pNode->pLeft;
		}
	}
}

BinaryTree* CreateBST(int* a,int len) {
	if (a == NULL || len <= 0)
		return NULL;

	BinaryTree* pTree = nullptr;
	for (int i = 0; i < len; i++) {
		addNode(&pTree, a[i]);
	}
	return pTree;
}

2.3.2 删除节点

要保留原BST的查找效率,不能简单的将一棵子树加到另一棵子树后面
将值替换为右子树的最小值或左子树的最大值,然后删掉替换的叶子节点

1. 查找

2. 分析孩子情况(0,1,2)

3. 值替换,删除替换节点

void DelNode(BinaryTree** pTree, int num) {
	//查找要删除的位置
	BinaryTree* pDel = *pTree;    //删除位置
	BinaryTree* pFather = NULL;    //父节点
	while (pDel) {
		if (pDel->nVlaue == num) {
			break;
		}
		else if (pDel->nVlaue < num) {
			pFather = pDel;
			pDel = pDel->pRight;
		}
		else if (pDel->nVlaue > num) {
			pFather = pDel;
			pDel = pDel->pLeft;
		}
	}

	//检测
	if (pDel == NULL) {
		printf("删除失败,%d节点不存在\n", num);
		return;
	}

	//孩子情况
	//度为2,查找替换删除节点(右子树的最小值或左子树的最大值)
	BinaryTree* pMark = NULL;	//标记原删除位置
	if (pDel->pLeft != NULL && pDel->pRight != NULL) {
		pMark = pDel;
		
		//左子树最大值
		pFather = pDel;
		pDel = pDel->pLeft;	//实际删除
		while (pDel->pRight != NULL) {
			pFather = pDel;
			pDel = pDel->pRight;
		}

		//值替换
		pMark->nVlaue = pDel->nVlaue;
	}

	//如果实际删除节点是根
	if (pFather == NULL) {
		*pTree = pDel->pLeft ? pDel->pLeft : pDel->pRight;
		free(pDel);
		pDel = NULL;
		return;
	}
	//非根,将度为2的情况和度为1、0的情况统一处理
	if (pDel == pFather->pLeft) {
		pFather->pLeft = pDel->pLeft ? pDel->pLeft : pDel->pRight;
	}
	else {
		pFather->pRight = pDel->pLeft ? pDel->pLeft : pDel->pRight;
	}
	free(pDel);
	pDel = NULL;
}

2.3.3 将一棵BST转变为有序的双向链表

不申请新空间,在原树的基础上修改。中序遍历,将树节点拆下来放到双向链表中。

pLeft -> Pre、pRight -> Next,表头、表尾

中序遍历,原表尾下一个是栈顶节点,栈顶节点变成新的表尾

void LinkedList(BinaryTree* pTree, BinaryTree** pHead, BinaryTree** pTail) {
	stack<BinaryTree*> s;
	while (pTree || !s.empty()) {
		while (pTree) {
			s.push(pTree);
			pTree = pTree->pLeft;
		}
		if (!s.empty()) {
			pTree = s.top();
			s.pop();
			if (*pHead == NULL) {
				*pHead = pTree;
				*pTail = pTree;
			}
			else {
				(*pTail)->pRight = pTree;
				pTree->pLeft = *pTail;
				*pTail = pTree;
			}
			pTree = pTree->pRight;
		}
	}
}

三、 平衡树

Data Structure Visualization (usfca.edu)        算法演示 

3.1 二叉平衡搜索树(AVL)

在BST的基础上,树中任意节点,左右子树深度差不超过1

3.1.1 平衡因子(BF)

二叉树上某结点平衡因子定义为:该结点的左子树的深度减去它的右子树的深度

3.1.2 旋转

当程序发现问题节点A后,对其进行旋转

enum colors { BLACK, RED };

typedef struct node {
	int nVlaue;
	node* pLeft;
	node* pRight;
	node* pFather;
	int nColor;
}RBT;

RBT* pRoot;
3.1.2.1 LL型平衡旋转 右旋

由根A的左子树B的左子树Bl引起的不平衡,对根A进行右旋。以根A的左子树B为中心,将A向右侧折断(向右把A压下去)

1.B的右子树成为A的左子树

2. A成为B的右子树

3. B代替A成为新根

void LL(RBT* pTree) {	//RightRotate右旋
	if (pTree == NULL || pTree->pLeft == NULL)
		return;

	RBT* A = pTree;
	RBT* B = pTree->pLeft;
	RBT* FA = A->pFather;

	//B的右子树变成A的左子树
	A->pLeft = B->pRight;
	if (B->pRight)
		B->pRight->pFather = A;

	//A成为B的右子树
	B->pRight = A;
	A->pFather = B;

	//B代替A成为子树的新根
	if (FA == NULL)	//A为根
		pRoot = B;
	else if (FA->pLeft == A)
		FA->pLeft = B;
	else FA->pRight = B;
	B->pFather = FA;

}

3.1.2.2 RR型平衡旋转 左旋

由根A的右子树B的右子树Br引起的不平衡,对根A进行左旋。以根A的右子树B为中心,将A向左侧折断(向左把A压下去)

1.B的左子树成为A的右子树

2. A成为B的左子树

3. B代替A成为新根

void RR(RBT* pTree) {	//LeftRotate左旋
	if (pTree == NULL || pTree->pLeft == NULL)
		return;

	RBT* A = pTree;
	RBT* B = pTree->pRight;
	RBT* FA = A->pFather;

	//B的左子树成为A的右子树
	A->pRight = B->pLeft;
	if (B->pLeft)
		B->pLeft->pFather = A;

	//A成为B的左子树
	B->pLeft = A;
	A->pFather = B;

	//B代替A成为子树的新根
	if (FA == NULL)	//A为根
		pRoot = B;
	else if (FA->pLeft == A)
		FA->pLeft = B;
	else FA->pRight = B;
	B->pFather = FA;
}

左旋右旋互为还原操作 

3.1.2.3 LR型平衡旋转 先左旋再右旋

 对B进行左旋,再对A进行右旋

3.1.2.4 RL型平衡旋转 先右旋再左旋

对B进行右旋,再对A进行左旋

3.2 红黑树(RBT)

基于BST的带有标记的树,通过红黑标记使得两侧数据更加均衡

满足以下5条性质:

1. 红黑树中每个节点,不是红的(R)就是黑B的(B)

2. 根节点必须是黑色B的

3. 空节点被称为终端节点,被认为是黑色的(黑哨兵)

4. 不允许两个红节点为父子关系

5. 从任意节点向下(孩子方向)出发,他所能到达的各终端节点的路径上的黑节点数目相同

结论:

1. 不会有一条路径长度超过其他路径长度的两倍

2. 红黑树的增删查的复杂度都为O(logn)

3.2.1 红黑树的添加

新加节点初始颜色为红色(对树的影响小,只影响该条路径)

步骤:

(一)查找父亲节点

(二)树的情况

一、空树,新节点Z为root,变黑色B

二、非空树

(三)父节点的颜色

(1) 父节点为黑色B,新节点Z直接放入

(2) 父节点为红色R

(父节点要变黑,但是会导致新路径变长,所以爷爷节点要变红,对于叔节点进行讨论)

(四)叔叔节点的颜色

1. 叔叔节点是红色,父节点变黑色为B 叔节点变黑色B 爷节点变红色R

爷爷节点作为新的操作节点Z,重新讨论(注意爷爷节点为根的情况)

2.叔叔是黑色(此时叔叔路径变少,要将新路径节点转移给叔路径——旋转)

(五)父节点的方向

1> 父是爷节点左

a. 新节点Z是父节点的右,父为新操作节点Z,Z为旋转点,左旋        LR

b.新节点Z是父节点的左,父变黑B,爷变红R,爷为旋转点,右旋        LL

2> 父为爷节点右

a. 新节点是父节点的左,父为新操作节点Z,Z为旋转点,右旋        RL

b. 新节点是父节点的右,父变黑B,爷变红R,爷为旋转点,左旋        RR

RBT* Search(RBT* pTree, int num) {
	if (pTree == NULL)
		return NULL;
	while (pTree) {
		if (pTree->nVlaue > num) {
			//左子树
			if (pTree->pLeft == NULL)
				return pTree;
			pTree = pTree->pLeft;
		}
		else if (pTree->nVlaue < num) {
			//右子树
			if (pTree->pRight == NULL)
				return pTree;
			pTree = pTree->pRight;
		}
		else {
			//数据错误
			printf("Data error:%d\n", num);
			exit(1);
		}
	}
}

RBT* GetUncle(RBT* pNode) {
	if (pNode == pNode->pFather->pLeft)
		return pNode->pFather->pRight;
	else return pNode->pFather->pLeft;
}

void AddNode(RBT* pTree, int num) {
	//查找
	RBT* pNode = Search(pTree, num);

	//节点申请
	RBT* pTemp = (RBT*)malloc(sizeof(RBT));
	pTemp->nVlaue = num;
	pTemp->nColor = RED;
	pTemp->pFather = pNode;
	pTemp->pLeft = NULL;
	pTemp->pRight = NULL;

	//空树
	if (pNode == NULL) {
		pRoot = pTemp;
		pRoot->nColor = BLACK;
		return;
	}

	//非空树
	//连接
	if (pNode->nVlaue > num) {
		//左侧
		pNode->pLeft = pTemp;
	}
	else {
		//右侧
		pNode->pRight = pTemp;
	}

	//父亲黑色
	if (pNode->nColor == BLACK) {
		return;
	}

	//父亲红色
	RBT* pGrandFather = NULL;
	RBT* pUncle = NULL;
	while (pNode->nColor == RED) {
		pGrandFather = pNode->pFather;
		pUncle = GetUncle(pNode);

		//叔叔红色
		if (pUncle != NULL && pUncle->nColor == RED) {
			pUncle->nColor = BLACK;
			pNode->nColor = BLACK;
			pGrandFather->nColor = RED;

			pTemp = pGrandFather;
			pNode = pTemp->pFather;

			//根
			if (pNode == NULL) {
				pRoot->nColor = BLACK;
				break;
			}
			continue;
		}

		//叔叔黑色
		if (pUncle == NULL || pUncle->nColor == BLACK) {
			//父亲在爷爷的左边
			if (pNode == pGrandFather->pLeft) {
				//新节点在父亲右侧
				if (pTemp == pNode->pRight) {
					pTemp = pNode;
					RR(pTemp);
					pNode = pTemp->pFather;
				}
				//新节点是父亲的左侧
				if (pTemp == pNode->pLeft) {
					pNode->nColor = BLACK;
					pGrandFather->nColor = RED;
					LL(pGrandFather);
					break;
				}
			}

			//父亲是爷爷的右侧
			if (pNode == pGrandFather->pRight) {
				//新节点是父亲的左侧
				if (pTemp == pNode->pLeft) {
					pTemp = pNode;
					LL(pTemp);
					pNode = pTemp->pFather;
				}

				//新节点是父亲的右侧
				if (pTemp == pNode->pRight) {
					pNode->nColor = BLACK;
					pGrandFather->nColor = RED;
					RR(pGrandFather);
					break;
				}
			}
		}
	}
}

void CreateRBT(int arr[], int len) {
	if (arr == NULL || len <= 0)
		return;
	for (int i = 0; i < len; i++) {
		AddNode(pRoot, arr[i]);
	}
}

例:当加入新节点4时,父5为红(2)树8为红1.:

父5 叔8变黑,爷7变红,此时叔8路径少一个黑,需要传递

爷7为新操作节点,此时父2为红(2),叔14为黑2.:(LR)

父是爷左1> 新7是父的右a. :父2为新操作节点,以2为旋转,左旋

此时新的操作节点为2,新2是父的左b. :父7变黑,爷11变红,对爷11右旋

3.2.2 红黑树的删除

BST的删除?度为2节点替换删除左子树最大值或右子树最小值

步骤:

一、查找

二、被删除节点Z有几个孩子

度为2要转变(左子树最大值或右子树最小值),统一为1或0的情况

三、对被删除节点Z(真正的删除位置)

(一)被删除节点Z是根

(1) 没有孩子:直接删除

(2) 有一个孩子(一定是红色的):删除Z,孩子变成黑色,成为新根

(二)非根

(1) 红色:直接删除

(2) 黑色:

1> 有1子:孩子变成黑色,爷孙相连,删除Z

2> 无子:删除Z(此时没有节点能补充删除黑色导致的不平衡,先找兄弟借)

1. 兄弟节点是红色的:父变红,兄变黑,以父为旋转点,旋转(看兄在父哪个方向,R左旋,L右旋)

2. 兄弟节点是黑色的:(找侄子借)

a) 两个侄子全黑

        1' 父红:父变黑,兄弟变红

        2' 父黑:兄变红,父为新操作节点(先将子树调平,再对父节点处理),重新讨论——>非根,黑,无子        【注意父为根的情况】

b) 右侄子红,左侄子黑:

        1' 兄是父左:兄变红,右侄子变黑,以兄弟为旋转点,左旋 LR < 变 /

        2‘ 兄是父右:父亲颜色给兄弟,父变黑,右侄子变黑,以父为旋转点,左旋  RR \ 变 ^

c) 左侄子红,右侄子黑:

        1' 兄是父左:父亲颜色给兄弟,父变黑,左侄子变黑,以父为旋转点,右旋 LL / 变 ^

        2' 兄是父右:兄变红,左侄子变黑,以兄为旋转点,右旋 RL > 变 \

void DeleteRBTNode(RBT* pTree, int val) {
	RBT* pDel = pTree;	//实际删除节点

	//查找
	while (pDel) {
		if (pDel->nVlaue == val)
			break;
		else if (pDel->nVlaue > val)
			pDel = pDel->pLeft;
		else if (pDel->nVlaue < val)
			pDel = pDel->pRight;
	}

	//检测
	if (pDel == NULL) {
		printf("删除失败,节点%d不存在\n", val);
		return;
	}
	
	//孩子情况,度为2转移
	RBT* pMark = NULL;	//标记原删除位置
	if (pDel->pLeft != NULL && pDel->pRight != NULL) {
		pMark = pDel;
		
		//左子树最大值
		pDel = pDel->pLeft;	//实际删除
		while (pDel->pRight != NULL) {
			pDel = pDel->pRight;
		}

		//值覆盖
		pMark->nVlaue = pDel->nVlaue;
	}

	RBT* pFa = pDel->pFather;
	//如果实际删除节点是根
	if (pFa == NULL) {
		//无子
		if (pDel->pLeft == NULL && pDel->pRight == NULL) {
			free(pDel);
			pDel = NULL;
			pRoot = NULL;
			return;
		}
		//有子
		else {
			pRoot = pDel->pLeft ? pDel->pLeft : pDel->pRight;
			pRoot->nColor = BLACK;
			pRoot->pFather = NULL;

			free(pDel);
			pDel = NULL;
			return;
		}
	}

	//非根,将度为2的情况和度为1、0的情况统一处理
	if (pDel->nColor == RED) {	//红色,直接删除
		if (pDel == pFa->pLeft) {
			pFa->pLeft = NULL;
		}
		else {
			pFa->pRight = NULL;
		}
		free(pDel);
		pDel = NULL;
		return;
	}
	else if (pDel->nColor == BLACK) {	//黑色
		//有1子,孩子变黑,爷孙相连
		if (pDel->pLeft || pDel->pRight) {
			if (pDel == pFa->pLeft) {
				pFa->pLeft = pDel->pLeft ? pDel->pLeft : pDel->pRight;
				pFa->pLeft->nColor = BLACK;
				pFa->pLeft->pFather = pFa;
			}
			else {
				pFa->pRight = pDel->pLeft ? pDel->pLeft : pDel->pRight;
				pFa->pRight->nColor = BLACK;
				pFa->pRight->pFather = pFa;
			}
			free(pDel);
			pDel = NULL;
			return;
		}
		
		//无子删除pDel,再调平
		RBT* pBrother = GetUncle(pDel);
		if (pDel == pFa->pLeft) {
			pFa->pLeft = NULL;
		}
		else {
			pFa->pRight = NULL;
		}
		free(pDel);
		pDel = NULL;
		
		while(1){	//此时兄弟不可能为空
			//兄弟是红色
			if (pBrother->nColor == RED) {
				pFa->nColor = RED;
				pBrother->nColor = BLACK;
				if (pBrother == pFa->pLeft) {
					LL(pFa);
					pBrother = pFa->pLeft;
					continue;
				}
				else {
					RR(pFa);
					pBrother = pFa->pRight;
					continue;
				}
			}

			//兄弟是黑色
			else if (pBrother->nColor == BLACK) {
				if ((pBrother->pLeft == NULL || pBrother->pLeft->nColor == BLACK) &&
					(pBrother->pRight == NULL || pBrother->pRight->nColor == BLACK)) {	//侄子全黑
					if (pFa->nColor == RED) {	//父红
						pFa->nColor = BLACK;
						pBrother->nColor = RED;
						break;
					}
					else if (pFa->nColor == BLACK) {	//父黑
						pBrother->nColor = RED;
						pDel = pFa;
						if (pFa->pFather==NULL) {
							pRoot = pFa;
							return;
						}
						pFa = pDel->pFather;
						pBrother = GetUncle(pDel);
						continue;
					}
				}
				if (pBrother->pRight && pBrother->pRight->nColor == RED ) {	//右红左黑
					if (pBrother == pFa->pLeft) {	//LR
						pBrother->nColor = RED;
						pBrother->pRight->nColor = BLACK;
						RR(pBrother);
						pBrother = pFa->pLeft;
						continue;
					}
					else if (pBrother == pFa->pRight) {	//RR
						pBrother->nColor = pFa->nColor;
						pFa->nColor = BLACK;
						pBrother->pRight->nColor = BLACK;
						RR(pFa);
						break;
					}
				}
				if (pBrother->pLeft && pBrother->pLeft->nColor == RED ) {	//左红右黑
					if (pBrother == pFa->pLeft) {	//LL
						pBrother->nColor = pFa->nColor;
						pFa->nColor = BLACK;
						pBrother->pLeft->nColor = BLACK;
						LL(pFa);
						break;
					}
					else if (pBrother == pFa->pRight) {	//RL
						pBrother->nColor = RED;
						pBrother->pLeft->nColor = BLACK;
						LL(pBrother);
						pBrother = pFa->pRight;
						continue;
					}
				}
			}
		}
	}
}

例: 删1

父黑,兄红:23变红,35变黑,以23为旋转点,左旋

此时不平衡,需要借,侄子全黑,找父亲借,父黑

23变黑,28变红

删23:

兄黑,侄子全黑,父黑:(先把父子树调平,再去找别人借)兄44变红,父35为新操作节点

删70:

 

兄黑,右侄子红:父gei'xion兄60变红,右侄子65变黑,以兄为旋转点,左旋

兄黑,左侄子红:父颜色给兄,兄65变红,父68变黑,左侄子60变黑,以父68为旋转点,右旋

最多三次旋转

3.3 多路平衡搜索树(B/B+)

多应用于底层(数据库、磁盘),面对较大的、存放在外存储器上的文件的算法

一个节点具有多个数据域和多个指针,相比RBT要大的多

3.3.1 B-Tree(B树)

对于一棵M阶B-Tree,满足以下条件:

1、一个节点最多有M个指针(M叉)

2、每个节点内最多有M-1条记录(Key-Data)

3、根节点内记录最少为1

4、其他节点内记录数>=ceil(M/2)-1

5、每个节点内记录的索引值key,从左至右从小到大有序

6、每个记录的索引值,大于等于左子树,小于等于右子树

3.3.1.1 B-Tree的添加

裂变:中间记录上移至父亲层,左右记录分别成为其左右子树

新记录会添加到叶子节点中,再从下往上裂变

对于一棵M阶B-Tree,步骤:

(1)将记录放入叶子

(2)讨论节点内记录的个数

① 节点个数<=M-1,结束

② 节点个数>M-1,裂变,讨论父亲层记录个数

3.3.1.2 B-Tree的删除

对于一棵M阶B-Tree,步骤:

(1)查找,若为非叶子节点转换成叶子节点,删除对应的记录

(2)讨论节点内记录的个数

① >=ceil(M/2)-1,结束

② <ceil(M/2)-1,节点内记录不满足条件,需要借一条记录来平衡。看兄弟节点内记录个数

1> 兄弟节点记录数 > ceil(M/2)-1,兄弟记录上移,父亲记录下移至当前节点

2> 兄弟节点记录数 = ceil(M/2)-1,父亲记录下移,与当前节点和兄弟节点合并成一个新结点,讨论父亲节点内记录个数

3.3.2 B+-Tree(B+树)

对于一棵M阶B+-Tree,满足以下条件:

(1)结点分为两种:索引结点/内部结点(Key)、叶子结点(记录和指向兄弟的指针)

(2)一个结点最多有M叉

(3)一个结点内索引/记录个数<=M-1

(4)根结点既可以是叶子结点,也可以是索引结点

(5)根结点内索引/记录个数>=1

(6)其他结点内索引/记录个数>=ceil(M/2)-1

(7)每个结点内索引值,从左至右从小到大有序

(8)每个结点内索引值,大于等于左子树,小于等于右子树

(9)相邻的叶子结点之间,有指针从左向右指向相邻的叶子结点

3.3.2.1 B+-Tree的添加

叶子结点裂变:前M/2个结点作为左侧,剩余为右侧,第M/2+1复制一份到父亲层

索引结点裂变:与B-Tree一样

(1)当前记录放入叶子结点

(2)讨论当前结点内记录个数

① <=M-1,完成

② >M-1,前M/2个记录成为左节点,剩余记录成为右结点,第M/2+1个记录的索引复制一份至父亲层

③ 讨论父亲层索引个数

1> <=M-1,完成

2> >M-1 裂变,中间索引上移至父亲层,左侧索引为左子树,右侧索引为右子树。讨论父亲层索引个数

3.3.2.2 B+-Tree的删除

(1)找到记录,删除

(2)讨论叶子结点记录个数

① >=ceil(M/2)-1,完成

② <ceil(M/2)-1,看当前兄弟结点记录个数

1> >ceil(M/2)-1,兄弟移动一个记录至当前结点,更新父亲索引值

2> =ceil(M/2)-1,兄弟结点与当前结点合并,删除当前父亲索引。

讨论父亲结点索引个数

1)>=ceil(M/2)-1,完成

2)<ceil(M/2)-1,看兄弟结点索引个数

a. >ceil(M/2)-1,父亲索引下移至当前结点,兄弟结点上移一个至父亲层

b. =ceil(M/2)-1,父亲索引下移至当前结点,和兄弟结点合并成新节点。讨论父亲层索引个数

【数据库】mysql索引底层数据结构原理(为什么采用B+树而不采用链表、BST、AVL)_索引为什么用b+树不用列表链表-CSDN博客

对于一个N个节点的M阶B树,深度为logMN。搜索效率为1~logMN,B+树为logMN

B+树链表结构的应用:范围查找(B树需要跨层访问,占用很多无用的内存)

3.3.2.3 B-Tree和B+-Tree的区别

四、哈夫曼树

为了进行哈夫曼编码,实现无损压缩和恢复

这棵树的带权路径长度WPL:W1L1+W2L2+...+WnLn

WPL最小的被称为最优二叉树(哈夫曼树)

4.1 构建哈夫曼树

1、排序

2、拿两个min,构成新节点

3、放回序列

重复直到只剩一个

4.2 哈夫曼编码

左侧边放0右侧放1

无前缀码

【数据结构】通过哈夫曼树实现对文本的压缩与解压_哈夫曼树压缩-CSDN博客

五、字典树 TrieTree

是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串。

Trie树本质,利用字符串之间的公共前缀,将重复的前缀合并在一起。

功能:查找、计数、排序

结构体:①指针数组*[256](哈希的思想,按字典序分组)

②单词末尾标记(计数器)

步骤:

(1)创建根结点

(2)添加单词

1> 每个字符对应一个指针

结点为空:创建新节点

结点非空:转到相应的子节点

2> 末尾标记

#include <iostream>

using namespace std;

typedef struct node {
	int cnt;
	node* next[26];
	string str;
	node() {
		cnt = 0;
		for (int i = 0; i < 26; i++)
			next[i] = nullptr;
	}
}TrieTree;

void add(TrieTree* pTree, string word) {
	for (int i = 0; i < word.length(); i++) {
		char c = word[i];
		if (pTree->next[c - 'a'] == nullptr)
			pTree->next[c - 'a'] = new TrieTree;
		pTree = pTree->next[c - 'a'];
	}
	pTree->cnt++;
	pTree->str = word;
}

int search(TrieTree* pTree, string word) {
	for (int i = 0; i < word.length(); i++) {
		char c = word[i];
		if (pTree->next[c - 'a'] == nullptr)
			return false;
		pTree = pTree->next[c - 'a'];
	}
	return pTree->cnt;
}

void Preorder(TrieTree* pTree) {
	if (pTree == NULL)
		return;
	if (pTree->cnt != 0)
		cout << pTree->str << endl;
	for (int i = 0; i < 26; i++) {
		Preorder(pTree->next[i]);
	}
}

int main() {
	TrieTree* tree = new TrieTree;
	
	string str[] = { "hello","asda","aab","abuno","sdaad" };
	for (int i = 0; i < 5; i++) {
		add(tree, str[i]);
	}
	cout << search(tree, "asda") << endl;
	Preorder(tree);
}

字典树不能为空树

应用:搜索引擎

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值