前文定義的圖結構大量使用指針:
typedef struct _LINE { int end, weight; struct _LINE* next; } LINE;
typedef struct _VECTEX { int start, number; LINE* neighbor; struct _VECTEX* next; } VECTEX;
typedef struct _GRAPH { int count; VECTEX* head; } GRAPH;
核心難題:指針存儲的是虛擬內存地址,寫入磁盤後重新讀取時地址映射完全改變,原指針失效。
將所有指針轉換為相對文件開頭的字節偏移量。存儲佈局規劃:
[ GRAPH頭部 ][ VECTEX數組 ][ LINE數組 ]
↓ ↓ ↓
count start end
head_offset number weight
neighbor_off (無next,隱式連續)
(無next,隱式連續)
利用連續存儲特性消除冗餘偏移:
// 磁盤存儲結構(無指針)
typedef struct _LINE_DISK { int end; int weight; } LINE_DISK;
typedef struct _VECTEX_DISK { int start; int number; int neighbor_offset; } VECTEX_DISK;
typedef struct _GRAPH_DISK { int count; } GRAPH_DISK; // head_offset 固定為 sizeof(GRAPH_DISK)
優化邏輯:
head_offset 固定,無需存儲next 隱含為下一個結構體位置,無需存儲LINE.next 隱含為下一個 LINE 位置,無需存儲VECTEX.neighbor_offset 指向該頂點第一條邊的位置
void save_graph(FILE* fp, GRAPH* graph) {
// 1. 寫入 GRAPH 頭部
GRAPH_DISK header = { graph->count };
fwrite(&header, sizeof(GRAPH_DISK), 1, fp);
// 2. 遍歷頂點,記錄每個頂點的邊起始位置
VECTEX* v = graph->head;
long* vtx_positions = malloc(graph->count * sizeof(long));
long* edge_start_pos = malloc(graph->count * sizeof(long));
for (int i = 0; i < graph->count; i++) {
vtx_positions[i] = ftell(fp);
VECTEX_DISK vd = { v->start, v->number, 0 }; // neighbor_offset 稍後回填
fwrite(&vd, sizeof(VECTEX_DISK), 1, fp);
v = v->next;
}
// 3. 寫入邊並回填 neighbor_offset
v = graph->head;
for (int i = 0; i < graph->count; i++) {
edge_start_pos[i] = ftell(fp);
LINE* e = v->neighbor;
while (e) {
LINE_DISK ld = { e->end, e->weight };
fwrite(&ld, sizeof(LINE_DISK), 1, fp);
e = e->next;
}
v = v->next;
}
// 4. 回填 neighbor_offset (需 fseek 回頂點區)
for (int i = 0; i < graph->count; i++) {
fseek(fp, vtx_positions[i] + offsetof(VECTEX_DISK, neighbor_offset), SEEK_SET);
fwrite(&edge_start_pos[i], sizeof(int), 1, fp);
}
free(vtx_positions); free(edge_start_pos);
}
GRAPH* load_graph(FILE* fp) {
// 1. 讀取頭部
GRAPH_DISK header;
fread(&header, sizeof(GRAPH_DISK), 1, fp);
GRAPH* graph = malloc(sizeof(GRAPH));
graph->count = header.count;
graph->head = NULL;
// 2. 讀取所有頂點到內存數組
VECTEX_DISK* vtx_array = malloc(header.count * sizeof(VECTEX_DISK));
fread(vtx_array, sizeof(VECTEX_DISK), header.count, fp);
// 3. 讀取所有邊到內存數組 (需預知總邊數或分兩遍讀取)
// 此處假設已知總邊數或使用動態數組
// 4. 重建內存指針結構
VECTEX* prev = NULL;
for (int i = 0; i < header.count; i++) {
VECTEX* v = malloc(sizeof(VECTEX));
v->start = vtx_array[i].start;
v->number = vtx_array[i].number;
v->next = NULL;
// 重建邊鏈表
LINE* e_head = NULL, *e_tail = NULL;
// 根據 vtx_array[i].neighbor_offset 讀取對應邊數據...
v->neighbor = e_head;
if (prev) prev->next = v;
else graph->head = v;
prev = v;
}
free(vtx_array);
return graph;
}
| 課題 | 解決方案 |
|---|---|
| 跨平台端序 | 統一使用小端序,讀寫時按字節序列化/反序列化 |
| 結構體對齊 | 使用 #pragma pack(1) 或手動按字節讀寫 |
| 版本兼容 | 文件頭添加 magic number + version 字段 |
| 大文件支持 | 使用 64 位偏移量 (long long/off_t) |
| 部分加載 | 建立索引區,支持隨機訪問特定頂點 |
圖的持久化本質是指針豐富圖向扁平序列的轉換。關鍵技巧:
此方案不僅適用於圖,對任意指針豐富的數據結構(樹、鏈表、NFA 圖等)均具通用指導意義。