数据结构第二章——线性表的链式表示

目录

一、单链表的定义

(一)头指针 

(二)头结点

二、单链表基本操作的实现

(一)采用头插法建立单链表

(二)采用尾插法建立单链表

(三)按序号查找节点值

(四)按值查找节点

(五)插入结点操作

(六)删除节点操作

(七)求表长操作

三、双链表

(一)双链表的定义

(二)双链表的后插操作

(三)双链表的删除操作

四、循环链表

(一)循环单链表的定义

(二)循环双链表的定义

 五、静态链表

六、经典例题

(一)选择题

(二)应用题


一、单链表的定义

        通过一组任意的存储单元来存储线性表中的数据元素

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

        对上述代码进行简单说明,LNode和*LinkList都是结构体LNode的别名,定义两个别名的目的是,LNode用来定义结点,而LinkList用来作为头指针的定义 ElemType是一个抽象概念,就是代表任意数据类型的意思,在实际操作中不存在这个类型,只是描述算法中使用,具体场景中使用int、char等真实存在的数据类型进行替换即可

(一)头指针 

        通常用头指针来标识一个单链表,如单链表L,头指针为NULL时标识一个空表

        不论有无头结点,头指针都始终指向链表的第一个结点

(二)头结点

        头结点的数据域可以不设任何信息,也可以记录表长等信息

  • 链表的第一个位置上的操作和其他位置的操作一致,无需特殊处理
  • 无论链表是否为空,其头指针都指向头结点的非空指针,因此空表和非空表的处理得到了统一

        判断是否带头结点,就看有没有给头指针申请分配内存


二、单链表基本操作的实现

(一)采用头插法建立单链表

1.带头结点的头插法算法思路:

        ①先申请一个头指针并将其指向空地址

        ②将头指针指向头结点

        也就是给头指针分配内存,此时头指针的值就是malloc分配的内存空间的地址值,也就是头指针指向头结点

        ③创建新的节点,将读取到的数据存放到新节点的数据域中

        再申请一个指针,给指针分配内存就算完成创建了

        ④将新节点插入到头结点后面

        先头结点指向新节点,再把新节点指向   原本头结点指向的位置

        ⑤然后重复第③、④步以此循环

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;


void List_HeadInsert(LinkList &L)
{
    LNode *s;
    int x;
    L = (LinkList)malloc(sizeof(LNode));        //创建头结点
    L->next = NULL;                             //初始为空链表
    scanf("%d",&X);                             //输入节点的值
    while(x != 9999)                            //输入9999表示结束
    {
        s = (LNode*)malloc(sizeof(LNode));      //创建新节点
        s->data = x;
        s->next = L->next;
        L->next = s;                            //新节点插入表中,L为头指针
        scanf("%d",&x);
    }
    return L;
}


int main()
{
    LinkList L = NULL;                          //空链表初始化
    List_HeadInsert(&L);
    return 0;
}

2.不带头结点的头插法算法思路: 

         ①先申请一个头指针并将其指向空地址

        ②创建一个新节点并为其赋值   

        再申请一个指针,给指针分配内存就算完成创建了

        ③将新节点插入到头指针后面

        判断头指针是否为空指针,如果是,把头指针指向新节点;

                                                   若不是,则把新建节点指向原本头指针指向的节点,头指针指向            新建节点

        ④重复第②、③步以此循环

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;


void NoNode_HeadInsert(LinkList &L)
{
    //初始化链表
    LNode *s;                                      //要插入的节点
    ElementType x;                                 //要插入的数据
    scanf("%d", &x);
    while (x != 999) 
    {
        s = (LNode*)malloc(sizeof(LNode));
        s->data = x;
        s->next = NULL;
        if (NULL == L) 
        {
            L = s;                                //将新结点置位首节点也是尾结点
        } 
        else 
        {
            s->next = L;
            L = s;                                //新结点置位尾结点
        }
        scanf("%d", &x);
    }

}


int main()
{
    LinkList L = NULL;                             //空链表初始化
    NoNode_HeadInsert(&L);
    return 0;
}

(二)采用尾插法建立单链表

1.带头结点的尾插法算法思路:

        ①先申请一个头指针并将其指向空地址

        ②将头指针指向头结点

        也就是给头指针分配内存,此时头指针的值就是malloc分配的内存空间的地址值,也就是头指针指向头结点

        ③创建一个表尾指针

        意思就是表尾指针初始化指向头结点,第一次使用时可充当头指针的作用

        ④新建一个结点并为其赋值

        再申请一个指针,给指针分配内存就算完成创建了

        ⑤将新节点挂在表尾

        用表尾指针将表尾结点指向新节点,然后再将表尾指针指向新建节点即可

        ⑥重复④、⑤,以此循环 

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;


void List_TailInsert(LinkList &L)
{
    int x;
    L = (LinkList)malloc(sizeof(LNode));
    LNode *s,*r = L;
    scanf("%d",&x);
    while(x != 9999)
    {
        s = (LNode *)malloc(sizeof(LNode));         //创建新节点
        s->data = x;
        r->next = s;
        r = s;
        scanf("%d",&x);
    }
    r->next = NULL;
}


int main()
{
    LinkList L = NULL;                          //空链表初始化
    List_TailInsert(&L);
    return 0;
}

        上述代码可能有个容易被误导的地方, s = (LNode *)malloc(sizeof(LNode)),明明前面定义时LNode *s,*r = L将头结点的地址赋给s了,而且头结点也已经完成了内存分配创建,s与L值一样的怎么能再次分配内存。malloc函数是随机分配一块内存并且把该内存的地址赋给指针s,而不是给s原本的地址赋值,也就是说这里指针定义的时候可以不用LNode *s = L,直接LNode *s也没关系,当然LNode *r = L这一步是不能不赋值的

2.不带头结点的尾插法算法思路:

        ①先申请一个头指针并将其指向空地址

        ②创建一个表尾指针

        意思就是表尾指针初始化指向头结点,第一次使用时可充当头指针的作用

        ④新建一个结点并为其赋值

        再申请一个指针,给指针分配内存就算完成创建了

        ⑤将新节点挂在表尾

        用表尾指针将表尾结点指向新节点,然后再将表尾指针指向新建节点即可

        ⑥重复④、⑤,以此循环 

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;


void NoNode_TailInsert(LinkList *L) {
    ElementType x;                                //要插入的元素
    LNode *s;                                     //要插入的节点
    LNode *r;                                     //尾指针
    scanf("%d", &x);
    while (x != 9999) {
        s = (LNode *) malloc (sizeof(LNode));
        s->data = x;
        s->next = NULL;
        if (NULL == L)                            //链表是否为空  
        {                                          
            L = s;                                //将新结点置位首节点也是尾结点
            r = L;
        } 
        else 
        {
            r->next = s;
            r = s;                                //新结点置位尾结点
        }
        scanf("%d", &x);
    }
}

int main()
{
    LinkList L = NULL;                          //空链表初始化
    NoNode_TailInsert(&L);
    return 0;
}

(三)按序号查找节点值

  • 先判断序号是否合法

        序号为0,直接传出头结点,为小于0的数则返回NULL

  • 再往下进行循环

        关键就在哪里停下来,满足节点不为空并且序号等于输入值的时候

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

LNode *GetElem(LinkList L,int i)
{
    int j = 1;
    LNode *p=L->next;                           //头结点赋给p
    if(i == 0)
        return L;
    if(i<1)
        return NULL                             //若i无效,则返回NULL
    while(p&&j < i)
    {
        p=p->next;
        j++;
    }
    return p;
}

int main()
{
    LinkList L = NULL;                          //空链表初始化
    LNode *x;
    x = GetElem(L,5);
    return 0;
}

         查找的时候,带头结点的,头结点为第0个节点,第一个带数据的节点才是第1个结点

(四)按值查找节点

        循环对比值即可 

        循环条件是不为空并且值不等于输入的值

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

LNode *LocateElem(LinkList L,ElemType e)
{
    LNode *p = L->next;
    while(p!=NULL&&p->data!=e)                  //循环条件是不为空以及值不等于要找的
        p = p->next;
    return p;
}

int main()
{
    LinkList L = NULL;                          //空链表初始化
    LNode *x;
    x = LocateElem(L,e);
    return 0;
}

(五)插入结点操作

1.后插节点

  • 先将新节点通过第一个结点指向下第二个节点
  • 再将第一个结点指向新结点
p = GetLem(L,i-1);
s->next = p->next;
p->next = s;

        顺序不能颠倒,如果颠倒了,就找不到第二个节点在哪了 

2.前插节点

        普通的前插节点要从头找该节点的前一个结点,插入时间复杂度为O(n),因此使用后插法再加上交换数据域方式来实现,时间复杂度为O(1)

s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;

(六)删除节点操作

1.找前驱删除法 

        ①删除节点

        先查找要删除位置的前驱节点,然后把前驱节点直接指向要删节点的后继节点

        ②释放删除节点空间

        使用free函数

p = GetElem(L,i-1);
q = p->next;
p->next = q->next;
free(q);

2.后驱交换删除法 

        ①找到要删除的节点位置

        ②要删节点与其后驱节点交换数据域

        ③删除后驱节点即可

q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);

(七)求表长操作

         先从头指针定位到头结点,然后一直循环往后查找到为空

        链表长度不包括头结点


三、双链表

(一)双链表的定义

         产生原因,单链表在某个节点只能找到后继节点,找前驱节点必须从头开始找,因此诞生了部分后驱节点交换数据域的算法。直接再设置一个前驱节点就完事了,这就是双链表

typedef struct DNode
{
    ElemType data;
    struct DNode *prior,*next;
}Dnode,*DLinkList;

(二)双链表的后插操作

1.算法思路  

2.代码实现 

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

(三)双链表的删除操作

1.算法思路 

2.代码实现 

p->next = q->next;
q->next->prior = p;
free(q);


四、循环链表

(一)循环单链表的定义

        与单链表的区别在于,表中最后一个节点的指针不是NULL,而改为指向头节点,从而整个链表形成一个环

        循环链表的判空条件不是头节点的指针是否为空,而是他是否等于头指针 

 

(二)循环双链表的定义

        循环双链表中,头节点的prior指针指向表尾节点

        判断循环双链表节点为空的条件,头节点的prior和next域都等于头指针L 


 五、静态链表

         静态链表借助数组来描述线性表的链式存储,指针域是节点的相对地址,也就是数组的下标,称为游标

        普通链表的指针里是绝对地址

        和顺序表一样,静态链表也要预先分配一块连续的内存空间

        指针指向-1的就是尾结点

#define MaxSize 50
typedef struct
{
    ElemType data;
    int next;
}SLinkList[MaxSize];

         总体来说,静态链表没有单链表使用方便,但在一些不支持指针的高级语言(如Basic)中,这是一种非常巧妙的设计方法


六、经典例题

(一)选择题

1.给定有n个元素的一维数组,建立一个有序单链表的最低时间复杂度是()

        A.O(1)               B.O(n)                C.O(n^2)                D.O(nlog_2n)

        如果先建立链表,然后依次插入建立有序表,每插一个元素就需遍历链表寻找插入位置,时间复杂度为O(n^2)

        如果先将数组排好序,再建立链表。建立链表的时间复杂度为O(n),数组排序最好时间复杂度为O(nlog_2n),因此总时间复杂度为O(nlog_2n)

2.将长度为n的单链表链接在长度为m的单链表后面,其算法的时间复杂度是()

        A.O(1)               B.O(n)                C.O(m)              D.O(m+n)

         插入的时间复杂度为O(1),但是要先找到m单链表的表尾,因此时间复杂度为O(m)

3.在一个长度为n的带头结点的单链表h上,设有尾指针r,则执行()操作与链表的表长有关

A.删除单链表的第一个元素

B.删除单链表的最后一个元素

C.在单链表第一个元素前插入一个新元素

D.在单链表最后一个元素后插入一个新元素

        删除单链表最后一个元素,需要把最后一个元素的前驱节点的指针域修改为NULL,因此从头遍历找到这个前驱节点,因此跟表长有关

4. 设对n(n>1)个元素的线性表的运算只有四种:删除第一个元素;删除最后一个元素;在第一个元素之前插入新元素;在最后一个元素之后插入新元素,最好使用()

A.只有尾结点指针没有头结点指针的循环单链表

B.只有尾结点指针没有头结点指针非循环双链表

C.只有头结点指针没有尾结点指针的循环双链表

D.既有头结点指针又有尾结点指针的循环单链表

        首先,只要有删除最后一个元素这个条件,单链表就不适合了,不论是否带尾结点或者循环,因为要修改表尾结点的前驱节点的指针,而单链表要找前驱节点,只能遍历循环,所以直接排除单链表选项A、D

        只有尾结点指针又不循环,则无法一步找到表头节点的位置,又需要遍历,因此排除选项B

(二)应用题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值