1. 程式人生 > >一.從零寫單鏈表的演算法之尾部&頭部插入節點

一.從零寫單鏈表的演算法之尾部&頭部插入節點

一.連結串列的引入

1.連結串列是怎麼來的?

在瞭解連結串列的來路之前,需要知道陣列的缺陷,陣列主要有兩個缺陷:

①陣列中所有元素型別必須相同

②陣列在定義時必須明確指定陣列元素的個數,且個數一般來說是不可改的。

==如果希望陣列的大小能夠實時擴充套件。怎麼解決那?==

譬如我剛開始定了一個元素個數是10,後來程式執行時覺得不夠因此動態擴充套件為20.普通的陣列顯然不行,我們可以對陣列進行封裝以達到這種目的;如下圖

這裡寫圖片描述

我們還可以使用一個新的資料結構來解決,這個新的資料結構就是連結串列。
即採用化整為零的思路,在原來不同的情況下,去外部擴充套件新的分基地(即新的記憶體空間),然後通過連線兩個記憶體空間的這種方法,即為連結串列。

可類比大學建立新分校區。而上面那種陣列封裝的方式就是一個老校區賣掉,再去簡歷一個新校區,這樣,損失的就是效率。

這裡寫圖片描述

==最後,我們就知道了連結串列出現的初衷就是為了解決陣列大小不能實時擴充套件的問題。所以連結串列也可以當作陣列理解,作用都是用來儲存資料的==

2.連結串列的組成結構

連結串列是由若干個節點組成的(連結串列的各個節點結構是完全類似的),節點是由有效資料和指標組成的。有效資料區域用來儲存資訊完成任務的,指標區域用於指向連結串列的下一個節點從而構成連結串列。

這裡寫圖片描述

二.單鏈表的實現

單鏈表的形象理解如下
這裡寫圖片描述

連結串列是由節點組成的,節點中包含:有效資料和指標兩部分;

由上圖可知,我們來構建一個簡單的單鏈表,則需要以下幾步:

①==定義頭指標==

②==建立第一個節點,並將頭指標指向第一個節==點

③==接著建立節點,並將新建立的節點從前一個節點的尾部插入進來==。

④==依次類推,最終形成上圖的連結串列。==

程式分步驟的實現如下

==1.實現一個連結串列的首要任務就是構造節點,在c語言中構構造節點的方法就是定義一個結構體:==

// 構建一個連結串列的節點
struct node
{
    int data;   // 有效資料
    struct node *pNext; // 指向下一個節點的指標
};

==2.使用堆記憶體建立一個節點==

因為連結串列的記憶體要求比較靈活,不能用棧,也不能用data資料段。只能用堆記憶體

建立節點的過程:

①==申請一個節點大小的堆記憶體==

②==檢查堆記憶體是否申請成功==

③==清理申請到的堆記憶體==

④==填充節點中的資料==

⑤==節點中的指標域初始化為NULL;==

程式碼的具體實現

**// 作用:建立一個連結串列節點
// 返回值:指標,指標指向我們本函式新建立的一個節點的首地址
struct node * create_node(int data)
{
    struct node *p = (struct node *)malloc(sizeof(struct node));
    if (NULL == p)
    {
        printf("malloc error.\n");
        return NULL;
    }
    // 清理申請到的堆記憶體
    bzero(p, sizeof(struct node));
    // 填充節點
    p->data = data;
    p->pNext = NULL;    
      //將來用來指向下一個節點的首地址,這裡沒有下一個節點,故初始化為NULL
    return p; //返回值,是剛建立的節點的首地址的指標**
}

==3.構造一個簡單的連結串列(只含一個節點的連結串列)==

具體構造的過程:這裡只有一個節點,依次類推,只需要後面新增新的節點單鏈表就形成連結串列了

/***構建一個簡單的單鏈表,對main1中建立節點,關聯節點的方法進行封裝********/
int main2(void)
{
    // 定義頭指標
     struct node *pHeader=NULL;

         pHeader = create_node(1);  
    // 將本節點和它前面的頭指標關聯起來     
}

三.單鏈表的實現之從尾部插入節點

==#### 思考下面圖中的問題?如何將多個節點連線在連結串列的後面?==

這裡寫圖片描述
我們可以通過在連結串列尾部插入節點的方法實現

尾部插入節點的任務分析:

思路:將從連結串列尾部插入節點的任務分為兩步:

①==找到連結串列的最後一個節點==

②==將新的節點和原來的最後一個節點連線起來==

這裡寫圖片描述

程式碼具體實現:

/**思路:將從連結串列尾部插入節點的任務分為兩步:
①找到連結串列的最後一個節點
②將新的節點和原來的最後一個節點連線起來
引數:struct node *ph:連結串列的頭指標ph
      struct node *new:插入新節點的首地址new
思路:由頭指標向後遍歷,直到走到原來的最後一個節點。原來最後一個節點裡面的pNext是NULL,現在我們只要將它改成new就可以了。添加了之後新節點就變成了最後一個。
**/
void insert_tail(struct node *ph,struct node *new)
{
    struct node *p=ph;
    while(NULL !=p->pNext)//第一步,找到連結串列的最後一個節點
    {
        p=p->pNext;      //這句話意思是如果沒有找到尾節點,就訪問下一個節點,直到找到最後一個節點為止,跳出迴圈
    }

    p->pNext=new;//將新節點的首地址和原來最後一個節點連線起來    
}
/**************單鏈表的演算法之尾部插入節點***********************/
int main3(void)
{
    // 定義頭指標
    // struct node *pHeader=NULL;
    //這樣令頭指標等於NULL,會導致insert_tail函數出錯引發段錯誤。
    //故這樣,直接定義一個頭指標後建立一個新的節點給予指標初始化
    struct node *pHeader=create_node(1);//注意這樣並不是頭節點,而是直接賦節點值

     //呼叫該函式使連結串列尾部插入新的節點
     insert_tail(pHeader,create_node(2));
     insert_tail(pHeader,create_node(3));
     insert_tail(pHeader,create_node(4));

    // 訪問連結串列第1個節點的有效資料
    printf("node1 data: %d.\n", pHeader->data); 

    // 訪問連結串列第2個節點的有效資料
    printf("node2 data: %d.\n", pHeader->pNext->data);  

    // 訪問連結串列第3個節點的有效資料
    printf("node3 data: %d.\n", pHeader->pNext->pNext->data);   

    // 訪問連結串列第4個節點的有效資料
    printf("node4 data: %d.\n", pHeader->pNext->pNext->pNext->data);    

    return 0;
}

四.單鏈表的實現之從尾部插入節點引出的頭節點問題

1.什麼是頭節點

(1)問題:因為我們在insert_tail中直接默認了頭指標指向的有一個節點,因此如果程式中直接定義了頭指標後就直接insert_tail就會報段錯誤。我們不得不在定義頭指標之後先create_node建立一個新節點給頭指標初始化,否則不能避免這個錯誤;但是這樣解決讓程式看起來邏輯有點不太順,因為看起來第一個節點和後面的節點的建立、新增方式有點不同。

(2)連結串列還有另外一種用法,就是把頭指標指向的第一個節點作為頭節點使用。
頭節點的特點是:第一,它緊跟在頭指標後面。第二,頭節點的資料部分是空的(有時候不是空的,而是儲存整個連結串列的節點數),指標部分指向下一個節點,也就是第一個節點。

(3)這樣看來,頭節點確實和其他節點不同。我們在建立一個連結串列時新增節點的方法也不同。頭節點在建立頭指標時一併建立並且和頭指標關聯起來;後面的真正的儲存資料的節點用節點新增的函式來完成,譬如insert_tail.

這裡寫圖片描述

程式碼具體實現:

/**思路:將從連結串列尾部插入節點的任務分為兩步:
①找到連結串列的最後一個節點
②將新的節點和原來的最後一個節點連線起來
引數:struct node *ph:連結串列的頭指標ph
      struct node *new:插入新節點的首地址new
思路:由頭指標向後遍歷,直到走到原來的最後一個節點。原來最後一個節點裡面的pNext是NULL,現在我們只要將它改成new就可以了。添加了之後新節點就變成了最後一個。
//計算添加了新的節點後總共有多少個節點,然後把這個數寫入頭節點中
**/
void insert_tail(struct node *ph,struct node *new)
{
    int cnt=0;
    struct node *p=ph;
    while(NULL !=p->pNext)//第一步,找到連結串列的最後一個節點
    {
        p=p->pNext;      //這句話意思是如果沒有找到尾節點,就訪問下一個節點,直到找到最後一個節點為止,跳出迴圈
        cnt++;
    }

    p->pNext=new;//將新節點的首地址和原來最後一個節點連線起來
    ph->data=cnt+1;
}
/**頭節點的引入,需要改insert_tail函式****************************/
int main(void)
{
    // 定義一個頭節點,用於存連結串列的節點數
    struct node *pHeader=create_node(0);

     //呼叫該函式使連結串列尾部插入新的節點
     insert_tail(pHeader,create_node(1));
     insert_tail(pHeader,create_node(2));
     insert_tail(pHeader,create_node(3));

    // 訪問連結串列頭結點的有效資料 
    printf("header node data: %d.\n", pHeader->data);   

    // 訪問連結串列第1個節點的有效資料
    printf("node1 data: %d.\n", pHeader->pNext->data);  

    // 訪問連結串列第2個節點的有效資料
    printf("node2 data: %d.\n", pHeader->pNext->pNext->data);   

    // 訪問連結串列第3個節點的有效資料
    printf("node3 data: %d.\n", pHeader->pNext->pNext->pNext->data);    

    return 0;
}

頭指標與頭結點的區別:

==但我們一般在預設連結串列的演算法中都使用頭結點。==

所以如果一個連結串列設計的時候就有頭節點那麼後面的所有演算法都應該這樣來處理;如果設計時就沒有頭節點,那麼後面的所有演算法都應該按照沒有頭節點來做

五.單鏈表的實現之從頭部插入節點

這裡寫圖片描述

頭結點插入的重要兩個步驟:

==①.新節點的pNext指向原來的第一個節點的首地址,即圖中的新節點和原來的第一個節點相連==

==②.頭節點的pNext指向新節點的首地址,即圖中頭結點和新節點相連==

思考:第一步和第二步可不可以交換一下順序?

==答案顯然是不能的,因為由上圖可知,步驟②是可以完成的,但是執行步驟①時就會發現,原來的第一個有效節點的地址已經丟失了,由圖中知道,我們原來第一個節點的首地址是在頭結點的pNext指標中儲存的,因為先執行了步驟②,故原來第一個節點的有效地址丟失了。==

程式碼實現思路:

第1步: 新節點的next指向原來的第一個節點
第2步: 頭節點的next指向新節點的地址
第3步: 頭節點中的計數要加1
// 思路:單鏈表之從頭部插入節點
void insert_head(struct node *pH, struct node *new)
{
    // 第1步: 新節點的next指向原來的第一個節點
    new->pNext = pH->pNext;

    // 第2步: 頭節點的next指向新節點的地址
    pH->pNext = new;

    // 第3步: 頭節點中的計數要加1
    pH->data += 1;
}

思考以下:連結串列可以從頭部插入,也可以從尾部插入的輸出資訊是什麼?

/****連結串列從頭部插入,也可以從尾部插入列印對應節點的資訊***********/
int main(void)
{
    // 定義一個頭節點,用於存連結串列的節點數
    struct node *pHeader=create_node(0);

     //呼叫該函式使連結串列尾部插入新的節點
         insert_head(pHeader,create_node(1));//頭部插入
     insert_tail(pHeader,create_node(2));//尾部插入
     insert_head(pHeader,create_node(3));//頭部插入

    // 訪問連結串列頭結點的有效資料 
    printf("header node data: %d.\n", pHeader->data);   

    // 訪問連結串列第1個節點的有效資料
    printf("node1 data: %d.\n", pHeader->pNext->data);  

    // 訪問連結串列第2個節點的有效資料
    printf("node2 data: %d.\n", pHeader->pNext->pNext->data);   

    // 訪問連結串列第3個節點的有效資料
    printf("node3 data: %d.\n", pHeader->pNext->pNext->pNext->data);    

    return 0;
}

畫個圖加深影響:一目瞭然,所以,分析連結串列時候多畫圖
這裡寫圖片描述

故列印結果為3 1 2
這裡寫圖片描述

相關推薦

.單鏈演算法尾部&頭部插入節點

一.連結串列的引入 1.連結串列是怎麼來的? 在瞭解連結串列的來路之前,需要知道陣列的缺陷,陣列主要有兩個缺陷: ①陣列中所有元素型別必須相同 ②陣列在定義時必須明確指定陣列元素的個數,且個數一般來說是不可改的。 ==如果希望陣列的大小能夠實時擴充

單鏈基礎增,刪,查!附贈一道連結串列題

**​一,文章簡介** 大家好,該文章是本人的公眾號第一篇文章,是想通過最基礎的資料結構來延伸出後面的演算法題目。想必大家都知道演算法和資料結構一直是程式設計師的必修課,但是我發現一味的刷題未必就會有好的效果,畢竟題目都是百變的。也正因為如此,個人想出一個方法先去熟悉下最基礎的知識,比如該篇的單鏈表結構。在

三.雙鏈到基本演算法的實現(終)

一.雙鏈表的引入和基本實現 1.雙鏈表的結構 首先,我們要明白雙鏈表並不是有兩條鏈的連結串列,而是有兩個遍歷方向的連結串列,因此我們所說的雙鏈表其實就是雙向連結串列的簡稱。 2.有效資料+2個指標的節點(雙鏈表) (1)單鏈表的節點 = 有效

一個編譯器(六):語法分析驅動語法分析

專案的完整程式碼在 C2j-Compiler 前言 上一篇已經正式的完成了有限狀態自動機的構建和足夠判斷reduce的資訊,接下來的任務就是根據這個有限狀態自動機來完成語法分析表和根據這個表來實現語法分析 reduce資訊 在完成語法分析表之前,還差最後一個任務,那就是描述reduce資訊,來指導自動機是

一個編譯器(七):語義分析符號的資料結構

專案的完整程式碼在 C2j-Compiler 前言 有關符號表的檔案都在symboltable包裡 前面我們通過完成一個LALR(1)有限狀態自動機和一個reduce資訊來構建了一個語法解析表,正式完成了C語言的語法解析。接下來就是進入語義分析部分,和在第二篇提到的一樣,語義分析的主要任務就是生成符號

一個編譯器(八):語義分析構造符號

專案的完整程式碼在 C2j-Compiler 前言 在之前完成了描述符號表的資料結構,現在就可以正式構造符號表了。符號表的建立自然是要根據語法分析過程中走的,所以符號表的建立就在LRStateTableParser裡的takeActionForReduce方法 不過在此之前,當然還需要一個方便對這個符號表

一個編譯器(十):程式碼生成Java位元組碼基礎

專案的完整程式碼在 C2j-Compiler 前言 第十一篇,終於要進入程式碼生成部分了,但是但是在此之前,因為我們要做的是C語言到位元組碼的編譯,所以自然要了解一些位元組碼,但是由於C語言比較簡單,所以只需要瞭解一些位元組碼基礎 JVM的基本機制 JVM有一個執行環境叫做stack frame 這個

一起學Spring BootLayIM項目長成記() 初見 Spring Boot

部分 基礎 依賴 com stat boot.s 情況下 比較 tar 項目背景   之前寫過LayIM的.NET版後端實現,後來又寫過一版Java的。當時用的是servlet,websocket和jdbc。雖然時間過去很久了,但是仍有些同學在關註。偶然間我聽說了Sprin

一個Java WEB框架(

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

Python+Flask+Gunicorn 專案實戰() 開始,一個Markdown解析器 —— 初體驗

      (一)前言 在開始學習之前,你需要確保你對Python, JavaScript, HTML, Markdown語法有非常基礎的瞭解。專案的原始碼你可以在 https://github.com/zhu-y/markdown-toolkit 找到,最後的

(四)單鏈演算法刪除節點

(四)單鏈接表演算法之刪除節點 思路分析程式碼實現 思路分析 第一種情況:刪除的節點不是尾節點 刪除的節點不是尾節點   找到需要刪除的某一節點; 儲存刪除節點的前一節點指標pPrev; pPrev-> pNext = p ->pNex

線性單鏈演算法

#include<stdio.h> #include<stdlib.h> typedef int datatype; typedef struct node { int data; struct node *next;

C++實現深度神經網路六——實戰手數字識別(sigmoid和tanh)

本文由@星沉閣冰不語出品,轉載請註明作者和出處。之前的五篇部落格講述的內容應該覆蓋瞭如何編寫神經網路的大部分內容,在經過之前的一系列努力之後,終於可以開始實戰了。試試寫出來的神經網路怎麼樣吧。一、資料準

[開始系列]AndroidApp研發路() 網路請求的封裝()

文章目錄: - 前言 - 封裝成果 - 封裝細節 - 如何使用 - 注意 - 作者 前言 寫在前面 技術選型 元件化設計 ReactNative-Android 的簡單實踐 阿里Atlas(外掛化)與該專案的簡單實踐 集and

一個編譯器(五):語法分析自動機的缺陷和改進

專案的完整程式碼在 C2j-Compiler 前言 在上一篇,已經成功的構建了有限狀態自動機,但是這個自動機還存在兩個問題: 無法處理shift/reduce矛盾 狀態節點太多,導致自動機過大,效率較低 這一節就要解決這兩個問題 shift/reduce矛盾 看上一節那個例子的一個節點 e ->

一個編譯器(九):語義分析構造抽象語法樹(AST)

專案的完整程式碼在 C2j-Compiler 前言 在上一篇完成了符號表的構建,下一步就是輸出抽象語法樹(Abstract Syntax Tree,AST) 抽象語法樹(abstract syntax tree 或者縮寫為 AST),是原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。樹上的

一個編譯器(十):編譯前傳直接解釋執行

專案的完整程式碼在 C2j-Compiler 前言 這一篇不看也不會影響後面程式碼生成部分 現在經過詞法分析語法分析語義分析,終於可以進入最核心的部分了。前面那部分可以稱作編譯器的前端,程式碼生成程式碼優化都是屬於編譯器後端,如今有關編譯器的工作崗位主要都是對後端的研究。當然現在寫的這個編譯器因為水平有限

一個編譯器(十二):程式碼生成生成邏輯

專案的完整程式碼在 C2j-Compiler 前言 在上一篇解釋完了一些基礎的Java位元組碼指令後,就可以正式進入真正的程式碼生成部分了。但是這部分先說的是程式碼生成依靠的幾個類,也就是用來生成指令的操作。 這一篇用到的檔案都在codegen下: Directive.java Instruction.

一個編譯器(十三):程式碼生成遍歷AST

專案的完整程式碼在 C2j-Compiler 前言 在上一篇完成對JVM指令的生成,下面就可以真正進入程式碼生成部分了。通常現代編譯器都是先把生成IR,再經過程式碼優化等等,最後才編譯成目標平臺程式碼。但是時間水平有限,我們沒有IR也沒有程式碼優化,就直接利用AST生成Java位元組碼 入口 進行程式碼生

PHP反向刪除單鏈元素的問題談起

在完成一個單鏈表的刪除指定元素的題目中,我發現了一件神奇的事情,php物件賦值給另外一個變數後,可以如同引用傳值一般繼續利用新的變數來實現連結串列的連結。 後面經過查證後發現: > PHP7.0版本除了物件,資源之外,其餘資料型別均已實現寫時複製 嘗試寫了一個簡單測試程式碼,如下所示: ``` va