圖
圖的基本介紹
爲什麼要有圖
- 前面我們學了線性表和樹,線性表侷限於一個直接前驅和一個直接後繼的關係,樹也只能有一個直接前驅也就是父節點。
- 當我們需要表示多對多的關係時, 這裏我們就用到了圖。
圖的舉例說明
- 圖是一種數據結構,其中結點可以具有零個或多個相鄰元素。兩個結點之間的連接稱爲邊。 結點也可以稱爲頂點。如圖:
圖的常用概念
圖的表示方式
圖的表示方式有兩種:二維數組表示(鄰接矩陣);鏈表表示(鄰接表)。
鄰接矩陣
- 鄰接矩陣是表示圖形中頂點之間相鄰關係的矩陣,對於n個頂點的圖而言,矩陣是的row和col表示的是1…n個點。
鄰接表
- 鄰接矩陣需要爲每個頂點都分配n個邊的空間,其實有很多邊都是不存在,會造成空間的一定損失。
- 鄰接表的實現只關心存在的邊,不關心不存在的邊。因此沒有空間浪費,鄰接表由數組+鏈表組成。
- 說明:
* 標號爲0的結點的相關聯的結點爲 1 2 3 4
* 標號爲1的結點的相關聯結點爲0 4
* 標號爲2的結點相關聯的結點爲 0 4 5
快速入門案例
需求
- 代碼實現如下圖結構:
- 思路分析:存儲頂點用String,使用 ArrayList (2) 保存矩陣 int[][] matrix
代碼實現
public class GraphDemo {
static class Graph {
/**
* 存儲頂點的集合
*/
private List<String> vertices;
/**
* 存儲邊
*/
private int[][] matrix;
/**
* 邊的個數
*/
private int edgeCount;
Graph(int n) {
// 初始化
matrix = new int[n][n];
vertices = new ArrayList<>(n);
edgeCount = 0;
}
/**
* 添加頂點
*/
void addVertex(String vertex) {
vertices.add(vertex);
}
/**
* 添加邊
*/
void addEdge(int v1, int v2, int weight) {
matrix[v1][v2] = weight;
matrix[v2][v1] = weight;
edgeCount++;
}
/**
* 返回結點的個數
*/
int getVertexCount() {
return vertices.size();
}
/**
* 返回邊的個數
*/
int getEdgeCount() {
return edgeCount;
}
/**
* 返回結點i(下標)對應的數據
*/
String getValueByIndex(int i) {
return vertices.get(i);
}
/**
* 返回v1和v2的權值
*/
int getWeight(int v1, int v2) {
return matrix[v1][v2];
}
/**
* 顯示圖對應的鄰接矩陣
*/
void show() {
for (int[] arr : matrix) {
System.out.println(Arrays.toString(arr));
}
}
}
public static void main(String[] args) {
// 結點的個數
int n = 5;
// 結點
String[] vs = {"A", "B", "C", "D", "E"};
// 創建圖
Graph graph = new Graph(n);
// 循環添加頂點
for (String v : vs) {
graph.addVertex(v);
}
// 添加邊
// A-B A-C B-C B-D B-E
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(1, 4, 1);
// 顯示
graph.show();
}
}
- 測試輸出:
[0, 1, 1, 0, 0]
[1, 0, 1, 1, 1]
[1, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
圖的遍歷
所謂圖的遍歷,即是對結點的訪問。一個圖有那麼多個結點,如何遍歷這些結點,需要特定策略,一般有兩種訪問策略:深度優先遍歷、廣度優先遍歷。
深度優先遍歷
基本介紹
圖的深度優先搜索**(Depth First Search,簡稱 DFS)**
- 深度優先遍歷,從初始訪問結點出發,初始訪問結點可能有多個鄰接結點,深度優先遍歷的策略就是首先訪問第一個鄰接結點,然後再以這個被訪問的鄰接結點作爲初始結點,訪問它的第一個鄰接結點, 可以這樣理解:每次都在訪問完當前結點後首先訪問當前結點的第一個鄰接結點。
- 我們可以看到,這樣的訪問策略是優先往縱向挖掘深入,而不是對一個結點的所有鄰接結點進行橫向訪問。
- 顯然,深度優先搜索是一個遞歸的過程。
代碼實現
- 算法步驟:
* 訪問初始結點v,並標記結點v爲已訪問
* 查找結點v的第一個鄰接結點w
* 若w存在,則繼續執行4,如果w不存在,則回到第1步,將從v的下一個結點繼續
* 若w未被訪問,對w進行深度優先遍歷遞歸(即把w當做另一個v,然後進行步驟123)
* 查找結點v的w鄰接結點的下一個鄰接結點,轉到步驟3
- 具體案例:要求對下圖進行深度優先搜索,從A開始遍歷
- 具體實現:
public class GraphDemo {
static class Graph {
/**
* 存儲頂點的集合
*/
private List<String> vertices;
/**
* 存儲邊
*/
private int[][] matrix;
/**
* 邊的個數
*/
private int edgeCount;
/**
* 定義一個數組boolen[],記錄某個結點是否被訪問
*/
private boolean[] isVisited;
Graph(int n) {
// 初始化
matrix = new int[n][n];
vertices = new ArrayList<>(n);
edgeCount = 0;
isVisited = new boolean[n];
}
/**
* 添加頂點
*/
void addVertex(String vertex) {
vertices.add(vertex);
}
/**
* 添加邊
*/
void addEdge(int v1, int v2, int weight) {
matrix[v1][v2] = weight;
matrix[v2][v1] = weight;
edgeCount++;
}
/**
* 返回結點的個數
*/
int getVertexCount() {
return vertices.size();
}
/**
* 返回邊的個數
*/
int getEdgeCount() {
return edgeCount;
}
/**
* 返回結點i(下標)對應的數據
*/
String getValueByIndex(int i) {
return vertices.get(i);
}
/**
* 返回v1和v2的權值
*/
int getWeight(int v1, int v2) {
return matrix[v1][v2];
}
/**
* 顯示圖對應的鄰接矩陣
*/
void show() {
for (int[] arr : matrix) {
System.out.println(Arrays.toString(arr));
}
}
/**
* 得到第一個鄰接結點的下標 w
*/
int getFirstNeighbor(int index) {
for (int j = 0; j < vertices.size(); j++) {
if (matrix[index][j] > 0) {
return j;
}
}
return -1;
}
/**
* 根據前一個鄰接結點的下標來獲取下一個鄰接結點
*/
int getNextNeighbor(int v1, int v2) {
for (int j = v2 + 1; j < vertices.size(); j++) {
if (matrix[v1][j] > 0) {
return j;
}
}
return -1;
}
/**
* 清空訪問記錄
*/
void clearVisited() {
for (int i = 0; i < isVisited.length; i++) {
isVisited[i] = false;
}
}
/**
* 對dfs進行一個重載,遍歷我們所有的結點,並進行dfs
*/
void dfs() {
// 遍歷所有的結點,進行dfs【回溯】
for (int i = 0; i < getVertexCount(); i++) {
if (!isVisited[i]) {
dfs(isVisited, i);
}
}
// 遍歷結束後把isVisited中的記錄清空、便於第二次遍歷
clearVisited();
System.out.println();
}
/**
* 對一個結點進行深度優先遍歷
* @param i 第一次就是0
*/
private void dfs(boolean[] isVisited, int i) {
// 首先訪問該結點,並輸出
System.out.print(getValueByIndex(i) + " -> ");
// 將該結點設置爲已訪問
isVisited[i] = true;
// 查找結點i的第一個鄰接結點w
int w = getFirstNeighbor(i);
while (w != -1) {
if (!isVisited[w]) {
dfs(isVisited, w);
}
// 如果w已經被訪問
w = getNextNeighbor(i, w);
}
}
}
public static void main(String[] args) {
// 結點的個數
int n = 5;
// 結點
String[] vs = {"A", "B", "C", "D", "E"};
// 創建圖
Graph graph = new Graph(n);
// 循環添加頂點
for (String v : vs) {
graph.addVertex(v);
}
// 添加邊
// A-B A-C B-C B-D B-E
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(1, 4, 1);
// 顯示
graph.show();
// 測試一下DFS
System.out.println("深度優先遍歷:");
graph.dfs();
}
}
- 結果輸出:
[0, 1, 1, 0, 0]
[1, 0, 1, 1, 1]
[1, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
深度遍歷:
A -> B -> C -> D -> E ->
廣度優先遍歷
基本介紹
圖的廣度優先搜索(Broad First Search),類似於一個分層搜索的過程,廣度優先遍歷需要使用一個隊列以保持訪問過的結點的順序,以便按這個順序來訪問這些結點的鄰接結點。
代碼實現
- 算法步驟:
* 訪問初始結點v並標記結點v爲已訪問
* 結點v入隊列
* 當隊列非空時,繼續執行,否則算法結束
* 出隊列,取得隊頭結點u
* 查找結點u的第一個鄰接結點w
* 若結點u的鄰接結點w不存在,則轉到步驟3;否則循環執行以下三個步驟:
* ① 若結點w尚未被訪問,則訪問結點w並標記爲已訪問
* ② 結點w入隊列
* ③ 查找結點u的繼w鄰接結點後的下一個鄰接結點w,轉到步驟6
- 具體實現:
/**
* 廣度優先遍歷
*/
public void bfs() {
// 遍歷所有的結點,進行bfs【回溯】
for (int i = 0; i < getVertexCount(); i++) {
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
// 遍歷結束後把isVisited中的記錄清空、便於第二次遍歷
clearVisited();
System.out.println();
}
/**
* 對一個結點進行廣度優先遍歷
*/
private void bfs(boolean[] isVisited, int i) {
// u:隊列的頭結點對應的下標,w:鄰接結點
int u, w;
// 隊列:記錄結點訪問的順序
LinkedList<Integer> queue = new LinkedList<>();
// 訪問結點
System.out.print(getValueByIndex(i) + " -> ");
isVisited[i] = true;
// 將結點加入隊列
queue.addLast(i);
while (!queue.isEmpty()) {
// 取出隊列的頭結點下標
u = queue.removeFirst();
// 得到第一個鄰接結點的下標
w = getFirstNeighbor(u);
while (w != -1) {
if (!isVisited[w]) {
System.out.print(getValueByIndex(w) + " -> ");
isVisited[w] = true;
// 入隊列
queue.addLast(w);
}
// 以u爲前驅結點,找w後面的鄰接點
w = getNextNeighbor(u, w); // 體現廣度優先
}
}
}
public static void main(String[] args) {
// 結點的個數
int n = 5;
// 結點
String[] vs = {"A", "B", "C", "D", "E"};
// 創建圖
Graph graph = new Graph(n);
// 循環添加頂點
for (String v : vs) {
graph.addVertex(v);
}
// 添加邊
// A-B A-C B-C B-D B-E
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(1, 4, 1);
// 顯示
graph.show();
// 測試一下DFS
System.out.println("深度優先遍歷:");
graph.dfs();
System.out.println("廣度優先遍歷:");
graph.bfs();
}
- 結果輸出:
[0, 1, 1, 0, 0]
[1, 0, 1, 1, 1]
[1, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
深度優先遍歷:
A -> B -> C -> D -> E ->
廣度優先遍歷:
A -> B -> C -> D -> E ->
深度優先 Vs 廣度優先
- 應用實例:
- 深度優先遍歷順序爲:1->2->4->8->5->3->6->7
- 廣度優先算法的遍歷順序爲:1->2->3->4->5->6->7->8
常用十種算法
非遞歸二分查找
基本介紹
- 前面我們講過了二分查找算法,是使用遞歸的方式,下面我們講解二分查找算法的非遞歸方式。
- 二分查找法只適用於從有序的數列中進行查找(比如數字和字母等),將數列排序後再進行查找。
- 二分查找法的運行時間爲對數時間O(㏒₂n) ,即查找到需要的目標位置最多隻需要㏒₂n步,假設從[0,99]的隊列(100個數,即n=100)中尋到目標數30,則需要查找步數爲㏒₂100 , 即最多需要查找7次( 26 < 100 < 27)。
代碼實現
public class BinarySearchNoRecur {
public static void main(String[] args) {
int[] arr = {1, 3, 8, 10, 11, 67, 100};
for (int a : arr) {
int index = search(arr, a);
System.out.printf("非遞歸二分查找 %d, index = %d\n", a, index);
}
System.out.printf("非遞歸二分查找 %d, index = %d\n", -8, search(arr, -8));
}
private static int search(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
}
- 結果輸出:
非遞歸二分查找 1, index = 0
非遞歸二分查找 3, index = 1
非遞歸二分查找 8, index = 2
非遞歸二分查找 10, index = 3
非遞歸二分查找 11, index = 4
非遞歸二分查找 67, index = 5
非遞歸二分查找 100, index = 6
非遞歸二分查找 -8, index = -1
分治算法
基本介紹
- 分治法是一種很重要的算法。字面上的解釋是“分而治之”,就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。這個技巧是很多高效算法的基礎,如排序算法(快速排序,歸併排序),傅立葉變換(快速傅立葉變換)……
- 分治算法可以求解的一些經典問題:二分搜索、大整數乘法、棋盤覆蓋、合併排序、快速排序、線性時間選擇、最接近點對問題、循環賽日程表、漢諾塔
基本步驟
- 分解:將原問題分解爲若干個規模較小,相互獨立,與原問題形式相同的子問題。
- 解決:若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題
- 合併:將各個子問題的解合併爲原問題的解。
算法的設計模式
- 其中|P|表示問題P的規模;n0爲一閾值,表示當問題P的規模不超過n0時,問題已容易直接解出,不必再繼續分解。ADHOC§是該分治法中的基本子算法,用於直接解小規模的問題P。因此,當P的規模不超過n0時直接用算法ADHOC§求解。算法MERGE(y1,y2,…,yk)是該分治法中的合併子算法,用於將P的子問題P1 ,P2 ,…,Pk的相應的解y1,y2,…,yk合併爲P的解。
最佳實踐-漢諾塔
基本介紹
- 漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。
- 假如每秒鐘一次,共需多長時間呢?移完這些金片需要5845.54億年以上,太陽系的預期壽命據說也就是數百億年。真的過了5845.54億年,地球上的一切生命,連同梵塔、廟宇等,都早已經灰飛煙滅。
思路分析
- 如果只有一個盤, A->C;
- 如果我們有 n >= 2 情況,我們總是可以看做是兩個盤:一個是最下邊的盤,一個是上面所有的盤【整體思想】;
- ① 先把最上面的盤 A->B
- ② 把最下邊的盤 A->C
- ③ 把B塔的所有盤 從 B->C
代碼實現
public class Hanoitower {
public static void main(String[] args) {
hanoiTower(5, 'A', 'B', 'C');
}
private static void hanoiTower(int num, char a, char b, char c) {
if (num == 1) {
// 1、如果只有一個盤, A->C
System.out.printf("第 %d 個盤從 %s -> %s\n", num, a, c);
} else {
// 2、如果我們有 n >= 2 情況,我們總是可以看做是兩個盤:一個是最下邊的盤,一個是上面所有的盤
// 先把最上面的盤 A->B
hanoiTower(num - 1, a, c, b);
// 把最下邊的盤 A->C
System.out.printf("第 %d 個盤從 %s -> %s\n", num, a, c);
// 把B塔的所有盤 從 B->C
hanoiTower(num - 1, b, a, c);
}
}
}
- 測試輸出:
第 1 個盤從 A -> C
第 2 個盤從 A -> B
第 1 個盤從 C -> B
第 3 個盤從 A -> C
第 1 個盤從 B -> A
第 2 個盤從 B -> C
第 1 個盤從 A -> C
第 4 個盤從 A -> B
第 1 個盤從 C -> B
第 2 個盤從 C -> A
第 1 個盤從 B -> A
第 3 個盤從 C -> B
第 1 個盤從 A -> C
第 2 個盤從 A -> B
第 1 個盤從 C -> B
第 5 個盤從 A -> C
第 1 個盤從 B -> A
第 2 個盤從 B -> C
第 1 個盤從 A -> C
第 3 個盤從 B -> A
第 1 個盤從 C -> B
第 2 個盤從 C -> A
第 1 個盤從 B -> A
第 4 個盤從 B -> C
第 1 個盤從 A -> C
第 2 個盤從 A -> B
第 1 個盤從 C -> B
第 3 個盤從 A -> C
第 1 個盤從 B -> A
第 2 個盤從 B -> C
第 1 個盤從 A -> C
動態規劃算法
動態規劃算法介紹
- 動態規劃(Dynamic Programming)算法的核心思想是:將大問題劃分爲小問題進行解決,從而一步步獲取最優解的處理算法。
- 動態規劃算法與分治算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。
- 與分治法不同的是:適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。 ( 即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解 )。
- 動態規劃可以通過填表的方式來逐步推進,得到最優解。
應用場景-揹包問題
- 有一個揹包,容量爲4磅 , 現有如下物品:
- 要求達到的目標爲裝入的揹包的總價值最大,並且重量不超出。
- 要求裝入的物品不能重複。
思路分析和圖解
- 揹包問題主要是指一個給定容量的揹包、若干具有一定價值和重量的物品,如何選擇物品放入揹包使物品的價值最大。其中又分01揹包和完全揹包(完全揹包指的是:每種物品都有無限件可用)。
- 這裏的問題屬於01揹包,即每個物品最多放一個。而無限揹包可以轉化爲01揹包。
- 算法的主要思想:利用動態規劃來解決。每次遍歷到的第i個物品,根據 w[i] 和 v[i] 來確定是否需要將該物品放入揹包中。即對於給定的n個物品,設v[i]、w[i]分別爲第i個物品的價值和重量,C爲揹包的容量。再令v[i][j] 表示在前i個物品中能夠裝入容量爲j的揹包中的最大價值。則我們有下面的結果:
- ① v[i][0]=v[0][j]=0:表示填入表 第一行和第一列是0
- ② 當 w[i]>j 時,v[i][j]=v[i-1][j]:當準備加入新增的商品的容量大於當前揹包的容量時,就直接使用上一個單元格的裝入策略
- ③ 當 j>=w[i] 時,v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :當準備加入的新增的商品的容量小於等於當前揹包的容量,則裝入的方式爲:
* v[i-1][j]:就是上一個單元格的裝入的最大值
* v[i]:表示當前商品的價值
* v[i-1][j-w[i]]:裝入i-1商品,到剩餘空間j-w[i]的最大值
* 當 j>=w[i] 時:v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
代碼實現
public class KnapsackProblem {
public static void main(String[] args) {
// 物品重量
int[] w = {1, 4, 3};
// 物品的價值
int[] val = {1500, 3000, 2000};
// 揹包的容量
int m = 4;
// 物品的個數
int n = val.length;
// 爲了記錄存放商品的情況,
int[][] path = new int[n + 1][m + 1];
// 創建二維數組
// v[i][j]: 表示在前i個物品中,能夠轉入容量爲j的揹包中的最大價值
int[][] v = new int[n + 1][m + 1];
// 1、初始化第一行、第一列,在本程序中可以不處理,因爲默認就是0
for (int i = 0; i < v.length; i++) {
v[i][0] = 0;
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0;
}
// 2、根據前面得到的公式來動態規劃處理
for (int i = 1; i < v.length; i++) {
for (int j = 1; j < v[0].length; j++) {
// 公式
if (w[i - 1] > j) {
v[i][j] = v[i - 1][j];
} else {
// 說明:因爲i是從1開始的,因此公式需要調整成如下
// v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
// 爲了記錄商品存放到揹包的情況,不能直接用上面的公式
if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
path[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
}
}
}
}
for (int i = 0; i < v.length; i++) {
for (int j = 0; j < v[i].length; j++) {
System.out.print(v[i][j] + " ");
}
System.out.println();
}
// 輸出最後我們是放入了哪些商品
// 下面這樣遍歷,會輸出所有放入揹包的情況,但其實我們只需要最後放入的
// for (int i = 0; i < path.length; i++) {
// for (int j = 0; j < path[i].length; j++) {
// if (path[i][j] == 1) {
// System.out.printf("第%d個商品放入揹包\n", i);
// }
// }
// }
int i = path.length - 1;
int j = path[0].length - 1;
while (i > 0 && j > 0) {
if (path[i][j] == 1) {
System.out.printf("第%d個商品放入揹包\n", i);
j -= w[i - 1];
}
i--;
}
}
}
- 測試輸出:
0 0 0 0 0
0 1500 1500 1500 1500
0 1500 1500 1500 3000
0 1500 1500 2000 3500
第3個商品放入揹包
第1個商品放入揹包
KMP算法
應用場景
- 有一個字符串 str1= ““硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好””,和一個子串 str2=“尚硅谷你尚硅你”;
- 現在要判斷 str1 是否含有 str2, 如果存在,就返回第一次出現的位置, 如果沒有,則返回-1。
暴力匹配算法
如果用暴力匹配的思路,並假設現在str1匹配到 i 位置,子串str2匹配到 j 位置,則有:
- 如果當前字符匹配成功(即str1[i] == str2[j]),則i++,j++,繼續匹配下一個字符;
- 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置爲0。
- 用暴力方法解決的話就會有大量的回溯,每次只移動一位,若是不匹配,移動到下一位接着判斷,浪費了大量的時間。(不可行!);
- 代碼實現:
public class ViolenceMatch {
public static void main(String[] args) {
System.out.println(match("硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好", "尚硅谷你尚硅你"));
}
private static int match(String src, String target) {
int i = 0, j = 0;
while (i < src.length() && j < target.length()) {
if (src.charAt(i) == target.charAt(j)) {
i++;
j++;
} else {
i = i - (j - 1);
j = 0;
}
if (j == target.length()) {
return i - j;
}
}
return -1;
}
}
// 結果:15
基本介紹
- KMP是一個解決模式串在文本串是否出現過,如果出現過,最早出現的位置的經典算法。
- Knuth-Morris-Pratt 字符串查找算法,簡稱爲 “KMP算法”,常用於在一個文本串S內查找一個模式串P 的出現位置,這個算法由Donald Knuth、Vaughan Pratt、James H. Morris三人於1977年聯合發表,故取這3人的姓氏命名此算法。
- KMP方法算法就利用之前判斷過信息,通過一個next數組,保存模式串中前後最長公共子序列的長度,每次回溯時,通過next數組找到,前面匹配過的位置,省去了大量的計算時間。
- 參考資料:添加鏈接描述
最佳實踐-字符串匹配問題
需求
- 有一個字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一個子串 str2=“ABCDABD”。
- 現在要判斷 str1 是否含有 str2, 如果存在,就返回第一次出現的位置, 如果沒有,則返回-1。
- 要求:使用KMP算法完成判斷,不能使用簡單的暴力匹配算法。
思路分析
- ① 首先,用Str1的第一個字符和Str2的第一個字符去比較,不符合則關鍵詞向後移動一位。
- ② 重複第一步,還是不符合,再後移。
- ③ 一直重複,直到Str1有一個字符與Str2的第一個字符符合爲止。
- ④ 接着比較字符串和搜索詞的下一個字符,還是符合。
- ⑤ 遇到Str1有一個字符與Str2對應的字符不符合。
- ⑥ 這時候,想到的是繼續遍歷Str1的下一個字符,重複第1步。其實是很不明智的,因爲此時BCD已經比較過了,沒有必要再做重複的工作,一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是”ABCDAB”。KMP 算法的想法是,設法利用這個已知信息,不要把”搜索位置”移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。
- ⑦ 怎麼做到把剛剛重複的步驟省略掉?可以對Str2計算出一張《部分匹配表》
- ⑧ 已知空格與D不匹配時,前面六個字符”ABCDAB”是匹配的。查表可知,最後一個匹配字符B對應的”部分匹配值”爲2,因此按照下面的公式算出向後移動的位數:移動位數 = 已匹配的字符數 - 對應的部分匹配值,因爲 6 - 2 等於4,所以將搜索詞向後移動 4 位。
- ⑨ 因爲空格與C不匹配,搜索詞還要繼續往後移。這時,已匹配的字符數爲2(“AB”),對應的“部分匹配值”爲0。所以,移動位數 = 2 - 0,結果爲 2,於是將搜索詞向後移 2 位。
- ⑩ 因爲空格與A不匹配,繼續後移一位。
- ⑪ 逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜索詞向後移動 4 位。
- ⑫ 逐位比較,直到搜索詞的最後一位,發現完全匹配,於是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向後移動 7 位,這裏就不再重複了。
部分匹配表怎麼產生?
代碼實現
public class KmpMatch {
public static void main(String[] args) {
String src = "BBC ABCDAB ABCDABCDABDE";
String target = "ABCDABD";
int[] next = kmpNext(target);
System.out.printf("Next[%s] = %s\n", target, Arrays.toString(next));
int index = match(src, target, next);
System.out.println("KMP匹配index = " + index);
}
/**
* kmp匹配算法
*/
private static int match(String src, String target, int[] next) {
for (int i = 0, j = 0; i < src.length(); i++) {
// kmp 算法的核心
while (j > 0 && src.charAt(i) != target.charAt(j)) {
j = next[j - 1];
}
if (src.charAt(i) == target.charAt(j)) {
j++;
}
if (j == target.length()) {
// 找到了
return i - j + 1;
}
}
return -1;
}
/**
* 獲取一個字符串(子串)的部分匹配值
*/
private static int[] kmpNext(String s) {
if (s == null) {
return null;
}
int len = s.length();
// 創建一個數組保存部分匹配值
int[] next = new int[len];
next[0] = 0;
for (int i = 1, j = 0; i < len; i++) {
// kmp 算法的核心
while (j > 0 && s.charAt(i) != s.charAt(j)) {
j = next[j - 1];
}
if (s.charAt(i) == s.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
// 結果輸出:
// Next[ABCDABD] = [0, 0, 0, 0, 1, 2, 0]
// KMP匹配index = 15
貪心算法
應用場景
- 假設存在下面需要付費的廣播臺,以及廣播臺信號可以覆蓋的地區。 如何選擇最少的廣播臺,讓所有的地區都可以接收到信號。
基本介紹
- **貪婪算法(貪心算法)**是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的算法。
- 貪婪算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果。
最佳實踐-集合覆蓋
思路分析
- 如何找出覆蓋所有地區的廣播臺的集合呢,使用窮舉法實現,列出每個可能的廣播臺的集合,這被稱爲冪集。假設總的有n個廣播臺,則廣播臺的組合總共有2ⁿ -1 個,假設每秒可以計算10個子集, 如圖:
- 使用貪婪算法,效率高:目前並沒有算法可以快速計算得到準備的值, 使用貪婪算法,則可以得到非常接近的解,並且效率高。選擇策略上,因爲需要覆蓋全部地區的最小集合:
- ① 遍歷所有的廣播電臺, 找到一個覆蓋了最多未覆蓋的地區的電臺(此電臺可能包含一些已覆蓋的地區,但沒有關係);
- ② 將這個電臺加入到一個集合中(比如ArrayList),想辦法把該電臺覆蓋的地區在下次比較時去掉;
- ③ 重複第1步直到覆蓋了全部的地區。
代碼實現
public class GreedyAlgorithm {
public static void main(String[] args) {
// 創建廣播電臺,放入Map
Map<String, Set<String>> broadcastMap = new LinkedHashMap<>(8);
// 將各個電臺放入broadcastMap
broadcastMap.put("K1", newHashSet("北京", "上海", "天津"));
broadcastMap.put("K2", newHashSet("廣州", "北京", "深圳"));
broadcastMap.put("K3", newHashSet("成都", "上海", "杭州"));
broadcastMap.put("K4", newHashSet("上海", "天津"));
broadcastMap.put("K5", newHashSet("杭州", "大連"));
// 所有地區的集合
Set<String> allAreas = getAllAreas(broadcastMap);
System.out.println("All Areas = " + allAreas);
// 創建一個List,存放選擇的電臺集合
List<String> selects = new ArrayList<>();
// 定義一個臨時的集合,在遍歷的過程中,存放遍歷過程中的電臺覆蓋的地區和當前還沒有覆蓋的地區的交集
Set<String> tempSet = new HashSet<>();
// 定義maxKey:保存在一次遍歷過程中,能夠覆蓋最大未覆蓋的地區對應的電臺的 key
// 如果 maxKey 不爲 null , 則會加入到 selects
String maxKey;
while (allAreas.size() > 0) {
// 每進行一次循環,都需要將maxKey置空
maxKey = null;
for (String key : broadcastMap.keySet()) {
tempSet.clear();
tempSet.addAll(broadcastMap.get(key));
// 求出 tempSet 和 allAreas 集合的交集, 交集會賦給 tempSet
tempSet.retainAll(allAreas);
// 如果當前這個集合包含的未覆蓋地區的數量,比 maxKey 指向的集合地區還多,就需要重置 maxKey
if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcastMap.get(maxKey).size())) {
maxKey = key;
}
}
if (maxKey != null) {
selects.add(maxKey);
// 將 maxKey 指向的廣播電臺覆蓋的地區,從 allAreas 去掉
allAreas.removeAll(broadcastMap.get(maxKey));
}
}
System.out.println("得到的結果是:" + selects);
}
@SafeVarargs
private static <E> Set<E> newHashSet(E... elements) {
return new HashSet<>(Arrays.asList(elements));
}
private static Set<String> getAllAreas(Map<String, Set<String>> broadcastMap) {
Set<String> res = new HashSet<>();
for (Set<String> value : broadcastMap.values()) {
res.addAll(value);
}
return res;
}
}
// 輸出:
// All Areas = [成都, 上海, 廣州, 天津, 大連, 杭州, 北京, 深圳]
// 得到的結果是:[K1, K2, K3, K5]
注意事項和細節
- 貪婪算法所得到的結果不一定是最優的結果(有時候會是最優解),但都是相對近似(接近)最優解的結果。
- 比如上題的算法選出的是K1, K2, K3, K5,符合覆蓋了全部的地區,但是我們發現 K2, K3,K4,K5 也可以覆蓋全部地區,如果K2 的使用成本低於K1,那麼我們上題的 K1, K2, K3, K5 雖然是滿足條件,但是並不是最優的。
普里姆算法
應用場景
看一個應用場景和問題:
- 勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在需要修路把7個村莊連通。
- 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里。
- 問:如何修路保證各個村莊都能連通,並且總的修建公路總里程最短?
- 一種思路是,將10條邊,連接即可,但是總的里程數不是最小。正確的思路,就是儘可能的選擇少的路線,並且每條路線最小,保證總里程數最少。
最小生成樹
修路問題本質就是就是最小生成樹問題, 先介紹一下最小生成樹(Minimum Cost Spanning Tree),簡稱MST。
- 給定一個帶權的無向連通圖,如何選取一棵生成樹,使樹上所有邊上權的總和爲最小,這叫最小生成樹。
- N個頂點,一定有N-1條邊,包含全部頂點,N-1條邊都在圖中,如下圖:
求最小生成樹的算法主要是普里姆算法和克魯斯卡爾算法
基本介紹
- 普利姆(Prim)算法求最小生成樹,也就是在包含n個頂點的連通圖中,找出只有(n-1)條邊包含所有n個頂點的連通子圖,也就是所謂的極小連通子圖。
- 普利姆的算法如下:
- ① 設G=(V,E)是連通網,T=(U,D)是最小生成樹,V,U是頂點集合,E,D是邊的集合。
- ② 若從頂點u開始構造最小生成樹,則從集合V中取出頂點u放入集合U中,標記頂點v的visited[u]=1。
- ③ 若集合U中頂點ui與集合V-U中的頂點vj之間存在邊,則尋找這些邊中權值最小的邊,但能構成迴路,將頂點vj加入集合U中,將邊(ui,vj)加入集合D中,標記visited[vj]=1。
- ④ 重複步驟②,直到U與V相等,即所有頂點都被標記爲訪問過,此時D中有n-1條邊。
最佳實踐-修路問題
思路分析
代碼實現
public class PrimCase {
/**
* 定義圖
*/
static class Graph {
/**
* 圖的頂點數據
*/
char[] vertices;
/**
* 圖的邊,採用鄰接矩陣
*/
int[][] matrix;
/**
* 圖的頂點數
*/
int vertexCount;
Graph(int n) {
vertices = new char[n];
matrix = new int[n][n];
vertexCount = n;
}
}
/**
* 定義最小生成樹
*/
static class MinTreeGraph {
Graph graph;
MinTreeGraph(Graph graph) {
this.graph = graph;
}
MinTreeGraph createGraph(char[] vertices, int[][] matrix) {
int vCount = graph.vertexCount;
for (int i = 0; i < vCount; i++) {
graph.vertices[i] = vertices[i];
System.arraycopy(matrix[i], 0, graph.matrix[i], 0, vCount);
}
return this;
}
/**
* 編寫普里姆算法,得到最小生成樹
*
* @param v 表示從圖的第幾個頂點開始生成
*/
void primTree(int v) {
int vCount = graph.vertexCount;
// 標記結點是否被訪問,1:被訪問
int[] visited = new int[vCount];
visited[v] = 1;
// 定義v1、v2記錄兩個頂點的下標
int v1 = -1, v2 = -1;
// 定義一個變量,存放最小權值的邊
int minW = Integer.MAX_VALUE;
for (int k = 1; k < vCount; k++) {
// 這個是確定每一次生成的子圖,和那個結點的距離最近
for (int i = 0; i < vCount; i++) {
// i結點表示被訪問過的結點,j結點表示沒有訪問過的結點
for (int j = 0; j < vCount; j++) {
if (visited[i] == 1 && visited[j] == 0 && graph.matrix[i][j] < minW) {
minW = graph.matrix[i][j];
v1 = i;
v2 = j;
}
}
}
// for循環結束後,就找到了一條最小的邊
System.out.printf("邊 <%s, %s>,權值:%d\n", graph.vertices[v1], graph.vertices[v2], minW);
// 將當前這個結點標記爲已訪問
visited[v2] = 1;
// 重置minW
minW = Integer.MAX_VALUE;
}
}
void show() {
for (int[] arr : graph.matrix) {
System.out.println(Arrays.toString(arr));
}
}
}
public static void main(String[] args) {
// 定義圖中的頂點和邊
char[] vertices = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int M = Integer.MAX_VALUE;
int[][] matrix = new int[][]{
{M, 5, 7, M, M, M, 2}, // A
{5, M, M, 9, M, M, 3}, // B
{7, M, M, M, 8, M, M}, // C
{M, 9, M, M, M, 4, M}, // D
{M, M, 8, M, M, 5, 4}, // E
{M, M, M, 4, 5, M, 6}, // F
{2, 3, M, M, 4, 6, M}, // G
};
MinTreeGraph minTreeGraph = new MinTreeGraph(new Graph(vertices.length))
.createGraph(vertices, matrix);
// minTreeGraph.show();
minTreeGraph.primTree(0);
}
}
- 結果輸出:
邊 <A, G>,權值:2
邊 <G, B>,權值:3
邊 <G, E>,權值:4
邊 <E, F>,權值:5
邊 <F, D>,權值:4
邊 <A, C>,權值:7
克魯斯卡爾算法
應用場景
- 公交站問題
- 某城市新增7個站點(A, B, C, D, E, F, G) ,現在需要修路把7個站點連通。
- 各個站點的距離用邊線表示(權) ,比如 A – B 距離 12公里。
- 問:如何修路保證各個站點都能連通,並且總的修建公路總里程最短?
基本介紹
- 克魯斯卡爾(Kruskal)算法,是用來求加權連通圖的最小生成樹的算法。
- **基本思想:**按照權值從小到大的順序選擇n-1條邊,並保證這n-1條邊不構成迴路。
- **具體做法:**首先構造一個只含n個頂點的森林,然後依權值從小到大從連通網中選擇邊加入到森林中,並使森林中不產生迴路,直至森林變成一棵樹爲止。
最佳實踐-公交站問題
思路分析
- 在含有n個頂點的連通圖中選擇n-1條邊,構成一棵極小連通子圖,並使該連通子圖中n-1條邊上權值之和達到最小,則稱其爲連通網的最小生成樹。
- 例如,對於如上圖G4所示的連通網可以有多棵權值總和不相同的生成樹。
- 克魯斯卡爾算法圖解步驟:
- 最終最小生成樹的邊依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
克魯斯卡爾算法分析
根據前面介紹的克魯斯卡爾算法的基本思想和做法,我們能夠了解到,克魯斯卡爾算法重點需要解決的以下兩個問題:
- 問題一:對圖的所有邊按照權值大小進行排序。
- 問題二:將邊添加到最小生成樹中時,怎麼樣判斷是否形成了迴路。
- 問題一很好解決,採用排序算法進行排序即可。
- 問題二處理方式是:記錄頂點在"最小生成樹"中的終點,頂點的終點是"在最小生成樹中與它連通的最大頂點"。然後每次需要將一條邊添加到最小生存樹時,判斷該邊的兩個頂點的終點是否重合,重合的話則會構成迴路。
- 如何判斷是否構成迴路-舉例說明:
- 在將<E,F> <C,D> <D,E>加入到最小生成樹R中之後,這幾條邊的頂點就都有了終點:C的終點是F、D的終點是F、E的終點是F、E的終點是F。【終點:就是將所有頂點按照從小到大的順序排列好之後;某個頂點的終點就是"與它連通的最大頂點"】
- 因此,接下來,雖然<C,E>是權值最小的邊。但是C和E的終點都是F,即它們的終點相同,因此,將<C,E>加入最小生成樹的話,會形成迴路。這就是判斷迴路的方式。也就是說,我們加入的邊的兩個頂點不能都指向同一個終點,否則將構成迴路。
代碼實現
public class KruskalCase {
/**
* 定義圖
*/
static class Graph {
/**
* 圖的頂點數據
*/
char[] vertices;
/**
* 圖的邊,採用鄰接矩陣
*/
int[][] matrix;
/**
* 圖的頂點數
*/
int vertexCount;
/**
* 圖的邊數
*/
int edgeCount;
Graph(int n) {
vertices = new char[n];
matrix = new int[n][n];
vertexCount = n;
}
}
/**
* 定義邊
*/
static class Edge implements Comparable<Edge> {
/**
* 邊的起點
*/
char fromV;
/**
* 邊的終點
*/
char toV;
/**
* 邊的權值
*/
int weight;
Edge(char fromV, char toV, int weight) {
this.fromV = fromV;
this.toV = toV;
this.weight = weight;
}
@Override
public String toString() {
return "Edge <" + fromV + "--" + toV + ">=" + weight;
}
@Override
public int compareTo(Edge o) {
// 從小到大排序
return this.weight - o.weight;
}
}
/**
* 定義最小生成樹
*/
static class MinTreeGraph {
Graph graph;
List<Edge> edges = new ArrayList<>();
MinTreeGraph(Graph graph) {
this.graph = graph;
}
MinTreeGraph createGraph(char[] vertices, int[][] matrix) {
int vCount = graph.vertexCount;
for (int i = 0; i < vCount; i++) {
graph.vertices[i] = vertices[i];
System.arraycopy(matrix[i], 0, graph.matrix[i], 0, vCount);
}
// 統計邊
for (int i = 0; i < vCount; i++) {
for (int j = i + 1; j < vCount; j++) {
if (matrix[i][j] != MAX) {
edges.add(new Edge(vertices[i], vertices[j], matrix[i][j]));
}
}
}
graph.edgeCount = edges.size();
return this;
}
List<Edge> kruskal() {
// 用於保存"已有最小生成樹" 中的每個頂點在最小生成樹中的終點
int[] ends = new int[graph.edgeCount];
// 創建結果集合, 保存最後的最小生成樹
List<Edge> results = new ArrayList<>();
// /遍歷 edges 數組,將邊添加到最小生成樹中時,判斷是準備加入的邊否形成了迴路,如果沒有,就加入 rets, 否則不能加入
List<Edge> edges = getSortedEdges();
for (Edge edge : edges) {
// 獲取邊的兩個頂點
int formIndex = getPosition(edge.fromV);
int toIndex = getPosition(edge.toV);
// 分別獲取這兩個頂點的終點
int fromEnd = getEndIndex(ends, formIndex);
int toEnd = getEndIndex(ends, toIndex);
// 是否構成迴路
if (fromEnd != toEnd) {
// 設置fromEnd在"已有最小生成樹"中的終點
ends[fromEnd] = toEnd;
results.add(edge);
}
}
return results;
}
List<Edge> getSortedEdges() {
Collections.sort(edges);
return edges;
}
/**
* 對邊進行排序處理
*/
void sortEdge() {
Collections.sort(edges);
}
/**
* 返回頂點的下標
*/
int getPosition(char c) {
for (int i = 0; i < graph.vertices.length; i++) {
if (graph.vertices[i] == c) {
return i;
}
}
return -1;
}
/**
* 獲取下標爲i的頂點的終點,用於後面判斷兩個頂點的終點是否相同
*
* @param ends 記錄了各個頂點對應的終點是哪個
* @param i 表示傳入的頂點對應的下標
*/
int getEndIndex(int[] ends, int i) {
while (ends[i] != 0) {
i = ends[i];
}
return i;
}
/**
* 打印邊
*/
void showEdges() {
System.out.println(edges);
}
/**
* 打印鄰接矩陣
*/
void show() {
System.out.println("鄰接矩陣:");
for (int[] arr : graph.matrix) {
for (int a : arr) {
System.out.printf("%12d\t", a);
}
System.out.println();
}
}
}
private static final int MAX = Integer.MAX_VALUE;
public static void main(String[] args) {
// 定義圖中的頂點和邊
char[] vertices = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = new int[][]{
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/{0, 12, MAX, MAX, MAX, 16, 14},
/*B*/{12, 0, 10, MAX, MAX, 7, MAX},
/*C*/{MAX, 10, 0, 3, 5, 6, MAX},
/*D*/{MAX, MAX, 3, 0, 4, MAX, MAX},
/*E*/{MAX, MAX, 5, 4, 0, 2, 8},
/*F*/{16, 7, 6, MAX, 2, 0, 9},
/*G*/{14, MAX, MAX, MAX, 8, 9, 0},
};
MinTreeGraph minTreeGraph = new MinTreeGraph(new Graph(vertices.length))
.createGraph(vertices, matrix);
minTreeGraph.show();
// System.out.println(minTreeGraph.graph.edgeCount);
System.out.println("排序前:");
minTreeGraph.showEdges();
minTreeGraph.sortEdge();
System.out.println("排序後:");
minTreeGraph.showEdges();
List<Edge> edges = minTreeGraph.kruskal();
System.out.println("克魯斯卡爾最小生成樹結果:" + edges);
}
}
- 結果輸出:
鄰接矩陣:
0 12 2147483647 2147483647 2147483647 16 14
12 0 10 2147483647 2147483647 7 2147483647
2147483647 10 0 3 5 6 2147483647
2147483647 2147483647 3 0 4 2147483647 2147483647
2147483647 2147483647 5 4 0 2 8
16 7 6 2147483647 2 0 9
14 2147483647 2147483647 2147483647 8 9 0
排序前:
[Edge <A--B>=12, Edge <A--F>=16, Edge <A--G>=14, Edge <B--C>=10, Edge <B--F>=7, Edge <C--D>=3, Edge <C--E>=5, Edge <C--F>=6, Edge <D--E>=4, Edge <E--F>=2, Edge <E--G>=8, Edge <F--G>=9]
排序後:
[Edge <E--F>=2, Edge <C--D>=3, Edge <D--E>=4, Edge <C--E>=5, Edge <C--F>=6, Edge <B--F>=7, Edge <E--G>=8, Edge <F--G>=9, Edge <B--C>=10, Edge <A--B>=12, Edge <A--G>=14, Edge <A--F>=16]
克魯斯卡爾最小生成樹結果:[Edge <E--F>=2, Edge <C--D>=3, Edge <D--E>=4, Edge <B--F>=7, Edge <E--G>=8, Edge <A--B>=12]
迪傑斯特拉算法
基本介紹
- 迪傑斯特拉(Dijkstra)算法是典型最短路徑算法,用於計算一個結點到其他結點的最短路徑。 它的主要特點是以起始點爲中心向外層層擴展(廣度優先搜索思想),直到擴展到終點爲止。
算法過程
設置出發頂點爲v,頂點集合V{v1,v2,vi…},v到V中各頂點的距離構成距離集合Dis,Dis{d1,d2,di…},Dis集合記錄着v到圖中各頂點的距離(到自身可以看作0,v到vi距離對應爲di)。
- 從Dis中選擇值最小的di並移出Dis集合,同時移出V集合中對應的頂點vi,此時的v到vi即爲最短路徑。
- 更新Dis集合,更新規則爲:比較v到V集合中頂點的距離值,與v通過vi到V集合中頂點的距離值,保留值較小的一個(同時也應該更新頂點的前驅節點爲vi,表明是通過vi到達的)。
- 重複執行兩步驟,直到最短路徑頂點爲目標頂點即可結束。
最佳實踐-最短路徑
需求
- 戰爭時期,勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在有六個郵差,從G點出發,需要分別把郵件分別送到 A, B, C , D, E, F 六個村莊,各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里。
- 問:如何計算出G村莊到 其它各個村莊的最短距離?
- 如果從其它點出發到各個點的最短距離又是多少?
思路分析
代碼實現
public class DijkstraCase {
/**
* 定義圖
*/
static class Graph {
char[] vertices;
int[][] matrix;
VisitedVertex vv;
Graph(char[] vertices, int[][] matrix) {
this.vertices = vertices;
this.matrix = matrix;
}
void show() {
System.out.println("鄰接矩陣:");
for (int[] arr : matrix) {
for (int a : arr) {
System.out.printf("%12d\t", a);
}
System.out.println();
}
}
/**
* 迪傑斯特拉算法實現
*
* @param index 出發頂點的索引下標
*/
void dsj(int index) {
System.out.printf("==>> 從頂點%s出發到各個頂點的最短路徑情況:\n", vertices[index]);
vv = new VisitedVertex(vertices.length, index);
// 更新 index 頂點到周圍頂點的距離和前驅頂點
update(index);
for (int j = 1; j < vertices.length; j++) {
// 選擇並返回新的訪問頂點
index = vv.updateArr();
update(index);
}
// 顯示結果
vv.show();
// 最短距離
int count = 0;
for (int i : vv.dis) {
if (i != M) {
System.out.print(vertices[count] + "(" + i + ")");
} else {
System.out.println("N ");
}
count++;
}
System.out.println();
}
/**
* 更新index下標頂點到周圍頂點的距離和周圍頂點的前驅結點
*/
void update(int index) {
int len;
for (int j = 0; j < matrix[index].length; j++) {
// len含義:出發頂點到index頂點的距離 + 從index頂點到j頂點的距離的和
len = vv.getDis(index) + matrix[index][j];
// 如果j頂點沒有被訪問過,並且len小於出發頂點到j頂點的距離,就需要更新
if (!vv.in(j) && len < vv.getDis(j)) {
// 更新 j 頂點的前驅爲 index 頂點
vv.updatePre(j, index);
// 更新出發頂點到 j 頂點的距離
vv.updateDis(j, len);
}
}
}
}
static class VisitedVertex {
/**
* 記錄各個頂點是否訪問過 1 表示訪問過,0 未訪問,會動態更新
*/
int[] alreadyArr;
/**
* 每個下標對應的值爲前一個頂點下標, 會動態更新
*/
int[] preVisited;
/**
* 記錄出發頂點到其他所有頂點的距離,比如 G 爲出發頂點,就會記錄 G 到其它頂點的距離,會動態更新,求
* 的最短距離就會存放到 dis
*/
int[] dis;
/**
* @param length 表示頂點的個數
* @param index 出發頂點對應的下標,比如G,下標爲6
*/
VisitedVertex(int length, int index) {
this.alreadyArr = new int[length];
this.preVisited = new int[length];
this.dis = new int[length];
// 初始化dis
Arrays.fill(dis, M);
// 設置出發頂點被訪問
this.alreadyArr[index] = 1;
// 設置出發頂點的訪問距離爲0
this.dis[index] = 0;
}
/**
* 判斷index頂點是否被訪問過
*/
boolean in(int index) {
return alreadyArr[index] == 1;
}
/**
* 更新出發頂點到index頂點的距離
*/
void updateDis(int index, int len) {
dis[index] = len;
}
/**
* 更新pre這個頂點的前驅頂點爲index頂點
*/
void updatePre(int pre, int index) {
preVisited[pre] = index;
}
/**
* 返回出發頂點到index的距離
*/
int getDis(int index) {
return dis[index];
}
/**
* 繼續選擇並返回新的訪問頂點,比如這裏的G完成後,就是A作爲新的訪問頂點(不是出發頂點)
*/
int updateArr() {
int min = M, index = 0;
for (int i = 0; i < alreadyArr.length; i++) {
if (alreadyArr[i] == 0 && dis[i] < min) {
min = dis[i];
index = i;
}
}
// 更新index頂點被訪問
alreadyArr[index] = 1;
return index;
}
/**
* 顯示訪問結果,即三個數組的情況
*/
void show() {
System.out.println("alreadyArr = " + Arrays.toString(alreadyArr));
System.out.println("preVisited = " + Arrays.toString(preVisited));
System.out.println("dis = " + Arrays.toString(dis));
}
}
private static final int M = 65535;
public static void main(String[] args) {
char[] vertices = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = {
{M, 5, 7, M, M, M, 2},
{5, M, M, 9, M, M, 3},
{7, M, M, M, 8, M, M},
{M, 9, M, M, M, 4, M},
{M, M, 8, M, M, 5, 4},
{M, M, M, 4, 5, M, 6},
{2, 3, M, M, 4, 6, M},
};
// 創建圖對象
Graph graph = new Graph(vertices, matrix);
// 打印圖
graph.show();
graph.dsj(6);
graph.dsj(2);
}
}
- 結果輸出:
鄰接矩陣:
65535 5 7 65535 65535 65535 2
5 65535 65535 9 65535 65535 3
7 65535 65535 65535 8 65535 65535
65535 9 65535 65535 65535 4 65535
65535 65535 8 65535 65535 5 4
65535 65535 65535 4 5 65535 6
2 3 65535 65535 4 6 65535
==>> 從頂點G出發的最短路徑情況:
alreadyArr = [1, 1, 1, 1, 1, 1, 1]
preVisited = [6, 6, 0, 5, 6, 6, 0]
dis = [2, 3, 9, 10, 4, 6, 0]
A(2)B(3)C(9)D(10)E(4)F(6)G(0)
==>> 從頂點C出發的最短路徑情況:
alreadyArr = [1, 1, 1, 1, 1, 1, 1]
preVisited = [2, 0, 0, 5, 2, 4, 0]
dis = [7, 12, 0, 17, 8, 13, 9]
A(7)B(12)C(0)D(17)E(8)F(13)G(9)
弗洛伊德算法
基本介紹
- 和Dijkstra算法一樣,弗洛伊德(Floyd)算法也是一種用於尋找給定的加權圖中頂點間最短路徑的算法。該算法名稱以創始人之一、1978年圖靈獎獲得者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德命名。
- 弗洛伊德算法(Floyd)計算圖中各個頂點之間的最短路徑。
- 迪傑斯特拉算法用於計算圖中某一個頂點到其他頂點的最短路徑。
- 弗洛伊德算法 VS 迪傑斯特拉算法:迪傑斯特拉算法通過選定的被訪問頂點,求出從出發訪問頂點到其他頂點的最短路徑;弗洛伊德算法中每一個頂點都是出發訪問點,所以需要將每一個頂點看做被訪問頂點,求出從每一個頂點到其他頂點的最短路徑。
算法分析
- 設置頂點vi到頂點vk的最短路徑已知爲Lik,頂點vk到vj的最短路徑已知爲Lkj,頂點vi到vj的路徑爲Lij,則vi到vj的最短路徑爲:min((Lik+Lkj),Lij),vk的取值爲圖中所有頂點,則可獲得vi到vj的最短路徑。
- 至於vi到vk的最短路徑Lik或者vk到vj的最短路徑Lkj,是以同樣的方式獲得。
- 弗洛伊德(Floyd)算法圖解分析-舉例說明:
最佳實踐-最短路徑
需求
- 勝利鄉有7個村莊(A, B, C, D, E, F, G),各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里,問:如何計算出各村莊到 其它各村莊的最短距離?
算法圖解
- 第一輪循環中,以A(下標爲:0)作爲中間頂點,距離表和前驅關係更新爲:
- 分析如下:
- ① 以A頂點作爲中間頂點是,B->A->C的距離由N->9,同理C到B;C->A->G的距離由N->12,同理G到C。
- ② 更換中間頂點,循環執行操作,直到所有頂點都作爲中間頂點更新後,計算結束。
代碼實現
public class FloydCase {
/**
* 定義圖
*/
static class Graph {
/**
* 頂點數組
*/
char[] vertices;
/**
* 記錄各個頂點出發到其它各個頂點的距離,最後的結果也是保留在該數組中
*/
int[][] dis;
/**
* 保存到達目標頂點的前驅頂點
*/
int[][] pre;
Graph(char[] vertices, int[][] matrix) {
this.vertices = vertices;
this.dis = matrix;
int len = vertices.length;
this.pre = new int[len][len];
// 對數組pre初始化
for (int i = 0; i < len; i++) {
Arrays.fill(pre[i], i);
}
}
/**
* 顯示pre和dis
*/
void show() {
for (int k = 0; k < dis.length; k++) {
// 輸出pre的一行數據
for (int i = 0; i < dis.length; i++) {
System.out.printf("%8s\t", vertices[pre[k][i]]);
}
System.out.println();
// 輸出dis的一行數據
for (int i = 0; i < dis.length; i++) {
System.out.printf("%8s\t", String.format("%s->%s(%s)", vertices[k], vertices[i], dis[k][i] == N ? "N" : dis[k][i]));
}
System.out.println();
System.out.println();
}
}
/**
* 弗洛伊德算法實現
*/
void floyd() {
// 保存距離
int len = 0;
// 對中間頂點進行遍歷,k就是中間頂點的索引下標
for (int k = 0; k < dis.length; k++) {
// 從i頂點出發[A, B, C, D, E, F, G]
for (int i = 0; i < dis.length; i++) {
for (int j = 0; j < dis.length; j++) {
len = dis[i][k] + dis[k][j];
if (len < dis[i][j]) {
// 更新距離
dis[i][j] = len;
// 更新前驅結點
pre[i][j] = pre[k][j];
}
}
}
}
System.out.println("==>> 弗洛伊德算法求圖的各個頂點的到其它頂點的最短路徑輸出:");
show();
}
}
private static final int N = 65535;
public static void main(String[] args) {
char[] vertices = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = {
{0, 5, 7, N, N, N, 2},
{5, 0, N, 9, N, N, 3},
{7, N, 0, N, 8, N, N},
{N, 9, N, 0, N, 4, N},
{N, N, 8, N, 0, 5, 4},
{N, N, N, 4, 5, 0, 6},
{2, 3, N, N, 4, 6, 0},
};
Graph graph = new Graph(vertices, matrix);
System.out.println("初始情況:");
graph.show();
graph.floyd();
}
}
- 結果輸出:
初始情況:
A A A A A A A
A->A(0) A->B(5) A->C(7) A->D(N) A->E(N) A->F(N) A->G(2)
B B B B B B B
B->A(5) B->B(0) B->C(N) B->D(9) B->E(N) B->F(N) B->G(3)
C C C C C C C
C->A(7) C->B(N) C->C(0) C->D(N) C->E(8) C->F(N) C->G(N)
D D D D D D D
D->A(N) D->B(9) D->C(N) D->D(0) D->E(N) D->F(4) D->G(N)
E E E E E E E
E->A(N) E->B(N) E->C(8) E->D(N) E->E(0) E->F(5) E->G(4)
F F F F F F F
F->A(N) F->B(N) F->C(N) F->D(4) F->E(5) F->F(0) F->G(6)
G G G G G G G
G->A(2) G->B(3) G->C(N) G->D(N) G->E(4) G->F(6) G->G(0)
==>> 弗洛伊德算法求圖的各個頂點的到其它頂點的最短路徑輸出:
A A A F G G A
A->A(0) A->B(5) A->C(7) A->D(12) A->E(6) A->F(8) A->G(2)
B B A B G G B
B->A(5) B->B(0) B->C(12) B->D(9) B->E(7) B->F(9) B->G(3)
C A C F C E A
C->A(7) C->B(12) C->C(0) C->D(17) C->E(8) C->F(13) C->G(9)
G D E D F D F
D->A(12) D->B(9) D->C(17) D->D(0) D->E(9) D->F(4) D->G(10)
G G E F E E E
E->A(6) E->B(7) E->C(8) E->D(9) E->E(0) E->F(5) E->G(4)
G G E F F F F
F->A(8) F->B(9) F->C(13) F->D(4) F->E(5) F->F(0) F->G(6)
G G A F G G G
G->A(2) G->B(3) G->C(9) G->D(10) G->E(4) G->F(6) G->G(0)
馬踏棋盤算法
基本介紹
- 馬踏棋盤算法也被稱爲騎士周遊問題,將馬隨機放在國際象棋的8×8棋盤Board[0~7][0~7]的某個方格中,馬按走棋規則(馬走日字)進行移動。要求每個方格只進入一次,走遍棋盤上全部64個方格。
- 遊戲演示:添加鏈接描述
最佳實踐-馬踏棋盤
思路分析
- 馬踏棋盤問題(騎士周遊問題)實際上是圖的深度優先搜索(DFS)的應用。
- 如果使用回溯(就是深度優先搜索)來解決,假如馬兒踏了53個點,如圖:走到了第53個,座標(1,0),發現已經走到盡頭,沒辦法,那就只能回退了,查看其他的路徑,就在棋盤上不停的回溯…… ,思路分析+代碼實現。
- 分析第一種方式的問題,並使用貪心算法(greedyalgorithm)進行優化,解決馬踏棋盤問題。
- 使用前面的遊戲來驗證算法是否正確。
代碼實現
public class HorseChessboard {
/**
* 棋盤的行數和列數
*/
private static int X;
private static int Y;
/**
* 創建一個數組,標記棋盤的各個位置是否被訪問過
*/
private static boolean[] visited;
/**
* 使用一個屬性,標記是否棋盤的所有位置都被訪問
* 如果爲 true,表示成功
*/
private static boolean finished;
public static void main(String[] args) {
X = 8;
Y = 8;
System.out.printf("騎士周遊[%d * %d]算法,開始運行~~\n", X, Y);
// 馬兒初始位置的行、列
int row = 0, column = 0;
// 創建棋盤
int[][] chessboard = new int[X][Y];
// 初始值都是 false
visited = new boolean[X * Y];
long startTime = System.currentTimeMillis();
traversalChessboard(chessboard, row, column, 1);
long endTime = System.currentTimeMillis();
System.out.println("共耗時: " + (endTime - startTime) + " 毫秒");
//輸出棋盤的最後情況
for (int[] rows : chessboard) {
for (int step : rows) {
System.out.print(step + "\t");
}
System.out.println();
}
}
private static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
chessboard[row][column] = step;
// 標記已訪問
visited[row * X + column] = true;
// 獲取當前位置可以走的下一個位置的集合
List<Point> points = next(new Point(column, row));
// 對 points 進行排序,排序的規則就是對 points 的所有的 Point 對象的下一步的位置的數目,進行非遞減排序
sort(points);
// 遍歷points
while (points.size() > 0) {
// 取出下一個可以走的位置
Point p = points.remove(0);
// 判斷是否訪問過
if (!visited[p.y * X + p.x]) {
traversalChessboard(chessboard, p.y, p.x, step + 1);
}
}
// 判斷馬兒是否完成了任務,使用 step 和應該走的步數比較,如果沒有達到數量,則表示沒有完成任務,將整個棋盤置 0
// 說明: step < X * Y 成立的情況有兩種:1、棋盤到目前位置,仍然沒有走完,2、棋盤處於一個回溯過程
if (step < X * Y && !finished) {
chessboard[row][column] = 0;
visited[row * X + column] = false;
} else {
finished = true;
}
}
private static List<Point> next(Point curPoint) {
List<Point> points = new ArrayList<>();
Point p = new Point();
/*0*/
if ((p.x = curPoint.x + 2) < X && (p.y = curPoint.y - 1) >= 0) points.add(new Point(p));
/*1*/
if ((p.x = curPoint.x + 2) < X && (p.y = curPoint.y + 1) < Y) points.add(new Point(p));
/*2*/
if ((p.x = curPoint.x + 1) < X && (p.y = curPoint.y + 2) < Y) points.add(new Point(p));
/*3*/
if ((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y + 2) < Y) points.add(new Point(p));
/*4*/
if ((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y + 1) < Y) points.add(new Point(p));
/*5*/
if ((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y - 1) >= 0) points.add(new Point(p));
/*6*/
if ((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y - 2) >= 0) points.add(new Point(p));
/*7*/
if ((p.x = curPoint.x + 1) < X && (p.y = curPoint.y - 2) >= 0) points.add(new Point(p));
return points;
}
/**
* 根據當前這一步的所有的下一步的選擇位置,進行非遞減排序, 減少回溯的次數
*/
private static void sort(List<Point> points) {
points.sort((p1, p2) -> {
//獲取到 p1 的下一步的所有位置個數
int count1 = next(p1).size();
//獲取到 p2 的下一步的所有位置個數
int count2 = next(p2).size();
if (count1 < count2) {
return -1;
} else if (count1 == count2) {
return 0;
} else {
return 1;
}
});
}
}
- 結果輸出:
騎士周遊[8 * 8]算法,開始運行~~
共耗時: 52 毫秒
1 16 43 32 3 18 45 22
42 31 2 17 44 21 4 19
15 56 53 60 33 64 23 46
30 41 58 63 54 61 20 5
57 14 55 52 59 34 47 24
40 29 38 35 62 51 6 9
13 36 27 50 11 8 25 48
28 39 12 37 26 49 10 7
賣油翁和老黃牛
賣油翁的故事