考研数据结构笔记——2.线性表的链式表示(单链表)

线性表的链式表示

单链表的定义

线性表的链式存储称为单链表;
每个链表节点,除存放元素自身的信息外,还需要存放一个指向其后继结点的指针;
data为数据域,存放数据;next为指针域,存放指针

单链表中结点类型的描述

typedef struct LNode{
    ElemType data;      //数据域
    struct LNode *next;     //指针域
}LNode,*Linklist;
  • 顺序表需要大量连续存储空间,单链表克服了这一缺点,即存储空间不一定要连续
  • 但是单链表相对于顺序表多了指针域,因此有浪费空间的缺点
  • 单链表的元素是离散地分布在存储空间的,所以无法实现随机存取,寻找特定的结点必须从表头开始遍历,依次查找

头指针与头节点

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

为了操作上的方便,在单链表主体的第一个结点前加一个结点,称为头结点;头结点的数据域可以记录表长等信息,也可以不记录任何信息

头指针和头节点的区别

  • 头指针始终指向链表的第一个结点,无论头结点是否存在
  • 头结点是带头结点链表的第一个结点

引入头结点带来的优点

  • 由于开始结点的位置被存放在头结点的指针域中,所以链表第一个位置的操作和其他位置相同(链表的第一个有效位置实际上是第二个结点)
  • 引入头结点后,无论链表是否为空,头指针都指向头结点的非空指针;否则空链表与非空链表的处理不同

单链表上基本操作的实现

单链表上的主要操作主要有建立(头插法、尾插法)、查找(按序号、按值)、插入结点、删除结点、求表长

采用头插法建立单链表

LinkList List_HeadInsert(LinkList &L){
    LNode *s;int x;
    L = (Linklist)malloc(sizeof(LinkList));  //创建头结点
    L->next = NULL;  //头结点指针为空,初始化为空链表
    scanf("d",&x);  输入(第一个)结点的值
    while(x != 9999){   //结束条件(如输入9999)
        s = (LinkList)malloc(sizeof(LinkList)); //创建一个新结点
        s->data = x;
        s->next = L->next;
        L->next = s;        //插入过程
        scanf("d",&x);
    }
    return L;
}
  • 头插法建立单链表时,读入数据的顺序和链表中元素的顺序是相反的
  • 每个结点插入的时间为O(1)
  • 设单链表长为n,则头插法生成单链表的总时间复杂度为O(n)

采用尾插法建立单链表

头插法虽然简单,但是输入顺序和生成链表中结点的顺序相反;尾插法将新结点插入当前链表的表尾,为此必须增加一个尾指针,使其始终指向当前链表的尾结点;时间复杂度与头插法相同

Linklist List_TailInsert(Linklist &L){
    int x;  //设元素类型为整型
    L = (LinkList)malloc(sizeof(LNode));
    LNode *s,*r = L;    //r为表尾指针
    scanf("%d",x);  //输入结点的data域值
    while(x != 9999){   //结束条件(如输入9999)
        s = (LNode *)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;    //r指向下一个表尾结点
        r = s;  //r再次成为表尾指针
        scanf("%d",x);
    }
    r->next = NULL; //尾结点指针为空
    return L;
}

按序号查找结点值

LNode *GetElem(LinkList L,int i){
    int j = 1;  //计数器,初始化为1
    LNode *p = L->next; //将头结点的指针赋值给p
    if(i == 0)
        return L;
    if(i <= 0)
        return NULL;
    while(p&&j < i){    //从第1个结点开始,查找到第i个结点
        p = p->next;
        j++;
    }
    return p;   //返回第i个结点的指针
}
  • 按序号查找结点的时间复杂度为O(n)

按值查找表结点

从单链表第一个结点开始,依次比较各个结点中数据域的值;如果链表中某结点数据域的值等于给定值e,则返回该结点的指针;若没有这样的结点,则返回NULL

LNode *LocateElem(LinkList L,ElemType e){
    LNode *p = L->next;
    while(p != NULL&&p->data != e)  //p后面无结点或该结点的data域等于被查找对象时跳出循环
        p = p->next;
    return p;   //找到后返回p,否则返回NULL
}

插入结点操作

插入节点操作将值为x的新结点插入到单链表的第i个位置上;先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新结点

  1. 调用序号查找算法GetElem(L,i-1),查找第i-1个结点

  2. 假设返回的第i-1个结点为 *p,令新结点 *s的指针域指向 *p的后继结点

  3. 令 *p的指针域指向新插入的结点 *s

1 p = GetElem(L,i-1);
2 s->next = p->next;
3 p->next = s;
  • 2句和3句的顺序不能颠倒,否则s将指向自己,显然是错误的
  • 算法的主要时间开销在查找第i-1个元素,算法时间复杂度为O(n),插入操作的时间复杂度仅为O(1)

对某一结点实现前插操作

以上面的算法为例,前插操作可以转化为后插操作,但需要调用GetElem函数找到第i-1个结点,而查找第i-1个元素的时间复杂度为O(n)

对于已知结点,可以采取另外一种方式进行前插转换为后插的操作;设待插入结点为 *s,将其插入到 *p的前面,我们仍然可以将其插入到 *p的后面,将p->data与s->data交换,这样既满足了逻辑关系,又能使算法时间复杂度降为O(1)

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

删除结点操作

删除结点操作是将单链表的第i个节点删除;假设结点 *p为被删除结点 *q的前驱节点,仅需修改 *p的指针域,将 *p的指针域next指向 *q的下一结点

p = GetElem(L,i-1);
q = p->next;
p->next = q->next;
free(q);
  • 和插入算法一样,该算法的时间主要耗费在查找上,因此算法的时间复杂度为O(n)

删除结点*p

删除结点 *p,通常的做法是用GetElem找到 *p的前驱节点,然后再执行删除操作,时间复杂度为O(n)

实际上,也可以通过删除 *p的后继结点的方法实现;将其后继结点的值赋给它,并且删除后继结点,可以使算法的时间复杂度降为O(1)

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

求表长操作

求表长操作就是计算单链表中数据结点(不带头结点)的个数;遍历+计数器,算法的时间复杂度为O(n);不带头结点和带头结点的单链表处理不同;对不带头结点的单链表,当表为空时要单独处理

你可能感兴趣的:(考研数据结构笔记——2.线性表的链式表示(单链表))