数据结构系列(三):线性表之顺序表与单链表

本系列博客是博主在学习数据结构时的笔记,希望与大家一起分享,如有对数据结构感兴趣的小伙伴可以加个联系方式一起交流。本系列参考程杰的《大话数据结构》

线性表就是零个或者多个数据元素的有限序列。线性表有两组存储方式:顺序结构存储和链式结构存储

1、顺序结构存储

引言:可以想象一群学生在教室占座,比如有9个人,那么就要占9个位置,因为预先已经知道了人的数目。但是当某同学事先去占位后,发现有3位同学没来,但是已经用书本帮他们占座了,其它同学也就没坐在这个位子上了,那么也就意味了这三个空位也浪费了。假如就来了这6位同学,那么该线性表的长度是多少呐?答案当然是6。虽然预先已经开辟了9个单元的空间了,但是线性表的长度是指具有元素的个数,不过该数组的长度依然还是9,这一点需要注意。然后上课上到一半,这3位同学过来了,还带了另外寝室的4位同学,此时总共只占了9个位置,仅能让这3位同学坐下,另外4位自然是坐不下的,也就是线性表的当前长度不能超过数组的长度

1.1读取

根据下标即可读取,此处省略。

1.2 插入

假设要在数组A = [1,2,4,5]的第i位下标(注意下标从0开始)插入3。首先找到第i位,把第i位及其后面的数据全部后移一位,线性表长度加1。注意在实现数据右移的过程中切记要从后往前实现赋值,否则造成数据的覆盖。

//插入
status ListInsert(SqList *L,int i,Elmtype e)
{
    if(L->length == MAXSIZE)//线性表预设最大长度
        return ERROR;
    if(i<0 || i>L->length-1)
        return ERROR;
    if(i<L->length)
    {
        for(int k = L->length-1;k > i;k--)
        {
            L->data[k] = L->data[k-1];//一直向后赋值,直到第i+1位得到了第i位的值
        }
    }
    L->data[i] = e;
    L-length++;
    return OK;
}

1.3 删除

比如在第2下标处插入3,现在的数组A = [1,2,3,4,5],现在想把第1下标的元素删除。删除的时候需要将后继元素向前推移,链表长度减1即可。

//删除
status ListInsert(SqList *L,int i,Elmtype e)
{
    if(L->length == MAXSIZE)//线性表预设最大长度
        return ERROR;
    if(i<0 || i>L->length-1)
        return ERROR;
    if(i<L->length)
    {
        for(int k = i; k < L->length-1; k++)
        {
            L->data[k] = L->data[k+1];//一直向前赋值
        }
    }
    L-length--;
    return OK;
}

1.4 优缺点

顺序存储结构读取数据的时间复杂度位O(1),插入和删除数据的时间复杂度为O(n)。因此优点在于可以快速的存取表中的任意位置的元素,此外无需为表中元素间的逻辑关系而增加额外的存储空间。缺点也很明显,首先是顺序存储的缺点,造成存储空间的碎片化;其次是由于需要实现确定好容量,因此当线性表长度变化较大时难以为继;第三点就是插入和删除数据的时间复杂度很大。

2、链式结构存储

链式结构的特点是哪里有空位我就往哪里连,这样无需开辟一块连续的空间,合理的利用存储空间。既然是哪里有空位就连哪里,不像顺序存储,前后的位置都是相连的,很容易可以找到前面和后面的人,链式这种跳跃性的存储方式怎么实现呐?答案很简单,前一个人只要知道他后面这个人的家庭住址不就行了嘛!也就是地址,实际是指向该元素的指针。那么这个人除了要记住自己是谁(也就是自己存储的内容),还要知道下一个人的家庭住址(指针)。由这两部分信息组成的数据元素(这里我们称为a_i)的存储映像称为结点(Node)。

对于线性表来说,链表的第一个结点的存储位置叫做头指针,其实也就是头指针指向线性表的第一个结点。尾结点指向为空,因此设置为NULL。

 

有时候为了方便对链表进行操作,会在链表的第一个结点前再设置一个头结点,头结点的指针域存储指向第一个结点的指针,如下图所示。头结点的数据域一般存储线性表长度等一些附加信息。

 有同学可能会疑问,头指针和头结点有啥区别啊?这里面区别大着呐,千万不能混淆这两个概念。首先链表必须要有头指针,没有头指针无法知道链表从哪开始,即使链表是空链表也必须要知道头指针。头指针是指向链表的第一个结点的指针,如果该链表有头结点,那就是指向头结点的指针,比如上面的头结点是地址是1000,那么头指针中存储的就是1000。而头结点只是为了方便操作才设立的。

下面是单链表的存储结构描述:

//结点定义
typedef struct Node
{
    Elmtype data;//数据
    struct Node *next;//指针
}Node;
typedef struct Node *LinkList;//给结构体Node另起个名字LinkList

 假设指针p是指向a_i的指针,那么a_i的数据为p->data,ai的指针域为p->next,其中p->next又指向a_i+1元素,a_i+1的数据域为p->next.data。如下图所示:

 2.1读取

单链表的读取的时间复杂度取决于i的位置,最坏是O(n),由于链表无法直接获取下标进行读取,只能通过遍历的方式。

//读取
status GetElem(LinkList L,int i,Elmtype e)
{
    LinkList p;//定义一个结点
    p = L->next;//让p指向链表的第一个结点
    int j = 1;//定义一个计数器,已经指向第一个结点了,因此从1开始

    while(p && j<i)//p不为空以及还没有遍历到i执行循环体
    {
        p = p->next;
        j++;
    }//获取第i个结点
    if(!p || j>i)r//如果第i个结点不存在或者j>i
    {
        return ERROR;
    }
    e = p->data;//获取数据
    return OK;
}

 博主在做这种题的时候总是搞不清循环判断条件,到底是到第i位还是第i-1位,主要总是将下标从0和从1开始弄混。这里的第i个结点是从第一个结点开始的,链表哪有什么第0个结点呐。上面的p开始指向第一个结点,循环i-1次之后,不就是指向第i个结点了嘛,这样理解就简单多了。

2.2插入

假设结点s要插到p和p的下一个结点之间,那么应该怎么做呐?

实际上很简单,把p和p->next的关系打破,再手牵手s不就行了。

 也就是s ->next = p ->next;p ->next = s;s ->data = e;切记不能先让p ->next = s,这样的话p指向的结点会被s给覆盖。

代码描述如下:

//在第i个结点前插入
status ListInsert(LinkList *L,int i,Elmtype e)
{
    LinkList p,s;
    p = *L;
    s = (LinkList)malloc(sizeof(Node));
    int j = 1;
    while(p && j < i)
    {
        p = p->next;
        j++;
    }//找到第i-1个结点
    if(!p || j>i)
        return ERROR;
        
    s->next = p->next;
    p->next = s;
    s->data = e;
    return OK;
}

2.3删除

删除操作比较简单,直接把删除元素前一个结点的指向变为该元素的下一个结点即可。

 假设删除第i个结点,首先要找到第i-1个结点记为p,则该结点为p->next,这里为了方便起见我们记为q,q->next指向q的下一个结点也就是p->next->next。那么删除q结点只需要p->next = q->next;然后再释放q即可。

代码描述如下:

//删除第i个结点
status ListDelete(LinkList *L,int i,Elmtype e)
{
    LinkList p,s;
    p = *L;
    s = (LinkList)malloc(sizeof(Node));
    int j = 1;
    while(p && j < i)
    {
        p = p->next;
        j++;
    }//找到第i-1个结点
    if(!p || j>i)
        return ERROR;
        
    q = p-> next;
    p->next = q->next;
    e = q.data;
    free(q);//收回q的内存
    return OK;
}

 2.4单链表的整表创建

头插法创建整表思路:

1、首先需要初始化空链表L,该链表具有头结点,头结点的指针指向NULL;声明一个结点指针p。

2、往空链表里面插数据,首先生成一个新结点赋值给p,然后随机生成一系列数字给p->data,最好在头结点和前一新结点之间插入p。

空链表如下:

 

 图解析如下:

代码描述如下:

//头插法创建带头结点的链表
void CreatListHead(LinkList *L,int n)
{
    L= (LinkList)malloc(sizeof(Node));
    L->next = NULL;
    LinkList p;//p的初始化应该在插入结点的时候
    //初始化随机数种子
    srand(time(0));

    //插入新结点
    for(int i=0;i<n;i++)
    {
        p = (LinkList)malloc(sizeof(Node));
        p->data = rand()%100+1;//生成100以内的数字
        p->next = L->next;
        L->next = p;

    }
}

每次加入一个新结点都要往头结点和前一新结点之间插,有点插队的意思。事实上我们可以遵循排队的思想,新来的结点要守规矩,接在上一个结点的后面,这种算法称为尾插法。

尾插法创建整表思路:

如下图新结点直接插在尾结点r(定义r指向L的尾部)的后面,即p = r->next;然后很显然的p成为了尾结点,则r = p;最后在链表结束时,需要尾结点指向NULL(切记!!!)。

代码描述如下:

//尾插法创建带头结点的链表
void CreatListTail(LinkList *L,int n)
{
    L= (LinkList)malloc(sizeof(Node));
    
    LinkList p,r;
    r = L;//定义r指向L的尾部
    //初始化随机数种子
    srand(time(0));

    //插入新结点
    for(int i=0;i<n;i++)
    {
        p = (LinkList)malloc(sizeof(Node));
        p->data = rand()%100+1;//生成100以内的数字
        r-> next = p;
        r = p;
    }
    r->next = NULL;//当前链表结束
}

 2.5单链表的整表删除

整表删除需要一个结点一个结点做个循环释放内存即可,但是在释放内存的时候,先要保存当前要释放的结点的指向,不然释放完这个结点后无法找到下一个结点的地址

在这里需要定义两个结点p和q,q充当临时保存的功能,p释放。

代码描述如下:

//整表删除
status ClearList(LinkList *L)
{
    LinkList p,q;
    p = L->next;//p指向第一个结点
    while(p)//没到表尾NULL
    {
        q = p->next;//保存下一结点位置
        free(p);
        p = q;
    }
    L->next = NULL;//头结点的指针域为空
    return OK;
}

以上是顺序表和链表的全部基本内容,如有不对之处请于评论处指正,谢谢!

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值