鏈表、隊列、圖、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讀寫次數就降低了。
 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章