1. 程式人生 > >4.9鏈表&狀態機與多線程

4.9鏈表&狀態機與多線程

new nbsp 單鏈表的實現 creat 有效 margin 必須 header 復制

4.9.1鏈表的引入

4.9.1.1、從數組的缺陷說起

  • (1)數組由兩個缺陷。一個是數組中所有元素的類型必須一致。數組的元素個數必須事先指定,並且一旦指定後不能更改。
  • (2)如何解決數組的2個缺陷:數組的第一個缺陷考結構體解決。結構體允許其中的元素類型不相同,因此解決了數組的第一個缺陷。因此結構體是因為數組不能解決某些問題而被發明出來的
  • (3)如何解決數組的第二個缺陷?我們希望數組的大小能夠實時擴展, 比如一開始我們定義了元素個數是10,後來程序運行時覺得不夠因此擴展為20。普通的數組顯然不行,我們可以對數組進行封裝以達到目的,也可以使用一個新的數據結構來解決,這個新的數據結構就是鏈表
  • 總結:幾乎可以這樣理解:鏈表就是一個元素個數可以實時變大/變小的數組。

4.9.1.2、大學為什麽要有新校區

  • (1)學校初建的時候就類似 變量的定義並初始化,因為旁邊全是荒地,因此學校的大小是由自己定的。但是學校建立了之後,旁邊慢慢的也有了其他的建築(就類似於,這個變量分配後,內存的相鄰的區域又分配了其他變量與這個變量地址相連)這時候你的校園發展感覺不夠用了想要擴展,卻發現鄰居已經住滿了,已經沒法擴展了,這時候學校擴展有兩條思路,第一種是拆遷,第二是搬遷,第三外部擴展
  • (2)拆遷基本行不通,不管是顯示生活中還是程序運行中,因為成本太高了。
  • (3)搬遷可以行得通。程序中解決數組大小擴展的一個思路就是整體搬遷。具體的思路是:先在空白內存出簡歷一個大的數組,然後把原來的數組中的元素整個復制到新數組的頭部。然後再釋放掉原來數組的內存空間,並且把我們新的數組去替代原來的數組。這種可變數組早C語言中不支持,但是在更高級語言中java c++是支持的。
  • (4)外部擴展是做常見的,也是最合理的,他的思路就是化整為0,在原來的不動的情況下,擴展新的分基地。外部擴展在學校的例子中就是新校區。外部擴展在編程中解決數組問題的方案就是鏈表

4.9.1.3、鏈表是什麽樣子的?

  • (1)顧名思義,鏈表就是用鎖鏈連接起來的表。

這裏的表只的是一個個節點(一個個校區),節點中有內存可以用愛存儲數據(所以叫表,數據表)。

這裏的鎖鏈指的是鎖鏈各個表的方法,C語言中用來連接2塊表

  • (2)鏈表是由若幹個節點組成的(鏈表的各個節點是類似的),節點是由有效數據和指針組成的。有效數據區域用來存儲信息完成任務的,指針區域用於指向鏈表的下一個節點從而構成鏈表。

4.9.1.4、時刻別忘了鏈表是用來幹嘛的

  • (1)時刻謹記:鏈表就是用來解決數組的大小不能動態擴展的問題,所以鏈表其實就是當數組用的。直白點:鏈表能完成的任務用鏈表也能完成,用數組能完成的任務用鏈表也能完成。但是靈活性不一樣。
  • (2)簡單點:鏈表就是用來存儲數據的。鏈表用來存數據相對於數組來說優點就是靈活性,需要多少個就動態分配多少個,不占用額外的內存。數組的優勢是使用簡單(簡單粗暴)。

4.9.2、單鏈表的實現

4.9.2.1、單鏈表的節點構成

  • (1)鏈表是由節點組成的,節點包含了有效數據部分和指針。
  • (2)定義的struct node 只是一個結構體,本身並沒有變量生成,也不占內存。結構體定義相當於鏈表節點定義了一個模板,但是還沒有一個節點,將來子啊實際創建李安表示需要一個節點時,用這個模板來復制即可。

4.9.2.2、堆內存的申請和使用

  • (1)鏈表的內存要求比較靈活,不能用棧,也不能用data數據段
  • (2)使用堆內存來創建一個鏈表節點的步驟:1、申請堆內存,大小為一個節點的大小(檢查申請結果是否正確)。2、清理申請到的堆內存。3、把申請到的堆內存當作一個新節點;4、填充新節點的有效數據和指針區域

4.9.2.3、鏈表的頭指針

  • (1)頭指針不是一個節點,而是一個普通指針,只占4字節。頭指針的類型是struct node * 類型的,所以它才能指向鏈表的節點
  • (2)一個典型的鏈表的實現就是:頭指針指向鏈表的第一個節點,然後第一個節點中的指針指向下一個節點,然後以此類推,一直到最後一個節點。這樣就構成了一個鏈

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

(1)目標:構建一個鏈表,將一個數據(譬如1,2,3三個數字)存儲在鏈表中。

4.9.3、單鏈表的算法值插入節點

4.9.3.1、繼續上節,訪問鏈表中各個節點的數據

  • (1)只能用頭指針,不能用各個字節自己的指針,因為實際當中我們保存鏈表的時候是不會保存各個節點的指針的,只能通過頭指針來訪問鏈表節點
  • (2)前一個節點內部的pNext指針能幫助我們找到下一個指節點

4.9.3.2創建節點的代碼封裝成一個函數

  • (1)封裝時的關鍵點就是函數的接口(函數參數和返回值)的設計
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

struct node
{
    int data;
    struct node * pNext;
};

struct node * creat_node(int data)
{
    struct node * p = (struct node *)malloc(sizeof(struct node));
    if(NULL == p)
    {
        printf("malloc error");
        return NULL;
    }
    bzero(p, sizeof(struct node));
    p->data = data;
    p->pNext = NULL;
    return p;
}

int main(void)
{
    struct node * pHeader = NULL;
    pHeader = creat_node(1);
    pHeader->pNext = creat_node(2);
    pHeader->pNext->pNext = creat_node(3);
    printf("pHeadef->data = %d.\n", pHeader->data);
    printf("pHeader->pNext.data = %d.\n", pHeader->pNext->data);
    printf("pHeader->pNext->pNext.data = %d.\n", pHeader->pNext->pNext->data);
    return 0;
}

4.9.3.3、從鏈表頭部插入新節點

4.9.3.4、從鏈表尾部插入新節點

(1)尾部插入鏈表簡單,因為前面已經建立的

技術分享
struct node
{
    int data;
    struct node * pNext;
};
//創建節點
struct node * creat_node(int data)
{
    struct node * p = (struct node *)malloc(sizeof(struct node));
    if(NULL == p)
    {
        printf("malloc error");
        return NULL;
    }
    memset(p, 0, sizeof(struct node));        //給申請的堆內存清0
    //bzero(p, sizeof(struct node));
    p->data = data;
    p->pNext = NULL;
    return p;
}
//從尾部插入
int insert_tail(struct node * pHeader, struct node * new)
{
    struct node * p = pHeader;
    if(NULL != p->pNext)
    {
        p = p->pNext;
    }
    p->pNext = new;
}

int main(void)
{
    struct node * pHeader = creat_node(1);
    insert_tail(pHeader, creat_node(2));
    insert_tail(pHeader, creat_node(3));
    printf("pHeader->data = %d.\n", pHeader->data);
    printf("pHeader->pNext->data = %d.\n", pHeader->pNext->data);
    printf("pHeader->pNext->pNext->data = %d.\n", pHeader->pNext->pNext->data);
    return 0;
}
無頭結點代碼 技術分享
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct node
{
    int data;
    struct node * pNext;
};
//創建節點
struct node * creat_node(int data)
{
    struct node * p = (struct node *)malloc(sizeof(struct node));
    if(NULL == p)
    {
        printf("malloc error");
        return NULL;
    }
    memset(p, 0, sizeof(struct node));        //給申請的堆內存清0
    //bzero(p, sizeof(struct node));
    p->data = data;
    p->pNext = NULL;
    return p;
}
//從尾部插入
int insert_tail(struct node * pHeader, struct node * new)
{
    int cnt = 0;
    struct node * p = pHeader;
    while(NULL != p->pNext)
    {
        p = p->pNext;
        cnt++;
    }
    p->pNext = new;
    pHeader->data = cnt + 1;
}

int main(void)
{
    struct node * pHeader = creat_node(0);
    insert_tail(pHeader, creat_node(1));
    insert_tail(pHeader, creat_node(2));
    insert_tail(pHeader, creat_node(3));
    printf("beader node data = %d.\n", pHeader->data);
    printf("node1 data = %d.\n", pHeader->pNext->data);
    printf("node2 data = %d.\n", pHeader->pNext->pNext->data);
    printf("node3 data = %d.\n", pHeader->pNext->pNext->pNext->data);
    return 0;
}
有頭結點代碼

4.9.4、單鏈表的算法之插入節點續

4.9.4.1、詳解鏈表頭部插入函數

4.9.4.2、什麽是頭節點

(1)問題:因為我們在insert_tail中直接默認了頭指針指向的有一個節點,因此如果程序中定義了頭指針後就直接insert_tail後會出現段錯誤。我們不得不在定義頭指針之後先creat_node後創建一個新節點給頭指針初始化,否則不能避免這個錯誤,但是這樣解決讓程序看起來邏輯有點不太順,看起來第一個節點和其他的節點有點不同,顯得有些另類。

(2)鏈表還有另外一種用法,就是把頭指針指向的第一個節點當作頭結點使用。頭結點的特點是:第一,他緊跟在頭指針的後面。第二,頭結點的數據部分是空的(有時候不是空的,而是存儲整個鏈表的節點數目 )。指針部分指向下一個節點也就是第一個節點。

(3)這樣看來頭結點和其他節點確實不太一樣。我們在創建鏈表時添加節點的方法也不同。頭結點在創建頭指針時一並創建並且和頭指針關聯起來,後面的真正的存儲數據的節點用節點添加的函數來完成,譬如insert_node

(4)鏈表有沒有頭結點是不同的,體現在鏈表的插入節點、刪除節點、遍歷節點、解析鏈表的各個算法函數都不太。所以如果一個鏈表設計的時候有頭結點,那麽後面所有的算法都應該這樣來處理。如果設計的時候沒有頭結點,那麽後面所有算法都應該按照沒有頭結點來做,實際編程中,兩種節點都有人用,所以在實際編程中,一應要看別人有沒有使用頭結點。

4.9鏈表&狀態機與多線程