鏈表和數(shù)組的區(qū)別
數(shù)組
數(shù)組需要一塊連續(xù)的內(nèi)存空間來存儲(chǔ), 對(duì)內(nèi)存要求比較高
鏈表
通過指針, 將一組零散的內(nèi)存塊串聯(lián)起來使用
鏈表類型
單鏈表、雙向鏈表、循環(huán)鏈表、雙向循環(huán)鏈表
鏈表和數(shù)組的優(yōu)缺點(diǎn)
時(shí)間復(fù)雜度
數(shù)組插入刪除操作時(shí)間復(fù)雜度是 O(n)
鏈表插入刪除操作時(shí)間復(fù)雜度是 O(1)
隨機(jī)訪問第 k 個(gè)元素
數(shù)組 : O(1)
鏈表: O(n)
鏈表使用場(chǎng)景分析
- 刪除操作
- 刪除節(jié)點(diǎn)中“值等于某個(gè)給定值”的節(jié)點(diǎn)
為了能找到節(jié)點(diǎn), 都需要從頭遍歷, 盡管刪除的時(shí)間復(fù)雜度是 O(1), 但是遍歷查找的時(shí)間是主要的耗時(shí)點(diǎn), 對(duì)應(yīng)的時(shí)間復(fù)雜度為O(n) - 刪除特定的節(jié)點(diǎn)
對(duì)于單鏈表來說, 需要找到前驅(qū)節(jié)點(diǎn), 需要重頭遍歷 , 時(shí)間復(fù)雜度是 O(n)
對(duì)于雙向鏈表, 已經(jīng)保存前驅(qū)節(jié)點(diǎn), 時(shí)間復(fù)雜度是 O(1)
- 插入操作
指定節(jié)點(diǎn)前面插入一個(gè)節(jié)點(diǎn)
- 單鏈表, 時(shí)間復(fù)雜度O(n)
- 雙向鏈表 O(1)
- 查詢
對(duì)于一個(gè)有序鏈表, 雙向鏈表的按值查詢的效率回比單鏈表高, 可以記錄上次查找的位置p, 每次查詢時(shí), 根據(jù)查找的值和p的大小關(guān)系, 決定是往前還是往后查找, 平均只需要查找一半的數(shù)據(jù)
性能分析
數(shù)組簡(jiǎn)單易用, 使用連續(xù)的內(nèi)存空間,可以借助 CPU 的緩存機(jī)制, 預(yù)讀數(shù)組中的數(shù)據(jù), 訪問效率更高。
鏈表在內(nèi)存中不連續(xù), 對(duì) CPU 緩存不友好
數(shù)組的確定是大小固定, 一經(jīng)聲明就要占用整塊連續(xù)的內(nèi)存空間。 如果聲明數(shù)組過大, 導(dǎo)致系統(tǒng)沒有足夠的連續(xù)內(nèi)存空間分配給它, 導(dǎo)致“內(nèi)存不足”. 聲明數(shù)組過小, 可能出現(xiàn)不夠用的情況, 只能再申請(qǐng)更大的內(nèi)存空間, 把原始數(shù)組拷貝過去, 非常耗時(shí)。
鏈表本身沒有限制, 天然支持動(dòng)態(tài)擴(kuò)容
如果代碼對(duì)內(nèi)存的使用非常苛刻, 數(shù)組更合適, 鏈表中的每個(gè)節(jié)點(diǎn)都需要消耗額外的存儲(chǔ)空間存儲(chǔ)下一個(gè)節(jié)點(diǎn)的指針。 對(duì)鏈表進(jìn)行頻繁的插入、刪除操作還會(huì)導(dǎo)致內(nèi)存頻繁的申請(qǐng)和釋放, 容易造成內(nèi)存碎片。 如果是 Java 語(yǔ)言, 可能導(dǎo)致 GC
技巧
- 插入節(jié)點(diǎn)需要注意操作順序, 刪除節(jié)點(diǎn)需要記得手動(dòng)釋放內(nèi)存空間
- 利用哨兵, 簡(jiǎn)化鏈表的插入、刪除操作
- 邊界值處理
- 鏈表為空
- 只有一個(gè)節(jié)點(diǎn)
- 兩個(gè)節(jié)點(diǎn)
- 處理頭節(jié)點(diǎn)和尾節(jié)點(diǎn)
練習(xí)
- 從尾到頭打印鏈表
思路:
如果可以改變鏈表結(jié)構(gòu)的話, 翻轉(zhuǎn)鏈表后重頭打印。
利用棧先進(jìn)后出的性質(zhì), 把鏈表所有數(shù)據(jù)壓棧, 然后按照棧的彈出順序打印。
既然可以用棧, 那么遞歸的本質(zhì)也是一個(gè)棧結(jié)構(gòu), 用遞歸也是可以打印的。
// 利用棧
void print(ListNode *head) {
Stack *stack = Stack new();
ListNode *node = head;
while (node ! = NULL) {
stack.push(node);
node = node -> next;
}
while (!stack.empty()) {
node = stack.top();
print(" %d\t", node -> value);
stack.pop();
}
}
// 遞歸
void recursivePrint(List *head) {
if (head == NULL) return;
if (head -> next != NULL) {
recursivePrint(head -> next);
}
printf("%d \t", head -> value);
}
- 刪除單鏈表指定節(jié)點(diǎn), 時(shí)間復(fù)雜度 O(1)
思路:
- 常規(guī)方法, 遍歷所有節(jié)點(diǎn), 找到指定節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn) p , 即 p -> next = target , 然后 p -> next = target -> next, 這種操作時(shí)間復(fù)雜度是O(n)
- 特殊操作: 把要?jiǎng)h除的節(jié)點(diǎn) p 的下一個(gè)節(jié)點(diǎn) q 的值賦值給p, 再把 p 的 next 指針指向 q 的下一個(gè)節(jié)點(diǎn)
對(duì)于 n -1 個(gè)非尾節(jié)點(diǎn), 可以在O(1) 時(shí)間內(nèi)把下一個(gè)節(jié)點(diǎn)的內(nèi)存復(fù)制到要?jiǎng)h除的節(jié)點(diǎn), 并刪除下一個(gè)節(jié)點(diǎn), 對(duì)于尾節(jié)點(diǎn)而言, 任然需要順序查找 O(n), 總的平均時(shí)間復(fù)雜度是 [(n-1)O(1) + O(n) ] / n, 結(jié)果還是O(1)
void deleteNode(ListNode *head, ListNode *deleteNode) {
if (head == NULL || deleteNode == NULL) return ;
if (deleteNode -> next != NULL) {
// 刪除的節(jié)點(diǎn)不是尾節(jié)點(diǎn)
ListNode *temp = deleteNode -> next;
deleteNode -> value = temp -> value;
deleteNode -> next = temp -> next;
} else if (head == deleteNode) {
// 刪除的是頭節(jié)點(diǎn)
head = NULL;
deleteNode = NULL;
} else {
// 刪除的是尾節(jié)點(diǎn)
ListNode *node = head;
while(node -> next != deleteNode) {
// 找到前驅(qū)節(jié)點(diǎn)
node = node -> next;
}
node -> next = NULL;
deleteNode = NULL;
}
}
- 鏈表中倒數(shù)第 k 個(gè)節(jié)點(diǎn)
思路:
假設(shè)鏈表的長(zhǎng)度是n,倒數(shù)第 k 個(gè)節(jié)點(diǎn), 如果從頭計(jì)數(shù)的話, 是 n - k + 1 個(gè)節(jié)點(diǎn), 第一次遍歷出節(jié)點(diǎn)的個(gè)數(shù)n, 第二次遍歷就能找到倒數(shù)的第 k 個(gè)節(jié)點(diǎn)
設(shè)定兩個(gè)指針, 第一個(gè)指針開始遍歷向前走 k - 1 步, 第二個(gè)指針步動(dòng); 從第 k 步開始, 第二個(gè)指針也開始移動(dòng), 兩個(gè)節(jié)點(diǎn)始終保持 k - 1。 當(dāng)?shù)谝粋€(gè)指針走到尾節(jié)點(diǎn)時(shí), 第二個(gè)指針剛好指向倒數(shù)第 k 個(gè)節(jié)點(diǎn)
ListNode* deleteKThNode(ListNode *head, int k) {
if (!head || k == 0) return 0;
ListNode *slow = head;
ListNode *fast = head;
for (int i = 0; i < k - 1; i ++) {
if (fast -> next == NULL) return; // 確保 K 的值小于鏈表長(zhǎng)度
fast = fast -> next;
}
while (fast -> next != NULL) {
fast = fast -> next;
slow = slow -> next;
}
return slow;
}
- 翻轉(zhuǎn)鏈表
//三個(gè)指針
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL) return NULL;
struct ListNode *pre = NULL;
struct ListNode *next = NULL;
struct ListNode *cur = head;
while (cur != NULL) {
next = cur -> next;
cur -> next = pre;
pre = cur;
cur = next;
}
return pre;
}
// 遞歸
struct ListNode* reverseList(struct ListNode* head) {
if (head == NULL || head -> next == NULL) return head;
struct ListNode *nextNode = head -> next;
struct ListNode *reverse_node = reverseList(nextNode);
nextNode -> next = head;
head -> next = NULL;
return reverse_node;
}