1. 程式人生 > >C語言高階篇 - 4.連結串列&狀態機與多執行緒

C語言高階篇 - 4.連結串列&狀態機與多執行緒

1.連結串列的引入

1、從陣列的缺陷說起

        (1)陣列有2個缺陷,一個是陣列中所有元素的型別必須一致;第二個是陣列的元素個數必須事先制定並且一旦指定之後不能更改。

        (2)如何解決陣列的2個缺陷:陣列的第一個缺陷靠結構體去解決。結構體允許其中的元素的型別不相同,因此解決了陣列的第一個缺陷。所以說結構體是因為陣列不能解決某些問題所以才發明的。

        (3)如何解決陣列的第二個缺陷?我們希望陣列的大小能夠實時擴充套件。譬如我剛開始定了一個元素個數是10,後來程式執行時覺得不夠因此動態擴充套件為20.普通的陣列顯然不行,我們可以對陣列進行封裝以達到這種目的;我們還可以使用一個新的資料結構來解決,這個新的資料結構就是連結串列。

        總結:幾乎可以這樣理解:連結串列就是一個元素個數可以實時變大/變小的陣列。

 

1.2、大學為什麼都有新校區?

        (1)學校初建的時候(類似於變數定義並初始化時),這時候因為旁邊都是荒地而沒有建築,因此學校的校園大小由自己定的;但是學校建立了之後旁邊慢慢的也有了其他建築(類似於這個變數分配了之後,記憶體的相鄰區域又分配了其他變數與這個變數地址相連),這時候你的校園隨著發展感覺不夠用了想要擴充套件,卻發現鄰居已經住滿了,校園的四周全部都是別人的建築,這時候學校要擴充套件有2個辦法:第一個是拆遷,第二個是搬遷,第三個是外部擴充套件。

        (2)拆遷基本行不通,因為成本太高了。

        (3)搬遷可以行的通。程式中解決陣列大小擴充套件的一個思路就是整體搬遷。具體步驟是:先在另外的空白記憶體處建立一個大的陣列,然後把原來的陣列中的元素的值整個複製到新陣列的頭部,然後再釋放掉原來陣列的記憶體空間,並且把新的陣列去替代原來的陣列。這種可變陣列在C語言中不支援,但是在更高階語言如C++、Java等裡面是支援的。

        (4)外部擴充套件的思路是最常見的,基本可以說是最合理的。它的一個思路就是化整為零,在原來的不動的前提下去外部擴充套件新的分基地。外部擴充套件在學校的例子中就是新校區;外部擴充套件在程式設計解決陣列問題的點上就是連結串列。

 

1.3、連結串列是什麼樣的?

        (1)顧名思義,連結串列就是用鎖鏈連線起來的表。這裡的表指的是一個一個的節點(一個節點就是一個校區),節點中有一些記憶體可以用來儲存資料(所以叫表,表就是資料表);這裡的鎖鏈指的是連結各個表的方法,C語言中用來連線2個表(其實就是2塊記憶體)的方法就是指標。

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

 

1.4、時刻別忘了連結串列是用來幹嘛的

        (1)時刻謹記:連結串列就是用來解決陣列的大小不能動態擴充套件的問題,所以連結串列其實就是當陣列用的。直白點:連結串列能完成的任務用陣列也能完成,陣列能完成的任務用連結串列也能完成。但是靈活性不一樣。

        (2)簡單說:連結串列就是用來儲存資料的。連結串列用來存資料相對於陣列來說優點就是靈活性,需要多少個動態分配多少個,不佔用額外的記憶體。陣列的優勢是使用簡單(簡單粗暴)。

 

 

2.單鏈表的實現

2.1、單鏈表的節點構成

        (1)連結串列是由節點組成的,節點中包含:有效資料和指標。

        (2)定義的struct node只是一個結構體,本身並沒有變數生成,也不佔用記憶體。結構體定義相當於為連結串列節點定義了一個模板,但是還沒有一個節點,將來在實際建立連結串列時需要一個節點時用這個模板來複制一個即可。

 

2.2、堆記憶體的申請和使用

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

        (2)使用堆記憶體來建立一個連結串列節點的步驟:1、申請堆記憶體,大小為一個節點的大小(檢查申請結果是否正確);2、清理申請到的堆記憶體;3、把申請到的堆記憶體當作一個新節點;4、填充你哦個新節點的有效資料和指標區域。

 

2.3、連結串列的頭指標

        (1)頭指標並不是節點,而是一個普通指標,只佔4位元組。頭指標的型別是struct node *型別的,所以它才能指向連結串列的節點。

        (2)一個典型的連結串列的實現就是:頭指標指向連結串列的第1個節點,然後第1個節點中的指標指向下一個節點,然後依次類推一直到最後一個節點。這樣就構成了一個鏈。

 

2.4、實戰:構建一個簡單的單鏈表

        (1)目標:構建一個連結串列,然後將一些資料(譬如1,2,3三個數字)儲存在連結串列中

#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
 
 
// 構建一個連結串列的節點
struct node
{
    int data;              // 有效資料
    struct node *pNext;       // 指向下一個節點的指標
};
 
 
int main(void)
{
    // 定義頭指標
    struct node *pHeader = NULL;
     
    /********************************************************************/
    // 每建立一個新的節點,把這個新的節點和它前一個節點關聯起來
    // 建立一個連結串列節點
    struct node *p = (struct node *)malloc(sizeof(struct node));
    if (NULL == p)
    {
        printf("malloc error.\n");
        return -1;
    }
    // 清理申請到的堆記憶體
    bzero(p, sizeof(struct node));
    // 填充節點
    p->data = 1;
    p->pNext = NULL;           // 將來要指向下一個節點的首地址
                                // 實際操作時將下一個節點malloc返回的指標賦值給這個
                                 
    pHeader = p;  // 將本節點和它前面的頭指標關聯起來                   
    /********************************************************************/
     
     
    /********************************************************************/
    // 每建立一個新的節點,把這個新的節點和它前一個節點關聯起來
    // 建立一個連結串列節點
    struct node *p1 = (struct node *)malloc(sizeof(struct node));
    if (NULL == p1)
    {
        printf("malloc error.\n");
        return -1;
    }
    // 清理申請到的堆記憶體
    bzero(p1, sizeof(struct node));
    // 填充節點
    p1->data = 2;
    p1->pNext = NULL;          // 將來要指向下一個節點的首地址
                                // 實際操作時將下一個節點malloc返回的指標賦值給這個
                                 
    p->pNext = p1; // 將本節點和它前面的頭指標關聯起來                   
    /********************************************************************/
     
     
    /********************************************************************/
    // 每建立一個新的節點,把這個新的節點和它前一個節點關聯起來
    // 建立一個連結串列節點
    struct node *p2 = (struct node *)malloc(sizeof(struct node));
    if (NULL == p2)
    {
        printf("malloc error.\n");
        return -1;
    }
    // 清理申請到的堆記憶體
    bzero(p2, sizeof(struct node));
    // 填充節點
    p2->data = 3;
    p1->pNext = p2;            // 將來要指向下一個節點的首地址
                                // 實際操作時將下一個節點malloc返回的指標賦值給這個            
    /********************************************************************/
     
    // 至此建立了一個有1個頭指標+3個完整節點的連結串列。
     
    // 下面是4.9.3節的程式碼
    // 訪問連結串列中的各個節點的有效資料,這個訪問必須注意不能使用p、p1、p2,而只能
    // 使用pHeader。
     
    // 訪問連結串列第1個節點的有效資料
    printf("node1 data: %d.\n", pHeader->data);  
    printf("p->data: %d.\n", p->data);          // pHeader->data等同於p->data
     
    // 訪問連結串列第2個節點的有效資料
    printf("node2 data: %d.\n", pHeader->pNext->data);
    printf("p1->data: %d.\n", p1->data);   
    // pHeader->pNext->data等同於p1->data
     
    // 訪問連結串列第3個節點的有效資料
    printf("node3 data: %d.\n", pHeader->pNext->pNext->data);  
    printf("p2->data: %d.\n", p2->data);           
    // pHeader->pNext->pNext->data等同於p2->data
     
    return 0;
}

3.單鏈表的演算法之插入節點

3.1、繼續上節,訪問連結串列中各個節點的資料

        (1)只能用頭指標,不能用各個節點自己的指標。因為在實際當中我們儲存連結串列的時候是不會儲存各個節點的指標的,只能通過頭指標來訪問連結串列節點。

        (2)前一個節點內部的pNext指標能幫助我們找到下一個節點。

 

3.2、將建立節點的程式碼封裝成一個函式

        (1)封裝時的關鍵點就是函式的介面(函式引數和返回值)的設計

// 作用:建立一個連結串列節點
// 返回值:指標,指標指向我們本函式新建立的一個節點的首地址
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;  
     
    return p;
}

3.3、從連結串列頭部插入新節點

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;
}

3.4、從連結串列尾部插入新節點

        (1)尾部插入簡單點,因為前面已經建立好的連結串列不用動。直接動最後一個就可以了。

//思路:由頭指標向後遍歷,直到走到原來的最後一個節點。原來最後一個節點裡面的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;
}

4.單鏈表的演算法之插入節點續

4.1、什麼是頭節點

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

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

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

        (4)連結串列有沒有頭節點是不同的。體現在連結串列的插入節點、刪除節點、遍歷節點、解析連結串列的各個演算法函式都不同。所以如果一個連結串列設計的時候就有頭節點那麼後面的所有演算法都應該這樣來處理;如果設計時就沒有頭節點,那麼後面的所有演算法都應該按照沒有頭節點來做。實際程式設計中兩種連結串列都有人用,所以大家在看別人寫的程式碼時一定要注意看它有沒有頭節點。

 

5.從連結串列頭部插入新節點

        (1)注意寫程式碼過程中的箭頭符號,和說話過程中的指標指向。這是兩碼事,容易搞混。箭頭符號實際上是用指標方式來訪問結構體,所以箭頭符號的實質是訪問結構體中的成員。更清楚一點說程式中的箭頭和連結串列的連線沒有任何關係;連結串列中的節點通過指標指向來連線,程式設計中表現為一個賦值語句(用=來進行連線),實質是把後一個節點的首地址,賦值給前一個節點中的pNext元素做為值。

        (2)連結串列可以從頭部插入,也可以從尾部插入。也可以兩頭插入。頭部插入和尾部插入對連結串列來說幾乎沒有差別。對連結串列本身無差別,但是有時候對業務邏輯有差別。

 

6.單鏈表的演算法之遍歷節點

6.1、什麼是遍歷

        (1)遍歷就是把單鏈表中的各個節點挨個拿出來,就叫遍歷。

        (2)遍歷的要點:一是不能遺漏、二是不能重複、追求效率。

 

6.2、如何遍歷單鏈表

        (1)分析一個數據結構如何遍歷,關鍵是分析這個資料結構本身的特點。然後根據本身特點來制定它的遍歷演算法。

        (2)單鏈表的特點就是由很多個節點組成,頭指標+頭節點為整個連結串列的起始,最後一個節點的特徵是它內部的pNext指標值為NULL。從起始到結尾中間由各個節點內部的pNext指標來掛接。由起始到結尾的路徑有且只有一條。單鏈表的這些特點就決定了它的遍歷演算法。

        (3)遍歷方法:從頭指標+頭節點開始,順著連結串列掛接指標依次訪問連結串列的各個節點,取出這個節點的資料,然後再往下一個節點,直到最後一個節點,結束返回。

 

6.3、程式設計實戰

(1)寫一個連結串列遍歷的函式,void bianli(struct node*pH);

void bianli2(struct node*pH)
{
    //pH->data               // 頭節點資料,不是連結串列的常規資料,不要算進去了
    struct node *p = pH;        // 頭指標後面是頭節點
 
    printf("-----------開始遍歷-----------\n");
    while (NULL != p->pNext)      // 是不是最後一個節點
    {
        p = p->pNext;              // 走到下一個節點,也就是迴圈增量
        printf("node data: %d.\n", p->data);
    }
 
    printf("-------------完了-------------\n");
}

7.單鏈表的演算法之刪除節點

7.1、為什麼要刪除節點

        (1)一直在強調,連結串列到底用來幹嘛的?

        (2)有時候連結串列節點中的資料不想要了,因此要刪掉這個節點。

 

7.2、刪除節點的2個步驟

        (1)第一步:找到要刪除的節點;第二步:刪除這個節點。

 

7.3、如何找到待刪除的節點

        (1)通過遍歷來查詢節點。從頭指標+頭節點開始,順著連結串列依次將各個節點拿出來,按照一定的方法比對,找到我們要刪除的那個節點。

 

7.4、如何刪除一個節點

        (1)待刪除的節點不是尾節點的情況:首先把待刪除的節點的前一個節點的pNext指標指向待刪除的節點的後一個節點的首地址(這樣就把這個節點從連結串列中摘出來了),然後再將這個摘出來的節點free掉介面。

        (2)待刪除的節點是尾節點的情況:首先把待刪除的尾節點的前一個節點的pNext指標指向null(這時候就相當於原來尾節點前面的一個節點變成了新的尾節點),然後將摘出來的節點free掉。

 

7.5、注意堆記憶體的釋放

        (1)前面幾節課我們寫的程式碼最終都沒有釋放堆記憶體。當程式都結束了的情況下那些沒有free的堆記憶體也被釋放了。

        (2)有時候我們的程式執行時間很久,這時候malloc的記憶體如果沒有free會一直被佔用直到你free釋放它或者整個程式終止。

// 從連結串列pH中刪除節點,待刪除的節點的特徵是資料區等於data
// 返回值:當找到並且成功刪除了節點則返回0,當未找到節點時返回-1
int delete_node(struct node*pH, int data)
{
    // 找到這個待刪除的節點,通過遍歷連結串列來查詢
    struct node *p = pH;            // 用來指向當前節點
    struct node *pPrev = NULL;      // 用來指向當前節點的前一個節點
 
    while (NULL != p->pNext)      // 是不是最後一個節點
    {
        pPrev = p;                    // 在p走向下一個節點前先將其儲存
        p = p->pNext;              // 走到下一個節點,也就是迴圈增量
        // 判斷這個節點是不是我們要找的那個節點
        if (p->data == data)
        {
            // 找到了節點,處理這個節點
            // 分為2種情況,一個是找到的是普通節點,另一個是找到的是尾節點
            // 刪除節點的困難點在於:通過連結串列的遍歷依次訪問各個節點,找到這個節點
            // 後p指向了這個節點,但是要刪除這個節點關鍵要操作前一個節點,但是這
            // 時候已經沒有指標指向前一個節點了,所以沒法操作。解決方案就是增加
            // 一個指標指向當前節點的前一個節點
            if (NULL == p->pNext)
            {
                // 尾節點
                pPrev->pNext = NULL;       // 原來尾節點的前一個節點變成新尾節點
                free(p);                    // 釋放原來的尾節點的記憶體
            }
            else
            {
                // 普通節點
                pPrev->pNext = p->pNext;    // 要刪除的節點的前一個節點和它的後一個節點相連,這樣就把要刪除的節點給摘出來了
                free(p);
            }
            // 處理完成之後退出程式
            return 0;
        }
    }
    // 到這裡還沒找到,說明連結串列中沒有我們想要的節點
    printf("沒找到這個節點.\n");
    return -1;
}

8.單鏈表的演算法之逆序

8.1、什麼是連結串列的逆序

        (1)連結串列的逆序又叫反向,意思就是把連結串列中所有的有效節點在連結串列中的順序給反過來。

 

8.2、單鏈表逆序演算法分析

        (1)當我們對一個數據結構進行一個操作時,我們就需要一套演算法。這就是資料結構和演算法的關係。

        (2)總結:演算法有2個層次。第一個層次是數學和邏輯上的演算法;第二次個層次是用程式語言來實現演算法。

        (3)從邏輯上來講,連結串列的逆序有很多種方法。這些方法都能實現最終的需要,但是效率是不一樣的。彼此的可擴充套件性、容錯性等不同。

        (4)思路:首先遍歷原連結串列,然後將原連結串列中的頭指標和頭節點作為新連結串列的頭指標和頭節點,原連結串列中的有效節點挨個依次取出來,採用頭插入的方法插入新連結串列中即可。

        (5)連結串列逆序 = 遍歷 + 頭插入

 

8.3、程式設計實戰

// 將pH指向的連結串列逆序
void reverse_linkedlist(struct node *pH)
{
    struct node *p = pH->pNext;      // pH指向頭節點,p指向第1個有效節點
    struct node *pBack;               // 儲存當前節點的後一個節點地址
     
    // 當連結串列沒有有效節點或者只有一個有效節點時,逆序不用做任何操作
    if ((NULL ==p) || (NULL == p->pNext))
        return;
     
    // 當連結串列有2個及2個以上節點時才需要真正進行逆序操作
    while (NULL != p->pNext)      // 是不是最後一個節點
    {
        // 原連結串列中第一個有效節點將是逆序後新連結串列的尾節點,尾節點的pNext指向NULL
        pBack = p->pNext;          // 儲存p節點後面一個節點地址
        if (p == pH->pNext)
        {
            // 原連結串列第一個有效節點
            p->pNext = NULL;
        }
        else
        {
            // 原連結串列的非第1個有效節點
            p->pNext = pH->pNext;
        }
        pH->pNext = p;
         
        //p = p->pNext;        // 這樣已經不行了,因為p->pNext已經被改過了
        p = pBack;            // 走到下一個節點
    }
    // 迴圈結束後,最後一個節點仍然缺失
    insert_head(pH, p);
}

9.雙鏈表的引入和基本實現

9.1、單鏈表的侷限性

        (1)單鏈表是對陣列的一個擴充套件,解決了陣列的大小比較死板不容易擴充套件的問題。使用堆記憶體來儲存資料,將資料分散到各個節點之間,其各個節點在記憶體中可以不相連,節點之間通過指標進行單向連結。連結串列中的各個節點記憶體不相連,有利於利用碎片化的記憶體。

        (2)單鏈表各個節點之間只由一個指標單向連結,這樣實現有一些侷限性。侷限性主要體現在單鏈表只能經由指標單向移動(一旦指標移動過某個節點就無法再回來,如果要再次操作這個節點除非從頭指標開始再次遍歷一次),因此單鏈表的某些操作就比較麻煩(演算法比較有侷限)。回憶之前單鏈表的所有操作(插入、刪除節點、 遍歷、從單鏈表中取某個節點的數·····),因為單鏈表的單向移動性導致了不少麻煩。

        總結:單鏈表的單向移動性導致我們在操作單鏈表時,當前節點只能向後移動不能向前移動,因此不自由,不利於解決更復雜的演算法。

 

9.2、解決思路:有效資料+2個指標的節點(雙鏈表)

        (1)單鏈表的節點 = 有效資料 + 指標(指標指向後一個節點)

        (2)雙向連結串列的節點 = 有效資料 + 2個指標(一個指向後一個節點,另一個指向前一個節點)

 

9.3、雙鏈表的封裝和程式設計實現

#include <stdio.h>
#include <stdlib.h>
 
// 雙鏈表的節點
struct node 
{
    int data;                  // 有效資料
    struct node *pPrev;           // 前向指標,指向前一個節點
    struct node *pNext;           // 後向指標,指向後一個節點
};
 
struct node *create_node(int data)
{
    struct node *p = (struct node *)malloc(sizeof(struct node));
    if (NULL == p)
    {
        printf("malloc error.\n");
        return NULL;
    }
    p->data = data;
    p->pPrev = NULL;
    p->pNext = NULL;       // 預設建立的節點前向後向指標都指向NULL
     
    return p;
}
 
 
int main(void)
{
    struct node *pHeader = create_node(0);      // 頭指標
     
     
     
     
    return 0;
}

10.雙鏈表的演算法之插入節點

10.1、尾部插入

// 將新節點new插入到連結串列pH的尾部
void insert_tail(struct node *pH, struct node *new)
{
    // 第一步先走到連結串列的尾節點
    struct node *p = pH;
    while (NULL != p->pNext)
    {
        p = p->pNext;          // 第一次迴圈走過了頭節點
    }
    // 迴圈結束後p就指向了原來的最後一個節點
    // 第二步:將新節點插入到原來的尾節點的後面
    p->pNext = new;                // 後向指標關聯好了。新節點的地址和前節點的next
    new->pPrev = p;                // 前向指標關聯好了。新節點的prev和前節點的地址
                                // 前節點的prev和新節點的next指標未變動
}

10.2、頭部插入

// 將新節點new前插入連結串列pH中。
// 演算法參照圖示進行連線,一共有4個指標需要賦值。注意的是順序。
void insert_head(struct node *pH, struct node *new)
{
    // 新節點的next指標指向原來的第1個有效節點的地址
    new->pNext = pH->pNext;
     
    // 原來第1個有效節點的prev指標指向新節點的地址
    if (NULL != pH->pNext)
        pH->pNext->pPrev = new;
     
    // 頭節點的next指標指向新節點地址
    pH->pNext = new;
     
    // 新節點的prev指標指向頭節點的地址
    new->pPrev = pH;
}

11.雙鏈表的演算法之遍歷節點

        (1)雙鏈表是單鏈表的一個父集。雙鏈表中如何完全無視pPrev指標,則雙鏈表就變成了單鏈表。這就決定了雙鏈表的正向遍歷(後向遍歷)和單鏈表是完全相同的。

        (2)雙鏈表中因為多了pPrev指標,因此雙鏈表還可以前向遍歷(從連結串列的尾節點向前面依次遍歷直到頭節點)。但是前向遍歷的意義並不大,主要是因為很少有當前當了尾節點需要前向遍歷的情況。

        (3)總結:雙鏈表是對單鏈表的一種有成本的擴充套件,但是這個擴充套件在有些時候意義不大,在另一些時候意義就比較大。因此在實踐用途中要根據業務要求選擇適合的連結串列。

// 後向遍歷一個雙鏈表
void bianli(struct node *pH)
{
    struct node *p = pH;
     
    while (NULL != p->pNext)
    {
        p = p->pNext;
         
        printf("data = %d.\n", p->data);
    }
}
 
// 前向遍歷一個雙遍歷,引數pTail要指向連結串列末尾
void qianxiang_bianli(struct node *pTail)
{
    struct node *p = pTail;
     
    while (NULL != p->pPrev)
    {
        printf("data = %d.\n", p->data);
         
        p = p->pPrev;
    }
}

 

12.雙鏈表的演算法之刪除節點

// 從連結串列pH中刪除一個節點,節點中的資料是data
int delete_node(struct node *pH, int data)
{
    struct node *p = pH;
     
    if (NULL == p)
    {
        return -1;
    }
     
    while (NULL != p->pNext)
    {
        p = p->pNext;
         
        // 在這裡先判斷當前節點是不是我們要刪除的那個節點
        if (p->data == data)
        {
            // 找到了,刪除之。當前上下文是:當前節點為p
            if (NULL == p->pNext)
            {
                // 尾節點
// p表示當前節點地址,p->pNext表示後一個節點地址,p->pPrev表示前一個節點的地址
                p->pPrev->pNext = NULL;
                //p->pPrev = NULL;         可以省略,因為後面整個都被銷燬了
                // 銷燬p節點
                //free(p);
            }
            else
            {
                // 不是尾節點,普通節點
                 
                // 前一個節點的next指標指向後一個節點的首地址
                p->pPrev->pNext = p->pNext;
                 
                // 當前節點的prev和next指標都不用管,因為後面會整體銷燬整個節點
                 
                // 後一個節點的prev指標指向前一個節點的首地址
                p->pNext->pPrev = p->pPrev;
                 
                //free(p);
            }
            free(p);
             
            return 0;
        }
    }
     
    printf("未找到目標節點.\n");
    return -1;
}

13.linux核心連結串列

13.1、前述連結串列資料區域的侷限性

        (1)之前定義資料區域時直接int data;我們認為我們的連結串列中需要儲存的是一個int型別的數。但是實際上現實程式設計中連結中的節點不可能這麼簡單,而是多種多樣的。

        (2)一般實際專案中的連結串列,節點中儲存的資料其實是一個結構體,這個結構體中包含若干的成員,這些成員加起來構成了我們的節點資料區域。

 

13.2、一般性解決思路:資料區封裝為一個結構體

        (1)因為連結串列實際解決的問題是多種多樣的,所以內部資料區域的結構體構成也是多種多樣的。這樣也導致了不同程式當中的連結串列總體構成是多種多樣的。導致的問題是:我們無法通過一個泛性的、普遍適用的操作函式來訪問所有的連結串列。這就意味著我們設計一個連結串列就得寫一套連結串列的操作函式(節點建立、插入、刪除、遍歷······)

        (2)實際上深層次分析會發現:不同的連結串列雖然這些方法不能通用需要單獨寫,但是實際上內部的思路和方法是相同的,只是函式的區域性地區有不同。(實際上鍊表操作是相同的,而涉及到資料區域的操作就有不同)

        (3)鑑於以上2點:我們的理念就是,能不能有一種辦法把所有連結串列中操作方法裡共同的部分提取出來用一套標準方法實現,然後把不同的部分留著讓具體連結串列的實現者自己去處理。

 

13.3、核心連結串列的設計思路

        (1)核心連結串列中自己實現了一個純連結串列(純連結串列就是沒有資料區域,只有前後向指標)的封裝,以及純連結串列的各種操作函式(節點建立、插入、刪除、遍歷······)。這個純連結串列本身自己沒有任何用處,它的用法是給我們具體連結串列作為核心來呼叫。

 

13.4、list.h檔案簡介

        (1)核心中核心純連結串列的實現在include/linux/list.h檔案中

        (2)list.h中就是一個純連結串列的完整封裝,包含節點定義和各種連結串列操作方法。

 

14.核心連結串列的基本演算法和使用簡介

14.1、核心連結串列的節點建立、刪除、遍歷等

 

14.2、核心連結串列的使用實踐

        (1)問題:核心連結串列只有純連結串列,沒有資料區域,怎麼使用?

        (2)設計的使用方法是將核心連結串列作為將來整個資料結構的結構體的一個成員內嵌進去。

#include <linux/list.h>
 
struct driver_info
{
    int data;
};
 
// driver結構體用來管理核心中的驅動
struct driver
{
    char name[20];             // 驅動名稱
    int id;                        // 驅動id編號
    struct driver_info info;  // 驅動資訊
    struct list_head head;        // 內嵌的核心連結串列成員
};
 
struct driver2
{
    char name[20];             // 驅動名稱
    int id;                        // 驅動id編號
    struct driver_info info;  // 驅動資訊
    //struct list_head head;      // 內嵌的核心連結串列成員
    struct driver *prev;
    struct driver *next;
};
 
// 分析driver結構體,可知:前三個成員都是資料區域成員(就是我們之前簡化為int data的東西),第4個成員是一個struct list_head型別的變數,這就是一個純連結串列。
// 本來driver結構體是沒有連結串列的,也無法用連結串列來管理。但是我們driver內嵌的head成員本身就是一個純連結串列,所以driver通過head成員給自己擴充套件了連結串列的功能。
// driver通過內嵌的方式擴充套件連結串列成員,本身不只是有了一個連結串列成員,關鍵是可以通過利用list_head本身事先實現的連結串列的各種操作方法來操作head。
 
// 最終效果:我們可以通過遍歷head來實現driver的遍歷;遍歷head的函式在list.h中已經事先寫好了,所以我們核心中去遍歷driver時就不用重複去寫了。
// 通過操作head來操作driver,實質上就是通過操作結構體的某個成員變數來操作整個結構體變數。這裡面要藉助container_of巨集

15.什麼是狀態機

15.1、有限狀態機

        (1)常說的狀態機是有限狀態機FSM。FSM指的是有有限個狀態(一般是一個狀態變數的值),這個機器同時能夠從外部接收訊號和資訊輸入,機器在接收到外部輸入的訊號後會綜合考慮當前自己的狀態和使用者輸入的資訊,然後機器做出動作:跳轉到另一個狀態。

        (2)考慮狀態機的關鍵點:當前狀態、外部輸入、下一個狀態

15.2、兩種狀態機:Moore型和Mealy型

        (1)Moore型狀態機特點是:輸出只與當前狀態有關(與輸入訊號無關)。相對簡單,考慮狀態機的下一個狀態時只需要考慮它的當前狀態就行了。

        (2)Mealy型狀態機的特點是:輸出不只和當前狀態有關,還與輸入訊號有關。狀態機接收到一個輸入訊號需要跳轉到下一個狀態時,狀態機綜合考慮2個條件(當前狀態、輸入值)後才決定跳轉到哪個狀態。

15.3、狀態機的主要用途:電路設計、FPGA程式設計、軟體設計

        (1)電路設計中廣泛使用了狀態機思想

        (2)FPGA程式設計

        (3)軟體設計(框架型別的設計,譬如作業系統的GUI系統、訊息機制)

 

15.4、狀態機解決了什麼問題

        (1)我們平時寫程式都是順序執行的,這種程式有個特點:程式的大體執行流程是既定的,程式的執行是遵照一定的大的方向有跡可尋的。

        (2)但是偶爾會碰到這樣的程式:外部不一定會按照既定流程來給程式輸入資訊,而程式還需要完全能夠接收並響應外部的這些輸入訊號,還要能做出符合邏輯的輸出。

 

16.C語言實現簡單的狀態機

16.1、題目:開鎖狀態機。功能描述:使用者連續輸入正確的密碼則會開鎖,如果密碼輸入過程錯誤則鎖會退回到初始狀態重新計入密碼,即:使用者只需要連續輸入出正確的密碼即可開鎖(輸入錯誤不用撤銷、也不用刪除)

 

16.2、題目分析

#include <stdio.h>
 
 
// 給狀態機定義狀態集
typedef enum 
{
    STATE1,
    STATE2,
    STATE3,
    STATE4,
    STATE5,
    STATE6,
    STATE7,
}STATE;
 
 
int main(void)
{
    int num = 0;
    // current_state記錄狀態機的當前狀態,初始為STATE1,使用者每輸入一個正確的
    // 密碼STATE就走一步,一直到STATE為STATE7後鎖就開了;其中只要有一次使用者
    // 輸入對不上就回到STATE1.
    STATE current_state = STATE1;        // 狀態機初始狀態為STATE1
     
    // 第一步:實現一個使用者迴圈輸入密碼的迴圈
    printf("請輸入密碼,密碼正確開鎖.\n");
    while (1)
    {
        scanf("%d", &num);
        printf("num = %d.\n", num);
         
        // 在這裡處理使用者的本次輸入
        switch (current_state)
        {
            case STATE1:
                if (num == 1)
                {
                    current_state = STATE2;       // 使用者輸入對了一步,STATE走一步
                }
                else
                {
                    current_state = STATE1;
                }
                break;
            case STATE2:
                if (num == 2)
                {
                    current_state = STATE3;       // 使用者輸入對了一步,STATE走一步
                }
                else
                {
                    current_state = STATE1;
                }
                break;
            case STATE3:
                if (num == 3)
                {
                    current_state = STATE4;       // 使用者輸入對了一步,STATE走一步
                }
                else
                {
                    current_state = STATE1;
                }
                break;
            case STATE4:
                if (num == 4)
                {
                    current_state = STATE5;       // 使用者輸入對了一步,STATE走一步
                }
                else
                {
                    current_state = STATE1;
                }
                break;
            case STATE5:
                if (num == 5)
                {
                    current_state = STATE6;       // 使用者輸入對了一步,STATE走一步
                }
                else
                {
                    current_state = STATE1;
                }
                break;
            case STATE6:
                if (num == 6)
                {
                    current_state = STATE7;       // 使用者輸入對了一步,STATE走一步
                }
                else
                {
                    current_state = STATE1;
                }
                break;
            default:
                current_state = STATE1;
        }
         
        if (current_state == STATE7)
        {
            printf("鎖開了.\n");
            break;
        }
    }
     
     
    return 0;
}

17.多執行緒簡介

17.1、作業系統下的並行執行機制

        (1)並行就是說多個任務同時被執行。並行分微觀上的並行和巨集觀上的並行。

        (2)巨集觀上的並行就是從長時間段(相對於人來說)來看,多個任務是同時進行的;微觀上的並行就是真的在並行執行。

        (3)作業系統要求實現巨集觀上的並行。巨集觀上的並行有2種情況:第一種是微觀上的序列,第二種是微觀上的並行。

        (4)理論來說,單核CPU本身只有一個核心,同時只能執行一條指令,這種CPU只能實現巨集觀上的並行,微觀上一定是序列的。微觀上的並行要求多核心CPU。多核CPU中的多個核心可以同時微觀上執行多個指令,因此可以達到微觀上的並行,從而提升巨集觀上的並行度。

 

17.2、程序和執行緒的區別和聯絡

        (1)程序和執行緒是作業系統的兩種不同軟體技術,目的是實現巨集觀上的並行(通俗一點就是讓多個程式同時在一個機器上執行,達到巨集觀上看起來並行執行的效果)。

        (2)程序和執行緒在實現並行效果的原理上不同。而且這個差異和作業系統有關。譬如windows中程序和執行緒差異比較大,在linux中程序和執行緒差異不大(linux中執行緒就是輕量級的程序)。

        (3)不管是多程序還是多執行緒,最終目標都是實現並行執行。

 

17.3、多執行緒的優勢

        (1)前些年多程序多一些,近些年多執行緒開始用得多。

        (2)現代作業系統設計時考慮到了多核心CPU的優化問題,保證了:多執行緒程式在執行的時候,作業系統會優先將多個執行緒放在多個核心中分別單獨執行。所以說多核心CPU給多執行緒程式提供了完美的執行環境。所以在多核心CPU上使用多執行緒程式有極大的好處。

 

17.4、執行緒同步和鎖

        (1)多執行緒程式執行時要注意執行緒之間的同步。