数据结构之美(五)静态链表、循环链表、双向链表

(总结源自《大话数据结构》,初学数据结构推荐此书)

目录

静态链表

静态链表的插入操作

静态链表的删除操作

循环链表

双向链表


静态链表

用数组描述的链表叫静态链表。

让数组的元素由两个数据域组成,data和cur,data存放数据,cur模拟链表里的指针,存放后继元素在数组中的下标。

#define MAXSIZE 1000;
typedef int ElemType;
typedef int Status;

typedef struct
{
    ElemType data;
    int cur;
}Component,StaticLinkList[MAXSIZE];

另外,数组的第一个和最后一个元素有其他妙用,是我们的特殊元素,不存数据。

(备用链:数组是连续的,一般来说若作为链表,肯定会有未被使用的地方,我们把未被使用的数组元素称为备用链表。)

如图这样定义有什么好处呢? 这样我们就知道了头结点的位置,也就知道了链表在哪儿开始(若如图为0,则表明此链表为空),还知道了备用链第一个结点的位置(方便后来的插入操作)

//初始化数组
//space[0].cur为头指针, “0”表示空指针
Status InitList(StaticLinkList space)
{
    int i;
    for(i=0;i<MAXSIZE-1;i++)
    {
        space[i].cur=i+!;
    }
    space[MAXSIZE-1].cur=0;
    return OK;
}

静态链表的插入操作

在链表中,结点的插入与删除分别用的是malloc()和free()两个函数。但在数组,不存在结点的申请和释放的问题,所以我们需要自己实现这两个函数,才可以进行插入和删除的操作。

插入的思路:我们知道下标为0的数组元素中存储着备用链的第一个结点的位置,那就是我们想要的插入位。

int Malloc_SLL(StaticLinkList space)
{
    int i=space[0].cur;  //我们想要存的位置,即第一个备用空闲的下标
    
    if(space[0].cur)
        space[0].cur=space[i].cur;  //插入后第一个备用空闲没有了,转到它的cur去

    return i;  //返回分配的结点下标
}

那么这样我们就解决了链表里data的问题,接下来我们要解决的是链表里next的问题。

//在L中第i个元素之前插入新的数据元素e

Status ListInsert(StaticLinkList L,int i,ElemType e)
{
    int j,k,l;
    k=MAX_SIZE-1; //k是最后一个元素的下标  它的cur是第一个非空下标

    if( i<1 || i>ListLength(L)+1 )
        return ERROR;

    j=Malloc_SSL(L);  //j是空闲分量的下标

    if( j )
    {
        L(j).data=e;

        for(l=1;l<=i-1;l++)  //找到第i个元素之前的位置
            k=L[k].cur;

        L(j).cur=L[k].cur;
        L[k].cur=j;
        return OK;
    }
    return ERROR;
}    

例子如图,即实现了静态链表的插入操作。

静态链表的删除操作

删除操作和插入操作一样,需要自己实现free()函数

//删除在L中的第i个数据元素e

Status ListDelete(StaticLinkList L,int i)
{
    int j,k;
    k=MAX_SIZE-1; //k是最后一个元素的下标  它的cur是第一个非空下标

    if( i<1 || i>ListLength(L)+1 )
        return ERROR;

    j=Malloc_SSL(L);  //j是空闲分量的下标


    for(l=1;l<=i-1;l++)  //找到第i个元素之前的位置
        k=L[k].cur;

    j=L[k].cur;
    L[k].cur=L[j].cur;
    Free_SSL(L,j);
    return OK;
}    



void Free_SSL(StaticLinkList space, int k)
{
    space[k].cur=space[0].cur;
    space[0].cur=k;
}



int ListLength(StaticLinkList L)
{
    int j=0;
    int i=L[MAXSIZE-1].cur;
    while(i)
    {
        i=L[i].cur;
        j++;
    }
    turn j;
}

循环链表

循环链表,顾名思义,就是将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,头尾相接。

循环链表和单链表最主要的区别就在于循环的判断条件上,单链表是判断p->next是否为空,而循环链表是判断p->next是否等于头结点。

循环链表有什么用呢?

循环链表的有点是从链尾到链头比较方便,适合处理有环形结构特点的数据

在单链表里,我们若想访问头结点和最后一个结点,那么分别需要O(1)和O(n)的时间。而在循环链表中,我们若取消头指针,改为尾指针,如图,那么访问头结点与最后一个结点则均只需用O(1)的时间了。

尾指针有什么用呢?

将两个循环链表合并为一个链表的时候,尾指针非常方便。

双向链表

双向链表就是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。

(上图来自极客时间的数据结构与算法之美专栏)

typedef struct DulNode
{
    ElemTyoe data;
    struct DuLNode *prior;  //直接前驱指针
    struct DuLNode *next;   //直接后继指针
}DulNode,*DuLinkList;

 双向链表相比存储同样多的单链表,多占了那么多的空间,具体有什么用呢?

先放结论:由于双向链表能够方便的找到结点的前驱结点,所以在某些情况下,它在插入与删除时比单链表要更加简单,高效

开玩笑的吧,单链表在插入和删除方面,时间复杂度都已经是O(1)了,还能怎么优化,欺负我读书少呢吧!

好,让我来仔细说说链表的两个操作:

1、删除操作

    淡定淡定,我们说O(1)其实很片面,单纯指的是删除这个操作,可是在删除之前,我们得去遍历找到要删除的地方,这个地方      才是耗时大户,对应的时间复杂度是O(n),双向链表就是在这个地方进行优化滴。

     从链表中删除⼀个数据⽆外乎这两种情况:删除结点中“值等于某个给定值”的结点 删除给定指针指向的结点

    前者无论是单链表还是双向链表,做法都一样,必须从头结点开始一个个遍历,直至找到给定值的结点在进行删除操作,大家的操作都一样,这没得说。

     我们来看看第二种情况:删除给定指针指向的结点。删除这个结点需要有这个结点的前驱结点帮忙,改改它的next指针的指向,可是我单链表找后继结点简单,前驱结点还真没辙,只能从头开始遍历,所以还是得遍历,这一遍了就是O(n)的时间复杂度。这时候双向链表笑嘻嘻的出来,向前一指,喏,这不是前驱结点么,居然还要遍历,太麻烦了。此时双向链表只需要O(1)的时间复杂度就能找到前驱结点进行删除操作,所以双向链表相比单链表,还是有优越感滴~

那么讲了循环链表和双向链表,我们自然而然的能想到超级大boss:双向循环链表,如下,我就不多讲啦

发布了38 篇原创文章 · 获赞 6 · 访问量 1908

猜你喜欢

转载自blog.csdn.net/weixin_43827227/article/details/100849408