大话西游之王道考研数据结构第二讲---线性表的链式表示

 

                                      第二讲---线性表的链式表示

  • 复习

上次我们讲到了线性表的顺序表示,这里我们做一个简单的复习:

   线性表的顺序表示有哪些优点?

  • 访问方面
  • 存储方面
  • 不同操作的复杂度方面
  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)

优点

方便查找

方便插入删除

 

猜你喜欢

转载自blog.csdn.net/zhangbaodan1/article/details/81179475
今日推荐