链表、队列、图、B树

一、链表

循环队列的实现

#include<iostream>
#include <string>
using namespace std;
 
template <typename T>
class Myloopqueue {
private:
	T *queue;//存储用的数组
	int capacity;//存放个数
	int head;//head+1是下一个出队列的坐标
	int tail;//指向最后进来的元素
public:
	Myloopqueue(int a);//无参构造
	Myloopqueue();//有参构造
	~Myloopqueue();//析构
	bool isEmpty();//判断空
	int getSize();//返回个数
	bool push(T a);//入队
	bool pop();//出队
	T top();//显示队首
};
 
template<typename T>
Myloopqueue<T>::Myloopqueue(int a) :head(0), tail(0), capacity(a), queue(nullptr) {
	queue = new T[capacity];
}
 
template<typename T>
Myloopqueue<T>::~Myloopqueue() {
	delete[] queue;
}
 
template<typename T>
bool Myloopqueue<T>::isEmpty() {
	if (head == tail)
		return true;
	else
		return false;
}
 
template<typename T>
int Myloopqueue<T>::getSize() {
	return (tail - head + capacity) % capacity;
}
 
template<typename T>
bool Myloopqueue<T>::push(T a) {
	if ((tail  + 1) % capacity == head)
		return false;
	tail = (tail + 1) % capacity;
	queue[tail] = a;
	return true;
}
 
template<typename T>
bool Myloopqueue<T>::pop() {
	if (tail==head)
		return false;
	head = (head + 1) % capacity;
	return true;
}
 
template<typename T>
T Myloopqueue<T>::top() {
	return queue[(head+1)%capacity];
}
 
int main()
{
	Myloopqueue<string> queue(6);
	queue.push("one");
	queue.push("two");
	queue.push("three");
	queue.push("four");
	queue.push("five");
	cout << "队列长度" << queue.getSize() << endl;
	while (!queue.isEmpty())
	{
		cout << queue.top() << endl;
		queue.pop();
	}
	system("pause");
	return 0;
 
}
 

二、队列

三、图

图的两种存储结构

邻接矩阵

邻接矩阵,顾名思义,是一个矩阵,一个存储着边的信息的矩阵,而顶点则用矩阵的下标表示。对于一个邻接矩阵M,如果M(i,j)=1,则说明顶点i和顶点j之间存在一条边,对于无向图来说,M (j ,i) = M (i, j),所以其邻接矩阵是一个对称矩阵;对于有向图来说,则未必是一个对称矩阵。邻接矩阵的对角线元素都为0。下图是一个无向图和对应的邻接矩阵:

                        

邻接表

对于顶点数很多但是边数很少的图来说,用邻接矩阵显得略为“奢侈”,因为矩阵元素为1的很少,即其中的有用信息很少,但却占了很大的空间。所以下面我们来看看邻接表。

1.图的遍历算法操作

深度优先搜索遍历DFS

图的DFS类似于二叉树的先序遍历。它的基本思想是:首先访问出发点v,并将其标记为已访问过;然后选取与v邻接的未被访问的任意一个顶点w,并访问它;再选取与w邻接的未被访问的任一顶点并访问,以此重复进行。当一个顶点所有的邻接顶点都被访问过时,则依次退回到最近被访问过的顶点,若该顶点还有其他邻接顶点未被访问,则从这些未被访问的顶点中取一个并重复上述访问过程,直至图中所有顶点都被访问过为止。
算法执行过程:选取一个顶点,访问之,然后检查这个顶点的所有邻接顶点,递归访问其中未被访问过的顶点。

void DFTCore(const vector<vector<int> >& Graph, int k, vector<bool> &visited)
{
	if (visited[k] == 1)
		return;
	cout << k << endl;
	visited[k] = 1;
	for (int i = 0; i < Graph.size(); ++i)
	{
		if (Graph[k][i] != -1)
			DFTCore(Graph, i, visited);
	}
	return;
}
void DFT(const vector<vector<int> >& Graph)
{
	int num_vertex = Graph.size();
	vector<bool> visited(num_vertex, false);
	for (int i = 0; i < num_vertex; ++i)
		DFTCore(Graph, i, visited);
	return;
}
void addEdge(const int& startP, const int& endP, const int& weight, vector<vector<int> >& Graph)
{
	Graph[startP][endP] = weight;
}

int main()
{
	vector<vector<int> > Graph(6, vector<int>(6, -1));
	addEdge(0, 1, 10, Graph);
	addEdge(0, 2, 3, Graph);
	addEdge(1, 3, 7, Graph);
	addEdge(2, 4, 5, Graph);
	addEdge(2, 5, 3, Graph);
	DFT(Graph);
	system("pause");
	return 0;
}

广度优先搜索遍历BFS

图的广度优先搜索遍历BFS类似于树的层次遍历。它的基本思想是:首先访问起始顶点v,然后选取与v邻接的全部顶w1,w2...wn进行访问,再依次访问与w1,w2,....wn邻接的全部顶点(已经访问过的除外),以此类推,直到所有顶点都被访问过为止。广度优先搜索遍历图时,需要用到一个队列(二叉树的层次遍历也要用到队列),算法执行过程可简单概括如下:

  • 任务图中一个顶点访问,入队,并将这个顶点标记为已访问。
  • 当队列不空时循环执行:出队,依次检查出队顶点的所有邻接顶点,访问没有被访问过的邻接顶点并将其入队。
  • 当队列为空时跳出循环,广度优先搜索即完成。
void BFTCore(const vector<vector<int> >& Graph, int k, vector<bool> &visited)
{
	if (visited[k] == true)
		return;
	queue<int> q;
	q.push(k);
	while (!q.empty())
	{
		int value = q.front();
		q.pop();
		if (visited[value] == false)
		{
			visited[value] = true;
			cout << value << endl;
			for (int i = 0; i < Graph.size(); ++i)
			{
				if (Graph[value][i] != -1 && visited[i] == false)
					q.push(i);
			}
		}
	}
	return;
}
void BFT(const vector<vector<int> >& Graph)
{
	int num_vertex = Graph.size();
	vector<bool> visited(num_vertex, false);
	//之所以循环,是防止那种图不相连的情况
	for (int i = 0; i < num_vertex; ++i)
		BFTCore(Graph, i, visited);
	return;
}
void addEdge(const int& startP, const int& endP, const int& weight, vector<vector<int> >& Graph)
{
	Graph[startP][endP] = weight;
}

int main()
{
	vector<vector<int> > Graph(6, vector<int>(6, -1));
	addEdge(0, 1, 10, Graph);
	addEdge(0, 3, 3, Graph);
	addEdge(1, 2, 7, Graph);
	addEdge(2, 5, 5, Graph);
	addEdge(2, 4, 3, Graph);
	addEdge(2, 0, 3, Graph);
	BFT(Graph);
	system("pause");
	return 0;
}

2.最短路径算法

迪杰斯特拉算法(O(N^2))

通常采用迪杰斯特拉算法求图中某一顶点到其余各点的最短路径。

Djkstra算法是求解单源(起始点固定)最短路径问题的一种经典方法,它采用了贪心策略(其实我觉得也是动态规划),可以求得图中的一个点到其他所有点的距离,计算复杂度是 O(E|V|),如果采用最小堆优化可以达到O(ElogV )。算法的整体思想是将顶点分成两类:已经构成最短路的点的集合V1和没有构成最短路的点的集合V2。我们将dist[i]设置为第 i个点到V1的距离,每次将V2中取距离V1集合最短的点P放入V1中,同时因为P被放入了V1,那么其他点到V1的最短路就有可能通过P了,所以我们更新所有集合V2内的点j到V1的距离dist[j] = min(  dist[j], dist[i_P] + G[i_P][j]  ),其中i_P表示P的下标, G[i_P][j]  表示图中P到j的距离。

https://blog.csdn.net/qq_30911665/article/details/78130709

#include <iostream>
#include<vector>
#include<queue>
using namespace std;
#define MAX_PATH 999999
int shortestId(const vector<int>& dist, const vector<bool>& isShortest) //寻找当前未放入最短路径集合的所有ID中路径最短的ID号
{
	int min_dist = INT_MAX;
	int min_ID = -1;
	for (int i = 0; i < dist.size(); i++)
	{
		if (false == isShortest[i]) {
			if (dist[i] < min_dist) {
				min_dist = dist[i];
				min_ID = i;
			}
		}
	}
	return min_ID;
}
vector<int> Djkstra(const vector<vector<int> >& Graph)
{
	int num_vertex = Graph.size();
	vector<bool> isShortest(num_vertex, false); //初始化只有第一个顶点(index = 0)被放入最短路的ID集合中
	isShortest[0] = true;
	vector<int> dist(num_vertex, INT_MAX); //dist[i]表示当前节点 i+1(下标i)到最短路的id集合中所有点的最短距离
	dist[0] = 0;

	for (int i = 1; i < num_vertex; i++)
	{
		if (Graph[0][i] <INT_MAX) //初始化dist,所有不与1号节点(下标0)相连的设置为正无穷
			dist[i] = Graph[0][i];
	}
	for (int i = 0; i < num_vertex - 1; i++) {
		int id = shortestId(dist, isShortest); //在所有非最短路的点集合中找到距离最短路集合最近的点,放入最短路集合
		isShortest[id] = true;
		for (int j = 0; j < num_vertex; j++) { //将 id放入最短路集合后,更新所有集合外的元素的距离,他们有可能有通过id的更短路
			if (!isShortest[j]) {
				//这个地方导致下面的Graph初始化的时候不能设置为INT_MAX,因为有可能相加之后超出INT_MAX
				dist[j] = min(dist[j], dist[id] + Graph[id][j]);
			}
		}
	}
	return dist;
}
void addEdge(const int& startP, const int& endP, const int& weight, vector<vector<int> >& Graph)
{
	Graph[startP][endP] = weight;
	//Graph[endP][startP] = weight;
}

int main()
{
	vector<vector<int> > Graph(6, vector<int>(6, MAX_PATH));
	addEdge(0, 1, 10, Graph);
	addEdge(0, 5, 3, Graph);
	addEdge(1, 2, 7, Graph);
	addEdge(1, 3, 5, Graph);
	addEdge(3, 0, 3, Graph);
	addEdge(3, 2, 4, Graph);
	addEdge(3, 4, 7, Graph);
	addEdge(5, 1, 2, Graph);
	addEdge(5, 3, 6, Graph);
	addEdge(5, 4, 1, Graph);
	/*   for(int i =0 ; i < Graph.size(); i++)
	{
	for(int j = 0; j < Graph.size(); j++)
	cout << Graph[i][j] << "\t";
	cout <<endl;
	}*/
	vector<int> shortestDist = Djkstra(Graph);
	for (int i = 0; i <shortestDist.size(); i++)
		cout << shortestDist[i] << endl;
	system("pause");
	return 0;
}

弗洛伊德算法

迪杰斯特拉算法是求图中某一顶点到其余各顶点的最短路径,如果求图中任一一对顶点间的最短路径,通常用弗洛伊德算法。

初始时要设置两个矩阵A和Path,A用来记录当前已经求得的任一两个顶点的最短路径的长度,Path用来记录当前两顶点间最短路径上要经过的中间节点。

对于每一个节点,参照上一步矩阵中的结果,进行计算。

3.最小生成树

普里姆算法(O(N^2))

普利姆算法的基本思想如下:从图中任意取出一个顶点,把它当成一棵树,然后从与这棵树相连接的变种选取一条最短(权值最小)的边,然后把这条边及其所连接的顶点也加入这棵树中,此时得到了一棵有两个顶点的树。然后从与这棵树相接的边中选取一条最短的边,并将这条边及其所连接的顶点加入这棵树中,得到一棵有三个顶点的树。以此类推,知道图中所有顶点都被并入树中为止。


#include <iostream>
#include<vector>
using namespace std;
pair<int, int> GetShortestEdge(const vector<vector<int> >& Graph, const vector<bool>& isIncluded)//求当前在MST之外距离MST最近的点的id
{
	int minDist = INT_MAX;
	pair<int, int> minEdge;
	for (int i = 0; i < Graph.size(); i++)//i为MST内的点
	{
		if (!isIncluded[i]) continue;//如果不在MST里面,则跳过
		for (int j = 0; j < Graph.size(); j++) //j为MST外的点
			if (!isIncluded[j] && Graph[i][j] < minDist) { //找到不在MST内但是距离MST最近的点
				minDist = Graph[i][j];
				minEdge = make_pair(i, j);
			}
	}
	return minEdge;
}
vector<pair<int, int> > Prim(const vector<vector<int> >& Graph) {
	vector<bool> isIncluded(Graph.size(), false);
	vector<pair<int, int> > MST;
	isIncluded[0] = true;
	//MST.push_back();
	for (int i = 1; i < Graph.size(); i++) {
		pair<int, int> minEdge = GetShortestEdge(Graph, isIncluded); //找到这次要放入的边i,j
		MST.push_back(minEdge); //放入
		isIncluded[minEdge.second] = true; //将j标记为已经放入
	}
	return MST;
}

void addEdge(const int& startP, const int& endP, const int& weight, vector<vector<int> >& Graph)
{
	Graph[startP][endP] = weight;
	Graph[endP][startP] = weight;
}

int main()
{
	int vertex_num = 6;
	vector<vector<int> > Graph(vertex_num, vector<int>(vertex_num, INT_MAX));
	addEdge(0, 1, 6, Graph);
	addEdge(0, 2, 1, Graph);
	addEdge(0, 3, 5, Graph);
	addEdge(1, 2, 5, Graph);
	addEdge(1, 4, 3, Graph);
	addEdge(2, 3, 5, Graph);
	addEdge(2, 4, 6, Graph);
	addEdge(2, 5, 4, Graph);
	addEdge(3, 5, 2, Graph);
	addEdge(4, 5, 6, Graph);
	vector<pair<int, int> >  MST = Prim(Graph);
	for (int i = 0; i < MST.size(); i++) //按照放入MST的顺序依次输出
		cout << MST[i].first + 1 << "->" << MST[i].second + 1 << endl;
	system("pause");
	return 0;
}

克鲁斯卡尔算法

基本思想
克鲁斯卡尔算法的思想比较简单,首先将图中按边权值从小到大排序,然后从最小边开始扫描各边,并检测当前边是否为候选边,即是否该边的并入会构成回路,如果不构成回路,则将该边并入生成树中,直到所有边都被检测完为止。

4.判断图是否有环

无向图

我们知道对于环1-2-3-4-1,每个节点的度都是2,基于此我们有如下算法(这是类似于有向图的拓扑排序):

  • 求出图中所有顶点的度,
  • 删除图中所有度<=1的顶点以及与该顶点相关的边,把与这些边相关的顶点的度减一
  • 如果还有度<=1的顶点重复步骤2
  • 最后如果还存在未被删除的顶点,则表示有环;否则没有环

有向图

利用拓扑排序:

  • 从有向图中选择一个 没有前驱(即入度为0)的顶点并输出。
  • 从图中删除该顶点和所有以它为起点的有向边。
  • 重复 1 和 2 直到当前的有向图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。

5.拓扑排序

对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若存在u到v的路径,则在拓扑排序序列中一定是u出现在v前边。

在一个有向图中找到一个拓扑排序序列的过程是:

  • 从有向图中选择一个 没有前驱(即入度为0)的顶点并输出。
  • 从图中删除该顶点和所有以它为起点的有向边。
  • 重复 1 和 2 直到剩余的图中不存在没有前驱的顶点为止。

四、B-树、B+树

1.B-树基本概念

B-树中所有结点中孩子结点个数的最大值成为B-树的阶,通常用m表示,从查找效率考虑,一般要求m>=3。一棵m阶B-树或者是一棵空树,或者是满足以下条件的m叉树。

(1)每个结点最多有m个分支(子树);而最少分支数要看是否为根结点,如果是根结点且不是叶子结点,则至少要有两个分支,非根非叶结点至少有ceil(m/2)个分支,这里ceil代表向上取整。

(2)如果一个结点有n-1个关键字,那么该结点有n个分支。这n-1个关键字按照递增顺序排列。

(3)每个结点的结构为:

n k1 k2 ... kn
p0 p1 p2 ... pn

其中,n为该结点中关键字的个数;ki为该结点的关键字且满足ki<ki+1;pi为该结点的孩子结点指针且满足pi所指结点上的关键字大于ki且小于ki+1,p0所指结点上的关键字小于k1,pn所指结点上的关键字大于kn。 

(4)结点内各关键字互不相等且按从小到大排列。

(5)叶子结点处于同一层;可以用空指针表示,是查找失败到达的位置。

平衡m叉查找树是指每个关键字的左侧子树与右侧子树的高度差的绝对值不超过1的查找树,其结点结构与上面提到的B-树结点结构相同,由此可见,B-树是平衡m叉查找树,但限制更强,要求所有叶结点都在同一层。

2.B+树基本概念

B+树是B-树的一种变形,它和B-树有很多相似之处。

(1)在B+树中,具有n个关键字的结点有n个分支,而在B-树中,具有n个关键字的结点含有n+1个关键字。

(2)在B+树中,每个结点(除根结点外)中的关键字个数n的取值为ceil(m/2) <= n <=m,根结点的取值范围为1<=n<=m,而在B-树中,他们的取值范围分别是ceil(m/2) -1<= n <=m-1和1<=n<=m-1。

(3)在B+树中叶子结点包含信息,并且包含了全部关键字,叶子结点引出的指针指向记录。

(4)在B+树中的所有非叶子结点仅起到一个索引的作用,即结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址,而在B-树中,每个关键字对应一个记录的存储地址。

(5)在B+树上有一个指针指向关键字最小的叶子节点,所有叶子节点链接成一个线性链表,而B-树没有。

3.B类树优势

为什么B类树可以进行优化存储呢?我们可以根据B类树的特点,构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的。

总的来说,B/B+树是为了磁盘或其它存储设备而设计的一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度(在下面B/B+树的性能分析中会提到)。B/B+树上操作的时间通常由存取磁盘的时间和CPU计算时间这两部分构成,而CPU的速度非常快,所以B树的操作效率取决于访问磁盘的次数,关键字总数相同的情况下B树的高度越小,磁盘I/O所花的时间越少。

为什么说B+树比B树更适合数据库索引?

数据库使用B+树肯定是为了提升查找效率。但是具体如何提升查找效率呢?查找数据,最简单的方式是顺序查找。但是对于几十万上百万,甚至上亿的数据库查询就很慢了。所以要对查找的方式进行优化,熟悉的二分查找,二叉树可以把速度提升到O(log(n,2)),查询的瓶颈在于树的深度,最坏的情况要查找到二叉树的最深层,由于,每查找深一层,就要访问更深一层的索引文件。在多达数G的索引文件中,这将是很大的开销。所以,尽量把数据结构设计的更为矮胖一点就可以减少访问的层数。在众多的解决方案中,B-/B+树很好的适合。

B树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点。也正因每个节点存储着非常多个关键字,树的深度就会非常的小。进而要执行的磁盘读取操作次数就会非常少,更多的是在内存中对读取进来的数据进行查找。

(1)B树在提高了IO性能的同时并没有解决元素遍历的效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

(2)B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。
 

 

发布了162 篇原创文章 · 获赞 60 · 访问量 7万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览