第2章 线性表之链式存储结构

202 阅读10分钟

2.4 线性表的链式存储结构

线性表链式存储结构的特点是: 用 一 组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成 一个链表,即为线性表的链式存储结构

首元结点、头结点、头指针三者的区分:

  • 首元结点是指链表中存储第一个数据元素a1 的结点。
  • 头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。
  • 头指针是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。

链表增加头结点的主要作用:

  • 便于首元结点的处理:头结点后,插入和删除数据元素的算法统一了,不再需要判断是否在第一个元素之前插入或删除第一个元素;
  • 便于空表和非空表的统一处理:不论链表是否为空,其头指针是指向头结点的非空指针,链表的头指针不变,因此空表和非空表的处理也就统一了。
typedef struct Lnode {
    ElemType data;//结点的数据域
    struct Lnode* next;//结点的指针域
}Lnode, * LinkList;//LinkList为指向结构体LNode的指针类型

① 为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode* ,两者本质上是等价的。通常习惯上用LinkList定义单链表,强调定义的是某个单链表的头指针;用LNode *定义指向单链表中任意结点的指针变量

② 注意区分指针变量和结点变量两个不同的概念,若定义 LinkList p或LNode *p,则p为指向某结点的指针变量,表示该结点的地址;而 *p 为对应的结点变量,表示该结点的名称。

2.5 单链表基本操作的实现

单链表的初始化

① 生成新结点作为头结点,用头指针L指向头结点。

② 头结点的指针域置空。

// 初始化单链表
Status InitList(LinkList& L) {
    //① 生成新结点作为头结点,用头指针L指向头结点。
    L = (Lnode*)malloc(sizeof(Lnode));//或者 L = new LNode;
    if (!L) exit(OVERFLOW);//空间分配失败
    //② 头结点的指针域置空。
    L->next = NULL;
    return OK;
}

单链表的基础操作

//判断单链表是否为空
Status ListEmpty(LinkList L) {
    if (!L->next) return 1;//为空
    return 0;
}
​
//单链表的销毁
Status DestroyList(LinkList& L) {
    Lnode* p;
    while (L) {
        p = L;//p用于循环删除结点
        L = L->next;//每循环一次让L指向下一元素
        free(p);//释放p指向的结点
    }
    printf("单链表已被销毁!\n");
    return OK;
}
​
//清空单链表
Status ClearList(LinkList& L) {
    Lnode* p, * q;
    p = L->next;//指针指向首元节点
    while (p) {
        q = p->next;//q指向要删除元素的下一节点
        delete p;
        p = q;
    }
    L->next = NULL;//头结点指针域置空
    printf("单链表已清空!\n");
    return OK;
}
​
//求单链表表长
Status ListLength(LinkList L) {
    if (!L->next) return 0;//空表
    Lnode* p;
    p = L;
    int count = 0;//计数器
    while (p) {
        p = p->next;//遍历单链表,统计节点数
        ++count;
    }
    return count - 1;//减去头结点
}
​
//单链表元素的遍历
Status ListPrint(LinkList L) {
    Lnode* p;
    p = L->next;
    if (!p) return ERROR;//空链表
    while (p) {
        printf("%d\t", p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}

单链表的查找

按位查找:

  • ①用指针p指向首元结点,用j做计数器初值赋为1。
  • ② 从首元结点开始依次顺着链域next向下访问,只要指向当前结点的指针p 不为空( NULL),并且没有到达序号为i的结点,则循环执行以下操作:p指向下一个结点;计数器j相应加1。
  • ③退出循环时,如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i大于表长n或i小于等于0),取值失败返回ERROR;否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。

按值查找返回地址:

  • ① 用指针p指向首元结点。
  • ②从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点。
  • ③返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL
//按位查找,找第i个元素
Status GetElement(LinkList L, int i, ElemType& e) {
    Lnode* p;
    p = L->next;//p指向首元节点
    int j = 1;//表示第一个元素
    if (!p || i < j) return ERROR;//查找的位置不存在
    while (p && i > j) {
        p = p->next;
        ++j;
    }
    e = p->data;
    return OK;
}
​
//按值查找,返回结点地址
Lnode* LocatElem(LinkList L, ElemType e) {
    Lnode* p;
    p = L->next;//初始化,p指向首元节点
    while (p && p->data != e) {//顺链域向后扫描,直到p为空或p所指结点的数据域等于e
        p = p->next;//p指向下一节点
    }
    return p;//查找成功返回e的结点p,查找失败返回NULL;
}
​
//按值查找,返回位序
int LocatElemN(LinkList L, ElemType e) {
    Lnode* p;
    p = L->next;
    int i = 1;//用于返回位序
    while (p && p->data != e) {
        ++i;
        p = p->next;
    }
    if (p) return i;//只有结点不为空才找到
    return 0;
}

单链表的插入

①查找结点ai -1并由指针p指向该结点。

② 生成一个新结点*s

③ 将新结点的数据域置为e

④ 将新结点的指针域指向结点ai

⑤ 将结点*p的指针域指向新结点

//单链表的插入
Status ListInsert(LinkList& L, int i, ElemType e) {
    Lnode* p;
    Lnode* s = new Lnode;
    p = L; int j = 0;
    if (!p || (i - 1 < j)) return ERROR;//插入位置不合法,i>n+1或者i<1;
    while (p && i - 1 > j) {
        p = p->next; j++;// 查找第i-1个结点,p指向该结点
    }
    s->data = e; s->next = p->next; p->next = s;//将新结点连接起来
    return OK;
}

单链表的删除

① 查找结点ai-1并由指针p指向该结点。

② 临时保存待删除结点ai的地址在q中,以备释放。

③将结点*p的指针域指向ai的直接后继结点。

④释放结点ai的空间。

//单链表的删除
Status ListDelete(LinkList& L, int i, ElemType& e) {
    Lnode* p, * q;
    p = L; int j = 0;
    if (!(p->next) || j > i - 1) return ERROR;//删除位置不合理
    while ((p->next) && j < i - 1) {
        p = p->next; j++;//查找第i-1个结点,p指向该结点
    }
    q = p->next;//临时保存被删除结点
    e = q->data;//保存删除结点的元素值
    p->next = q->next;//改变删除结点前驱结点的指针域
    free(q);//释放删除结点的空间.
    return OK;
}

单链表的头插法和尾插法

前插法

  • ① 创建一个只有头结点的空链表。 ② 根据待创建链表包括的元素个数n,循环n次执行以下操作:生成一个新结点* p;输入元素值赋给新结点*p的数据域;将新结点 *p插人到头结点之后。

尾插法

  • ① 创建一个只有头结点的空链表。
  • ② 尾指针r初始化,指向头结点。
  • ③ 根据创建链表包括的元素个数n,循环n次执行以下操作:生成一个新结点* p;输入元素值赋给新结点* p的数据域;将新结点* p插入到尾结点* r之后;尾指针r指向新的尾结点* p。
//单链表的头插法插入多个元素
void CreateList_H(LinkList& L, int n) {
    Lnode* p;
    if (L) {//确保已初始化,创建了头结点
        for (int i = 0; i < n; i++) {
            p = new Lnode;
            printf("请输入需要插入的第%d个值:", i + 1);
            scanf("%d", &p->data);//输入元素赋值给新结点*p的数据域
            p->next = L->next; L->next = p;//将新结点*p插入到头结点之后
        }
    }
}
​
//单链表的尾插法(仅适用于空表后插)
void CreateList_R(LinkList L, int n) {
    Lnode* p, * r;//新结点声明和创建尾指针
    r = L;//尾指针指向头结点
    if (L) {//确保已初始化,创建了头结点
        for (int i = 0; i < n; i++) {
            p = new Lnode;
            printf("请输入需要插入的第%d个值:", i + 1);
            scanf("%d", &p->data);//输入元素赋值给新结点*p的数据域
            p->next = NULL;
            r->next = p;
            r = p;
        }
    }
}

循环链表

循环链表:是一种头尾相连的链表,表中的最后一个结点的指针域指向头结点,整个链表形成一个环。

优点:从表中的任一结点出发均可找到表中其他节点。

注意:循环链表没有NULL指针,因此终止条件是判断它们是否等于头指针。

头指针表示单循环链表尾指针表示单循环链表
找a1的时间复杂度O(1)O(1)
找an的时间复杂度O(n)O(1)
LinkList Connect(LinkList Ta, LinkList Tb){
    //假设Ta、Tb都是非空的单循环链表
    p=Ta->next;//p存表头结点
    Ta->next=Tb->next->next ;//②Tb表头连结Ta表尾
    delete Tb->next;//③释放Tbfue'b>next);
    Tb->next=p;//④修改指针
    return Tb;//时间复杂度是O(1)。
}

双向链表

//双向链表的存储结构
typedef struct DuLNode{
    ElemType data;//数据域
    struct DuLNode *prior,*next;//直接前驱和直接后继
} DuLNode, * DuLinkList;

双向链表的插入

status ListInsert_DuL ( DuLinkList &L,int i,ElemType e){
    //在带头结点的双向链表工中第i个位置之前插人元素e
    if(!(p=GetElem_DuL(L,i))) return ERROR;
    s=new DuLNode; s->data=e;
    s->prior=p->prior;  p->prior->next=s;
    s->next=p;   p->prior=s;
    return OK;
}

双向链表的删除

status ListDelete_DuL ( DuLinkList &L,int i,ElemType &e){ //删除带头结点的双向链表工中的第i个元素
    if(!(p=GetElem_DuL(L,i))) return ERROR;
    e = p->data;
    p->prior->next=p->next; //修改被删结点的前驱结点的后继指针
    p->next->prior=p->prior;//修改被删结点的后继结点的前驱指针
    delete p;//释放被删结点的空间
    return OK;
}

2.6 单链表、循环链表、双向链表的时间效率比较

image.png

2.7 顺序表和链表的优缺点

优点(顺序表)缺点(顺序表)
存储密度大,无须为顺序表中元素之间的逻辑关系增加额外存储空间插入、删除某一元素时,需要移动大量元素
可以随机存取表中任一元素造成存储空间的“碎片”,浪费存储空间


优点(链表)缺点(链表)
插入和删除不需要移动数据元素存储密度小,指针需要额外占用存储空间
结点空间可以动态申请和释放元素需要顺序存取

存储密度 = 节点数据本身占用的空间 / 结点占用的空间总量

2.8 顺序表和链表的比较

image.png

2.9 线性表的应用

已知两个有序集合A和B,数据元素按值非递减有序排列,现要求一个新的集合C= A∪B,使集合C中的数据元素仍按值非递减有序排列。

Status ListUnion(SqList La, SqList Lb, SqList& Lc) {
    //① 获取三个顺序表的长度
    int La_len = La.length;
    int Lb_len = Lb.length;
    int Lc_len = Lc.length;
    //② 将La中的数据进行遍历
    for (int i = 0; i < La_len; i++) {
        //③ 插入数据La到Lc时,先判断此时Lc中是否还有表空间
        if (Lc_len == Lc.MaxSize) CreateList(Lc, 10);
        //④ 将La中的每一个数据存入到Lc中
        ListInsert(Lc, i + 1, La.data[i]);
    }
    //⑤ 遍历Lb中的每一个数据元素与Lc中的元素进行比较
    for (int j = 0; j < Lb_len; j++) {
        //⑥ Lb中的数据插入到Lc之前,需要拿Lb中的数据与Lc中每一个元素进行比对吗,不相同则插入
        if (LocatElem(Lc, Lb.data[j]) == 0) {//根据按位查找,没找到相同元素则返回0
            //⑦ 插入Lb中的元素时,先判断此时Lc中是否还有表空间
            if (Lc.length == Lc.MaxSize) CreateList(Lc, 10);
            //⑧ 调用插入函数,每次都插入到Lc中的最后一个位置
            ListInsert(Lc, Lc.length+1, Lb.data[j]);
        }
    }
    //⑨ 将Lc中的数据进行排序,调用冒泡排序函数
    ListSort(Lc);
    return OK;
}