從無序鏈表中移除重複節點是經典面試題。本文提供 C 與 Java 雙語言實現,對比兩種核心算法:無緩存 O(n²) 雙指針法 與 哈希表 O(n) 空間換時間法,含完整代碼、圖解邏輯、複雜度分析與工程化選型建議。
外層指針 `current` 遍歷每個節點,內層指針 `runner` 檢查 `current` 之後的所有節點,發現重複即刪除。類似選擇排序的去重版。
void delete_duplicate1(Node* head) {
if (head == NULL || head->next == NULL) return;
Node* current = head->next; // 假設帶頭節點,首個數據節點
while (current != NULL) {
Node* prev = current;
Node* runner = current->next; // 內層掃描後續節點
while (runner != NULL) {
if (current->data == runner->data) {
// 刪除 runner
Node* del = runner;
prev->next = runner->next;
runner = prev->next;
free(del);
} else {
prev = runner;
runner = runner->next;
}
}
current = current->next;
}
}
// 版本一:雙層循環(標準)
public static void deleteDups2(LinkedListNode head) {
if (head == null) return;
LinkedListNode current = head;
while (current != null) {
LinkedListNode prev = current;
LinkedListNode runner = current.next;
while (runner != null) {
if (current.data == runner.data) {
prev.next = runner.next; // 刪除 runner
runner = prev.next;
} else {
prev = runner;
runner = runner.next;
}
}
current = current.next;
}
}
// 版本二:遞歸寫法(教學展示)
public static void deleteDupsRecursive(LinkedListNode head) {
if (head == null) return;
removeDupsFrom(head, head.next);
deleteDupsRecursive(head.next);
}
private static void removeDupsFrom(LinkedListNode current, LinkedListNode runner) {
if (runner == null) return;
if (current.data == runner.data) {
// 需傳入 prev 才能刪除,此處略
}
removeDupsFrom(current, runner.next);
}
遍歷鏈表,用 `HashSet`/`Hashtable` 記錄已出現值。遇到新值加入集合,遇到重複值刪除。單次遍歷完成。
void delete_duplicate2(Node* head) {
if (head == NULL || head->next == NULL) return;
// 假設整數範圍有限,用陣列模擬哈希表
// 實際項目用 uthash.h 或自定義哈希表
bool* seen = calloc(MAX_VALUE_RANGE, sizeof(bool));
if (!seen) return;
Node* prev = head;
Node* curr = head->next;
seen[head->data] = true; // 頭節點值標記
while (curr != NULL) {
if (seen[curr->data]) {
// 重複:刪除
Node* del = curr;
prev->next = curr->next;
curr = prev->next;
free(del);
} else {
seen[curr->data] = true;
prev = curr;
curr = curr->next;
}
}
free(seen);
}
public static void deleteDups(LinkedListNode head) {
if (head == null) return;
Set seen = new HashSet<>();
LinkedListNode prev = null;
LinkedListNode current = head;
while (current != null) {
if (seen.contains(current.data)) {
// 重複:刪除 current
prev.next = current.next;
current = current.next;
} else {
seen.add(current.data);
prev = current;
current = current.next;
}
}
}
// 節點定義
class LinkedListNode {
LinkedListNode next = null;
int data;
public LinkedListNode(int d) { data = d; }
}
| 算法 | 時間複雜度 | 空間複雜度 | 適用場景 |
|---|---|---|---|
| 雙指針 O(n²) | O(n²) | O(1) | 內存極度受限、數據量小(<1000)、嵌入式/面試要求無額外空間 |
| 哈希表 O(n) | O(n) 平均 | O(n) | 通用生產環境、數據量大、內存充足、追求性能 |
// Java 測試主類
public class LinkedListDedupeTest {
public static void main(String[] args) {
// 構建:1 -> 2 -> 3 -> 2 -> 4 -> 1 -> 5
LinkedListNode head = new LinkedListNode(1);
head.appendToTail(2);
head.appendToTail(3);
head.appendToTail(2);
head.appendToTail(4);
head.appendToTail(1);
head.appendToTail(5);
System.out.print("原始: ");
printList(head); // 1 2 3 2 4 1 5
deleteDups(head); // O(n) 哈希法
System.out.print("去重後: ");
printList(head); // 1 2 3 4 5
// 重構測試 O(n²) 法
LinkedListNode head2 = buildList(1,2,3,2,4,1,5);
deleteDups2(head2);
System.out.print("O(n²)去重: ");
printList(head2); // 1 2 3 4 5
}
static LinkedListNode buildList(int... vals) {
if (vals.length == 0) return null;
LinkedListNode head = new LinkedListNode(vals[0]);
for (int i = 1; i < vals.length; i++) head.appendToTail(vals[i]);
return head;
}
static void printList(LinkedListNode head) {
while (head != null) {
System.out.print(head.data + " ");
head = head.next;
}
System.out.println();
}
}
掌握無序鏈表去重的兩種經典算法,理解時空權衡本質,是數據結構面試與工程實踐的雙重保障。生產環境首選哈希表法,特殊約束下再退化至雙指針法。