數(shù)據(jù)結(jié)構(gòu)與算法 - 單鏈表

本文首發(fā)于 個(gè)人博客

對(duì)于非空的線性表和線性結(jié)構(gòu)劣光,具有以下特點(diǎn):

  • 存在唯一的一個(gè)被稱作 第一個(gè) 的數(shù)據(jù)元素
  • 存在唯一的一個(gè)被稱作 最后一個(gè) 的數(shù)據(jù)元素
  • 除了第一個(gè)元素以外儡嘶,其他每個(gè)數(shù)據(jù)元素都有 一個(gè)前驅(qū)
  • 除了最后一個(gè)元素以外颁督,其他每個(gè)數(shù)據(jù)元素都有 一個(gè)后繼

線性表是最基本也是最常用的一種線性結(jié)構(gòu)躬它,同時(shí)它也是其他數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)顷牌。尤其是 單鏈表 沟优,這篇文章主要講述一下單鏈表的結(jié)構(gòu)以及如何用 C語(yǔ)言 實(shí)現(xiàn)一個(gè)單鏈表岔擂。

單鏈表的實(shí)現(xiàn)

單鏈表是一種線性數(shù)據(jù)結(jié)構(gòu),我們考察的主要是它的初始化惩嘉、添加罢洲、刪除、遍歷等方法

初始化

單向鏈表是由一個(gè)個(gè)節(jié)點(diǎn)組成的文黎,每一個(gè)節(jié)點(diǎn)都包含一個(gè)數(shù)據(jù)段和指針段惹苗,數(shù)據(jù)段主要保存節(jié)點(diǎn)的相關(guān)信息,指針段主要保存后繼節(jié)點(diǎn)的地址耸峭。

#define ERROR 0
#define TRUE 1
#define FAILURE 0
#define SUCCESS 1

typedef int Status; /* Status是函數(shù)的類型,其值是函數(shù)結(jié)果狀態(tài)代碼桩蓉,如 SUCCESS、FAILURE等 */
typedef int ListData;/* ListData類型根據(jù)實(shí)際情況而定劳闹,這里假設(shè)為int */

// 定義節(jié)點(diǎn)
typedef struct Node{
    ListData data;
    struct Node *next;
}Node;

typedef struct Node *LinkList;
// 初始化單鏈表
Status InitList(LinkList *L) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULL) return ERROR;
    (*L)->next = NULL;
    return SUCCESS;
}

添加節(jié)點(diǎn)

既然鏈表的節(jié)點(diǎn)已經(jīng)定義而且鏈表的數(shù)據(jù)結(jié)構(gòu)已經(jīng)初始化院究,接下來(lái)我們看看如何去添加元素,這里我們看兩種情況:

  • 不帶頭節(jié)點(diǎn)的單鏈表

如圖所示玷或,我們要對(duì)單鏈表進(jìn)行插入操作的時(shí)候要進(jìn)行額外判斷儡首,如果要在首元節(jié)點(diǎn)之前添加節(jié)點(diǎn)片任,就要挪動(dòng)List指針偏友,如果其他位置則不用。

  • 帶頭節(jié)點(diǎn)的單鏈表

這里帶頭節(jié)點(diǎn)的插入就很簡(jiǎn)單了对供,所有的地方一視同仁位他,而不需要額外去操作鏈表的指針。PS:其中頭節(jié)點(diǎn)中的信息我們不需要關(guān)心产场,因?yàn)樗喈?dāng)于我們默認(rèn)放進(jìn)去的一個(gè)節(jié)點(diǎn)鹅髓。

所以后續(xù)我們使用的單鏈表默認(rèn) 添加頭節(jié)點(diǎn)

我們要插入節(jié)點(diǎn)京景,就要對(duì)鏈表進(jìn)行修改窿冯,那么我們需要把 鏈表的指針 作為參數(shù)傳入番刊。

// location from 1...
Status InsertNode(LinkList *L,int location,ListData data) {
    // 找到需要location-1位置的節(jié)點(diǎn)
    Node *pre = *L;
    // 因?yàn)?位置被頭節(jié)點(diǎn)占了芙粱,所以要從1位置開始
    int currentLocation = 1;
    while (pre && currentLocation < location) {
        pre = pre->next;
        currentLocation ++;
    }
    if (!pre || currentLocation < location) return ERROR;

    // 根據(jù)data生成一個(gè)新節(jié)點(diǎn)
    Node *insertNode = (Node *)malloc(sizeof(Node));
    insertNode->data = data;
    // 讓新節(jié)點(diǎn)的next 指向 pre->next
    insertNode->next = pre->next;
    // 讓前一個(gè)節(jié)點(diǎn)的next指向新節(jié)點(diǎn)
    pre->next = insertNode;
    return SUCCESS;
}

此處邏輯跟圖上一一對(duì)應(yīng),接下來(lái)我們要驗(yàn)證就要打印鏈表每個(gè)位置的值,我們添加一個(gè)打印的方法桅狠,我們只是打印鏈表,沒必要把傳遞指針椎工。

// 打印方法 我們不用修改鏈表 無(wú)需傳指針
Status printList (LinkList L) {
    LinkList p = L->next;
    while (p) {
        printf("%d\n",p->data);
        p = p->next;
    }
    return SUCCESS;
}

我們驗(yàn)證一下:

int main(int argc, const char * argv[]) {
    LinkList L;
    Status status = InitList(&L);
    printf("s is %d\n",status);
    // 插入元素
    for (int i = 10; i >= 1; i --) {
        InsertNode(&L, 1, i);// 此處1表示冒冬,總是從頭節(jié)點(diǎn)后面插入新節(jié)點(diǎn),也就是頭插法缠沈,比較簡(jiǎn)單膘壶,因?yàn)槲膊宸ㄟ€要保留鏈表長(zhǎng)度
    }
    // 打印鏈表
    printList(L);
    return 0;
}

打印結(jié)果如下:


刪除節(jié)點(diǎn)

結(jié)果如我們所愿,鏈表創(chuàng)建以及插入讀取都正常洲愤,接下來(lái)我們看看鏈表是如何刪除節(jié)點(diǎn)的:


  • 創(chuàng)建臨時(shí)變量指向我們即將刪除的節(jié)點(diǎn)颓芭,一方面為了找到下一個(gè)節(jié)點(diǎn),另外一個(gè)方面為了釋放內(nèi)存柬赐,否則就內(nèi)存泄漏了畜伐。
  • 直接將臨時(shí)變量的上一個(gè)節(jié)點(diǎn)直接指向臨時(shí)變量的下一個(gè)節(jié)點(diǎn)
  • 釋放臨時(shí)變量
Status DeleteNode (LinkList *L ,int location,ListData *deleteData) {
    Node *pre = *L;
    int currentLocation = 1;
    // 還是找到location-1位置的節(jié)點(diǎn)
    while (pre && currentLocation < location) {
        pre = pre->next;
        currentLocation++;
    }
    if (!pre || currentLocation < location) return ERROR;
    // 創(chuàng)建臨時(shí)變量 保存即將被刪除的節(jié)點(diǎn)
    Node *temp = pre->next;
    if (!temp) return ERROR;
    // 前驅(qū)節(jié)點(diǎn)指向后驅(qū)節(jié)點(diǎn)
    pre->next = temp->next;
    // 將我們刪除的內(nèi)容返回出去
    *deleteData = temp->data;
    // 釋放內(nèi)存
    free(temp);
    return SUCCESS;
}
//在main方法中添加如下代碼驗(yàn)證
// 刪除第五個(gè)節(jié)點(diǎn)
    ListData data;
    DeleteNode(&L, 5, &data);
    printf("刪除第五個(gè)元素后的鏈表是 :\n");
    printList(L);
    printf("被刪除的值是 %d\n",data);

清空單鏈表

  1. 指針指向首元節(jié)點(diǎn) 注意不是頭節(jié)點(diǎn) ,并將該節(jié)點(diǎn)釋放
  2. 指針偏移到下一個(gè)節(jié)點(diǎn) 中間節(jié)點(diǎn)
  3. 釋放下一個(gè)節(jié)點(diǎn) 中間節(jié)點(diǎn)
  4. 指針以此類推到 尾節(jié)點(diǎn)
  5. 釋放 尾結(jié)點(diǎn)
  6. 頭節(jié)點(diǎn)指向 NULL 此處如果不處理躺率,頭節(jié)點(diǎn)的 next就是 野指針
Status clearList(LinkList *L) {
    // 由于第一個(gè)是頭節(jié)點(diǎn)玛界,我們從第二個(gè)節(jié)點(diǎn)開始刪除,這個(gè)地方可以根據(jù)實(shí)際情況來(lái)
    Node *pre = (*L)->next;
    Node *nextNode;
    while (pre) {
        // 用一個(gè)臨時(shí)變量保存當(dāng)前要?jiǎng)h除的節(jié)點(diǎn)指向的下一個(gè)節(jié)點(diǎn)悼吱,有點(diǎn)像遞歸的意思
        nextNode = pre->next;
        // 釋放
        free(pre);
        // 將要?jiǎng)h除的指針偏移到下一個(gè)指針
        pre = nextNode;
    }
    // 此處將頭節(jié)點(diǎn)指向NULL 慎框,否則就出現(xiàn)野指針了
    (*L)->next = NULL;
    return SUCCESS;
}

頭插法初始化

根據(jù)名字就知道了,從表頭處添加節(jié)點(diǎn)后添,之前一篇文章 @synchronized底層探索 里的數(shù)據(jù)結(jié)構(gòu)用的就是哈希表笨枯,內(nèi)部就是通過(guò)頭插法進(jìn)行操作的。

Status InitFromHead(LinkList *L,int n) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULL) return ERROR;
    (*L)->next = NULL;
    Node *pre = *L;
    for (int i = 1; i <= n; i ++) {
        Node *temp = (Node *)malloc(sizeof(Node));
        temp->data = i;
        temp->next = pre->next;
        pre->next = temp;
    }
    return SUCCESS;
}

// 在main中添加如下遇西,會(huì)倒序打印30---1 就是頭插法
    clearList(&L);
    InitFromHead(&L, 30);
    printf("鏈表是 :\n");
    printList(L);

上述打印結(jié)果會(huì)從30倒序到1馅精,足以證明是頭插法。

尾插法初始化

尾插法就是從鏈表尾部依次插入數(shù)據(jù)粱檀,這樣就跟我們平常的數(shù)組的邏輯差不多了洲敢,相當(dāng)于addObject,這里跟頭插法不同的是,頭插法依賴頭節(jié)點(diǎn)茄蚯,此處依賴尾節(jié)點(diǎn)压彭,所以我們要用一個(gè)臨時(shí)的指針指向尾結(jié)點(diǎn)并依次保存。

Status InitFromTail(LinkList *L,int n) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULL) return ERROR;
    // 初始化的時(shí)候尾結(jié)點(diǎn)就是頭節(jié)點(diǎn)
    Node *tail = *L;
    for (int i = 1; i <= n; i ++) {
        Node *temp = (Node *)malloc(sizeof(Node));
        temp->data = i;
        temp->next = NULL;
        tail->next = temp;
        // 尾節(jié)點(diǎn)偏移
        tail = tail->next;
    }
    return SUCCESS;
}
// 在main函數(shù)中添加如下代碼
    clearList(&L);
    InitFromTail(&L, 30);
    printf("鏈表是 :\n");
    printList(L);

但是細(xì)心的觀察你會(huì)發(fā)現(xiàn)渗常,這個(gè)往鏈表尾部添加節(jié)點(diǎn)的方法的關(guān)鍵點(diǎn)在于先要找到尾節(jié)點(diǎn)壮不,無(wú)非是通過(guò)循環(huán)直到找到一個(gè)節(jié)點(diǎn)的 nextNULL ,對(duì)于這個(gè)方法如果要初始化一個(gè)包含100個(gè)數(shù)字的的鏈表就要循環(huán)1+2+3+....+100 = 5050次,而用它上面那個(gè)函數(shù)添加的話只用循環(huán)100次皱碘,這個(gè)函數(shù)的時(shí)間復(fù)雜度是n*(n+1)/2 即是 O(n^2) 而上面那個(gè)是 O(n),所以這個(gè)方法只能針對(duì)初始化之后需要從尾部額外添加一個(gè)節(jié)點(diǎn)使用询一。

單向循環(huán)鏈表

看到 循環(huán) 兩個(gè)字我們就大概知道了,就是所有的節(jié)點(diǎn)組成一個(gè) 閉合的環(huán),看起來(lái)應(yīng)該是這樣:

因?yàn)樯厦娴膯捂湵砦覀冞x用了使用頭節(jié)點(diǎn)的方式健蕊,下面我沒使用不帶頭節(jié)點(diǎn)的方式實(shí)現(xiàn)單向循環(huán)鏈表缓醋。具體以代碼體現(xiàn),其原理跟單向鏈表差不多绊诲,有不清楚的結(jié)合單向鏈表的圖連接首位即可送粱。

初始化

這里我們采用符合我們正常邏輯的尾插法來(lái)實(shí)現(xiàn)單向循環(huán)鏈表的初始化,因?yàn)槲覀兪褂貌粠ь^節(jié)點(diǎn)的方式掂之,這里我們就要對(duì)鏈表的首元節(jié)點(diǎn)進(jìn)行判斷:

  • 首元節(jié)點(diǎn)不存在抗俄,初始化并創(chuàng)建賦值給鏈表地址
  • 存在即找到當(dāng)前鏈表的尾節(jié)點(diǎn),依次插入后續(xù)節(jié)點(diǎn)
// 輸入的方式尾插法創(chuàng)建單向循環(huán)鏈表
Status InitList(LinkList *L) {
    int number;
    Node *tail = NULL;
    while (1) {
        scanf("%d",&number);
        // 輸入0結(jié)束創(chuàng)建
        if (number == 0) break;
        if (*L == NULL) {
            *L = (LinkList)malloc(sizeof(Node));
            if (*L == NULL)return ERROR;
            (*L)->data = number;
            (*L)->next = *L;
            tail = *L;
        } else {
            //找尾結(jié)點(diǎn)  方法1
            for (tail = *L; tail->next != *L; tail = tail->next);
            Node *temp = (Node *)malloc(sizeof(Node));
            if (!temp) return ERROR;
            temp->data = number;
            temp->next = *L;
            tail->next = temp;
            if (tail == NULL) return ERROR;
            //方法2
            Node *node = (Node *)malloc(sizeof(Node));
            if (!node) return ERROR;
            node->data = number;
            node->next = *L;
            tail->next = node;
            tail = tail->next;
        }
    }
    return SUCCESS;
}

上述我們找尾節(jié)點(diǎn)展示了兩種方法:

  • 方法①:每次從首元節(jié)點(diǎn)依次循環(huán)直至找到 節(jié)點(diǎn)的next = 首元節(jié)點(diǎn) 即是當(dāng)前的尾結(jié)點(diǎn)世舰。

  • 方法②:用一個(gè)臨時(shí)變量指向尾節(jié)點(diǎn)(初始化的時(shí)候尾結(jié)點(diǎn)指向首元節(jié)點(diǎn))动雹,每次插入新節(jié)點(diǎn),臨時(shí)變量進(jìn)行偏移繼續(xù)指向尾結(jié)點(diǎn)跟压。

    顯然方法②的時(shí)間復(fù)雜度更小一點(diǎn) O(n)胰蝠,方法①每插入一個(gè)新數(shù)據(jù)都要循環(huán)遍歷整個(gè)鏈表其時(shí)間復(fù)雜度是O(n^2)

插入節(jié)點(diǎn)

插入節(jié)點(diǎn)的邏輯其實(shí)跟單向鏈表差不多震蒋,無(wú)非就是指針的一些指向茸塞,但是這里要注意一些細(xì)節(jié)點(diǎn):

  • 插入新的首元節(jié)點(diǎn),就要找到尾節(jié)點(diǎn)查剖,然后尾節(jié)點(diǎn)指向新首元節(jié)點(diǎn)钾虐,新首元節(jié)點(diǎn)指向原首元節(jié)點(diǎn),鏈表地址指向新首元節(jié)點(diǎn)笋庄。

  • 其他地方同單向鏈表邏輯

void InsertNode(LinkList *List,int location, ListData data) {
    // 創(chuàng)建待插入節(jié)點(diǎn)
    Node *insertNode = (Node *)malloc(sizeof(Node));
    if (insertNode == NULL) return;
    insertNode->data = data;
    insertNode->next = NULL;
    if (location == 1) {
        // 找到最后一個(gè)節(jié)點(diǎn)即尾結(jié)點(diǎn)
        Node *tail = NULL;
        for (tail = *List; tail->next != *List; tail = tail->next);
        insertNode->next = tail->next;
        tail->next = insertNode;
        *List = insertNode;
    } else {
        Node *preNode = *List;
        // 找到插入位置的的前一個(gè)節(jié)點(diǎn)
        for (int i = 1; preNode->next != *List && i != location-1; preNode = preNode->next,i++);
        insertNode->next = preNode->next;
        preNode->next = insertNode;
    }
}

刪除節(jié)點(diǎn)

刪除節(jié)點(diǎn)同樣我們也要對(duì)首元節(jié)點(diǎn)的處理單獨(dú)拎出來(lái):

Status DeleteNode(LinkList *List,int location,ListData *deleteData) {
    Node *temp = *List;
    if (temp == NULL) return ERROR;
    Node *target;
    if (location == 1) {// 刪除首元節(jié)點(diǎn)
        // 找到尾節(jié)點(diǎn)
        for (target = *List; target->next != *List; target = target->next);
        if (target == *List) {
            target->next = NULL;
            *List = NULL;
            *deleteData = temp->data;
            free(target);
            return SUCCESS;
        }
        target->next = temp->next;
        *List = target->next;
        *deleteData = temp->data;
        free(temp);
    } else {
        // 找到待刪除節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)
        target = *List;
        int i;
        for (i = 1,target = *List; i < location-1; i ++) {
            target = target->next;
        }
        Node *deleteNode = target->next;
        target->next = deleteNode->next;
        *deleteData = deleteNode->data;
        free(deleteNode);
    }
    return SUCCESS;
}

總結(jié)

至此我們 單鏈表 效扫、單向循環(huán)鏈表 的一系列方法都已實(shí)現(xiàn),使用頭節(jié)點(diǎn)直砂、不使用頭節(jié)點(diǎn) 的方式都有菌仁,最后對(duì)比我們發(fā)現(xiàn)使用頭節(jié)點(diǎn)讓我們對(duì)于處理鏈表的插入數(shù)據(jù)以及刪除數(shù)據(jù)的處理會(huì)更簡(jiǎn)單,因?yàn)闆]有針對(duì)首節(jié)點(diǎn)的單獨(dú)處理静暂,針對(duì)此大家可以根據(jù)具體情況自行斟酌济丘。其實(shí)還有很多方法和實(shí)現(xiàn)等著你去發(fā)掘,希望這篇文章能將單鏈表的概念和實(shí)現(xiàn)講清楚籍嘹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闪盔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辱士,更是在濱河造成了極大的恐慌,老刑警劉巖听绳,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颂碘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)头岔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門塔拳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人峡竣,你說(shuō)我怎么就攤上這事靠抑。” “怎么了适掰?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵颂碧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我类浪,道長(zhǎng)载城,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任费就,我火速辦了婚禮诉瓦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘力细。我一直安慰自己睬澡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布眠蚂。 她就那樣靜靜地躺著猴贰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪河狐。 梳的紋絲不亂的頭發(fā)上米绕,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音馋艺,去河邊找鬼栅干。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捐祠,可吹牛的內(nèi)容都是我干的碱鳞。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼踱蛀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼窿给!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起率拒,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤崩泡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后猬膨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體角撞,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谒所。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片热康。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖劣领,靈堂內(nèi)的尸體忽然破棺而出姐军,到底是詐尸還是另有隱情,我是刑警寧澤尖淘,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布奕锌,位于F島的核電站,受9級(jí)特大地震影響德澈,放射性物質(zhì)發(fā)生泄漏歇攻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一梆造、第九天 我趴在偏房一處隱蔽的房頂上張望缴守。 院中可真熱鬧,春花似錦镇辉、人聲如沸屡穗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)村砂。三九已至,卻和暖如春屹逛,著一層夾襖步出監(jiān)牢的瞬間础废,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工罕模, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留评腺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓淑掌,卻偏偏與公主長(zhǎng)得像蒿讥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抛腕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容