大話西遊之王道考研數據結構第二講---線性表的鏈式表示

 

                                      第二講---線性表的鏈式表示

  • 複習

上次我們講到了線性表的順序表示,這裏我們做一個簡單的複習:

   線性表的順序表示有哪些優點?

  • 訪問方面
  • 存儲方面
  • 不同操作的複雜度方面
  1. 元素個數咋樣,元素之間咋樣,每個元素大小咋樣,元素之間的順序和元素的內容有沒有關係。
  2. 線性表的結構體創建方式
  3. 線性表有幾種表示方式
  4. 線性表的順序表示是順序存取還是隨機存取
  5. LineInsert、LineDelete分別有哪些操作過程
  6. 什麼是邏輯結構,什麼是存儲結構
  7. 如果我們要刪除一個線性表中,所有數據元素是“4”的元素,應該怎麼辦?

一、線性表的鏈式表示

由於線性表的順序表示中,對於插入和刪除需要移動大量元素。所以我們引入了線性表的鏈式表示。鏈式存儲線性表示後,不需要使用地址連續的存儲單元,即它不要求在邏輯上相鄰的兩個元素在物理上也相鄰。對鏈表的插入和刪除只需要修改指針!

舉個栗子

       上次我們講到唐僧五人成立了一個公司,公司給分配集體房屋。但是,由於這幫人沒有經驗,公司賠的就差賣金箍棒了。唐僧想了想,最近P2P(傳銷)好像挺賺錢的,他打算重新成立家傳銷公司。

        但是呢,大家都懂的,這玩意不能公開透明的去搞,而且呢還得發展下線,爲了防止亂套,唐僧定了如下規矩:

1.一個人只能發展一個下線,通過和下線的電話號碼聯繫。

2.下線不能知道上線是誰,因爲怕出事以後,把上線供出來,連鎖反應後大家都得進去。

OK~公司制度定下來了,唐僧是公司的頭頭,公司就叫大唐實業(註冊號爲999)吧(大氣~),所以我們現在有這樣一個記錄:

唐僧作爲公司的第一個人加入,其電話號碼是666,所以有:

剛開始只有唐僧,他還沒有發展下線,所以下線電話號佔時是空(NULL)。他在某個公用電話亭上面給猴哥打了個電話(之前有猴哥的電話是222)的方式鼓動猴哥去當他的下線(不當就念緊箍咒~),所以現在公司成了這個樣子:

 

 然後猴哥發展了八戒(電話號111),八戒發展了沙僧(電話號777),沙僧發展了小白龍(電話號000)。一個龐大的制度清晰的傳銷帝國的雛形就成立了:

 

可以看到:

1.大唐實業的註冊號位999,工商局以及外人想查找這個公司,需要通過999這個號碼來查詢

(通常用頭指針來標識一個單鏈表)

2.唐僧是公司的第一人,但是大唐實業是第一個結點,大唐實業是整個傳銷的標識,工商局通過大唐實業來聯繫到整個公司。爲什麼不採取通過唐僧來聯繫整個公司呢,因爲這樣如果唐僧離開了公司,這樣工商局就找不到這個公司了。所以我們會在之前加一個結點。

(爲了操作方便,在單鏈表的第一個結點之前附加一個結點)

3.頭結點和頭指針的區分:頭指針是指的聯繫到這個公司的號碼,可以是999,也可以是666。但是999這樣的方式會更加的方便。頭結點指的是第一個結點。也就是上面鏈表中的第一個元素。

4.引入頭結點的兩個優點:

        a)講究!在查找時候,所有人都是下線(唐僧可以理解爲是公司的下線),方便一點。

        b)這樣就算公司人都跑了,公司還存在,這樣方便以後再加人。如果不用頭結點,都跑光以後,公司就不曉得哪裏去                         了。     

 

二、單鏈表上基本操作實現

1.與前一講一樣,我們先定義一下單鏈表的結構體

typedef struct LNode{
    int data;
    struct LNode *next;
}LNode,*LinkList;

這裏面LNode是結構體名字,*LinkList是結構體指針,至於爲什麼這麼創建......(也不會考,睜一隻眼閉一隻眼就過去了,細究反而會浪費很多時間)。

下面會多次用到 LNode 和LinkList,其實LNode *    =  LinkList,我們一般用LNode * 去表示鏈表中的一個結點,用LinkList去表示一個鏈表,二者互換了也不會報錯,但是最好這麼寫。

2.採用頭插法建立單鏈表

//頭插法--每一次插入一個人,這個人插在公司這個頭結點後面,相當於每次發展一個總負責人
//之前的總負責人做他的下線
void CreatListHead(LinkList &L){
    LinkList s; //創建待發展的新人
    int x; //待發展新人的名字
    L = (LinkList)malloc(sizeof(LNode)); //創建公司999

    L->next = NULL;  //公司總負責人(第一個下線)置爲空,還沒加呢

    scanf("%d",&x); //創建新人的名字(第一個下線)

    while(x!=-1){ //如果一個新人的名字是-1的話,意思就是不再發展了,結束了

        //1.申請空間---手機號
        s = (LinkList)malloc(sizeof(LNode)); //給新人創建個手機號,這裏s的地址相當於手機號
        //也就是這個人新人的聯繫方式
        
        //2.初始化名字
        s->data = x; //加入新人的名字

        //3.處理一下他的下線
        s->next = L->next; //把之前的總負責人當做這個新人的下線

        
        //4.上鍊
        L->next = s;//新人成功上位爲總負責人

        scanf("%d",&x);//再讀入下一個新人
    }

}

頭插法相當於每次插入一個新人,這個新人當做公司的第一人(總負責人),也就是唐僧的位置,之前的唐僧將會變成新人的下線。邏輯上面不好理解,是我們正常發展下線的過程的逆過程,最後的結果是 公司->小白龍->沙僧->八戒->猴哥->唐僧

3.採用尾插法建立單鏈表

void CreatListTail(LinkList &L){
    LinkList s; //創建待發展的新人
    int x; //待發展新人的名字

    L = (LinkList)malloc(sizeof(LNode)); //創建公司
    L->next = NULL;  //公司總負責人(第一個下線)置爲空,還沒加呢

    //爲什麼會單獨創建tail,而不是用L去操作
    LinkList tail;
    tail = L;

    scanf("%d",&x); //創建新人的名字(第一個下線)

    while(x!=-1){ //如果一個新人的名字是-1的話,意思就是不再發展了,結束了
            
        //1.申請空間---手機號
        s = (LinkList)malloc(sizeof(LNode)); //給新人創建個手機號,這裏s的地址相當於手機號
        //也就是這個人新人的聯繫方式
        
        //2.初始化名字
        s->data = x; //加入新人的名字
    
        //3.處理一下他的下線--注意這裏和頭插法的區別
        //之所以有這樣的差別,是應爲他們發展下線的方式不同,所以不要死記代碼
        //要記住爲什麼這麼做
        s->next = NULL;

        //4.上鍊
        tail->next = s;

        //5.更新尾部
        tail = s;

        scanf("%d",&x);//再讀入下一個新人
    }

}

尾插法就是咱正常發展公司下線的方法,這裏面有幾個注意的點:

a).爲什麼申請一個tail?

 因爲每次都需要在尾部插入一個人,所以我們需要一個變量去保存當前的尾部,這樣才能把新人的手機號給這個當前的尾部。

b).爲什麼需要更新尾部?

因爲我們採用的是尾插法,意思就是在當前鏈表的最後一個元素後面再插入一個新人,如果一個新人插入後,尾部應該就是這個新人了,不是原來的尾部了。所以我們要在插入一個新人以後更新尾部。

c).爲什麼不用L代替tail?

我們在頭插法中,一直是在L的後面插元素。如果尾插法,每次把尾部更新爲L,最後我們得到的L地址就是最後一個元素的地址,前面的完全丟了。所以我們需要最先把頭結點的地址給L,然後申請一個tail讓他當尾部。這樣不管怎麼加人,L始終指向的是頭結點。

4.遍歷鏈表中所有結點操作

void PrintList(LinkList L){
    /*我們一般不直接對L進行操作,雖然這裏L傳入的不是地址,裏面操作了無所謂
    但是有些操作需要傳入L的地址,如果直接讓L=L->next的話,會破壞整個列表*/
    
    LNode *p = L->next;  //我們只打印出人,最開始是頭結點,先跳過
    
    while(p!=NULL){
        printf("%d ",p->data);
        p = p->next;
    }
    printf("\n");
}

5.按序號查找結點值

LNode* GetElem(LinkList L,int i){
    //同樣,我們先檢查下i的範圍對不對,由於鏈表中,我們不能知道當前鏈表一共有多少人
    //所以只能檢查一下i的下界,上界的話等遍歷時候再檢查

    if(i<=0)
        return NULL; //之前錯誤一般返回false,但是這裏返回的類型是結構體,所以錯誤是NULL


    LNode *p = L->next;  //我們只打印出人,最開始是頭結點,先跳過
    int j = 1;  //表示現在我們p指向的是第一個人
    //你也可以定義j = 0 代表第一個下標,都可以,但是後面需要i-1
    //所以就看怎麼定義了,只要最後的操作對就行


    //如果p爲空了但是j還是小於i,那麼說明i超出了上界,我們最後也會返回一個NULL
    while(p!=NULL){
        if(j == i){
            return p;  //找到了
        }
        p = p->next; 
        j++;
    }
    return NULL;
}

6.按值查找表結點

LNode* LocateElem(LinkList L,int e){
    /*我們一般不直接對L進行操作,雖然這裏L傳入的不是地址,裏面操作了無所謂
    但是有些操作需要傳入L的地址,如果直接讓L=L->next的話,會破壞整個列表*/

    LNode *p = L->next;  //我們只打印出人,最開始是頭結點,先跳過
    while(p!=NULL){
        if(p->data == e){
            return p;
        }
        p = p->next;
    }
    return NULL;
}

7.插入節點操作(在單鏈表的第i個位置上插入新的結點)

(找到第i個人的上司,讓第i個人變成新人的下線,讓新人變成上司的下線,順序不能變)

bool ListInsert(LinkList &L,int i,int e){
    //同樣,先檢查下下界
    if(i<=0){
        return false; //爲什麼這裏是false了
        //因爲我不需要返回一個結構體,只需要返回是否成功,所以返回值類型是bool
    }
    LNode *p = L; //同樣,我們一般不直接對L進行處理
    int j = 0;
    //這裏,我們需要考慮一下,爲什麼j是從0開始的,並且p = L而不是P = L->next
    //因爲我們執行的是插入操作,那麼就有可能插入到第一個位子,也就是i = 1
    //如果P = L->next,j =1 那麼相當於我們跳過了第一位,這樣i=1這樣合理的訴求就滿足不了了
    //而之前查找時候,爲什麼P = L->next呢?
    //因爲查找嘛!

    //我們在插入時候,需要找到i前面那個人
    //前面那個人原來的下線是第i個人,現在我們讓他的下線變爲待插入的新人
    //然後讓原來第i個人成爲新人的下線

    //p!=NULL可以輔助我們檢測i是否超過上界
    while(p!=NULL){
        if(j==i-1){ //找到i前面那個人
            LNode *newGuy = (LinkList)malloc(sizeof(LNode));//給新人開闢一個空間
            newGuy->data = e;

            newGuy->next = p->next;
            p->next = newGuy;
            //這倆順序一定不能變,如果先p->next = newGuy
            //那麼原來第i個人的手機號就被這個新人覆蓋掉了
            //接下來新人是需要把原來第i個人當成下線的,這時候,誰告訴他第i個人的手機號?

            //可以想想p->next = newGuy,newGuy->next = p->next的後果有多麼美麗(恐怖)
            return true;
        }
        p = p->next;
        j++;

    }
    return false;



}

8.刪除結點的操作(找到待刪除結點的上司,讓他的下線變成上司的下線,這樣他就被刪掉了)

bool ListDelete(LinkList &L,int i){
    //同樣,先檢查下下界
    if(i<=0){
        return false; //爲什麼這裏是false了
        //因爲我不需要返回一個結構體,只需要返回是否成功,所以返回值類型是bool
    }
    LNode *p = L; //同樣,我們一般不直接對L進行處理
    int j = 0;
    //與插入相同,注意上面p和j的初始值

    //我們在刪除時候,需要找到i前面那個人
    //前面那個人原來的下線是第i個人,現在我們讓他的下線變爲待插入的新人

    //然後讓第i個人就地爆炸

    //p!=NULL可以輔助我們檢測i是否超過上界
    while(p!=NULL){
        if(j==i-1){ //找到i前面那個人
            LNode *d = p->next;//待刪除的那個人

            p->next = d->next;//讓待刪除的那個人的下線變成他原來上司的下線
            //如果你想騷一點的話可以寫成:
            //p->next = p->next->next;

            free(d);//你也可以不執行這一步,當然這樣的習慣會不好
            //如果這裏面有密碼什麼的,你也沒有free掉,它就一直在裏面,會被別人利用
            return true;
        }
        p = p->next;
        j++;
    }
    return false;
}

三、雙鏈表

公安局覺得唐僧這個只有上司知道下線,下線不知道上司的理念太牛B了。他們不知道如何把這個公司一鍋端,所以他們強制唐僧修改公司理念(一旦找不到解決問題的方法,就解決掉了製造問題的人)。 

當然,這是含有頭結點的雙鏈表,雙鏈表中主要考的是插入和刪除操作,先看下雙鏈表的結構體定義吧:


typedef struct DNode{
    int data;
    struct DNode *prior,*next;
}DNode,*DLinkList;

這裏面多了一個*prior,我們可以把*prior和*next理解爲一個人的左右手,並且整個鏈表中的人都在懸崖上面一個吊着一個,一旦一個人右手(*next)鬆開了,那麼下面的人都沒了,除非在他鬆開前,有另外一個人把他下面那個人拽住。如果能理解這一點,插入刪除就很好處理了。

首先我們看插入

舉個栗子:

唐僧和猴哥現在處於相互拉着的狀態,唐僧的右手(*next)拉着猴哥,猴哥的左手(*prior)拉着唐僧,現在丘比特想插進來。有如下操作

1.唐僧右手拉丘比特

2.丘比特左手拉唐僧

3.丘比特右手拉猴哥

4.猴哥左手拉丘比特

我們可以想一想,這第一步和第三步應該先做哪一步。如果先執行第一步,那麼猴哥就死掉了。如果先執行第三步,這時候猴哥的左手被兩個人拉着,然後執行第一步,唐僧鬆開右手,這時候有人會問,丘比特的左手是空的,那不丘比特和猴哥一起掉下去了(因爲丘比特會飛..我盡力舉一個看似恰當的例子了.......在計算機中,是因爲我們有變量去保存帶插入結點(丘比特)的地址,所以即使唐僧鬆開,我們還是有辦法找到丘比特和猴哥)。然後,唐僧右手把丘比特左手一拉,就插進來了。

    qiuBt->next = houGe; //丘比特的右手拉着猴哥
    houGe->prior = qiuBt; //猴哥左手拉着丘比特
    
    qiuBt->prior = TangSeng; //丘比特左手拉着唐僧
    TangSeng->next = qiuBt; //唐僧右手拉着丘比特

當然,我們可以先讓丘比特左手拉唐僧,順序有很多種,我們只需要保證:

1.猴哥不掉下去

2.最後大家左右手都有人拉

就OK!

然後我們看刪除:

舉個栗子:

現在是唐僧,丘比特,猴哥這三個人手拉着手,如果丘比特要出去的話,唐僧最後右手抓猴哥,猴哥左手抓唐僧,我們想一下一下幾個順序:

1.唐僧的右手抓猴哥

2.猴哥的左手抓唐僧

我們可以發現,兩個順序都不會導致出問題。所以這裏先後順序無所謂。

四、循環鏈表

循環鏈表可以分爲循環單鏈表和循環雙鏈表,其特性就是,最後一個結點的*next指針指的是頭結點,每一次在插入和刪除時候需要額外考慮一下,也沒什麼難度,只要理解了就行。

五、課後習題

6.在一個單鏈表中,一直q所知節點是p所知結點的前驅結點,若在q和p之間插入節點s,需要執行:

7.給定有n個元素的一維數組,建立一個有序的單鏈表的最低時間複雜度是

這個題中,有序的話我們得先排序,最好的排序算法的時間複雜度是O(nlog_2n),而建立一個單鏈表的複雜度是O(n
),我們知道複雜度主要看的是最複雜的那一項,所以是O(nlog_2n)

14.在雙鏈表中向p所指的結點之前插入一個結點q的操作爲

六、總結

在做鏈表的題時候,其本身比較抽象,做的時候一定要在紙上畫呀畫。線性表的順序表示和鏈式表示有一定的區別,這裏也比較愛考,我做一下總結:

內容

順序表

鏈表

存取方式

隨機存取/順序存取

順序存取

邏輯結構與物理結構

邏輯結構相鄰的兩個元素其對應存儲位置(物理結構)也相鄰(門牌號的關係)

邏輯結構相鄰的兩個元素其對應存儲位置(物理結構)不一定相鄰(電話號的關係)

按序查找

O(1)

O(n)

插入、刪除

O(n)

O(1)

優點

方便查找

方便插入刪除

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章