A*演算法(超級詳細講解,附有舉例的詳細手寫步驟)
背景:專案需要接觸此演算法,以下是一些自學成果,如有不足之處,歡迎指出,必虛心接受。做了一份PPT來彙報,此處直接使用自己PPT的截圖。部分圖片來源網路,如有侵權立馬刪除,以下博文僅作為學習筆記。
目錄
A*尋路演算法
A*演算法解決什麼問題
A*演算法的基本原理
A*演算法的詳細原理
A*演算法的詳細原理之定義
A*演算法的詳細原理之初始設定
A*演算法的詳細原理之尋路原理
A*演算法的詳細原理之結束條件
A*演算法的尋路詳細步驟
S(start)起點 E(End)終點
A*演算法的舉例說明
如果還不懂的話,可以參考我手寫的計算步驟,再不懂可以私信我。(手稿字有點醜)
A*演算法的虛擬碼
A*演算法的定義虛擬碼 (C++)
//定義
int G;// 表示從起點 A 移動到網格上指定方格的移動耗費 (上下左右,還可沿斜方向移動)
int old_G;//舊G 第一次:從起點 A 直接移動到 A 四周方格的移動耗費 ;上次更新得到的G
int new_G; //新G 從起點 A 經過當前搜尋中心點到其四周指定點的移動耗費
int H;//表示從指定的方格移動到終點 B 的預計耗費 (H 有很多計算方法, 這裡我們設定只可以上下左右移動)
int F=G+H;//表示該點的總耗費
int open_list//一個記錄下所有被考慮來尋找最短路徑的格子
int close_list //一個記錄下不會再被考慮的格子
typedef struct point{
bool Is_Wall;
struct point* father;//父節點
}Point;
point* start_point;
point* end_point
point* min_point;
point* now_point;
A*演算法的尋路虛擬碼(C++)
//FindPath
do{
//確定中心搜尋點,上一個中心點關閉,新的中心點開啟
查詢:Find the minimumm "point" of "F" from the "open_list" center
"now_point" = "min_point";//minimumm point
"now_point"新增到"close_list";
//新中心點的周圍點開啟,新中心點關閉
迴圈遍歷:"now_point"相鄰的周圍8格"s_now_point"中的每一個
//這一塊它指的就是now_point周圍8點當前搜尋點 s_now_point,為了簡單直接用它表示
if (它不可通過||它已經在"close_list"中){
什麼也不做;
} else if (它不在開啟列表中){
把它新增進"open_list";
把"now_point"作為這它的"father",計算它的"F","G","H";
}else if (它已經在開啟列表中){//通過G來判斷是否需要更新
if (new_G < old_G){
更新它的"father"為當前中心搜尋點"now_point";
更新它的"G"與"F" ;
} else{
不更新,保持原來的"father", "G"與"F" ;
}
}
} while(目標格"end_point"已經在"open_list"||"open_list"==NULL)
//存在路徑:目標格"end_point"已經在"open_list"
//不存在路徑: "open_list"==NULL,搜尋了所有可能的點
Python+PyQt程式碼實現
程式碼內容(可執行)
import time,sys
from PyQt5.QtWidgets import QDialogButtonBox,QDialog,QMainWindow,QGridLayout,QTextEdit,QLineEdit,QWidget, QMessageBox, QApplication,QLabel,QPushButton,QHBoxLayout,QVBoxLayout
from PyQt5.QtCore import Qt,QTimer,QObject,pyqtSignal,QBasicTimer
from PyQt5.QtGui import QPainter, QColor, QFont,QPen
import json
class config:
WIDTH=20#地圖列數
HEIGHT=20#地圖行數
blockLength=30#繪製畫面時每一個節點方塊的邊長
class point:#點類(每一個唯一座標只有對應的一個例項)
_list=[]#儲存所有的point類例項
_tag=True#標記最新建立的例項是否為_list中的已有的例項,True表示不是已有例項
def __new__(cls,x,y):#重寫new方法實現對於同樣的座標只有唯一的一個例項
for i in point._list:
if i.x==x and i.y==y:
point._tag=False
return i
nt=super(point,cls).__new__(cls)
point._list.append(nt)
return nt
def __init__(self,x,y):
if point._tag:
self.x=x
self.y=y
self.father=None
self.F=0#當前點的評分 F=G+H
self.G=0#起點到當前節點所花費的消耗
self.cost=0#父節點到此節點的消耗
else:
point._tag=True
@classmethod
def clear(cls):#clear方法,每次搜尋結束後,將所有點資料清除,以便進行下一次搜尋的時候點資料不會衝突。
point._list=[]
def __eq__(self,T):#重寫==運算以便實現point類的in運算
if type(self)==type(T):
return (self.x,self.y)==(T.x,T.y)
else:
return False
def __str__(self):
return'(%d,%d)[F=%d,G=%d,cost=%d][father:(%s)]'%(self.x,self.y,self.F,self.G,self.cost,str((self.father.x,self.father.y)) if self.father!=None else 'null')
class A_Search:#核心部分,尋路類
def __init__(self,arg_start,arg_end,arg_map):
self.start=arg_start#儲存此次搜尋的開始點
self.end=arg_end#儲存此次搜尋的目的點
self.Map=arg_map#一個二維陣列,為此次搜尋的地圖引用
self.open=[]#開放列表:儲存即將被搜尋的節點
self.close=[]#關閉列表:儲存已經搜尋過的節點
self.result=[]#當計算完成後,將最終得到的路徑寫入到此屬性中
self.count=0#記錄此次搜尋所搜尋過的節點數
self.useTime=0#記錄此次搜尋花費的時間--在此演示中無意義,因為process方法變成了一個逐步處理的生成器,統計時間無意義。
#開始進行初始資料處理
self.open.append(arg_start)
def cal_F(self,loc):
print('計算值:',loc)
G=loc.father.G+loc.cost
H=self.getEstimate(loc)
F=G+H
print("F=%d G=%d H=%d"%(F,G,H))
return {'G':G,'H':H,'F':F}
def F_Min(self):#搜尋open列表中F值最小的點並將其返回,同時判斷open列表是否為空,為空則代表搜尋失敗
if len(self.open)<=0:
return None
t=self.open[0]
for i in self.open:
if i.F<t.F:
t=i
return t
def getAroundPoint(self,loc):#獲取指定點周圍所有可通行的點,並將其對應的移動消耗進行賦值。
l=[(loc.x,loc.y+1,10),(loc.x+1,loc.y+1,14),(loc.x+1,loc.y,10),(loc.x+1,loc.y-1,14),(loc.x,loc.y-1,10),(loc.x-1,loc.y-1,14),(loc.x-1,loc.y,10),(loc.x-1,loc.y+1,14)]
for i in l[::-1]:
if i[0]<0 or i[0]>=config.HEIGHT or i[1]<0 or i[1]>=config.WIDTH:
l.remove(i)
nl=[]
for i in l:
if self.Map[i[0]][i[1]]==0:
nt=point(i[0],i[1])
nt.cost=i[2]
nl.append(nt)
return nl
def addToOpen(self,l,father):#此次判斷的點周圍的可通行點加入到open列表中,如此點已經在open列表中則對其進行判斷,如果此次路徑得到的F值較之之前的F值更小,則將其父節點更新為此次判斷的點,同時更新F、G值。
for i in l:
if i not in self.open:
if i not in self.close:
i.father=father
self.open.append(i)
r=self.cal_F(i)
i.G=r['G']
i.F=r['F']
else:
tf=i.father
i.father=father
r=self.cal_F(i)
if i.F>r['F']:
i.G=r['G']
i.F=r['F']
# i.father=father
else:
i.father=tf
def getEstimate(self,loc):#H :從點loc移動到終點的預估花費
return (abs(loc.x-self.end.x)+abs(loc.y-self.end.y))*10
def DisplayPath(self):#在此演示中無意義
print('搜尋花費的時間:%.2fs.迭代次數%d,路徑長度:%d'%(self.useTime,self.count,len(self.result)))
if self.result!=None:
for i in self.result:
self.Map[i.x][i.y]=8
for i in self.Map:
for j in i:
if j==0:
print('%s'%'□',end='')
elif j==1:
print('%s'%'▽',end='')
elif j==8:
print('%s'%'★',end='')
print('')
else:
print('搜尋失敗,無可通行路徑')
def process(self):#使用yield將process方法變成一個生成器,可以逐步的對搜尋過程進行處理並返回關鍵資料
while True:
self.count+=1
tar=self.F_Min()#先獲取open列表中F值最低的點tar
if tar==None:
self.result=None
self.count=-1
break
else:
aroundP=self.getAroundPoint(tar)#獲取tar周圍的可用點列表aroundP
self.addToOpen(aroundP,tar)#把aroundP加入到open列表中並更新F值以及設定父節點
self.open.remove(tar)#將tar從open列表中移除
self.close.append(tar)#已經迭代過的節點tar放入close列表中
if self.end in self.open:#判斷終點是否已經處於open列表中
e=self.end
self.result.append(e)
while True:
e=e.father
if e==None:
break
self.result.append(e)
yield (tar,self.open,self.close)
break
# self.repaint()
# print('返回')
yield (tar,self.open,self.close)
time.sleep(5)#暫停
self.useTime=time2-time1
class GameBoard(QMainWindow):#視覺化類,pyqt5進行編寫。
def __init__(self):
print('初始化地圖...')
self.Map=[]
for i in range(config.HEIGHT):
col=[]
for j in range(config.WIDTH):
col.append(0)
self.Map.append(col)
self.startPoint=None
self.endPoint=None
self.search=None
self.centerTimer=None
self.yi=None
self.special=None
self.displayFlush=False
super().__init__()
print('初始化UI...')
self.initUI()
def initUI(self):
#開始初始化UI部分
#建立UI控制元件
self.label_tips=QLabel("<p style='color:green'>使用說明:</p>右鍵單擊格子選定起始點,左鍵格子選定格子為牆壁或刪除牆壁。\n<p style='color:green'>顏色說明:</p>\n黃色代表起點,綠色代表終點,黑色代表牆壁,紅色代表待搜尋的open列表,灰色代表已搜尋過的close列表,藍色代表當前搜尋到的路徑",self)
self.label_display=QLabel("",self)
self.button_start=QPushButton("開始搜尋",self)
self.button_clearSE=QPushButton("重選起始點",self)
self.button_clearWall=QPushButton("清空地圖牆壁",self)
self.button_saveMap=QPushButton("儲存地圖",self)
self.button_loadMap=QPushButton("載入地圖",self)
#設定控制元件屬性
self.label_tips.setWordWrap(True)
self.label_display.setWordWrap(True)
#設定控制元件樣式
self.label_display.setStyleSheet("border:1px solid black")
self.label_display.setAlignment(Qt.AlignLeft)
self.label_display.setAlignment(Qt.AlignTop)
#設定控制元件的尺寸和位置
self.label_tips.resize(200,150)
self.button_saveMap.resize(80,30)
self.button_loadMap.resize(80,30)
self.label_display.resize(200,300)
self.label_tips.move(100+(config.WIDTH-1)*config.blockLength,0)
self.label_display.move(100+(config.WIDTH-1)*config.blockLength,400)
self.button_start.move(100+(config.WIDTH-1)*config.blockLength,200)
self.button_clearSE.move(100+(config.WIDTH-1)*config.blockLength,250)
self.button_clearWall.move(100+(config.WIDTH-1)*config.blockLength,300)
self.button_saveMap.move(100+(config.WIDTH-1)*config.blockLength,350)
self.button_loadMap.move(200+(config.WIDTH-1)*config.blockLength,350)
#給控制元件繫結事件
self.button_start.clicked.connect(self.button_StartEvent)
self.button_clearSE.clicked.connect(self.button_Clear)
self.button_clearWall.clicked.connect(self.button_Clear)
self.button_saveMap.clicked.connect(self.button_SaveMap)
self.button_loadMap.clicked.connect(self.button_LoadMap)
#UI初始化完成
self.setGeometry(0, 0, 150+(config.WIDTH*config.blockLength-config.blockLength)+200, 150+(config.HEIGHT*config.blockLength-config.blockLength))
self.setMinimumSize(150+(config.WIDTH*config.blockLength-config.blockLength)+200, 150+(config.HEIGHT*config.blockLength-config.blockLength))
self.setMaximumSize(150+(config.WIDTH*config.blockLength-config.blockLength)+200, 150+(config.HEIGHT*config.blockLength-config.blockLength))
self.setWindowTitle('A*搜尋')
self.show()
def addDisplayText(self,text):
if self.displayFlush:
self.label_display.setText(text+'\n')
self.displayFlush=False
else:
self.label_display.setText(self.label_display.text()+text+'\n')
def mousePressEvent(self,event):
x,y=event.x()-50,event.y()-50
x=x//config.blockLength
y=y//config.blockLength
if x>=0 and x<config.WIDTH and y>=0 and y<config.HEIGHT:
if event.button()==Qt.LeftButton:
if (x,y)!=self.startPoint and (x,y)!=self.endPoint:
self.Map[y][x]=(1 if self.Map[y][x]==0 else 0)
if event.button()==Qt.RightButton:
if self.Map[y][x]==0:
if self.startPoint==None:
self.startPoint=(x,y)
self.addDisplayText('新增了一個起點:(%d,%d)'%(x,y))
elif self.endPoint==None and self.startPoint!=(x,y):
self.endPoint=(x,y)
self.addDisplayText('新增了一個終點:(%d,%d)'%(x,y))
self.repaint()
def button_StartEvent(self):
sender=self.sender()
print(sender)
if self.startPoint!=None and self.endPoint!=None:
if self.centerTimer==None:
self.centerTimer=QBasicTimer()
self.button_start.setEnabled(False)
self.button_clearSE.setEnabled(False)
self.button_clearWall.setEnabled(False)
self.centerTimer.start(200,self)
self.search=A_Search(point(self.startPoint[1],self.startPoint[0]),point(self.endPoint[1],self.endPoint[0]),self.Map)
self.yi=self.search.process()
self.addDisplayText('開始進行搜尋')
def button_SaveMap(self):
with open('map.txt','w') as f:
f.write(json.dumps(self.Map))
self.addDisplayText('地圖儲存成功-->map.txt')
# else:
# self.addDisplayText('地圖儲存失敗')
def button_LoadMap(self):
try:
with open('map.txt','r') as f:
self.Map=json.loads(f.read())
config.HEIGHT=len(self.Map)
config.WIDTH=len(self.Map[0])
self.addDisplayText('地圖載入成功')
self.repaint()
except Exception as e:
print('失敗',e,type(e))
if type(e)==FileNotFoundError:
self.addDisplayText('地圖載入失敗:地圖檔案不存在')
elif type(e)==json.decoder.JSONDecodeError:
self.addDisplayText('地圖載入失敗:錯誤的地圖檔案')
def button_Clear(self):
sender=self.sender()
print(self.button_clearSE,type(self.button_clearSE))
if sender==self.button_clearSE:
self.startPoint=None
self.endPoint=None
self.repaint()
self.addDisplayText('清空起始點')
elif sender==self.button_clearWall:
for i in range(len(self.Map)):
for j in range(len(self.Map[i])):
self.Map[i][j]=0
self.repaint()
self.addDisplayText('清空所有牆壁')
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
self.drawBoard(event,qp)
qp.end()
def drawBoard(self, event, qp):
self.drawMap(qp)
def drawMap(self,qp):#畫面繪製方法,每次地圖有所改動都將重繪
time1=time.time()
if self.search!=None:
if self.special!=None:
e=self.special[0]
path=[e]
while True:
e=e.father
if e!=None:
path.append(e)
else:
break
else:
path=None
pen=QPen(QColor(0,0,0),1,Qt.SolidLine)
qp.setPen(pen)
for i in range(len(self.Map)):
for j in range(len(self.Map[i])):
wordTag=False
if i==self.search.start.x and j==self.search.start.y:
qp.setBrush(QColor(255,255,0))
elif i==self.search.end.x and j==self.search.end.y:
qp.setBrush(QColor(100,200,50))
else:
if self.Map[i][j]==0:
tagx=True
if path:
for k in path:
if k.x==i and k.y==j:
tagx=False
qp.setBrush(QColor(0,100,255))
if tagx:
if self.special!=None:
if i==self.special[0].x and j==self.special[0].y:
qp.setBrush(QColor(0,255,0))
else:
tag=True
for k in self.special[1]:
if k.x==i and k.y==j:
tag=False
wordTag=True
word=str(k.F)
qp.setBrush(QColor(150,0,0))
break
else:
qp.setBrush(QColor(220,220,220))
if tag:
for k in self.special[2]:
if k.x==i and k.y==j:
qp.setBrush(QColor(150,150,150))
break
else:
qp.setBrush(QColor(220,220,220))
else:
qp.setBrush(QColor(220,220,220))
elif self.Map[i][j]==1:
qp.setBrush(QColor(0,0,0))
else:
qp.setBrush(QColor(255,0,0))
qp.drawRect(50+j*config.blockLength,50+i*config.blockLength,config.blockLength,config.blockLength)
if wordTag:
qp.setFont(QFont('楷體',5,QFont.Light))
qp.drawText(50+10+j*config.blockLength,50+10+i*config.blockLength,word)
wordTag=False
#time.sleep(20)
else:
for i in range(len(self.Map)):
for j in range(len(self.Map[i])):
if (j,i)==self.startPoint:
qp.setBrush(QColor(255,255,0))
elif (j,i)==self.endPoint:
qp.setBrush(QColor(100,200,50))
else:
if self.Map[i][j]==0:
qp.setBrush(QColor(220,220,220))
elif self.Map[i][j]==1:
qp.setBrush(QColor(0,0,0))
else:
qp.setBrush(QColor(255,0,0))
qp.drawRect(50+j*config.blockLength,50+i*config.blockLength,config.blockLength,config.blockLength)
time2=time.time()
#time.sleep(20)
# print('繪製時間:',time2-time1)
def timerEvent(self,e):
try:
data=next(self.yi)
except Exception as e:
self.addDisplayText('搜尋結束:')
print('搜尋結束!')
if self.search.result==None:
self.addDisplayText('未找到可行路徑')
print('搜尋結束!')
else:
self.addDisplayText('總計搜尋節點數:%d'%self.search.count)
self.addDisplayText('最終路徑長度:%d'%len(self.search.result))
self.centerTimer.stop()
self.search=None
self.yi=None
self.special=None
point.clear()
self.button_start.setEnabled(True)
self.button_clearSE.setEnabled(True)
self.button_clearWall.setEnabled(True)
self.displayFlush=True
else:
self.special=data
self.repaint()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = GameBoard()
sys.exit(app.exec_())
注意:程式碼執行可以設定動態遍歷的時候暫停時間(大概在145行的time.sleep(5)語句)
執行結果
輸出每次計算的每個點的F和父結點,直接看圖吧!
詳細列表
初始化地圖...
初始化UI...
<PyQt5.QtWidgets.QPushButton object at 0x0000017CC699AC18>
計算值: (2,3)[F=0,G=0,cost=10][father:((2, 2))]
F=40 G=10 H=30
計算值: (3,3)[F=0,G=0,cost=14][father:((2, 2))]
F=54 G=14 H=40
計算值: (3,2)[F=0,G=0,cost=10][father:((2, 2))]
F=60 G=10 H=50
計算值: (3,1)[F=0,G=0,cost=14][father:((2, 2))]
F=74 G=14 H=60
計算值: (2,1)[F=0,G=0,cost=10][father:((2, 2))]
F=60 G=10 H=50
計算值: (1,1)[F=0,G=0,cost=14][father:((2, 2))]
F=74 G=14 H=60
計算值: (1,2)[F=0,G=0,cost=10][father:((2, 2))]
F=60 G=10 H=50
計算值: (1,3)[F=0,G=0,cost=14][father:((2, 2))]
F=54 G=14 H=40
計算值: (3,3)[F=54,G=14,cost=10][father:((2, 3))]
F=60 G=20 H=40
計算值: (3,2)[F=60,G=10,cost=14][father:((2, 3))]
F=74 G=24 H=50
計算值: (1,2)[F=60,G=10,cost=14][father:((2, 3))]
F=74 G=24 H=50
計算值: (1,3)[F=54,G=14,cost=10][father:((2, 3))]
F=60 G=20 H=40
計算值: (4,4)[F=0,G=0,cost=14][father:((3, 3))]
F=68 G=28 H=40
計算值: (4,3)[F=0,G=0,cost=10][father:((3, 3))]
F=74 G=24 H=50
計算值: (4,2)[F=0,G=0,cost=14][father:((3, 3))]
F=88 G=28 H=60
計算值: (3,2)[F=60,G=10,cost=10][father:((3, 3))]
F=74 G=24 H=50
計算值: (1,2)[F=60,G=10,cost=10][father:((1, 3))]
F=74 G=24 H=50
計算值: (0,2)[F=0,G=0,cost=14][father:((1, 3))]
F=88 G=28 H=60
計算值: (0,3)[F=0,G=0,cost=10][father:((1, 3))]
F=74 G=24 H=50
計算值: (0,4)[F=0,G=0,cost=14][father:((1, 3))]
F=68 G=28 H=40
計算值: (4,3)[F=74,G=24,cost=14][father:((3, 2))]
F=74 G=24 H=50
計算值: (4,2)[F=88,G=28,cost=10][father:((3, 2))]
F=80 G=20 H=60
計算值: (4,1)[F=0,G=0,cost=14][father:((3, 2))]
F=94 G=24 H=70
計算值: (3,1)[F=74,G=14,cost=10][father:((3, 2))]
F=80 G=20 H=60
計算值: (2,1)[F=60,G=10,cost=14][father:((3, 2))]
F=74 G=24 H=50
計算值: (3,1)[F=74,G=14,cost=10][father:((2, 1))]
F=80 G=20 H=60
計算值: (3,0)[F=0,G=0,cost=14][father:((2, 1))]
F=94 G=24 H=70
計算值: (2,0)[F=0,G=0,cost=10][father:((2, 1))]
F=80 G=20 H=60
計算值: (1,0)[F=0,G=0,cost=14][father:((2, 1))]
F=94 G=24 H=70
計算值: (1,1)[F=74,G=14,cost=10][father:((2, 1))]
F=80 G=20 H=60
計算值: (1,2)[F=60,G=10,cost=14][father:((2, 1))]
F=74 G=24 H=50
計算值: (1,1)[F=74,G=14,cost=10][father:((1, 2))]
F=80 G=20 H=60
計算值: (0,1)[F=0,G=0,cost=14][father:((1, 2))]
F=94 G=24 H=70
計算值: (0,2)[F=88,G=28,cost=10][father:((1, 2))]
F=80 G=20 H=60
計算值: (0,3)[F=74,G=24,cost=14][father:((1, 2))]
F=74 G=24 H=50
計算值: (4,5)[F=0,G=0,cost=10][father:((4, 4))]
F=68 G=38 H=30
計算值: (5,5)[F=0,G=0,cost=14][father:((4, 4))]
F=82 G=42 H=40
計算值: (5,4)[F=0,G=0,cost=10][father:((4, 4))]
F=88 G=38 H=50
計算值: (5,3)[F=0,G=0,cost=14][father:((4, 4))]
F=102 G=42 H=60
計算值: (4,3)[F=74,G=24,cost=10][father:((4, 4))]
F=88 G=38 H=50
計算值: (3,5)[F=0,G=0,cost=14][father:((4, 4))]
F=62 G=42 H=20
計算值: (3,6)[F=0,G=0,cost=10][father:((3, 5))]
F=62 G=52 H=10
計算值: (4,6)[F=0,G=0,cost=14][father:((3, 5))]
F=76 G=56 H=20
計算值: (4,5)[F=68,G=38,cost=10][father:((3, 5))]
F=82 G=52 H=30
計算值: (2,5)[F=0,G=0,cost=10][father:((3, 5))]
F=62 G=52 H=10
計算值: (2,6)[F=0,G=0,cost=14][father:((3, 5))]
F=56 G=56 H=0
搜尋結束!
可執行檔案
已經將程式打包成exe可執行檔案,點選即可用,不需要py環境。
連結:https://pan.baidu.com/s/1TzFK0ODsf1JhfaMHn37uzQ 提取碼:3kyr
相關文章
- Redis 超詳細的手動搭建Cluster叢集步驟Redis
- Go Struct超詳細講解GoStruct
- Spring AOP全面詳解(超級詳細)Spring
- LVM建立的詳細步驟LVM
- Java註解最全詳解(超級詳細)Java
- Spring6 對 整合MyBatis 開發運用(附有詳細的操作步驟)SpringMyBatis
- UBUNTU手動安裝JDK的詳細步驟UbuntuJDK
- 手寫 Promise 詳細解讀Promise
- MySQL的安裝步驟(詳細)MySql
- 配置PLSQL Developer詳細步驟SQLDeveloper
- iozone磁碟讀寫測試工具的使用以及命令詳解、下載(網站最詳細講解步驟)網站
- 最詳細的 SAP ABAP Web Service 建立和消費步驟講解Web
- 情人節到了,手把手教你做一個表白牆,附有詳細步驟
- Linux安裝jdk的詳細步驟。LinuxJDK
- Linux安裝jdk的詳細步驟LinuxJDK
- MyBatis的逆向工程詳細步驟操作MyBatis
- OpenHarmony執行docker詳細步驟Docker
- Github配置ssh key詳細步驟Github
- docker安裝portainer詳細步驟DockerAI
- 指標的詳細講解指標
- Vue 超詳細手記Vue
- dart類詳細講解Dart
- 10天搞定Python,執行環境(超詳細步驟)Python
- 超詳細oracle 11g安裝步驟 win版本Oracle
- 超詳細Dkhadoop虛擬機器圖文安裝步驟Hadoop虛擬機
- 手寫 call apply bind 詳細解讀APP
- 抖音去水印詳細原理步驟及介面編寫
- 超詳細講解頁面載入過程
- 演算法--揹包九講(詳細講解+程式碼)演算法
- ubuntu 18.04安裝kalibr(詳細步驟)Ubuntu
- 安裝fbprophet模組詳細步驟
- CentOS 7 安裝MongoDB詳細步驟CentOSMongoDB
- 最詳細的測試用例設計方法講解
- Tarjan演算法及其應用 總結+詳細講解+詳細程式碼註釋演算法
- react的詳細知識講解!React
- Java中的static詳細講解Java
- Postman 安裝與漢化超詳細步驟全解析教程Postman
- Flutter | 超詳細教你如何自定義一個 Stepper 步驟元件Flutter元件