景點(diǎn)管理系統(tǒng):數(shù)據(jù)結(jié)構(gòu)課設(shè)實(shí)戰(zhàn),從圖到Dijkstra完整實(shí)現(xiàn))
簡(jiǎn)介這份資源是面向計(jì)算機(jī)相關(guān)專(zhuān)業(yè)學(xué)生的數(shù)據(jù)結(jié)構(gòu)課程設(shè)計(jì)參考方案主題為旅游區(qū)景點(diǎn)管理系統(tǒng)適合正在完成課程設(shè)計(jì)或希望將數(shù)據(jù)結(jié)構(gòu)理論落地到實(shí)際項(xiàng)目的學(xué)習(xí)者。項(xiàng)目圍繞景點(diǎn)信息的存儲(chǔ)、管理與檢索展開(kāi)涉及鏈表、數(shù)組、樹(shù)、堆、圖、哈希表、隊(duì)列、棧等核心結(jié)構(gòu)并融入排序算法、字符串處理、設(shè)計(jì)模式、數(shù)據(jù)庫(kù)操作與接口調(diào)用等工程實(shí)踐內(nèi)容可幫助讀者理解抽象數(shù)據(jù)模型如何服務(wù)于真實(shí)業(yè)務(wù)場(chǎng)景。資源以zip壓縮包形式提供整體約1.37MB文件類(lèi)型明細(xì)上游暫未提供具體內(nèi)容以實(shí)際解壓為準(zhǔn)。目前已有788人學(xué)習(xí)下載具備一定參考熱度。讀者可借助該方案梳理系統(tǒng)模塊劃分、數(shù)據(jù)結(jié)構(gòu)選型與算法實(shí)現(xiàn)思路對(duì)照完成需求分析、架構(gòu)設(shè)計(jì)與測(cè)試調(diào)試提升綜合編程與項(xiàng)目實(shí)踐能力。1. 旅游區(qū)景點(diǎn)管理系統(tǒng)一個(gè)把數(shù)據(jù)結(jié)構(gòu)課設(shè)做成能跑項(xiàng)目的實(shí)戰(zhàn)樣本很多計(jì)算機(jī)專(zhuān)業(yè)的學(xué)生對(duì)“數(shù)據(jù)結(jié)構(gòu)課程設(shè)計(jì)”這幾個(gè)字是有心理陰影的——題目發(fā)下來(lái)要求寫(xiě)個(gè)管理系統(tǒng)最后交上去的往往是一個(gè)控制臺(tái)里跑著幾個(gè)printf、數(shù)據(jù)全靠手動(dòng)輸入、關(guān)掉窗口就全沒(méi)了的“演示程序”。答辯的時(shí)候老師問(wèn)一句“你的圖結(jié)構(gòu)用在哪了”當(dāng)場(chǎng)卡殼。這份旅游區(qū)景點(diǎn)管理系統(tǒng)源碼就是沖著解決這個(gè)尷尬去的。它把圖、查找、排序這些課本上的抽象結(jié)構(gòu)真正落到了一個(gè)有景點(diǎn)、有路徑、有查詢、有推薦的小型管理系統(tǒng)里。適合兩類(lèi)人一是正在被課設(shè)折磨、想找個(gè)能跑通的參考實(shí)現(xiàn)的學(xué)生二是想拿一個(gè)完整小項(xiàng)目練手、把數(shù)據(jù)結(jié)構(gòu)從“考試知識(shí)點(diǎn)”變成“工程工具”的開(kāi)發(fā)者。它不花哨但結(jié)構(gòu)完整能編譯、能運(yùn)行、能改。2. 系統(tǒng)架構(gòu)與數(shù)據(jù)結(jié)構(gòu)選型為什么用圖而不是數(shù)組2.1 從業(yè)務(wù)場(chǎng)景倒推數(shù)據(jù)結(jié)構(gòu)旅游區(qū)景點(diǎn)管理系統(tǒng)核心業(yè)務(wù)其實(shí)就三件事景點(diǎn)信息管理、景點(diǎn)間路徑管理、基于路徑的查詢與推薦。景點(diǎn)信息管理是典型的增刪改查用數(shù)組或鏈表都能做。但一旦涉及“從大門(mén)到山頂怎么走最近”“哪幾個(gè)景點(diǎn)離得近可以一起逛”問(wèn)題就變成了圖論問(wèn)題——景點(diǎn)是頂點(diǎn)道路是邊距離或步行時(shí)間是權(quán)值。這就是這個(gè)項(xiàng)目最值得看的地方它沒(méi)有把所有功能都塞進(jìn)一個(gè)數(shù)組里而是根據(jù)業(yè)務(wù)場(chǎng)景選了不同的結(jié)構(gòu)。景點(diǎn)基礎(chǔ)信息用順序表存儲(chǔ)因?yàn)樾枰l繁按編號(hào)隨機(jī)訪問(wèn)景點(diǎn)間的連通關(guān)系用鄰接矩陣或鄰接表存儲(chǔ)因?yàn)樾枰鲎疃搪窂胶瓦B通性判斷。這種“按需選結(jié)構(gòu)”的思路比單純背“圖有哪幾種存儲(chǔ)方式”有用得多。常見(jiàn)做法是景點(diǎn)數(shù)量不大幾十個(gè)以內(nèi)時(shí)用鄰接矩陣實(shí)現(xiàn)簡(jiǎn)單判斷兩點(diǎn)是否連通是 O(1)如果景點(diǎn)數(shù)量多且邊稀疏再換鄰接表。這個(gè)項(xiàng)目默認(rèn)走的是鄰接矩陣路線代碼里能看到二維數(shù)組的身影。2.2 核心數(shù)據(jù)結(jié)構(gòu)定義先看景點(diǎn)結(jié)構(gòu)體和圖結(jié)構(gòu)的定義這是整個(gè)系統(tǒng)的地基// 景點(diǎn)信息結(jié)構(gòu)體 typedef struct { int id; // 景點(diǎn)編號(hào)唯一標(biāo)識(shí) char name[50]; // 景點(diǎn)名稱 char category[20]; // 景點(diǎn)類(lèi)型自然景觀/人文景觀/服務(wù)設(shè)施 int visitTime; // 建議游玩時(shí)長(zhǎng)分鐘 char description[200]; // 景點(diǎn)簡(jiǎn)介 } Spot; // 圖的鄰接矩陣存儲(chǔ) typedef struct { Spot spots[MAX_SPOTS]; // 頂點(diǎn)數(shù)組存景點(diǎn)信息 int edges[MAX_SPOTS][MAX_SPOTS]; // 鄰接矩陣存路徑距離 int spotCount; // 當(dāng)前景點(diǎn)數(shù)量 int edgeCount; // 當(dāng)前路徑數(shù)量 } SpotGraph;Spot里幾個(gè)字段的設(shè)計(jì)是有講究的。id作為唯一標(biāo)識(shí)后續(xù)所有查找、刪除都靠它定位category用來(lái)做分類(lèi)篩選visitTime是后面做游覽路線推薦時(shí)的權(quán)重參考。SpotGraph把頂點(diǎn)信息和邊信息放在一個(gè)結(jié)構(gòu)體里好處是傳參時(shí)只需要傳一個(gè)指針不用到處傳兩個(gè)數(shù)組。edges矩陣?yán)锶绻麅蓚€(gè)景點(diǎn)之間沒(méi)有直接道路通常用一個(gè)極大值比如INF表示不可達(dá)而不是 0。這一點(diǎn)新手容易翻車(chē)——用 0 表示不可達(dá)在最短路徑算法里會(huì)把“沒(méi)有路”算成“距離為 0”結(jié)果推薦出一條瞬移路線。2.3 模塊劃分與文件組織項(xiàng)目雖小但模塊劃分是清晰的。典型目錄結(jié)構(gòu)如下spot_system/ ├── main.c // 主函數(shù)菜單循環(huán)與調(diào)度 ├── graph.c // 圖操作初始化、加邊、刪邊、遍歷 ├── graph.h // 圖結(jié)構(gòu)與函數(shù)聲明 ├── spot.c // 景點(diǎn)增刪改查 ├── spot.h // 景點(diǎn)結(jié)構(gòu)與函數(shù)聲明 ├── algorithm.c // 最短路徑、排序、查找算法 ├── algorithm.h // 算法函數(shù)聲明 └── data.txt // 景點(diǎn)與路徑的持久化數(shù)據(jù)文件這種分法把“數(shù)據(jù)怎么存”和“算法怎么算”分開(kāi)了。graph.c只管圖的結(jié)構(gòu)操作algorithm.c只管算法實(shí)現(xiàn)main.c只負(fù)責(zé)菜單和調(diào)用。改的時(shí)候目標(biāo)明確想換最短路徑算法只動(dòng)algorithm.c想改存儲(chǔ)方式只動(dòng)graph.c。我一般會(huì)建議課設(shè)至少做到這個(gè)粒度不然所有代碼堆在一個(gè)main.c里改一處崩三處。3. 核心功能實(shí)現(xiàn)從加邊到最短路徑的完整鏈路3.1 圖的初始化與邊的插入系統(tǒng)啟動(dòng)后第一件事是初始化圖把所有景點(diǎn)間的距離設(shè)為不可達(dá)#define INF 99999 // 表示兩點(diǎn)之間不可達(dá) void initGraph(SpotGraph *g) { g-spotCount 0; g-edgeCount 0; for (int i 0; i MAX_SPOTS; i) { for (int j 0; j MAX_SPOTS; j) { // 自己到自己距離為0其余初始化為INF g-edges[i][j] (i j) ? 0 : INF; } } } // 添加一條無(wú)向邊兩個(gè)景點(diǎn)之間的道路 void addEdge(SpotGraph *g, int from, int to, int distance) { if (from 0 || from g-spotCount || to 0 || to g-spotCount) { printf(景點(diǎn)編號(hào)越界添加路徑失敗\n); return; } g-edges[from][to] distance; g-edges[to][from] distance; // 無(wú)向圖對(duì)稱賦值 g-edgeCount; }initGraph里那個(gè)(i j) ? 0 : INF是必須的。對(duì)角線表示景點(diǎn)到自己的距離設(shè)為 0 符合直覺(jué)其余設(shè)為INF表示初始狀態(tài)下所有景點(diǎn)互不連通。addEdge里做了邊界檢查這是血淚經(jīng)驗(yàn)——課設(shè)演示時(shí)如果手動(dòng)輸入了一個(gè)不存在的景點(diǎn)編號(hào)沒(méi)有檢查就會(huì)越界寫(xiě)內(nèi)存程序直接崩答辯現(xiàn)場(chǎng)很尷尬。無(wú)向圖對(duì)稱賦值那兩行也不能省。旅游區(qū)的道路通常是雙向可走的如果只賦edges[from][to]那從 A 到 B 有路、從 B 到 A 沒(méi)路最短路徑算出來(lái)就是單向的推薦路線會(huì)出玄學(xué)問(wèn)題。3.2 最短路徑Dijkstra 算法的落地細(xì)節(jié)最短路徑是這個(gè)系統(tǒng)最有“數(shù)據(jù)結(jié)構(gòu)味”的部分。給定入口景點(diǎn)算出到其他所有景點(diǎn)的最短距離和路徑void dijkstra(SpotGraph *g, int start, int dist[], int path[]) { int visited[MAX_SPOTS] {0}; // 標(biāo)記是否已確定最短路徑 // 初始化距離和路徑 for (int i 0; i g-spotCount; i) { dist[i] g-edges[start][i]; // 有直接路徑的前驅(qū)記為start否則記為-1 path[i] (g-edges[start][i] INF) ? start : -1; } visited[start] 1; dist[start] 0; // 主循環(huán)每次選一個(gè)未訪問(wèn)的最近頂點(diǎn) for (int i 1; i g-spotCount; i) { int minDist INF, u -1; // 找當(dāng)前未訪問(wèn)頂點(diǎn)中距離最小的 for (int j 0; j g-spotCount; j) { if (!visited[j] dist[j] minDist) { minDist dist[j]; u j; } } if (u -1) break; // 剩余頂點(diǎn)均不可達(dá) visited[u] 1; // 松弛操作用u作為中轉(zhuǎn)點(diǎn)更新其他頂點(diǎn)距離 for (int v 0; v g-spotCount; v) { if (!visited[v] g-edges[u][v] INF dist[u] g-edges[u][v] dist[v]) { dist[v] dist[u] g-edges[u][v]; path[v] u; // 記錄v的前驅(qū)是u } } } }這段代碼有幾個(gè)參數(shù)需要說(shuō)清楚。dist[]是輸出數(shù)組dist[i]表示從start到景點(diǎn)i的最短距離path[]也是輸出數(shù)組path[i]記錄的是景點(diǎn)i在最短路徑上的前驅(qū)節(jié)點(diǎn)。想還原完整路徑時(shí)從終點(diǎn)不斷找path的前驅(qū)直到回到起點(diǎn)再倒序輸出。松弛操作里那個(gè)dist[u] g-edges[u][v] dist[v]是 Dijkstra 的核心。它的含義是如果從起點(diǎn)先到 u、再?gòu)?u 到 v 的總距離比當(dāng)前記錄的到 v 的距離更短就更新。g-edges[u][v] INF這個(gè)判斷不能少否則INF 某個(gè)數(shù)可能溢出成負(fù)數(shù)導(dǎo)致錯(cuò)誤更新。常見(jiàn)翻車(chē)點(diǎn)是visited數(shù)組的初始化。如果忘了把visited[start]設(shè)為 1起點(diǎn)會(huì)被重復(fù)選中雖然結(jié)果可能碰巧對(duì)但邏輯上是錯(cuò)的。另外如果圖中有負(fù)權(quán)邊Dijkstra 不適用得換 Bellman-Ford。旅游區(qū)場(chǎng)景里距離都是正數(shù)所以 Dijkstra 夠用。3.3 景點(diǎn)查找與排序按名稱和熱度組織除了圖算法系統(tǒng)還提供了按名稱查找景點(diǎn)、按游玩時(shí)長(zhǎng)排序等功能。查找用順序查找或二分查找排序用冒泡或快速排序// 按名稱順序查找景點(diǎn)返回下標(biāo)找不到返回-1 int findSpotByName(SpotGraph *g, const char *name) { for (int i 0; i g-spotCount; i) { if (strcmp(g-spots[i].name, name) 0) { return i; } } return -1; } // 按建議游玩時(shí)長(zhǎng)降序排列景點(diǎn)簡(jiǎn)單選擇排序 void sortSpotsByVisitTime(SpotGraph *g) { for (int i 0; i g-spotCount - 1; i) { int maxIdx i; for (int j i 1; j g-spotCount; j) { if (g-spots[j].visitTime g-spots[maxIdx].visitTime) { maxIdx j; } } if (maxIdx ! i) { Spot temp g-spots[i]; g-spots[i] g-spots[maxIdx]; g-spots[maxIdx] temp; } } }findSpotByName用的是strcmp返回 0 表示兩個(gè)字符串相等。這里有個(gè)細(xì)節(jié)如果景點(diǎn)數(shù)量大順序查找是 O(n)可以考慮先按名稱排序再用二分查找降到 O(log n)。課設(shè)規(guī)模下順序查找足夠但答辯時(shí)如果能說(shuō)出這個(gè)優(yōu)化方向是加分的。sortSpotsByVisitTime用的是選擇排序每輪找最大值放到前面。交換的是整個(gè)Spot結(jié)構(gòu)體不是指針。如果Spot很大頻繁結(jié)構(gòu)體拷貝會(huì)有性能開(kāi)銷(xiāo)常見(jiàn)做法是存指針數(shù)組再排序指針。課設(shè)數(shù)據(jù)量小直接拷貝結(jié)構(gòu)體更直觀。3.4 數(shù)據(jù)持久化讓程序關(guān)掉不丟數(shù)據(jù)很多課設(shè)的致命傷是數(shù)據(jù)只在內(nèi)存里一關(guān)就沒(méi)了。這個(gè)項(xiàng)目用文件讀寫(xiě)做了持久化// 保存所有景點(diǎn)和路徑到文件 void saveToFile(SpotGraph *g, const char *filename) { FILE *fp fopen(filename, w); if (!fp) { printf(文件打開(kāi)失敗數(shù)據(jù)未保存\n); return; } // 先寫(xiě)景點(diǎn)數(shù)量 fprintf(fp, %d\n, g-spotCount); // 逐個(gè)寫(xiě)景點(diǎn)信息 for (int i 0; i g-spotCount; i) { fprintf(fp, %d %s %s %d\n, g-spots[i].id, g-spots[i].name, g-spots[i].category, g-spots[i].visitTime); } // 寫(xiě)路徑數(shù)量 fprintf(fp, %d\n, g-edgeCount); // 寫(xiě)所有邊只寫(xiě)上三角避免重復(fù) for (int i 0; i g-spotCount; i) { for (int j i 1; j g-spotCount; j) { if (g-edges[i][j] INF) { fprintf(fp, %d %d %d\n, i, j, g-edges[i][j]); } } } fclose(fp); }寫(xiě)文件時(shí)先寫(xiě)數(shù)量再寫(xiě)內(nèi)容讀的時(shí)候才能知道要循環(huán)多少次。邊只寫(xiě)j i的上三角部分因?yàn)闊o(wú)向圖的邊是對(duì)稱的寫(xiě)兩遍讀的時(shí)候會(huì)重復(fù)加邊導(dǎo)致edgeCount翻倍。這個(gè)坑我在早期項(xiàng)目里踩過(guò)讀回來(lái)發(fā)現(xiàn)路徑數(shù)量對(duì)不上排查了半天。讀文件的邏輯是寫(xiě)文件的逆過(guò)程用fscanf按格式讀取。注意fscanf讀字符串時(shí)遇到空格會(huì)停所以景點(diǎn)名稱里不能有空格或者改用fgets按行讀再解析。這個(gè)項(xiàng)目默認(rèn)名稱不含空格屬于簡(jiǎn)化處理。4. 編譯運(yùn)行與功能驗(yàn)證從源碼到可交互系統(tǒng)4.1 編譯環(huán)境與命令項(xiàng)目是標(biāo)準(zhǔn) C 語(yǔ)言實(shí)現(xiàn)沒(méi)有第三方依賴Linux 和 Windows 都能編譯。Linux 下用 gcc# 在項(xiàng)目根目錄下編譯所有 .c 文件 gcc -o spot_system main.c graph.c spot.c algorithm.c -I. # 運(yùn)行 ./spot_systemWindows 下用 MinGW 或 Visual Studio 新建控制臺(tái)項(xiàng)目把所有.c和.h加進(jìn)去即可。如果用的是 Dev-C 或 Code::Blocks直接新建項(xiàng)目導(dǎo)入源碼注意頭文件路徑要包含當(dāng)前目錄。編譯時(shí)如果報(bào)undefined reference to initGraph這類(lèi)錯(cuò)誤通常是某個(gè).c文件沒(méi)加到編譯命令里。-I.是告訴編譯器在當(dāng)前目錄找頭文件如果頭文件和源文件不在同一目錄要改成實(shí)際路徑。4.2 功能驗(yàn)證步驟程序跑起來(lái)后是一個(gè)菜單驅(qū)動(dòng)的交互界面。建議按這個(gè)順序驗(yàn)證功能確保每個(gè)模塊都正常第一步初始化并加載數(shù)據(jù)。如果data.txt存在選擇加載不存在則從空?qǐng)D開(kāi)始。加載后選擇“顯示所有景點(diǎn)”確認(rèn)景點(diǎn)列表正確讀出。第二步添加景點(diǎn)和路徑。手動(dòng)添加兩個(gè)景點(diǎn)再添加它們之間的一條路徑然后查詢這兩個(gè)景點(diǎn)間的最短路徑確認(rèn)能算出正確距離。第三步測(cè)試邊界情況。查詢一個(gè)不存在的景點(diǎn)編號(hào)看程序是否給出友好提示而不是崩潰添加一條重復(fù)路徑看是覆蓋還是報(bào)錯(cuò)。第四步保存并重啟。保存數(shù)據(jù)后退出程序重新運(yùn)行并加載確認(rèn)之前添加的景點(diǎn)和路徑都還在。提示驗(yàn)證最短路徑時(shí)可以先手工在紙上畫(huà)一個(gè)小圖標(biāo)好距離再和程序輸出對(duì)比。這樣能快速發(fā)現(xiàn)算法實(shí)現(xiàn)是否正確。4.3 菜單交互的實(shí)現(xiàn)方式主菜單用一個(gè)while循環(huán)加switch實(shí)現(xiàn)這是 C 語(yǔ)言課設(shè)的標(biāo)準(zhǔn)做法int main() { SpotGraph graph; initGraph(graph); int choice; while (1) { printMenu(); // 打印菜單選項(xiàng) scanf(%d, choice); switch (choice) { case 1: loadFromFile(graph, data.txt); break; case 2: addSpotInteractive(graph); break; case 3: addEdgeInteractive(graph); break; case 4: queryShortestPath(graph); break; case 5: listAllSpots(graph); break; case 6: saveToFile(graph, data.txt); break; case 0: printf(退出系統(tǒng)\n); return 0; default: printf(無(wú)效選項(xiàng)請(qǐng)重新輸入\n); } } }scanf讀整數(shù)時(shí)如果用戶輸入了字母會(huì)陷入死循環(huán)——因?yàn)榉欠ㄝ斎肓粼诰彌_區(qū)里每次循環(huán)都讀到同樣的非法數(shù)據(jù)。常見(jiàn)做法是在scanf后檢查返回值如果返回值不是 1就用getchar()清空緩沖區(qū)。這個(gè)項(xiàng)目里如果沒(méi)做這個(gè)處理輸入字母會(huì)看到菜單瘋狂刷屏屬于經(jīng)典翻車(chē)現(xiàn)場(chǎng)。5. 避坑與常見(jiàn)問(wèn)題課設(shè)演示前必須排查的幾件事5.1 輸入非法字符導(dǎo)致菜單死循環(huán)現(xiàn)象在菜單輸入字母或符號(hào)程序不報(bào)錯(cuò)但菜單瘋狂滾動(dòng)無(wú)法繼續(xù)操作。原因scanf(%d, choice)遇到非數(shù)字字符時(shí)讀取失敗字符留在輸入緩沖區(qū)下一次循環(huán)又讀到同一個(gè)字符形成死循環(huán)。解決在scanf后判斷返回值失敗時(shí)清空緩沖區(qū)if (scanf(%d, choice) ! 1) { while (getchar() ! \n); // 清空到行尾 printf(請(qǐng)輸入數(shù)字選項(xiàng)\n); continue; }5.2 鄰接矩陣不可達(dá)值用 0 導(dǎo)致路徑推薦錯(cuò)誤現(xiàn)象兩個(gè)沒(méi)有直接道路的景點(diǎn)最短路徑算出來(lái)距離是 0推薦出一條“瞬移”路線。原因初始化時(shí)把不可達(dá)設(shè)為 0Dijkstra 松弛時(shí)認(rèn)為“距離為 0”是合法路徑直接更新。解決不可達(dá)必須用極大值INF如 99999且松弛前判斷edges[u][v] INF避免INF參與加法運(yùn)算。5.3 文件讀取時(shí)邊重復(fù)加載現(xiàn)象保存再加載后路徑數(shù)量翻倍或者同一條邊出現(xiàn)兩次。原因無(wú)向圖的邊在矩陣?yán)锸菍?duì)稱的保存時(shí)如果兩個(gè)方向都寫(xiě)讀取時(shí)就會(huì)加兩次邊。解決保存時(shí)只寫(xiě)j i的上三角部分讀取時(shí)addEdge內(nèi)部對(duì)稱賦值保證每條邊只加一次。5.4 刪除景點(diǎn)后編號(hào)不連續(xù)導(dǎo)致越界現(xiàn)象刪除中間某個(gè)景點(diǎn)后再按編號(hào)訪問(wèn)其他景點(diǎn)出現(xiàn)數(shù)據(jù)錯(cuò)亂或崩潰。原因刪除景點(diǎn)后如果直接移動(dòng)數(shù)組元素后續(xù)景點(diǎn)的下標(biāo)變了但邊矩陣?yán)锏南聵?biāo)沒(méi)同步更新。解決刪除景點(diǎn)時(shí)要么把最后一個(gè)景點(diǎn)移到被刪位置并更新所有邊要么標(biāo)記刪除而不移動(dòng)。課設(shè)里推薦用標(biāo)記刪除簡(jiǎn)單且不易出錯(cuò)。5.5 字符串輸入帶空格被截?cái)喱F(xiàn)象輸入景點(diǎn)名稱時(shí)如果名稱里有空格只讀到了空格前的部分。原因scanf(%s, name)遇到空格就停止讀取。解決改用fgets(name, sizeof(name), stdin)讀整行然后手動(dòng)去掉末尾換行符?;蛘呒s定景點(diǎn)名稱不含空格在輸入時(shí)提示用戶。6. 二次開(kāi)發(fā)與改造思路從課設(shè)到能拿得出手的小工具如果你已經(jīng)跑通了這個(gè)系統(tǒng)接下來(lái)最值得做的是把它從“課設(shè)”改造成“能演示給別人看的小工具”。我一般會(huì)從三個(gè)方向下手。第一個(gè)方向是換存儲(chǔ)結(jié)構(gòu)。當(dāng)前用鄰接矩陣景點(diǎn)數(shù)量一多矩陣就爆炸??梢愿某舌徑颖碛面湵砘騽?dòng)態(tài)數(shù)組存每個(gè)景點(diǎn)的鄰接點(diǎn)。改造時(shí)只需要?jiǎng)觛raph.c里的addEdge、dijkstra和文件讀寫(xiě)main.c和spot.c基本不用改。這就是模塊劃分清晰的好處。改完后可以對(duì)比一下同樣 100 個(gè)景點(diǎn)、200 條邊鄰接矩陣占 10000 個(gè) int鄰接表只占 400 個(gè)節(jié)點(diǎn)差距很明顯。第二個(gè)方向是加功能。比如加一個(gè)“推薦游覽路線”功能給定總時(shí)間預(yù)算用貪心或動(dòng)態(tài)規(guī)劃選出一組景點(diǎn)使總游玩時(shí)長(zhǎng)不超過(guò)預(yù)算且覆蓋盡可能多的類(lèi)型。這個(gè)功能用到了排序和背包思想答辯時(shí)能講出東西。再比如加一個(gè)“景點(diǎn)熱度排序”用文件記錄每個(gè)景點(diǎn)被查詢的次數(shù)按次數(shù)排序展示。第三個(gè)方向是換界面??刂婆_(tái)菜單雖然能跑但演示效果一般??梢杂?ncurses 做一個(gè)終端里的彩色界面或者用 GTK/Qt 做一個(gè)簡(jiǎn)單的圖形界面。如果不想引入 GUI 庫(kù)至少把菜單輸出排版做好用表格對(duì)齊關(guān)鍵信息加顏色ANSI 轉(zhuǎn)義碼。改造時(shí)有個(gè)習(xí)慣我一直保持每改一個(gè)模塊先寫(xiě)一個(gè)最小測(cè)試用例驗(yàn)證它。比如改完鄰接表先手動(dòng)構(gòu)造一個(gè)三個(gè)景點(diǎn)兩條邊的圖跑一遍最短路徑確認(rèn)輸出和鄰接矩陣版本一致再繼續(xù)改其他部分。這樣出問(wèn)題時(shí)能快速定位是哪個(gè)模塊引入的。還有一個(gè)容易被忽略的點(diǎn)把MAX_SPOTS從宏定義改成動(dòng)態(tài)分配。課設(shè)里固定 100 個(gè)景點(diǎn)夠用但改成malloc動(dòng)態(tài)擴(kuò)容后項(xiàng)目就更像真實(shí)工程了。改造時(shí)注意所有用到MAX_SPOTS的地方都要跟著改包括循環(huán)邊界和文件讀寫(xiě)。從那以后我每次改數(shù)據(jù)結(jié)構(gòu)相關(guān)的代碼都強(qiáng)制先畫(huà)一遍內(nèi)存布局圖確認(rèn)指針和下標(biāo)對(duì)得上再動(dòng)手。希望這個(gè)項(xiàng)目能幫你把數(shù)據(jù)結(jié)構(gòu)課設(shè)從“交差”變成“作品”。本文還有配套的精品資源點(diǎn)擊獲取