嵌入式Day13
一段話總結
文檔主要介紹帶有頭指針和尾指針的單鏈表的實現及操作,涵蓋創建、銷毀、頭插、尾插、按索引/數據增刪查、遍歷等核心操作,強調頭插/尾插時間復雜度為O(1),按索引/數據操作需遍歷鏈表、時間復雜度為O(n),并對比提及雙向鏈表因存儲前后指針、操作更高效,是典型的空間換時間策略。
思維導圖
## **單鏈表結構**
- 頭指針(head)
- 尾指針(tail)
- 節點(Node):data+next指針
- 鏈表結構體(LinkedList):包含head、tail、size
## **核心操作**
- 創建:calloc分配LinkedList結構體
- 銷毀:先釋放所有節點,再釋放結構體
- 遍歷:按"x->y->z->NULL"格式打印
- 新增- 頭插(add_before_head):更新head,首個節點時更新tail- 尾插(add_behind_tail):有節點時尾節點next指向新節點,否則更新head- 按索引(idx):idx=0頭插,idx=size尾插,中間需找idx-1前驅節點
- 查詢- 按索引(search_by_idx):遍歷找idx節點- 按數據:遍歷找匹配data節點
- 刪除- 按索引(delete_by_idx):idx=0刪頭節點,其他找前驅節點,刪尾節點需更新tail- 按數據(delete_by_data):遍歷找節點及前驅,刪頭/尾節點特殊處理
## **時間復雜度**
- O(1):頭插、尾插、刪頭節點
- O(n):按索引/數據增刪查、刪非頭節點
## **雙向鏈表對比**
- 節點增加前驅指針
- 操作更靈活,附近操作O(1)
- 空間換時間,如C++ list/Java LinkedList底層實現
詳細總結
一、單鏈表結構與核心操作概述
- 結構組成:由
LinkedList
結構體管理head
(頭指針)、tail
(尾指針)、size
(節點數),每個Node
包含data
(數據)和next
(后繼指針)。 - 核心操作:涵蓋創建、銷毀、遍歷、增刪查等,操作時需注意頭/尾節點特殊情況(如首個節點需同時更新
head
和tail
)。
二、具體操作實現要點
- 創建與銷毀
- 創建:使用
calloc
分配LinkedList
結構體,初始head=tail=NULL
,size=0
。 - 銷毀:先遍歷釋放所有
Node
節點,再釋放LinkedList
結構體。
- 創建:使用
- 新增操作
- 頭插法:新節點直接指向原
head
,更新head
;若為首個節點,同時更新tail
。 - 尾插法:若鏈表非空,原
tail->next
指向新節點,更新tail
;若為空,直接更新head
和tail
。 - 按索引插入:索引范圍
[0, size]
,idx=0
頭插,idx=size
尾插,中間需遍歷找到idx-1
前驅節點,時間復雜度O(n)。
- 頭插法:新節點直接指向原
- 查詢操作
- 按索引查詢:索引范圍
[0, size-1]
,遍歷找到對應節點,時間復雜度O(n)。 - 按數據查詢:遍歷全鏈表匹配數據,時間復雜度O(n)。
- 按索引查詢:索引范圍
- 刪除操作
- 按索引刪除:
idx=0
直接刪頭節點,更新head
;若為尾節點(idx=size-1
),需更新tail
為前驅節點;其他情況遍歷找前驅節點,時間復雜度O(n)(除刪頭節點為O(1))。 - 按數據刪除:遍歷找到節點及其前驅,刪頭節點時更新
head
,刪尾節點時更新tail
,時間復雜度O(n)(除刪頭節點為O(1))。
- 按索引刪除:
三、時間復雜度對比表
操作類型 | 具體操作 | 時間復雜度 | 是否需遍歷 |
---|---|---|---|
新增 | 頭插/尾插 | O(1) | 否 |
按索引插入 | O(n) | 是(中間) | |
查詢 | 按索引/數據查詢 | O(n) | 是 |
刪除 | 刪頭節點 | O(1) | 否 |
刪非頭節點(含尾) | O(n) | 是 |
四、雙向鏈表對比
- 優勢:節點增加前驅指針(
prev
),可直接訪問前后節點,附近操作(如刪除當前節點)時間復雜度為O(1),性能更優。 - 應用:C++
list
、JavaLinkedList
底層均為雙向鏈表,體現空間換時間策略。
關鍵問題
-
為什么頭插法和尾插法的時間復雜度是O(1)?
答:頭插法直接通過頭指針操作首個節點,尾插法通過尾指針直接定位最后一個節點,無需遍歷鏈表,因此時間復雜度為O(1)。 -
刪除單鏈表尾節點的時間復雜度為什么是O(n)?
答:刪除尾節點需先遍歷找到其前驅節點(需O(n)時間),才能更新前驅節點的next
指針為NULL,因此整體時間復雜度為O(n)。 -
雙向鏈表相比單鏈表的核心優化點是什么?
答:雙向鏈表每個節點增加前驅指針(prev
),可直接訪問前后節點,使在確定節點附近的操作(如刪除當前節點)時間復雜度從O(n)降至O(1),以空間換時間提升性能。
文檔主要介紹了單鏈表常見的五道經典面試題及解法,包括求中間結點、判斷是否有環、反轉鏈表、合并兩條有序鏈表(兩種方法),具體如下:
六、求鏈表中間結點
問題:給定單鏈表,找到中間結點(奇數長度取中間結點,偶數長度取中間偏右結點)。
解法:
- 遍歷統計法
- 先遍歷鏈表統計長度
list_len
,再計算中間索引mid_idx = list_len / 2
,再次遍歷找到對應結點。 - 時間復雜度:O(n)(兩次遍歷)。
- 先遍歷鏈表統計長度
- 快慢指針法(雙指針法)
- 思路:定義兩個指針
slow
(每次走1步)和fast
(每次走2步)。當fast
到達鏈表末尾(fast == NULL
或fast->next == NULL
)時,slow
指向中間結點。 - 代碼邏輯:
int find_mid_ele2(Node *head) {Node *fast = head, *slow = head;while (fast != NULL && fast->next != NULL) { // 循環條件確保快慢指針有效移動slow = slow->next;fast = fast->next->next;}return slow->data; // 直接返回中間結點數據 }
- 時間復雜度:O(n)(單次遍歷),效率更高。
- 思路:定義兩個指針
七、判斷單鏈表是否有環
問題:判斷單鏈表是否存在環(尾結點 next
指向鏈表中任意結點)。
解法:快慢指針法
- 思路:若鏈表有環,快慢指針最終會在環內相遇;若無環,
fast
會先到達NULL
,循環結束。 - 代碼邏輯:
bool has_circle(Node *head) {Node *fast = head, *slow = head;while (fast != NULL && fast->next != NULL) {slow = slow->next;fast = fast->next->next;if (slow == fast) return true; // 相遇即有環}return false; // 循環正常結束,無環 }
- 關鍵點:
- 環的形成:尾結點
next
指向頭結點、中間結點或自身。 - 快慢指針速度差確保在環內必然相遇(數學證明:快指針每次比慢指針多走1步,環長有限,最終會追上)。
- 環的形成:尾結點
八、單鏈表反轉(循環迭代法)
問題:反轉單鏈表,返回新鏈表的頭指針(原尾結點)。
解法:三指針法(prev、curr、succ)
- 思路:
- 初始化
prev = NULL
(前驅指針,指向反轉后的尾部),curr = head
(當前結點),succ = head->next
(后繼結點,防止鏈表斷開)。 - 遍歷鏈表,每次將
curr->next
指向prev
,然后prev
、curr
、succ
依次后移。 - 遍歷結束后,
prev
指向新鏈表的頭結點(原尾結點)。
- 初始化
- 代碼實現:
Node *reverse(Node *head) {Node *prev = NULL, *curr = head, *succ = head ? head->next : NULL; // 處理頭結點為空的情況while (curr != NULL) {curr->next = prev; // 反轉當前結點指向prev = curr;curr = succ;succ = (succ != NULL) ? succ->next : NULL; // 避免空指針訪問}return prev; // 返回新頭指針 }
- 變種:通過二級指針直接修改原頭指針(
reverse2
函數),適用于需要修改原始鏈表頭指針的場景。
九、合并兩條有序單鏈表(循環迭代法)
問題:合并兩條升序單鏈表,返回合并后的升序鏈表頭指針。
解法1:無虛擬頭結點(需處理頭部特殊情況)
- 步驟:
- 校驗空鏈表,直接返回非空鏈表。
- 找到兩條鏈表的最小結點,作為新鏈表的頭指針
head
和尾指針tail
,并移動對應鏈表指針(list1
或list2
)。 - 循環比較
list1
和list2
的當前結點,將較小結點接入tail->next
,更新tail
和對應鏈表指針。 - 循環結束后,將剩余非空鏈表接入
tail
。
- 代碼邏輯:
Node *merge_lists(Node *list1, Node *list2) {if (!list1 || !list2) return list1 ? list1 : list2; // 處理空鏈表Node *head, *tail;if (list1->data < list2->data) {head = tail = list1;list1 = list1->next;} else {head = tail = list2;list2 = list2->next;}while (list1 && list2) { // 合并剩余結點if (list1->data < list2->data) {tail->next = list1;tail = list1;list1 = list1->next;} else {tail->next = list2;tail = list2;list2 = list2->next;}}tail->next = (list1 != NULL) ? list1 : list2; // 接入剩余鏈表return head; }
解法2:使用虛擬頭結點(簡化頭部處理)
- 優化點:在鏈表頭部添加一個虛擬結點
dummy_node
,避免單獨處理頭指針初始化,統一邏輯。 - 代碼邏輯:
Node *merge_lists2(Node *list1, Node *list2) {if (!list1 || !list2) return list1 ? list1 : list2;Node dummy_node = {0, NULL}; // 棧上創建虛擬頭結點Node *tail = &dummy_node; // tail指向虛擬結點,作為初始尾結點while (list1 && list2) { // 直接合并所有結點,無需頭部特殊處理if (list1->data < list2->data) {tail->next = list1;tail = list1;list1 = list1->next;} else {tail->next = list2;tail = list2;list2 = list2->next;}}tail->next = (list1 != NULL) ? list1 : list2;return dummy_node.next; // 返回虛擬結點的下一個結點(真正的頭結點) }
- 優勢:虛擬頭結點使頭部和后續結點的處理邏輯一致,代碼更簡潔,減少邊界條件判斷。
十、核心算法總結
面試題 | 關鍵思路 | 時間復雜度 | 空間復雜度 |
---|---|---|---|
求中間結點 | 快慢指針法(單次遍歷) | O(n) | O(1) |
判斷是否有環 | 快慢指針法(相遇即有環) | O(n) | O(1) |
反轉鏈表 | 三指針迭代反轉(prev、curr、succ) | O(n) | O(1) |
合并有序鏈表(無虛擬頭) | 比較結點+尾插法(處理頭部特殊情況) | O(n+m) | O(1) |
合并有序鏈表(有虛擬頭) | 虛擬頭結點統一邏輯 | O(n+m) | O(1) |
注意事項:
- 指針操作需避免空指針訪問(如判斷
succ != NULL
再取值)。 - 處理邊界條件(如空鏈表、單結點鏈表、環的特殊情況)。
- 虛擬頭結點常用于簡化鏈表操作的頭部邏輯,提高代碼魯棒性。
文檔主要介紹單鏈表反轉與合并的遞歸實現方法,包括遞歸思路、代碼實現及復雜度分析,以下是詳細總結:
十一、單鏈表反轉(遞歸實現)
核心思路:通過遞歸分解問題,將長鏈表反轉拆解為短鏈表反轉,逐步構建反轉后的鏈表。
- 遞歸分解邏輯
- 問題分解:反轉
n
個結點的鏈表 = 反轉第 1 個結點 + 反轉后續n-1
個結點。 - 遞歸出口:當鏈表為空或只剩一個結點時,直接返回頭結點(無需反轉)。
- 關鍵操作:
- 先遞歸反轉后續結點(從第 2 個結點開始),得到新鏈表頭指針
new_head
。 - 修改第 2 個結點的指針,使其指向第 1 個結點(
head->next->next = head
)。 - 將第 1 個結點的指針置為
NULL
,使其成為新鏈表的尾結點。
- 先遞歸反轉后續結點(從第 2 個結點開始),得到新鏈表頭指針
- 問題分解:反轉
- 代碼實現
Node *reverse_recursion(Node *head) {// 遞歸出口:空鏈表或單結點鏈表if (head == NULL || head->next == NULL) {return head;}// 遞歸反轉后續結點,得到新鏈表頭指針Node *new_head = reverse_recursion(head->next);// 讓第 2 個結點指向第 1 個結點head->next->next = head;// 第 1 個結點置為尾結點(指針域為 NULL)head->next = NULL;return new_head; // 返回新鏈表頭指針 }
- 復雜度分析
- 時間復雜度:O(n),每個結點遞歸處理一次。
- 空間復雜度:O(n),遞歸棧深度最多為
n
(鏈表長度),存在棧溢出風險。
十二、合并兩條有序單鏈表(遞歸實現)
核心思路:通過遞歸逐層比較兩條鏈表的當前結點,將較小結點作為當前層結果,并遞歸合并剩余結點。
- 遞歸分解邏輯
- 問題分解:合并鏈表
list1
和list2
= 選擇當前較小結點 + 遞歸合并剩余結點。 - 遞歸出口:當任一鏈表為空時,直接返回另一鏈表(已處理完所有結點)。
- 關鍵操作:
- 比較
list1
和list2
的當前結點,選擇較小者作為當前層合并結果。 - 將較小結點的
next
指針指向遞歸合并剩余結點的結果(形成鏈式合并)。
- 比較
- 問題分解:合并鏈表
- 代碼實現
Node *merge_lists3(Node *list1, Node *list2) {// 遞歸出口:處理空鏈表if (list1 == NULL) return list2;if (list2 == NULL) return list1;// 選擇較小結點,并遞歸合并剩余結點if (list1->data < list2->data) {list1->next = merge_lists3(list1->next, list2); // 遞歸合并 list1 剩余結點和 list2return list1; // 返回當前較小結點} else {list2->next = merge_lists3(list1, list2->next); // 遞歸合并 list1 和 list2 剩余結點return list2; // 返回當前較小結點} }
- 復雜度分析
- 時間復雜度:O(m+n),每條鏈表的每個結點遞歸處理一次(m、n 為鏈表長度)。
- 空間復雜度:O(m+n),遞歸棧深度最多為
m+n
,大鏈表場景下可能導致棧溢出。
十三、遞歸與迭代實現對比
操作 | 實現方式 | 時間復雜度 | 空間復雜度 | 優缺點對比 |
---|---|---|---|---|
單鏈表反轉 | 迭代 | O(n) | O(1)(原地算法) | 無額外空間,效率高,適合大鏈表;邏輯較直觀,需維護三指針(prev、curr、succ)。 |
遞歸 | O(n) | O(n)(遞歸棧) | 代碼簡潔,利用遞歸分解問題;但存在棧溢出風險,大鏈表場景下不推薦。 | |
合并有序鏈表 | 迭代 | O(m+n) | O(1)(原地算法) | 需處理頭部特殊情況(或用虛擬頭結點簡化),邏輯稍復雜,無棧風險。 |
遞歸 | O(m+n) | O(m+n)(遞歸棧) | 代碼更簡潔,遞歸分解自然;但棧空間占用隨鏈表長度增長,大鏈表易棧溢出。 |
十四、注意事項與擴展
- 遞歸的局限性
- 遞歸深度受系統棧空間限制,處理長鏈表(如上萬結點)時可能引發棧溢出錯誤。
- 迭代實現更適合生產環境,尤其對性能和穩定性要求高的場景。
- 虛擬頭結點的應用
- 迭代合并鏈表時,虛擬頭結點可統一頭部和后續結點的處理邏輯,減少邊界條件判斷(如
merge_lists2
函數)。
- 迭代合并鏈表時,虛擬頭結點可統一頭部和后續結點的處理邏輯,減少邊界條件判斷(如
- 其他反轉思路
- 復制頭插法:遍歷原鏈表,復制每個結點并采用頭插法構建新鏈表。時間復雜度 O(n),但需額外空間且涉及內存分配,效率較低。
十五、總結
- 遞歸的核心思想:將大問題分解為同類型小問題,通過遞歸出口終止分解,逐層構建結果。
- 適用場景:遞歸適合邏輯簡潔、鏈表長度較小的場景;迭代更適合長鏈表或對性能敏感的場景。
- 指針操作要點:遞歸實現中需注意指針指向的正確性(如反轉時避免鏈表斷開),確保每一步遞歸調用后鏈表結構有效。
文檔主要介紹七種基于比較的排序算法,包括簡單排序(冒泡、選擇、插入)、希爾排序及高級排序(歸并、快速、堆排序),以下是從第十六條開始的詳細總結:
十六、插入排序(重點)
- 核心思想:將未排序元素逐個插入到已排序序列的合適位置,類似撲克牌整理。
- 操作流程:
- 從第二個元素開始(視為未排序元素),與前序已排序元素比較。
- 若未排序元素小于前序元素,則前序元素后移,直至找到合適位置插入。
- 代碼邏輯(偽代碼):
for (i = 1; i < n; i++) { temp = arr[i]; j = i - 1; while (j >= 0 && arr[j] > temp) { arr[j+1] = arr[j]; j--; } arr[j+1] = temp; }
- 復雜度:
- 時間復雜度:O(n2)(最壞/平均),但常數項和系數優于冒泡、選擇排序,實際性能更好。
- 空間復雜度:O(1)(原地排序)。
- 穩定性:穩定(相鄰交換,不改變相同元素相對順序)。
- 適用場景:小規模數據排序的首選算法。
十七、希爾排序(了解)
- 核心思想:插入排序的改進版,通過增量遞減將數組分組,對每組進行插入排序,逐步縮小增量至1。
- 操作流程:
- 選擇初始增量(如
gap = n/2
),將數組分為gap
組(每組元素下標差為gap
)。 - 對每組進行插入排序,縮小增量(如
gap = gap/2
),重復直至gap=1
(此時退化為插入排序)。
- 選擇初始增量(如
- 復雜度:
- 時間復雜度:依賴增量選擇,介于 O(nlogn) 到 O(n2) 之間(通常優于插入排序)。
- 空間復雜度:O(1)(原地排序)。
- 穩定性:不穩定(跨組交換可能改變相同元素相對順序)。
- 評價:適用場景有限,大數據集下不如高級排序,小數據集下犧牲穩定性但性能提升不明顯。
十八、歸并排序(高級排序)
- 核心思想:分治策略,遞歸將數組分成兩半,分別排序后合并有序子數組。
- 關鍵步驟:
- 分解:將數組從中間分成左右兩部分,直至子數組長度為1(有序)。
- 合并:將兩個有序子數組合并為一個有序數組(需額外空間存儲臨時合并結果)。
- 代碼邏輯(偽代碼):
void merge_sort(int arr[], int left, int right) { if (left < right) { mid = (left + right)/2; merge_sort(arr, left, mid); merge_sort(arr, mid+1, right); merge(arr, left, mid, right); // 合并兩個有序子數組 } }
- 復雜度:
- 時間復雜度:O(nlogn)(所有情況均穩定)。
- 空間復雜度:O(n)(需額外數組存儲合并結果)。
- 穩定性:穩定(合并時保留相同元素順序)。
- 適用場景:需要穩定排序的大數據集,如外部排序(磁盤數據排序)。
十九、快速排序(高級排序,重點)
- 核心思想:分治策略,選擇基準值(pivot),將數組分為小于/大于基準值的左右兩部分,遞歸排序子數組。
- 關鍵步驟:
- 分區:選擇基準值,通過交換將數組分為左(< pivot)、中(= pivot)、右(> pivot)三部分。
- 遞歸:對左右子數組重復分區操作,直至子數組長度為1。
- 優化點:
- 基準值選擇:隨機選、三數取中(避免最壞情況O(n2))。
- 雙向分區:從兩端向中間掃描,減少交換次數。
- 復雜度:
- 時間復雜度:平均 O(nlogn),最壞 O(n2)(可通過基準值優化規避)。
- 空間復雜度:O(logn)(遞歸棧深度,平均情況),最壞 O(n)(退化為鏈表遞歸)。
- 穩定性:不穩定(跨區交換可能改變相同元素順序)。
- 適用場景:大數據集排序的首選(性能最優),但需注意棧溢出風險(大遞歸深度)。
二十、堆排序(高級排序)
- 核心思想:基于堆(大頂堆/小頂堆)結構,每次將堆頂元素(最大值/最小值)與末尾元素交換,逐步構建有序數組。
- 關鍵步驟:
- 建堆:將數組轉換為大頂堆(父節點≥子節點)。
- 排序:交換堆頂(最大值)與末尾元素,對前
n-1
個元素重新建堆,重復直至排序完成。
- 復雜度:
- 時間復雜度:O(nlogn)(建堆 O(n),每次調整堆 O(logn))。
- 空間復雜度:O(1)(原地排序,僅利用數組本身空間)。
- 穩定性:不穩定(堆頂交換可能破壞相同元素順序)。
- 適用場景:無需遞歸、空間受限的大數據集排序,如操作系統進程調度。
二十一、七種排序算法對比表
算法 | 時間復雜度(最壞) | 時間復雜度(平均) | 空間復雜度 | 穩定性 | 適用場景 |
---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 穩定 | 玩具算法,僅學習用途 |
選擇排序 | O(n2) | O(n2) | O(1) | 不穩定 | 玩具算法,僅學習用途 |
插入排序 | O(n2) | O(n2) | O(1) | 穩定 | 小規模數據首選 |
希爾排序 | O(n2) | O(nlogn)~O(n2) | O(1) | 不穩定 | 了解即可,實際應用少 |
歸并排序 | O(nlogn) | O(nlogn) | O(n) | 穩定 | 大數據集穩定排序 |
快速排序 | O(n2) | O(nlogn) | O(logn) | 不穩定 | 大數據集首選(性能最優) |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不穩定 | 無需遞歸、空間受限場景 |
二十二、總結與建議
- 簡單排序選擇:
- 小規模數據:優先使用插入排序(性能最佳,穩定)。
- 學習用途:了解冒泡、選擇排序邏輯,但無需用于實際項目。
- 高級排序選擇:
- 需要穩定性:選歸并排序(如數據庫排序、物流訂單排序)。
- 性能優先:選快速排序(默認場景,需注意基準值優化)。
- 空間受限/禁止遞歸:選堆排序(如嵌入式系統、內核排序)。
- 算法實現建議:
- 必學:插入排序(簡單高效)、快速排序(工業級應用)。
- 擴展學習:歸并排序(分治思想)、堆排序(數據結構應用)。
- 復雜度注意點:
- 時間復雜度是趨勢描述,O(n2) 算法在小規模下可能比 O(nlogn) 更快(如插入排序 vs 快排)。
- 空間復雜度需關注額外堆空間(如歸并排序),棧空間(如遞歸快排)可能導致棧溢出。