数据结构与算法——线性表(2)

目录

一、链式存储

二、单链表

1、单链表的读取

2、单链表的插入

3、单链表的删除

三、单链表的整表创建

1、头插法

 2、尾插法

四、单链表的整表删除

五、单链表结构与顺序存储结构的优缺点


一、链式存储

链表的定义是递归的,它或者为空null,或者指向另一个节点node的引用,这个节点含有下一个节点或链表的引用。

链表中的第一个节点的存储位置叫做头指针,最后一个节点指针为空(NULL)。

 

与顺序存储相比,允许存储空间不连续,插入删除时不需要移动大量的元素,只需修改指针即可,但查找某个元素,只能从头遍历整个链表。

二、单链表

        单链表是链表的一种。链表由节点所构成,节点内含一个指向下一个节点的指针,节点依次链接成为链表。因此,链表这种数据结构通常在物理内存上是不连续的。链表的通常含有一个头节点,头节点不存放实际的值,它含有一个指针,指向存放元素的第一个节点。

单链表图:

 空链表图:

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

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

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

1、单链表的读取

获得链表第i个数据的算法思路

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

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

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

——否则查找成功,返回结点 p 的数据;

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */

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

    p = L->next;
    j = 1;

    while( p && j<i )
    {
        p = p->next;
        ++j;
    }

    if( !p || j>i )
    {
        return ERROR;
    }

    *e = p->data;

    return OK;
}

 由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需要遍历,而i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n)。

2、单链表的插入

如何将S插入到ai、ai+1之间?

                s->next = p->next;

                p->next = s;  (上下两句不能颠倒)

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

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

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

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

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

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

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

        –返回成功。

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */

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

    p = *L;
    j = 1;

    while( p && j<i )   // 用于寻找第i个结点
    {
        p = p->next;
        j++;
    }

    if( !p || j>i )
    {
        return ERROR;
    }

    s = (LinkList)malloc(sizeof(Node));
    s->data = e;

    s->next = p->next;
    p->next = s;

    return OK;
}

3、单链表的删除

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

即可以表示为:p->next = p->next->next;

             或者: q=p->next; p->next=q->next;

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

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

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

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

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

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

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

        –释放q结点

/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */

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

    p = *L;
    j = 1;

    while( p->next && j<i )
    {
        p = p->next;
        ++j;
    }

    if( !(p->next) || j>i )
    {
        return ERROR;
    }

    q = p->next;
    p->next = q->next;

    *e = q->data;
    free(q);         //释放q节点

    return OK;
}

 无论是单链表插入还是删除算法,它们其实都是由两个部分组成:第一部分就是遍历查找第i个元素,第二部分就是实现插入和删除元素。从整个算法来说,它们的时间复杂度都是O(n)。

但对于插入或者删除数据越频繁的操作,单链表的料率优势越来越明显,例如从第i个位置来说,插入连续10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个位置,所以每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。

三、单链表的整表创建

创建单链表的过程是一个动态生成链表的过程,从“空表”的初始状态起,依次建立各元素结点并逐个插入链表

单链表整表创建的算法思路如下:

        –声明一结点p和计数器变量i;

        –初始化一空链表L;

        –让L的头结点的指针指向NULL,即建立一个带头结点的单链表;

        –循环实现后继结点的赋值和插入。

1、头插法

将新节点插入到当前链表的表头,(头结点之后),插入的顺序与链表中的顺序相反,关键点就是记住旧的表头,生成一个新的放到旧表头前面,如图:

/* 头插法建立单链表示例 */

void CreateListHead(LinkList *L, int n)
{
    LinkList p;
    int i;

    srand(time(0));   // 初始化随机数种子

    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;

    for( i=0; i < n; i++ )
    {
        p = (LinkList)malloc(sizeof(Node));  // 生成新结点
        p->data = rand()%100+1;
        p->next = (*L)->next;
        (*L)->next = p;
    }
}

 2、尾插法

增加一个尾指针,新节点插到链表的尾部,插入的顺序和链表的顺序一致,如图:

/* 尾插法建立单链表演示 */

void CreateListTail(LinkList *L, int n)
{
    LinkList p, r;
    int i;

    srand(time(0));
    *L = (LinkList)malloc(sizeof(Node));
    r = *L;

    for( i=0; i < n; i++ )
    {
        p = (Node *)malloc(sizeof(Node));
        p->data = rand()%100+1;
        r->next = p;
        r = p;                 // 备注:初学者可能很难理解这句,重点解释。
    }

    r->next = NULL;
}

四、单链表的整表删除

当我们不打算使用单链表时,我们需要把它销毁,即在内存中将它释放,以便于留出空间给其它程序或者软件使用。

        –声明结点p和q;

        –将第一个结点赋值给p,下一结点赋值给q;

        –循环执行释放p和将q赋值给p的操作;

Status ClearList(LinkList *L)
{
    LinkList p, q;

    p = (*L)->next;

    while(p)
    {
        q = p->next;
        free(p);
        p = q;
    }

    (*L)->next = NULL;

    return OK;
}

五、单链表结构与顺序存储结构的优缺点

分别从存储分配方式、时间性能、空间性能三方面来做对比

存储分配方式:

        –顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。

        –单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。

时间性能:

        –查找

                •顺序存储结构O(1)

                •单链表O(n)

        –插入和删除

                •顺序存储结构需要平均移动表长一半的元素,时间为O(n)

                •单链表在计算出某位置的指针后,插入和删除时间仅为O(1)

空间性能:

        –顺序存储结构需要预分配存储空间,分大了,容易造成空间浪费,分小了,容易发生溢出。

        –单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。

结论:

        –若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。

        –若需要频繁插入和删除时,宜采用单链表结构 

        –当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。

        –而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值