四、線性表(四)、線性表的鏈?zhǔn)酱鎯Y(jié)構(gòu)、單鏈表

數(shù)據(jù)結(jié)構(gòu)目錄

定義:用一組任意的存儲單元存儲線性表的數(shù)據(jù)元素,這組存儲單元可以存在內(nèi)存中未被占用的任意位置。

在鏈?zhǔn)酱鎯Y(jié)構(gòu)中,每個數(shù)據(jù)元素除了要存儲數(shù)據(jù)元素信息外,還要存儲它的后繼元素的存儲地址(指針).我們把存儲數(shù)據(jù)元素信息的域稱為數(shù)據(jù)域,把存儲直接后繼位置的域稱為指針域。指針域中存儲的信息稱為指針或鏈。這兩部分信息組成數(shù)據(jù)元素稱為存儲印象,稱為結(jié)點(Node)。

n個結(jié)點鏈接成一個鏈表,即為線性表(a1,a2,a3,…,an)的鏈?zhǔn)酱鎯Y(jié)構(gòu)

因為此鏈表的每個結(jié)點中只包含一個指針域,所以叫做單鏈表

我們把鏈表中的開始結(jié)點的存儲位置叫做頭指針,最后一個結(jié)點的指針為空(NULL)

單鏈表

1.頭指針、頭結(jié)點、開始結(jié)點

開始結(jié)點:指鏈表中的第一個結(jié)點,它沒有直接前驅(qū)

頭指針:指向開始結(jié)點的指針(沒有頭結(jié)點的情況下,若鏈表有頭結(jié)點,則是指向頭結(jié)點的指針),一個單鏈表可以由其頭指針唯一確定,一般用其頭指針來命名單鏈表,無論鏈表是否為空,頭指針均不為空,頭指針是鏈表的必要元素

單鏈表結(jié)構(gòu)

頭結(jié)點:頭結(jié)點是為了操作的統(tǒng)一和方便而設(shè)立的,放在第一個元素的結(jié)點之前,其數(shù)據(jù)域一般無意義(但也可以用來存放鏈表的長度),有了頭結(jié)點,對在第一元素節(jié)點前插入節(jié)點和刪除第一節(jié)點起操作與其它節(jié)點的操作就統(tǒng)一了,頭結(jié)點不一定是鏈表的必須元素

頭指針、頭結(jié)點
空鏈表下的頭指針、頭結(jié)點

2.單鏈表的定義

//結(jié)點的定義
typedef struct Node{
    ElemType data;//數(shù)據(jù)域
    struct Node *next;//指針域
}Node;
typedef struct Node *LinkList;

我們可以看到節(jié)點由存放數(shù)據(jù)元素的數(shù)據(jù)域和存放后繼結(jié)點地址的指針域組成

問題:

如果p->data = ai,那么p->next->data = ?

答案: p->next->data = ai+1

注意:下方的所有算法都是基于該單鏈表存在頭結(jié)點

3.單鏈表的讀取

獲取鏈表第i個數(shù)據(jù)的算法思路:

  • 聲明一個結(jié)點p指向鏈表第一個結(jié)點,初始化j從1開始
  • 當(dāng)j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一個結(jié)點,j++
  • 若到鏈表末尾p為空,則說明第i個元素不存在
  • 否則查找成功,返回結(jié)點p的數(shù)據(jù)
/**
 使用頭指針表示單鏈表 LinkList L表示頭指針,頭指針指向頭結(jié)點
 */
Status GetElem(LinkList L,int i,ElemType *e){
    LinkList p = L->next;
    int j = 1;
    while (p && j < i) {
        p = p->next;
        j++;
    }

    if (!p || j > i) {
        return ERROR;
    }
    *e = p->data;
    return OK;
}

可以看出,此算法的時間復(fù)雜度為O(n),此算法的核心思想叫做”工作指針后移”,這其實也是很多算法的常用技術(shù)

4.單鏈表的插入

單鏈表第i個數(shù)據(jù)插入結(jié)點的算法思路:

  • 聲明一個節(jié)點p指向鏈表頭結(jié)點,初始化j從1開始
  • 當(dāng)j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一結(jié)點,j累加1
  • 若到鏈表末尾p為空,則說明第i個元素不存在
  • 否則查找成功,在系統(tǒng)中生成一個空結(jié)點s
  • 將數(shù)據(jù)元素e復(fù)制給s->data
  • 將i的next賦值給s,將i的next指向s
Status insertElem(LinkList L,int i,ElemType e){
    LinkList p = L;//獲取到頭結(jié)點
    int j = 1;
    while (p && j < i) {
        p = p->next;
        j++;
    }
    if (!p || j > i) {
        //超出范圍了
        return ERROR;
    }
    //分配空間,并插入
    LinkList s = (LinkList)malloc(sizeof(Node));
    s->data = e;
    s->next = p->next;
    p->next = s;
    
    return OK;
}
單鏈表的插入

5.單鏈表的刪除

單鏈表第i個數(shù)據(jù)刪除結(jié)點的算法思路:

  • 聲明結(jié)點p指向鏈表的頭結(jié)點,初始化j=1
  • 當(dāng)j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一個結(jié)點,j累加1
  • 若到鏈表末尾p為空,則說明第i個元素不存在
  • 否則查找成功,將欲刪除結(jié)點p->next賦值給q
  • 單鏈表的刪除標(biāo)準(zhǔn)語句p->next = q->next
  • 將q結(jié)點中的數(shù)據(jù)復(fù)制給e,作為返回
  • 釋放q結(jié)點
Status deleteElem(LinkList L,int i,ElemType *e){
    //頭指針,指向頭結(jié)點
    LinkList p = L;
    int j = 1;
    while (p && j < i) {
        p = p->next;
        j++;
    }
    if (!p || j > i) {
        return ERROR;
    }
    LinkList q = p->next;
    //返回值
    *e = q->data;
    p->next = q->next;
    //釋放
    free(q);
    
    return OK;
}
單鏈表的刪除

6.單鏈表與線性表的順序存儲結(jié)構(gòu)的效率PK

在單個元素插入與刪除算法中,單鏈表的時間復(fù)雜度為O(n),線性表的順序存儲結(jié)構(gòu)的時間復(fù)雜度也為O(n),效率差別不大。

但是,如果我們希望從第i個位置開始,插入連續(xù)10個元素,對于順序存儲結(jié)構(gòu)而言,每次插入都要移動n-i個位置,所以每次都是O(n)

而單鏈表,我們只需要在第一次時候,找到第i個位置的指針,此時為O(n),接下來只是簡單的通過賦值移動指針而已,時間復(fù)雜度為O(1)

顯然,對于插入或刪除數(shù)據(jù)越頻繁的操作,單鏈表的效率優(yōu)勢就越是明顯

7.單鏈表的整表創(chuàng)建

順序存儲結(jié)構(gòu)的線性表的整表創(chuàng)建,我們可以用數(shù)組的初始化來直觀理解。單鏈表不同,它的數(shù)據(jù)可以是分散的,它的增長是動態(tài)的,所以它的創(chuàng)建是根據(jù)系統(tǒng)情況和實際需求即時生成。

創(chuàng)建單鏈表的過程是一個動態(tài)生成鏈表的過程,從“空表”的初始狀態(tài)起,依次建立各元素結(jié)點并逐個插入鏈表。

創(chuàng)建單鏈表的算法思路:

  • 聲明一結(jié)點p和計數(shù)器變量i
  • 初始化一個空鏈表L
  • 讓L的頭結(jié)點的指針指向NULL,即建立一個帶頭結(jié)點的單鏈表
  • 循環(huán)實現(xiàn)后繼結(jié)點的賦值和插入

.頭插法建立單鏈表

頭插法是從一個空表開始,生成新結(jié)點,讀取數(shù)據(jù)存放到新結(jié)點的數(shù)據(jù)域中,然后將新結(jié)點插入到當(dāng)前鏈表的表頭上,直到結(jié)束為止。

簡單來講,就是把新加入的元素放在表頭后的第一個為止:

  • 先讓新結(jié)點的next指向頭結(jié)點之后
  • 然后讓表頭的next指向新節(jié)點
void createListHead(LinkList *L,int n){
    //創(chuàng)建頭結(jié)點
    LinkList p = (LinkList)malloc(sizeof(Node));
    p->next = NULL;
    
    //將頭結(jié)點的指針賦值給單鏈表
    *L = p;
    srand(time(0));
    
    //循環(huán)從頭部插入
    for (int i = 0; i < n; i++) {
        p = (LinkList)malloc(sizeof(Node));
        p->data = rand()%100+1;
        //核心代碼:新結(jié)點插入在頭結(jié)點之后
        p->next = (*L)->next;
        (*L)->next = p;
    }
}

.尾插法創(chuàng)建單鏈表

頭插法建立鏈表雖然算法簡單,但生成的鏈表中結(jié)點的次序與輸入的順序相反

尾插法剛好相反,每次把新結(jié)點都插入到最后,這種算法稱之為尾插法

void createListTail(LinkList *L,int n){
    //創(chuàng)建頭結(jié)點
    LinkList p = (LinkList)malloc(sizeof(Node));
    p->next = NULL;
    
    //將頭結(jié)點的指針賦值給單鏈表
    *L = p;
    
    //中間變量
    LinkList r = p;
    srand(time(0));
    //循環(huán)從尾部插入
    for (int i = 0; i < n; i++) {
        //創(chuàng)建新結(jié)點
        p = (LinkList)malloc(sizeof(Node));
        p->data = rand()%100 + 1;
        //將前面結(jié)點的next指向新結(jié)點
        r->next = p;
        //讓中間變量r指向新結(jié)點,作為后面創(chuàng)建結(jié)點的前驅(qū)
        r = p;
    }
    //最后一個節(jié)點的next置為NULL
    r->next = NULL;
}

8.單鏈表的整表刪除

單鏈表整表刪除的算法思路如下:

  • 聲明結(jié)點p和q
  • 將第一個結(jié)點賦值給p,下一結(jié)點賦值給q
  • 循環(huán)執(zhí)行釋放p和將q賦值給p的操作
Status clearList(LinkList *L){
    //獲取到第一個結(jié)點
    LinkList p = (*L)->next;
    LinkList q;
    while (p) {
        q = p->next;
        free(p);
        p = q;
    }
    (*L)->next = NULL;
    //釋放頭結(jié)點
    free((*L));
    //頭指針置為NULL,防止懸空指針
    *L = NULL;
    
    return OK;
}

9.單鏈表結(jié)構(gòu)與順序存儲結(jié)構(gòu)的優(yōu)缺點

存儲分配方式:

  • 順序存儲結(jié)構(gòu)用一段連續(xù)的存儲單元依次存儲線性表的數(shù)據(jù)元素
  • 單鏈表采用鏈?zhǔn)酱鎯Y(jié)構(gòu),用一組任意的存儲單元存放線性表的元素

時間性能:

  1. 查找

    . 順序存儲結(jié)構(gòu)O(1)

    . 單鏈表O(n)

  2. 插入和刪除
    . 順序存儲結(jié)構(gòu)需要平均移動表長一般的元素,時間為O(n)

    . 單鏈表在計算出某位置的指針后,插入和刪除時間僅為O(1)

空間性能:

-- 順序存儲結(jié)構(gòu)需要預(yù)分配存儲空間,分大了,容易造成空間浪費,分小了,容易發(fā)生溢出

-- 單鏈表不需要分配存儲空間,只要有就可以分配,元素個數(shù)也不受限制

結(jié)論:

  1. 若線性表需要頻繁查找,很少進(jìn)行插入和刪除操作時,宜采用順序存儲結(jié)構(gòu)

  2. 若需要頻繁插入和刪除時,宜采用單鏈表結(jié)構(gòu)

10.單鏈表小結(jié)騰訊面試題

.題目:快速找到未知長度的單鏈表的中間節(jié)點

普通方法思路:

首先遍歷一遍單鏈表以確定單鏈表的長度L,然后再次從頭結(jié)點觸發(fā)循環(huán)L/2次找到單鏈表的中間節(jié)點,其算法復(fù)雜度為:O(L+L/2) = O(3L/2)

巧妙方法:

利用快慢指針原理:設(shè)置兩個指針search、mid都指向單鏈表的頭結(jié)點。其中search的移動速度是mid的兩倍,當(dāng)*search指向末尾結(jié)點的時候,mid正好就在中間了,這也是標(biāo)尺的思想

/*
 快速找到未知長度的單鏈表的中間節(jié)點
 
 */
Status GetMiddleNode(LinkList L,ElemType *e,int *index){
    //使用快慢指針方法 search遍歷到末尾,mid速率是search的一半
    LinkList search,mid;
    mid = search = L;
    *index = 0;
    while (search->next != NULL) {
        (*index)++;
        if (search->next->next != NULL) {
            search = search->next->next;
            mid = mid->next;
        } else {
            search = search->next;
            break;
        }
    }
    *e = mid->data;
    
    return OK;
}


11.將單鏈表翻轉(zhuǎn)(倒序)

思路一:一個個的將單鏈表的結(jié)點剝下來,然后使用頭插法插在頭結(jié)點之后,這樣就實現(xiàn)了單鏈表的翻轉(zhuǎn)

void reverseLinkList(LinkList *L){
    //pre表示上一個剝解下來的結(jié)點
    LinkList pre = (*L)->next;
    //next表示單鏈表的下一個要剝離出來的結(jié)點,初始化為NULL
    LinkList next = NULL;
    
    (*L)->next = NULL;
    
    //將所有結(jié)點一個個剝離下來,按照頭插法插入
    while (pre) {
        next = pre->next;
        pre->next = (*L)->next;
        (*L)->next = pre;
        pre = next;
    }
}

思路二:建立一個數(shù)據(jù)為單鏈表結(jié)點的棧,利用棧后進(jìn)先出的特性,先一個個將結(jié)點加入棧,然后再一個個推出棧,這樣也實現(xiàn)了翻轉(zhuǎn)。(算法略)

12、判斷單鏈表中有環(huán)

有環(huán)的定義是,鏈表的尾結(jié)點指向了鏈表中的某個結(jié)點


有環(huán)的鏈表

那么,判斷單鏈表中是否有環(huán),主要有以下兩種方法:

  1. 使用p、q兩個指針,p總是向前走,但q每次都從頭開始走,對于每個節(jié)點,看p走的步數(shù)是否和q一樣。如圖,當(dāng)p從6走到3時,用了6步,此時若q從head觸發(fā),則只需兩步就到3,因而步數(shù)不等,出現(xiàn)矛盾,存在環(huán)。
  2. 使用p、q兩個指針,p每次向前走一步,q每次向前走兩步,若在某個時候p == q,則存在環(huán)

方法一:

int isHasLoop1(LinkList L){
    LinkList p = L,q;
    int pos1 = 0,pos2;
    
    while (p) {
        q = L;
        pos2 = 0;
        while (q) {
            if (p == q) {
                //兩個指向的結(jié)點相同
                if (pos1 == pos2) {
                    //步數(shù)相同 也就是走到p的位置上,則重新開始遍歷
                    break;
                } else {
                    //步數(shù)不相同,確定有環(huán)
                    printf("環(huán)的位置在第%d個結(jié)點處",pos2);
                    return 1;
                }
            }
            q = q->next;
            pos2++;
        }
        p = p->next;
        pos1++;
    }
    
    //如果走到了最后一個結(jié)點,那么說明沒有環(huán),返回false
    return 0;
}

方法二:

int isHasLoop2(LinkList L){
    //p是一步步走的,q是兩步兩步走的
    LinkList p = L,q = L;
    while (p && q && q->next) {
        p = p->next;
        if (q->next) {
            q = q->next->next;
        }
        printf("p:%d, q:%d \n", p->data, q->data);
        
        if (p == q) {
            return 1;
        }
    }
    
    return 0;
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,627評論 2 380