數據結構:反轉鏈表(reverse the linked list)

目錄

通過交換元素值實現反轉(reverse by swapping elements)

?滑動指針(sliding pointers)

使用滑動指針反轉鏈表(Reversing a Linked List using Sliding Pointers)

對比分析

如何用遞歸(Recursion)反轉一個單鏈表


通過交換元素值實現反轉(reverse by swapping elements)

為什么我們要講這個“交換值法”而不是先講“指針反轉”?

因為指針反轉屬于結構反轉,要修改鏈表的連接方式(更復雜)
而“交換值法”是數據反轉,結構不變,只是調換節點里的值,適合入門理解。

(可以參考類似問題:

數據結構:數組:反轉數組(Reverse the Array)-CSDN博客

數據結構:反轉字符串(Reversing a String)_string反轉字符串方法-CSDN博客)

第一性分析:什么是“反轉鏈表”?

假設你有如下鏈表:

head → [1] → [3] → [5] → [7] → NULL

反轉之后應該是:

head → [7] → [5] → [3] → [1] → NULL

你可以看到:鏈表結構(指針連接順序)完全不動,只是每個節點的 data 內容被調換了。

那么,第一性問題來了:

? 單鏈表只能順著走,你怎么訪問“最后一個節點”?

我們要反轉鏈表的“數據內容”,但不能直接隨機訪問鏈表,因為單鏈表只能一個方向逐個訪問。

所以,我們換一個角度:

用數組作為“中間容器”,把鏈表的數據轉移出來操作,然后再拷貝回鏈表。

為什么可以這樣做?

  • 數組支持隨機訪問,反轉變得容易

  • 鏈表結構不變,只是內容(data 字段)被替換

  • 指針不涉及改動 → 安全、簡單

① 創建數組,邊遍歷鏈表邊復制 data

int arr[SIZE];  // 假設鏈表最多 SIZE 個節點
int count = 0;struct Node* temp = head;
while (temp != NULL) {arr[count++] = temp->data;temp = temp->next;
}

② 再次遍歷鏈表,這次寫入反轉值

temp = head;
for (int i = count - 1; i >= 0; i--) {temp->data = arr[i];temp = temp->next;
}

?? 注意:

  • 你需要預估鏈表最多多少個節點(用 #define SIZE 100

  • 或者你可以動態分配數組

完整代碼實現

#include <stdio.h>
#include <stdlib.h>#define SIZE 100  // 預設最大節點數struct Node {int data;struct Node* next;
};void reverseDataWithArray(struct Node* head) {int arr[SIZE];int count = 0;struct Node* temp = head;// Step 1: 復制 data 到數組中while (temp != NULL) {arr[count++] = temp->data;temp = temp->next;}// Step 2: 再次遍歷鏈表,寫入反轉值temp = head;for (int i = count - 1; i >= 0; i--) {temp->data = arr[i];temp = temp->next;}
}void printList(struct Node* head) {while (head != NULL) {printf("%d → ", head->data);head = head->next;}printf("NULL\n");
}struct Node* createList() {struct Node* a = malloc(sizeof(struct Node));struct Node* b = malloc(sizeof(struct Node));struct Node* c = malloc(sizeof(struct Node));struct Node* d = malloc(sizeof(struct Node));a->data = 10; a->next = b;b->data = 20; b->next = c;c->data = 30; c->next = d;d->data = 40; d->next = NULL;return a;
}int main() {struct Node* head = createList();printf("Original list:\n");printList(head);reverseDataWithArray(head);printf("After reversing data (using array):\n");printList(head);return 0;
}

?滑動指針(sliding pointers)

第一性推導:從鏈表遍歷說起

單個指針遍歷鏈表:

你最開始遍歷鏈表時,通常會寫這樣的代碼:

struct Node* temp = head;
while (temp != NULL) {// 做點什么,比如打印 temp->datatemp = temp->next;
}

這沒問題,但注意:

當你執行 temp = temp->next; 之后,你就永遠失去了對原來 temp 的訪問

??問題出現:

如果你想在遍歷的過程中修改鏈表結構,比如:

  • 刪除當前節點

  • 插入新節點

  • 調換兩個節點順序

  • 反轉鏈表指向方向

你會發現只用一個 temp 指針是不夠的。在某些操作后失去訪問某個節點 → 鏈表斷開 → 崩潰?

舉個例子:嘗試改變指針方向

想象鏈表這樣:

head → [10] → [20] → [30] → NULL

?你拿著 curr = head,執行:

curr->next = NULL;

你現在就徹底失去了對 [20][30] 的訪問!因為鏈條被斷開,你再也找不到它們

🔍 第一性結論:

在進行結構操作(比如反轉、插入、刪除)時,你必須在操作之前保存“下一步”的信息

所以你就需要多個指針同時存在,它們滑動地一起遍歷鏈表,互相幫助保存位置

?這就是“滑動指針”的由來。

滑動指針是指你使用多個指針同時向前推進,每個指針記錄鏈表當前不同的“位置狀態”,它們就像滑動的窗口一樣,彼此協同完成操作。

舉例說明最基本的三個滑動指針:

假設我們有這些:

struct Node* prev = NULL;
struct Node* curr = head;
struct Node* next = NULL;

含義如下:

指針名稱含義
prev當前節點的前一個節點
curr當前正在處理的節點
next當前節點的下一個節點(提前保存)

它們在遍歷中這樣滑動:

每次迭代:

  1. 提前保存 curr->nextnext

  2. 修改 curr->next = prev(如果你打算反轉)

  3. 整體滑動向前:

prev = curr;
curr = next;

?圖示(從左往右):

prev → curr → next → ...

下一輪:

     prev → curr → next → ...

?什么時候需要滑動指針?

你需要三個滑動指針的操作包括但不限于:

  • ?反轉鏈表

  • ?刪除當前節點

  • ?插入節點前后結構

  • ?在不丟失鏈表結構的情況下做復雜重構

?? 為什么不只用一個或兩個指針?

你可以自己試一下用一個或兩個指針做“指針反轉”這種操作,你會發現:

總是有一個節點你無法訪問,就沒法完成鏈表重構,操作順序也錯了。

滑動指針讓你在每一步都有“前后備份”,所以操作安全可控。

?操作順序

next = curr->next;     // 提前保存下一步
curr->next = prev;     // 操作當前指針(反轉、插入、刪改)
prev = curr;           // 所有指針前滑
curr = next;

💡 第一性總結:

概念解釋
為什么用多個指針避免失去訪問鏈表節點的能力
指針的職責prev:記錄已處理區域,curr:當前操作,next:保存未來節點
應用場景修改鏈表結構時必須使用
本質思想一邊操作鏈表,一邊維持三個位置的狀態,讓鏈不斷、訪問安全

使用滑動指針反轉鏈表(Reversing a Linked List using Sliding Pointers)

注意:不是改變節點里面的值,而是改變指針方向。

🚨 單鏈表只能從前往后遍歷

你不能往回走。所以:

每當你打算修改 curr->next,你必須 提前保存 curr->next

所以,我們必須使用三個滑動指針:

struct Node* prev = NULL;
struct Node* curr = head;
struct Node* next = NULL;
名字含義
prev當前節點反轉后應該指向的“前一個”
curr當前正在訪問的節點
next提前保存的下一個節點

第一性步驟:每一次“滑動”都做這三步

  1. next = curr->next;??→ 保存下一步

  2. curr->next = prev;?→ 改變指向方向

  3. prev = curr; curr = next;?→ 所有指針前移

空鏈表、一個節點怎么辦?

完全沒問題。curr == NULL 時循環直接跳過。

一個節點時,只走一輪,head 自動變成自己,反轉不出錯

?

步驟目的
保存下一節點防止修改 curr->next 后丟失后續鏈
修改 curr->next實現反轉(原來向右,現在向左)
滑動三個指針向前推進處理下一組節點
更新 head將最終的 prev 作為新的鏈表頭

一步步構建反轉函數:reverseUsingPointers

void reverseUsingPointers(struct Node** head) {struct Node* prev = NULL;struct Node* curr = *head;struct Node* next = NULL;while (curr != NULL) {// Step 1: 提前保存下一步next = curr->next;// Step 2: 改變方向curr->next = prev;// Step 3: 所有指針滑動prev = curr;curr = next;}// 最終 prev 是新的頭部*head = prev;
}

對比分析

我們要比較兩種單鏈表反轉方式:

方法名稱核心思路
方法一:拷貝數據法將鏈表的 data 拷貝到數組,反轉后再寫回
方法二:滑動指針法(改變結構)修改鏈表中每個節點的 next 指針方向

我們將從它們的本質、適用性、性能、安全性等方面一一分析。

1. 操作對象本質

?數據拷貝法:

只動數據,不動結構(next 不變)

arr[i] = temp->data;
temp->data = arr[j];

所以它適合在結構不能變動的情況下做反轉(如考試題、受限數據結構)

滑動指針法:

完全不動數據,只動結構(指針方向反過來)

curr->next = prev;

這本質上是“鏈表重構”,適合任何允許你改變鏈結構的環境,效率也更高

?2. 數據結構的復雜性影響

? 如果每個節點只存一個 int,拷貝沒什么壓力

但現實中常常這樣設計鏈表節點:

struct Node {char name[100];int id;float salary;struct Node* next;
};

這時候每個節點里的 data 是一個結構體(甚至可能更復雜)

→ 如果你用“拷貝數據法”,你必須完整復制這些字段

→ 可能需要自定義拷貝函數、深拷貝、數組容量等問題

當節點的 data 很復雜(結構體、嵌套對象、大量數據)時,滑動指針法明顯更好

3. 時間復雜度對比

操作時間復雜度原因
數據拷貝法O(n) + O(n) = O(n)兩次遍歷:拷貝 + 寫回
滑動指針法O(n)一次遍歷:邊滑動邊反轉指針

雖然復雜度都是 O(n),但滑動指針法更省,只有一次遍歷和一次拷貝

4. 空間復雜度對比

方法空間開銷說明
拷貝數據法O(n) 額外數組空間存儲每個 data 的值
滑動指針法O(1)(只用 3 個指針)不需要額外數組

5. 安全性與一致性

?滑動指針法:

  • 不會拷貝錯誤

  • 不關心 data 內容是什么(結構再復雜都不管)

  • 不會遇到內存越界等數組問題

?數據拷貝法:

  • 要保證數組空間夠大

  • 要小心拷貝結構體或深拷貝問題

  • 容易寫錯或越界(尤其是動態鏈表)

因此,滑動指針法更通用、更安全、更節省空間,更適合實際工程和復雜數據結構,?數據拷貝法僅適合學習、數據簡單、結構不允許改動時使用?。

你應該選擇滑動指針法的理由總結:

  1. 不管節點里放什么,它都不在乎

  2. 永遠只用三個指針,不會爆內存

  3. 一次遍歷搞定,比拷貝更省時間

  4. 改變結構更符合“鏈表反轉”的語義


如何用遞歸(Recursion)反轉一個單鏈表

第一性原則出發點:什么是遞歸?

遞歸是指:一個函數在內部調用自己,并且有:

  1. 基本情況(Base Case):終止條件

  2. 遞歸情況(Recursive Case):把當前問題拆分成更小問題交給自己來做

第一步:遞歸函數的思考

?我們要問:我們可以用遞歸處理誰?

答案是:

讓遞歸先反轉鏈表的后面部分,我們只處理“當前節點和它的 next”

所以遞歸函數的含義可以設定為:

void Reverse(Node* prev, Node* curr)
  • curr:當前節點(當前要處理的)

  • prev:當前節點 curr?的前驅(上一個)

這是一個滑動指針式遞歸反轉方案。

你會從頭開始調用:

Reverse(NULL, head);

每一次遞歸,你都把當前 curr 當作下一輪的 prev,curr->next 當作下一輪的 curr

也就是說,每一層你都像這樣“向前推進”:

Reverse(curr, curr->next);

一層層推進(調用棧):

調用層prevcurr
1NULL1
212
323
434
54NULL ← base case

第二步:Base Case(終止條件)

當你遞歸到最后一個節點,鏈表已經不能再往下了:

if (curr == NULL) {first = prev;return;
}
  • 如果當前處理節點 curr?已經是 NULL,說明我們走到了原鏈表的末尾

  • 此時 prev?是最后一個有效節點,應該變成新的頭節點

  • 所以設置 first = prev(這個變量是全局的鏈表頭)

第三步:Recursive Case(遞歸處理)

每一層執行:

Reverse(curr, curr->next);

也就是說:

  • 下一輪,curr?成為 prev

  • curr->next 成為新的 curr

這相當于向后滑動一格,保持一個“兩個指針的窗口”,像滑動指針一樣

第四步:反轉指針

遞歸完成之后,再回來時:

curr->next = prev;

你把當前節點的 next 指向前一個節點

🔄 反轉動作在哪里發生?

不是在前進時做的,而是在遞歸“回溯”過程中做的

用第一性逐層模擬

假設鏈表為:[1] → [2] → [3] → NULL

初始調用:

Reverse(NULL, 1)

→ 遞歸進入 Reverse(1, 2)
→ 遞歸進入 Reverse(2, 3)
→ 遞歸進入 Reverse(3, NULL)
→ 此時:curr?== NULL, 執行:first = prev?→ 即 first = 3?

然后回溯階段

curr = 3, prev = 2 → 3->next = 2  
curr = 2, prev = 1 → 2->next = 1  
curr = 1, prev = NULL → 1->next = NULL

得到新鏈表

first → [3] → [2] → [1] → NULL
階段操作含義
遞歸推進Reverse(curr, curr->next)保存前一個和當前節點,深入到末尾
遞歸終止if (curr == NULL)到達末尾,設置新的鏈表頭 first = prev
回溯反轉指向curr->next = prev每層反轉指向,把當前節點指向前一個節點

完整代碼

void Reverse(Node* prev, Node* curr) {if (curr != NULL) {Reverse(curr, curr->next);  // 向后推進curr->next = prev;          // 回溯過程中修改指向} else {first = prev;            // 遞歸到底,設置新頭}
}

為什么這種方法優雅?

  • 結構清晰,變量少(只用兩個滑動指針)

  • 不需要返回值,不需要臨時數組

  • 完全遞歸思維,沒有額外變量干擾

  • 最后由最深層設置頭結點,不需要手動更新 head = ...

改進建議(可選):

如果你希望去掉全局變量 first,可以把第三個參數傳進去(如 Node** head
或者讓 Reverse 返回新的頭結點(需要改寫遞歸結構)

但在這個思路在學習遞歸本質、滑動窗口邏輯上是極好的。

?? 補充:遞歸的缺點

雖然遞歸很優雅,但它有函數調用棧的開銷,在鏈表很長(如幾萬節點)時,可能會造成棧溢出

如果你要處理長鏈表,推薦用迭代方式(滑動指針法)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/92017.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/92017.shtml
英文地址,請注明出處:http://en.pswp.cn/web/92017.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【C#】基于SharpCompress實現壓縮包解壓功能

1.SharpCompress安裝 在vs的nuget下搜索安裝SharpCompress&#xff0c;如圖所示2.解壓縮包功能實現 /// <summary> /// 解壓壓縮包 /// </summary> /// <param name"filePath">壓縮包文件路徑</param> /// <param name"directoryPat…

mybatis連接PGSQL中對于json和jsonb的處理方法

pgsql數據庫表字段設置了jsonb格式&#xff1b;在java的實體里使用String或者對象轉換會一直提示一個錯誤&#xff1a; Caused by: org.postgresql.util.PSQLException: ERROR: column “xx” is of type jsonb but expression is of type character varying 需要加一個轉換方法…

Spring AI Alibaba Graph 深度解析:原理、架構與應用實踐

1. 引言概述 1.1 什么是 Spring AI Alibaba Graph Spring AI Alibaba Graph 是阿里云團隊基于 Spring AI 生態開發的一個強大的工作流編排框架&#xff0c;專門用于構建復雜的 AI 應用。它采用聲明式編程模型&#xff0c;通過圖結構來定義和管理 AI 工作流&#xff0c;讓開發…

C++少兒編程(二十一)—軟件執行流程

讓我們將以下程序視為用C編寫的示例程序。步驟1&#xff1a;預處理器將源代碼轉換為擴展代碼。當您運行程序時&#xff0c;源代碼首先被發送到稱為預處理器的工具。預處理器主要做兩件事&#xff1a;它會從程序中刪除注釋。它擴展了預處理器指令&#xff0c;如宏或文件包含。它…

精通Webpack搭建Vue2.0項目腳手架指南

本文還有配套的精品資源&#xff0c;點擊獲取 簡介&#xff1a;在Web應用程序開發中&#xff0c;Vue 2.0因其虛擬DOM、單文件組件、增強的生命周期鉤子和Vuex及Vue Router狀態管理與路由解決方案&#xff0c;成為了提高開發效率和代碼組織性的關鍵。Webpack作為必不可少的模…

無償分享120套開源數據可視化大屏H5模板

數據可視化跨越了語言、技術和專業的邊界&#xff0c;是能夠推動實現跨界溝通&#xff0c;實現國際間跨行業的創新的工具。正如畫家用顏料表達自我&#xff0c;作者用文字講述故事&#xff0c;而統計人員用數字溝通 ...... 同樣&#xff0c;數據可視化的核心還是傳達信息。而設…

Qt按鍵響應

信號與槽機制是一個非常強大的事件通信機制&#xff0c;是 Qt 最核心的機制之一&#xff0c;初學者掌握它之后&#xff0c;幾乎可以做任何交互操作。信號&#xff08;Signal&#xff09; 是一種“事件”或“通知”&#xff0c;比如按鈕被點擊、文本改變、窗口關閉等。 槽&#…

【Git】常見命令整理

Git分區與操作關系&#xff1a;Working Directory&#xff08;工作區&#xff0c;對于本地的編輯和修改在此進行&#xff09;->Staging Area&#xff08;暫存區/Index&#xff0c;在工作區進行git add操作后的位置&#xff09;->Git Repository&#xff08;本地倉庫&…

Linux-Shell腳本基礎用法

1.變量定義變量命名規則&#xff1a;可以包含字母&#xff0c;數字&#xff0c;下劃線&#xff0c;首字母不能用數字開頭&#xff0c;中間不能又空格&#xff1b;為變量賦值等號之間不能為空格&#xff1b;變量命名不能使用標點符號&#xff0c;不能使用bash的關鍵字&#xff1…

JS中的Map和WeakMap區別和聯系

JavaScript 中 Map 與 WeakMap 的區別、聯系及示例核心區別特性MapWeakMap鍵的類型允許任意類型的鍵&#xff08;對象、原始值&#xff09;鍵必須是對象&#xff08;非原始值&#xff09;垃圾回收強引用鍵 → 阻止垃圾回收弱引用鍵 → 不影響垃圾回收可遍歷性支持遍歷&#xff…

Linux 環境 libpq加載異常導致psql 連接 PostgreSQL 庫失敗失敗案例

文章目錄局點現象定位結論局點環境補充知識點如下庫文件加載順序關鍵事實&#xff1a;您系統中的證據&#xff1a;優先級對比表&#xff1a;解決方案強化&#xff1a;最終檢查&#xff1a;本局點解決方法局點現象 數據庫 mdm 升級失敗檢查日志, 發現是由于 psql 連接數據庫報錯…

C# XML 文件

在 C# 中處理 XML 文件是非常常見的操作&#xff0c;可以使用System.Xml命名空間中的類來實現。以下是一些常用的 XML 操作示例&#xff1a; 手冊鏈接&#xff1a; System.Xml 命名空間 XmlDocument 創建一個xml數據格式的文檔 XmlDocument xml new XmlDocument(); Xml…

LOVON——面向足式Open-Vocabulary的物體導航:LLM做任務分解、YOLO11做目標檢測,最后L2MM將指令和視覺映射為動作(且解決動態模糊)

前言 因為項目需要(比如我們在做的兩個展廳講解訂單)&#xff0c;近期我一直在研究VLN相關&#xff0c;有些工作哪怕暫時還沒開源(將來可能會開源)&#xff0c;但也依然會解讀&#xff0c;比如好處之一是構建完整的VLN知識體系&#xff0c;本文便是其中一例 我在解讀過程中&am…

【Django】-3- 處理HTTP響應

HttpResponse 家族” 的常用操作&#x1f31f;1. 設置狀態碼 &#x1f44b;狀態碼是服務器告訴客戶端 “請求處理結果” 的數字暗號&#xff08;比如 404 表示 “沒找到頁面”&#xff09;。Django 里有 3 種設置方式&#xff1a;方式 1&#xff1a;直接寫數字&#xff08;簡單…

《React Router深解:復雜路由場景下的性能優化與導航流暢性構建》

路由系統是連接用戶操作與應用功能的中樞神經,而React Router作為React生態中處理路由邏輯的核心工具,其在復雜應用中的表現直接決定著用戶體驗的優劣。當應用規模擴張至數十甚至上百個路由,嵌套層級跨越多層,導航控制中的性能問題便會逐漸凸顯——從首屏加載的延遲到路由切…

網絡與信息安全有哪些崗位:(4)應急響應工程師

想知道網絡與信息安全領域有哪些具體崗位嗎&#xff1f; 網絡與信息安全有哪些崗位&#xff1a;&#xff08;1&#xff09;網絡安全工程師-CSDN博客 網絡與信息安全有哪些崗位&#xff1a;&#xff08;2&#xff09;滲透測試工程師_網絡安全滲透工程師-CSDN博客 網絡與信息安…

Leetcode 3634. Minimum Removals to Balance Array

Leetcode 3634. Minimum Removals to Balance Array 1. 解題思路2. 代碼實現 題目鏈接&#xff1a;3634. Minimum Removals to Balance Array 1. 解題思路 這一題思路上就是一個滑動窗口的思路。 我們首先將整個數組有序排列&#xff0c;然后分別從左向右考察每一個元素作為…

C#/.NET/.NET Core優秀項目和框架2025年7月簡報

前言 每月定期推廣和分享的C#/.NET/.NET Core優秀項目和框架&#xff08;每周至少會推薦兩個優秀的項目和框架當然節假日除外&#xff09;&#xff0c;推文中有項目和框架的詳細介紹、功能特點、使用方式以及部分功能截圖等。注意&#xff1a;排名不分先后&#xff0c;都是十分…

第 10 篇:深度學習的“軍火庫”——CNN、RNN與Transformer,AI如何看懂世界?

《人工智能AI之機器學習基石》系列⑩ 專欄核心理念: 用通俗語言講清楚機器學習的核心原理,強調“洞察 + 技術理解 + 應用連接”,構建一個完整的、富有啟發性的知識體系。 引

深度學習—功能性函數代碼 common.py

函數&#xff1a;返回GPU def try_gpu(i0): #save"""如果存在&#xff0c;則返回gpu(i)&#xff0c;否則返回cpu()"""if torch.cuda.device_count() > i 1: # 如果存在第 i 個 GPUreturn torch.device(fcuda:{i}) # 返回第 i 個 GPU 設…