最短路径
基本概念
最短路径:从图中某一顶点(源点)到达另一顶点(终点)找到一条路径,沿此路径上各边的权值总和(称为路径长度)达到最小。
单源最短路径:已知有向带权图(简称有向网)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的最短路径相矛盾。因此该性质得证。(如图)
迪杰斯特拉(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,停止。
重点
从一个顶点到另一个顶点最短的路径
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性质)
-
二叉排序树中任一结点x,其左(右)子树中任一结点y(若存在)的关键字必小(大)于x的关键字。
-
二叉排序树中,各结点关键字是惟一的。
注意:
实际应用中,不能保证被查找的数据集中各元素的关键字互不相同 所以可将二叉排序树定义中BST性质(1)里的"小于"改为"大于等于", 或将BST性质(2)里的"大于"改为"小于等于",甚至可同时修改这两个性质。
-
按中序遍历该树所得到的中序序列是一个递增有序序列。
重点结论
中序遍历结果就是升序序列
插入的新节点作为叶子存在
最小值是最左的子结点,最大值是最右子结点
删除二叉树中的节点
三种形式
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)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
衡二叉树实现的大部分过程和二叉查找树是一样的(学平衡二叉树之前一定要会二叉查找树),区别就在于插入和删除之后要写一个旋转算法去维持平衡,维持平衡需要借助一个节点高度的属性。
满足平衡二叉树的条件
一棵空树是平衡二叉树;
若 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 的右孩子。
RR型
新结点 X 插在 A 的右孩子的右子树里。调整方法见下图 (b) 。图中以 B 为轴心,将 A 结点从 B 的左上方转到 B 的左下侧,使 A 成为 B 的左孩子。
LR型
新结点 X 插在 A 的左孩子的右子树里。调整方法见图 (c) 。分为两步进行:第一步以 X 为轴心,将 B 从 X 的左上方转到 X 的左下侧,使 B 成为 X 的左孩子, X 成为 A 的左孩子。第二步跟 LL 型一样处理 ( 应以 X 为轴心 ) 。
RL型
新结点 X 插在 A 的右孩子的左子树里。调整方法见图 (d) 。分为两步进行:第一步以 X 为轴心,将 B 从 X 的右上方转到 X 的右下侧,使 B 成为 X 的右孩子, X 成为 A 的右孩子。第二步跟 RR 型一样处理 ( 应以 X 为轴心 ) 。
复杂型
从小开始,找到不平衡的最小二叉树,逐个调整
练习
插入节点的时候,保证一棵树的平衡性
红黑树
红黑树的介绍
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树节点的组成
红黑树上每个结点内含五个域,color,key,left,right,parent。如果相应的指针域没有,则设为NULL。
红黑树的性质
一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:
每个结点要么是红的,要么是黑的。
根结点是黑的。
每个叶结点,即空结点(NULL)是黑的。
如果一个结点是红的,那么它的俩个儿子都是黑的。
从根到叶节点的每条路径,必须包含相同数目的黑色节点。
下图所示,即为一颗红黑树
红黑树的操作
当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。
为了保持红黑树的性质,我们可以通过对树进行旋转,即修改树种某些结点的颜色及指针结构,以达到对红黑树进行插入、删除结点等操作时,红黑树依然能保持它特有的性质(如上文所述的,五点性质)。
修正方式:
改变节点的颜色
旋转