数据结构与算法(六)线性表的链式存储结构-1

一、链式存储结构的引入

        前面我们讲的线性表的顺序存储结构,他最大的特点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。那么我们能不能针对这个缺陷来提出解决问题的方法呢?要来解决这个问题,我们就得考虑一下导致这个问题的原因是什么:为什么当插入和删除时,就要移动大量的元素?

        原因就在于相邻两元素的存储位置也具有邻居关系,它们在内存中的位置是紧挨着的,中间没有间隔,当然就无法快速插入和删除。那么我们在相邻元素间留出一点空间,这样就能解决了吗?

        但是显然,无论你留下多少个,都是不合适的,太少的话无法插入较长的数据,太多的话又占用太大的空间。反正我们在相邻元素间留多少空间都是有可能不够的,那么不如干脆不要考虑相邻位置这个问题了。哪里有空位我们就放在哪,利用指针对元素进行定位。每个元素多用一个位置来存放指向下一个元素的位置的指针。这样子从第一个元素可以找到第二个元素,第二个元素可以找到第三个元素,依次类推,所有的元素我们就都能通过遍历来找到了,这就是线性表的链式存储结构。

二、链式存储结构的定义

        线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。

        比起顺序存储结构每个数据元素只需要存储一个位置就可以的情况,现在链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址(指针)。也就是说链式存储需要两个位置来存放元素,一个是存放他本身,一个是存放他下一个数据元素的指针地址

        我们把存储数据元素信息的域(所谓的域,其实就是一个地方而已)称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素称为存储映像,称为结点(Node)。

        n个结点链接成一个链表,即为线性表(a_1,a_2,a_3...,a_n)的链式存储结构。因为此链表的每个结点中只包含一个指针域,所以叫做单链表

三、单链表

单链表图解

        对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中的第一个结点从存储位置叫做头指针,最后一个结点指针为空(NULL)

四、头指针和头节点的异同

        上文我们提到了,头结点的数据域一般不存储任何信息,这是它作为第一个结点的特性。就像下图,你只需要拿个小旗子就可以了(^_^)。

        那么这个时候就会有疑惑了,既然头结点的数据域不存储任何信息,那么头指针和头结点又有什么异同呢?

        1、头指针

        -头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。

        -头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)

        -无论链表是否为空,头指针均不为空。

        -头指针是链表的必要元素。

        2、头结点

        -头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)

        -有了头结点,对在第一个元素结点前插入结点和删除第一结点起操作域其他结点从操作就统一了。

        -头结点不一定是链表的必须要素。

单链表图例
空链表图示

        我们在C语言中可以用结构指针来描述单链表

typedef struct Node
{
    ElemType data;//数据域
    struct Node* Next;//指针域
}Node;
typedef struct Node*LinkList;

        我们看到结点由存放数据元素的数据域和存放后继结点地址的指针域组成。

        假设p是指向线性表第i个元素的指针,则该节点a_i的数据域可以用p->data的值是一个数据元素,结点a_i的指针域可以用p—>next来表示,p—>next的值是一个指针。

        那么p—>next指向谁呢?当然指向第i+1个元素!也就是指向a_i+1的指针。

        一道题辅助理解:

        如果p—>data=a_i,那么p—>next—>data=?

        答案:p—>next—>data=a_i+1

五、单链表的读取

        在线性表的顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。但在单链表中,由于第i个元素到底在哪?我们无法一开始就知道,所以必须从第一个结点开始挨个查找。

        因此,对于单链表实现获取第i个元素的数据的操作GetElem,在算法上相对要麻烦一些,那么我们要来获取链表第i个数据的算法思路如下:

        -声明一个结点p指向链表第一个结点,初始化j从1开始;

        -当j<i时,就遍历链表,让p的指针向后移动,不断指向一下结点,j+1;

        -若到链表末尾p为空,则说明第i个元素不存在;

        -否则查找成功,返回结点p的数据。

        有了以上的思路提示,我们就可以通过以下代码来实现:

Status GetElem(LinkList L,int i,ElemType *e)
{
    int j;
    LinkList p;

    p=L—>next;
    j=1;

    while(p && j<1)
    {
        p=p—>next;
        ++j;
    }
    if(!p ||j>i)
    {
        return ERROR;
    }
    *e = p —>data;
    return 1;
}

        说白了,就是从头开始找,直到第i个元素为止。由于这个算法的事件复杂度取决于i的位置,当i=1时,则不需要遍历,而i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n)

        由于单链表的结构中没有定义表长,所以不能实现要循环多少次,因此也就不方便使用for来控制循环。其核心思想叫做“工作指针后移”,这其实也是很多算法的常用技术。

六、单链表的插入

        关于单链表的插入,假设存储元素e的结点为S,要实现结点p,p—>next和s之间逻辑关系的变化,可以参考下图思考一下:

        由图可见,插入数据元素根本用不着惊动其他结点,只需让s—>next和p—>next的指针做一点改变。

        -s—>next=p—>next

        -p—>next = s;

        从图片来解读这两句代码:

        那么我们来思考一个问题:这两句代码的顺序可不可以交换过来,即:

        先p—>next = s;

        再s—>next=p—>next;

        那么,我们仔细思考,如果先执行p—>next 的话会先被覆盖为s的地址,那么s—>next=p—>next;其实就等于s—>next = s了。所以对于初学来说,这两个语句是千万不能弄反的。

        单链表第i个数据插入结点的算法思路:

        -声明一结点p指向链表头结点,初始化j从1开始;

        -当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;

        -若到链表末尾p为空,则说明第i个元素不存在;

        -否则查找成功,在系统中生成一个空结点s;

        -将数据元素e赋值给s—>data;

        -单链表的插入刚才两个标准语句;

        -返回成功。

Status ListInsert(LinkList *L,int i,ElemType e)
{
    int j;
    LinkList p,s;
    
    p=*L;
    j = 1;
    
    while(p && j<1)//用于寻找第i个结点
    {
        p = p—> next;
        j++;
    }
    if( !p || j>1)
    {
        return ERROR;
    }

    S = (LinkList)malloc(sizeof(Node));
    s —>data = e;
    
    s —> next = p —> next;
    p —> next =s;
    return 1;
}

七、单链表的删除

        我们通过一张图来看单链表的删除操作

        假设元素a_2的结点为q,要实现结点q删除单链表的操作,其实就是将它的前继结点的指针绕过指向后继结点即可。

        那我们所要做的,实际上就是一步操作:

        可以是这样:p —> next =  p —> next—> next;

        也可以是:q = p—> next;p —> next = q—> next;

        单链表第i个数据删除结点的算法思路:

        -声明结点p指向链表第一个结点,初始化j=1;

        -当j<1时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;

        -若到链表末尾o为空,则说明第i个元素不存在;

        -否则查找成功,将欲删除结点p —> next赋值给q;

        -单链表的删除标准语句 p —> next = q—> next;

        -将q结点中的数据赋值给e,作为返回;

        -释放q结点。

        代码如下:

Status LinkDelete(LinkList *L,int i,ElemType *e)
{
    int j;
    LinkList p,q;
    
    p = *L;
    j = 1;
    
    while( p—>next && j<1)
    {
        p = p—>next;
        ++j;
    }
`   
    if(!(p—>next) || j>1)
    {
        return ERROR;
    }
    
    q = p—>next;
    p —> next = q—>next;
    
    *e = q—>data;
    free(q);
    
    return 1;
} 

八、链表与顺序存储结构的效率比较

        我们发现,无论是单链表插入还是删除算法,它们其实都是由两个部分组成:第一部分就是遍历查找第i个元素,第二部分就是实现插入和删除元素。

        从整个算法来说,我们很容易可以推出它们的时间复杂度都是O(n)

        再详细点分析:如果在我们不知道第i个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果我们希望从第i个位置开始,插入连续10个元素,对于顺序存储结构意味着每次插入都需要移动n-i个位置,所以每次都是O(n)

        而单链表,我们只需要在第一次,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)

        显然,对于插入或删除数据就越频繁的操作,单链表的效率优势就越是明显。

        (由于篇幅过长,又要保持适当的更新频率,本节第一部分完结,第二部分将在下周更新,为方便读者阅读,下周将把线性表的链式存储结构合并起来,重新发布,感谢大家支持!)

        (本节第一部分完)


参考资料:

1、《数据结构教程》李春葆主编-清华大学出版社-2022.7

2、线性表6_哔哩哔哩_bilibili 鱼C小甲鱼

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值