C++笔记day25 最短路径,二叉排序树

178 阅读10分钟

最短路径

基本概念

最短路径:从图中某一顶点(源点)到达另一顶点(终点)找到一条路径,沿此路径上各边的权值总和(称为路径长度)达到最小。
单源最短路径:已知有向带权图(简称有向网)G=(V,E),找出从某个源点s∈V到V中其余各顶点的最短路径。

习惯上称路径开始顶点为源点,路径的最后一个顶点为终点。

最短路径的最优子结构性质

该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。

假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。(如图)

image.png

迪杰斯特拉(Dijkstra)算法

由上述性质可知,如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点。那么(Vi...Vk)也必定是从i到k的最短路径。为了求出最短路径,Dijkstra就提出了以最短路径长度递增,逐次生成最短路径的算法。譬如对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist[i]+matrix[i][j]}。根据这种思路,假设存在G=<V,E>,源顶点为V0,U={V0},dist[i]记录V0到i的最短距离,path[i]记录从V0到i路径上的i前面的一个顶点。

从V-U中选择使dist[i]值最小的顶点i,将i加入到U中;

更新与i直接相邻顶点的dist值。

    (dist[j]=min{dist[j],dist[i]+matrix[i][j]})

直到U=V,停止。

image.png

重点

从一个顶点到另一个顶点最短的路径

dist数组 path数组

示例

// 最短路径
// 迪杰斯特拉(Dijkstra)算法
// path哪一顶点到当前点的距离最近
void dijkstraPath(Graph &g, int *path, int *dist, int v0)
{
	int min = 0;
	int pos = v0;	// 访问的起始顶点
	//定义一个数组, 标记顶点是否已经被访问
	bool *visited = new bool[g.vertexNum];
	//初始化
	for (int i = 0; i < g.vertexNum; ++i)
	{
		visited[i] = false;	//顶点未访问
		if (i != v0) //排除顶点到出发点的计算
		{
			//初始化所有点的最近邻接点都是V0点
			path[i] = v0;
			// v0到各个顶点的权重
			dist[i] = g.edge[v0][i];
			cout << g.vertex[v0] << " 到 " << g.vertex[i]
				<< " 距离: dist[" << i << "]=" << dist[i] << endl;
		}
		else
		{
			// path[]数组 - 到当前点的最近的邻接点
			// dist[] 数组 - 从出发点到各个点的最短距离
			// i == v0没有任何意义, 不存在路径
			path[i] = -1;
			dist[i] = INT_MAX;
		}
	}
	//把v0标记为已访问
	visited[v0] = true;

	for (int i = 0; i < g.vertexNum; ++i)
	{
		min = INT_MAX;
		for (int j = 0; j < g.vertexNum; ++j)
		{
			// 没有被访问, 并且找到了拥有更小权值的边
			// path[]数组 - 到当前点的最近的邻接点
			// dist[] 数组 - 从出发点到各个点的最短距离
			if (!visited[j] && min>dist[j])
			{
				//保存最小值
				min = dist[j];
				//保存位置
				pos = j;
				cout << "+++ 顶点更新: pos =" << pos
					<< "顶点为: " << g.vertex[pos] << endl;
			}
		}
		//pos位置的顶点标记为已访问
		visited[pos] = true;

		// dist V0点到各个点的距离
		for (int j = 0; j < g.vertexNum; ++j)
		{
			// g.edge[pos][j] < INT_MAX - 判断pos->j这条边是存在的
			if (!visited[j] && dist[pos] + g.edge[pos][j] < dist[j] && g.edge[pos][j] < INT_MAX)
			{
				// 更新最短距离
				//例如 将j看成E  pos看成B,求出A到E最短路径
				dist[j] = dist[pos] + g.edge[pos][j];
				//更新路径, 到顶点j最近的顶点是pos
				path[j] = pos;
				cout << "=== 更新最短距离: dist[" << j
					<< "] = " << dist[j] << endl;
			}
		}
	}
}

// v0 - 起始点
// v  - 到达点
void showPath(Graph &g, int *path, int v0, int v)
{
	//创建一个栈对象
	stack<int> st;
	int temp = v;
	while (temp != v0)
	{
		st.push(temp);
		//寻找上一个顶点
		temp = path[temp];
	}
	st.push(v0);

	//打印路径
	while (!st.empty())
	{
		cout << g.vertex[st.top()] << " ";
		st.pop();
	}
}

int main()
{
	//定义一个图的对象
	Graph g;
	//用邻接矩阵创建图
	createGraph(g);
	//打印
	printGraph(g);

	//深度优先搜索
	cout << "深度优先搜索" << endl;
	DFS(g);
	cout << endl;

	//广度优先搜索
	cout << "广度优先搜索" << endl;
	BFS(g);
	cout << endl;

	/*==================================================
	==================================================*/
	cout << "迪杰斯特拉(Dijkstra)算法" << endl;
	// path数组 - 到当前点的最近的邻接点
	int path[50];
	// dist[] 数组 - 从出发点到各个点的最短距离
	int dist[50];
	// 出发点
	int v0 = 0;
	dijkstraPath(g, path, dist, v0);
	// A->D怎么走?多么长?
	// dist[3] = A->D的最短距离
	// path[3]
	for (int i = 1; i < g.vertexNum; ++i)
	{
		cout << "路径: ";
		showPath(g, path, v0, i);
		cout << "路径长度: " << dist[i] << endl;
	}

	cout << "Keyboard not found, press F1 to continue..." << endl;
	system("pause");
	return 0;
}

二叉排序树

二叉排序树定义

简介

二叉排序树(Binary Sort Tree)又称二叉查找(搜索)树(Binary Search Tree)。

满足条件

它是一颗空树,或者是满足如下性质的二叉树:

①若它的左子树非空,则左子树上所有结点的值均小于根结点的值;

②若它的右子树非空,则右子树上所有结点的值均大于根结点的值;

③左、右子树本身又各是一棵二叉排序树。

上述性质简称二叉排序树性质(BST性质),故二叉排序树实际上是满足BST性质的二叉树。

二叉排序树的特点

特点(BST性质)

  1. 二叉排序树中任一结点x,其左(右)子树中任一结点y(若存在)的关键字必小(大)于x的关键字。

  2. 二叉排序树中,各结点关键字是惟一的。

    注意:

    实际应用中,不能保证被查找的数据集中各元素的关键字互不相同 所以可将二叉排序树定义中BST性质(1)里的"小于"改为"大于等于", 或将BST性质(2)里的"大于"改为"小于等于",甚至可同时修改这两个性质。

  3. 按中序遍历该树所得到的中序序列是一个递增有序序列。

image.png

重点结论

中序遍历结果就是升序序列

插入的新节点作为叶子存在

最小值是最左的子结点,最大值是最右子结点

删除二叉树中的节点

三种形式
    
    1. 只有叶子
    2. 只有左子树或者右子树
    3. 既有左子树又有右子树
        找到删除节点的直接前驱或者后继
        将前驱或后继的节点数字替换到删除节点上
        再将前驱或者后继删掉(维护前驱或者后继的儿子)

示例

#include <iostream>
using namespace std;

//二叉查找树结点描述  
typedef int KeyType;
typedef struct Node
{
	KeyType key;          //关键字  - int
	struct Node * left;   //左孩子指针  
	struct Node * right;  //右孩子指针
	//struct Node * parent;
}Node, *PNode;

//往二叉查找树中插入结点  
//插入的话,可能要改变根结点的地址,所以传的是二级指针  
void insertBST(PNode *root, KeyType key)
{
	//初始化插入结点  
	PNode p = new Node;
	p->key = key;
	p->left = p->right = NULL;
	//空树时,直接作为根结点  
	if ((*root) == NULL)
	{
		*root = p;
		return;
	}
	//插入到当前结点(*root)的左孩子  
	if ((*root)->left == NULL && (*root)->key > key)
	{
		(*root)->left = p;
		return;
	}
	//插入到当前结点(*root)的右孩子  
	if ((*root)->right == NULL && (*root)->key < key)
	{
		(*root)->right = p;
		return;
	}
	//本次循环没有插入节点, 删除创建的节点
	delete p;
	//查找左子树
	if ((*root)->key > key)
	{
		insertBST(&(*root)->left, key);
	}
	//查找右子树
	else if ((*root)->key < key)
	{
		insertBST(&(*root)->right, key);
	}
}

//根据关键字删除某个结点,删除成功返回1,否则返回0  
//如果把根结点删掉,那么要改变根结点的地址,所以传二级指针  
bool deleteBST(PNode* root, KeyType key)
{
	//空树直接返回
	if (*root == NULL)
	{
		return false;
	}
	//如果找到值为key的节点
	if (key == (*root)->key)
	{
		PNode q, s;	// 节点类型的指针
		//右子树空则只需重接它的左子树(待删结点是叶子也走此分支)
		if ((*root)->right == NULL)
		{
			//保存要删除的节点的地址
			q = *root;
			//左子树向上移动
			// 指针重新指向了其左子树的地址
			*root = (*root)->left;
			//删除节点,释放内存
			delete q;
		}
		//左子树为空,只需重接它的右子树
		else if ((*root)->left == NULL)
		{
			//保存要删除的节点的地址
			q = *root;
			//右子树向上移动
			*root = (*root)->right;
			//删除节点, 释放内存
			free(q);
		}
		//左右子树均不空
		else
		{
			//保存要删除的节点的地址
			q = *root;
			//保存待删除节点左孩子地址
			s = (*root)->left;
			//向右到尽头(找待删结点的前驱,按照中序遍历找到的节点是待删除节点的前驱)
			while (s->right)
			{
				// q为s的父节点
				q = s;
				s = s->right;
			}
			//s指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值)
			(*root)->key = s->key;
			//节点发生了下移,即root的左子树不是叶子节点
			if (q != *root)
			{
				//重接q的右子树
				q->right = s->left;
			}
			else
			{
				//q没有移动还是指向root
				//重接q的左子树
				q->left = s->left;
			}
			free(s);
		}
		return true;
	}
	//查找左子树
	else if (key < (*root)->key)
	{
		return deleteBST(&(*root)->left, key);
	}
	//查找右子树
	else if (key >(*root)->key)
	{
		return deleteBST(&(*root)->right, key);
	}
	return false;
}

//查找元素,找到返回关键字的结点指针,没找到返回NULL  
PNode searchBST(PNode root, KeyType key)
{
	//没找到的话root为NULL
	if (root == NULL)
	{
		return NULL;
	}
	//查找右子树  
	if (key > root->key)
	{
		return searchBST(root->right, key);
	}
	//查找左子树 
	else if (key < root->key)
	{
		return searchBST(root->left, key);
	}
	else
	{
		return root;
	}
}

//查找最小关键字,空树时返回NULL  
PNode searchMinBST(PNode root)
{
	//空树
	if (root == NULL)
	{
		return NULL;
	}
	//找到最左的孩子
	if (root->left == NULL)
	{
		return root;
	}
	//一直往左孩子找,直到没有左孩子的结点  
	else
	{
		return searchMinBST(root->left);
	}
}

//查找最大关键字,空树时返回NULL  
PNode searchMaxBST(PNode root)
{
	//空树
	if (root == NULL)
	{
		return NULL;
	}
	//找到最右的孩子
	if (root->right == NULL)
	{
		return root;
	}
	//一直往右孩子找,直到没有右孩子的结点  
	else
	{
		return searchMaxBST(root->right);
	}
}

//创建一棵二叉查找树  
void createBST(PNode* root, KeyType *keyArray, int length)
{
	//逐个结点插入二叉树中  
	for (int i = 0; i < length; i++)
	{
		insertBST(root, keyArray[i]);
	}
}

//中序遍历二叉排序树
void inorderTraversalBST(PNode root)
{
	if (root != NULL)
	{
		//遍历左子树
		inorderTraversalBST(root->left);
		//打印根节点
		cout << root->key << " ";
		//遍历右子树
		inorderTraversalBST(root->right);
	}
}

//创建二叉排序树以及遍历
void test01()
{
	PNode root = NULL;
	KeyType nodeArray[11] = { 15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9 };

	//创建二叉查找树
	cout << "正在创建二叉查找树..." << endl;
	createBST(&root, nodeArray, 11);
	cout << "二叉查找树创建完毕!!!" << endl << endl;

	//遍历二叉查找树
	cout << "中序遍历二叉查找树(升序排列):" << endl;
	inorderTraversalBST(root);
	cout << endl << endl;

}

//查找指定的节点,和插入过程类似
void test02()
{
	PNode root = NULL;
	KeyType nodeArray[11] = { 15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9 };

	//创建二叉查找树
	cout << "正在创建二叉查找树..." << endl;
	createBST(&root, nodeArray, 11);
	cout << "二叉查找树创建完毕!!!" << endl << endl;

	//遍历二叉查找树
	cout << "中序遍历二叉查找树(升序排列):" << endl;
	inorderTraversalBST(root);
	cout << endl << endl;

	cout << "中序遍历二叉树查找:" << endl;
	PNode node = searchBST(root, 17);
	if (node != NULL)
	{
		cout << "找到指定节点: " << node->key << endl;
	}
	else
	{
		cout << "没有找到指定的节点!!!" << endl;
	}
	cout << endl;

}

//查找极值
void test03()
{
	PNode root = NULL;
	KeyType nodeArray[11] = { 15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9 };

	//创建二叉查找树
	cout << "正在创建二叉查找树..." << endl;
	createBST(&root, nodeArray, 11);
	cout << "二叉查找树创建完毕!!!" << endl << endl;

	//遍历二叉查找树
	cout << "中序遍历二叉查找树(升序排列):" << endl;
	inorderTraversalBST(root);
	cout << endl << endl;


	//查找最小节点
	PNode minNode = searchMinBST(root);
	if (minNode != NULL)
	{
		cout << "找到最小值: " << minNode->key << endl;
	}
	else
	{
		cout << "这棵树为空树!!!" << endl;
	}
	cout << endl;

	//查找最大节点
	PNode maxNode = searchMaxBST(root);
	if (maxNode != NULL)
	{
		cout << "找到最大值: " << maxNode->key << endl;
	}
	else
	{
		cout << "这棵树为空树!!!" << endl;
	}
	cout << endl;
}

void test04()
{

	PNode root = NULL;
	KeyType nodeArray[11] = { 15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9 };

	//创建二叉查找树
	cout << "正在创建二叉查找树..." << endl;
	createBST(&root, nodeArray, 11);
	cout << "二叉查找树创建完毕!!!" << endl << endl;

	//遍历二叉查找树
	cout << "中序遍历二叉查找树(升序排列):" << endl;
	inorderTraversalBST(root);
	cout << endl << endl;

	//删除指定节点
	bool bl = deleteBST(&root, 18);
	if (bl)
	{
		cout << "删除节点成功!!!" << endl;
	}
	else
	{
		cout << "删除节点失败!!!" << endl;
	}
	//遍历二叉查找树
	cout << "中序遍历二叉查找树(升序排列):" << endl;
	inorderTraversalBST(root);
	cout << endl << endl;
}

int main()
{
	//test01();

	//test02();
	
	//test03();

	test04();


	system("pause");
	return 0;
}

平衡二叉树

平衡二叉树概念                

平衡二叉树(Balanced Binary Tree)是二叉查找树的一个进化体,也是第一个引入平衡概念的二叉树。1962年,G.M. Adelson-Velsky 和 E.M. Landis发明了这棵树,所以它又叫AVL树。平衡二叉树要求对于每一个节点来说,它的左右子树的高度之差不能超过1,如果插入或者删除一个节点使得高度之差大于1,就要进行节点之间的旋转,将二叉树重新维持在一个平衡状态。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

image.png

image.png

衡二叉树实现的大部分过程和二叉查找树是一样的(学平衡二叉树之前一定要会二叉查找树),区别就在于插入和删除之后要写一个旋转算法去维持平衡,维持平衡需要借助一个节点高度的属性。

满足平衡二叉树的条件

一棵空树是平衡二叉树;

若 T 是一棵非空二叉树,其左、右子树为 TL 和 TR ,令 hl 和 hr 分别为左、右子树的深度。当且仅当 TL 、 TR 都是平衡二叉树; | hl - hr |≤ 1; 时,则 T 是平衡二叉树。

相应地定义 hl - hr 为二叉平衡树的平衡因子 (balance factor) 。因此,平衡二叉树上所有结点的平衡因子可能是 -1,0 ,1 。换言之,若一棵二叉树上任一结点的平衡因子的绝对值都不大于 1 ,则该树是就平衡二叉树。

最小不平衡子树

以离插入结点最近、且平衡因子绝对值大于 1 的结点作根结点的子树。为了简化讨论,不妨假设二叉排序树的最小不平衡子树的根结点为 A ,则调整该子树的规律可归纳为下列四种情况:

LL型

新结点 X 插在 A 的左孩子的左子树里。调整方法见下图 (a) 。图中以 B 为轴心,将 A 结点从 B 的右上方转到 B 的右下侧,使 A 成为 B 的右孩子。

image.png

RR型

新结点 X 插在 A 的右孩子的右子树里。调整方法见下图 (b) 。图中以 B 为轴心,将 A 结点从 B 的左上方转到 B 的左下侧,使 A 成为 B 的左孩子。

image.png

LR型

新结点 X 插在 A 的左孩子的右子树里。调整方法见图 (c) 。分为两步进行:第一步以 X 为轴心,将 B 从 X 的左上方转到 X 的左下侧,使 B 成为 X 的左孩子, X 成为 A 的左孩子。第二步跟 LL 型一样处理 ( 应以 X 为轴心 ) 。

image.png

RL型

新结点 X 插在 A 的右孩子的左子树里。调整方法见图 (d) 。分为两步进行:第一步以 X 为轴心,将 B 从 X 的右上方转到 X 的右下侧,使 B 成为 X 的右孩子, X 成为 A 的右孩子。第二步跟 RR 型一样处理 ( 应以 X 为轴心 ) 。

image.png

复杂型

从小开始,找到不平衡的最小二叉树,逐个调整

image.png

练习

插入节点的时候,保证一棵树的平衡性

image.png

红黑树

红黑树的介绍

红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。

通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

红黑树节点的组成

红黑树上每个结点内含五个域,color,key,left,right,parent。如果相应的指针域没有,则设为NULL。

image.png

红黑树的性质

一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:

每个结点要么是红的,要么是黑的。

根结点是黑的。

每个叶结点,即空结点(NULL)是黑的。

如果一个结点是红的,那么它的俩个儿子都是黑的。

从根到叶节点的每条路径,必须包含相同数目的黑色节点。

下图所示,即为一颗红黑树

image.png

红黑树的操作

当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。

为了保持红黑树的性质,我们可以通过对树进行旋转,即修改树种某些结点的颜色及指针结构,以达到对红黑树进行插入、删除结点等操作时,红黑树依然能保持它特有的性质(如上文所述的,五点性质)。

修正方式:

改变节点的颜色

旋转