C数据结构 - 线性表

线性表是数据结构中最简单的基本数据结构。线性表的使用和维护都很简单,这一特点使其成为很多算法的基础。数组、链表、栈、队列是4中最常见的线性表,其外部行为和接口都各有特色。

线性表(List):零个或多个数据元素的一有限序列

  • 序列:元素之间是有顺序的,若元素存在多个则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
  • 有限:元素个数是有限制的
4933701-a7bc33bb24cc7409.png
线性表

线性表元素的个数n(n>0)定义为线性表的长度,当n=0时称为空表。

4933701-45560804e47908f8.png
星座列表

在较复杂的线性表中,一个数据元素可以由若干个数据项组成。

4933701-3a459c58401db3c1.png
复杂线性表中数据元素可由若干数据项组成

线性表的抽象数据类型

ADT 线性表(List)
Data
  线性表的数据对象集合为{a1,a2,a3...an},每个元素的类型均为DataType。
  其中除第一个元素a1外,每个元素有且只有一个直接前驱元素。
  除了最后一个元素an外,每个元素有且只有一个直接后继元素。
  数据元素之间的关系是一对一的关系。
Operation
  InitList(*L) 初始化建立一个空的线性表L
  ListEmpty(L) 若线性表为空则返回true否则返回false
  ClearList(*L) 将线性表清空
  GetElem(L,i,*e) 将线性表L中的第i个位置元素值返回给e
  LocateElem(L,e) 在线性表L中查找与给定值e相等的元素,若查找成功返回该元素在表中序号表示成功,否则返回0表示失败。
  ListInsert(*L, i, e) 在线性表L中的第i个位置插入新元素e
  ListDelete(*L, i, *e) 删除线性表L中第i个位置元素并用e返回其值
  ListLength(L) 返回线性表L的元素个数
endADT

线性表的顺序存储结构

线性表的顺序存储结构指的是一段地址连续的存储单元依次存储线性表的数据元素。

4933701-63417f12c920edd1.png
线性表(a1,a2,...an)的顺序存储

顺序存储结构需要三个属性:

  • 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
  • 线性表的最大存储容量:数组长度MAXSIZE
  • 线性表的当前长度:length
#define MAXSIZE 20 //线性表的最大存储空间,存储空间初始大小,数组data的存储位置就是存储空间的存储位置。

typedef int ElemType;//元素类型
typedef struct
{
    ElemType data[MAXSIZE];//使用数组存储数据元素,元素最大个数为MAXSIZE。
    int length;//线性表当前长度
}List;

数据长度和线性表长度区别

  • 数组的长度时存储空间的长度,存储分配后这个量一般是不变的。
  • 线性表的长度时线性表中数据元素的个数,随着线性表插入和删除操作而变化。
  • 在任意时刻,线性表的长度应该小于等于数组的长度。

地址计算方法

4933701-d1159726ab62b352.png
线性表当前长度

C语言中数组从0开始第一个下标,线性表的第i个元素是要存储在数组下标i-1的位置,即数据元素的序号和存放它的数组下标之间的对应关系。

用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。

存储器中的每个存储单元都有自己的编号,这个编号称为地址。

4933701-5bb2a372cbf45631.png
计算线性表中任意位置的地址

线性表顺序存储结构的插入和删除

获取元素

/*获取元素*/
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int Status;
//将线性表list中第pos个位置元素值返回,只要pos的数值在数组下标范围内,就将数组第pos-1下标的值返回即可。
Status GetListElem(List list, int pos, ElemType *ele)
{
    //初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
    if(list.length == 0 || pos<1 || pos>list.length){
        return ERROR;
    }
    *ele = list.data[pos-1];//用e返回L中第i个数据元素的值
    return OK;
}

插入元素

4933701-7b331a596d0aa08d.png
插入操作
/*插入元素*/
Status InsertListElem(List *list, int pos, ElemType ele)
{
    int i;
    //判断线性表是否已经满
    if(list->length == MAXSIZE){
        return ERROR;
    }
    //判断插入位置是否在数组范围内
    if(pos < 1 || pos > list->length+1){
        return ERROR;
    }
    //若插入位置不在线性表末尾
    if(pos <= list->length){
        //将要插入位置后数组元素向后移动一位
        for(i = list->length-1; i >= pos-1; i--){
            list->data[i+1] = list->data[i];
        }
    }
    //将新元素插入线性表中
    list->data[pos-1] = ele;
    list->length++;
    return OK;
}

删除元素

4933701-aa22b70a67a4bad3.png
线性表的顺序存储结构删除元素
/*删除元素*/
Status DeleteListElem(List *list, int pos, ElemType *ele)
{
    int i;
    //判断线性表是否为空
    if(list->length == 0){
        return ERROR;
    }
    //判断删除位置是否在有效,且删除位置是否有效。
    if(pos<1 || pos > list->length){
        return ERROR;
    }
    //获取要删除元素
    *ele = list->data[pos-1];
    //判断删除位置是否为线性表的最后位置
    if(pos < list->length){
        //将删除位置后续元素前移
        for(i = pos; i < list->length; i++){
            list->data[pos-1] = list->data[i];
        }
    }

    list->length--;
    return OK;
}
/*将两线性表中差集元素插入某线性表中*/
void union(List *l1, List l2)
{
    int l1len, l2len;
    int i;
    ElemType ele;
    //获取线性表的长度
    l1len = ListLength(l1);
    l2len = ListLength(l2);

    for(i=1; i<l1len; i++){
        //获取线性表list2第i个数据元素赋给临时元素ele
        GetListElem(l2, i, ele);
        //判断线性表list1中是否存在和临时元素ele相同的元素
        if(!LocateListElem(l1, ele, equal)){
            //向线性表list1末尾插入临时元素ele
            InsertListElem(l1, ++l1len, ele);
        }
    }
}

线性表顺序存储结构的优缺点

4933701-91ef2a2f24795128.png
线性表顺序存储结构的优缺点

线性表的链式存储结构

线性表的顺序存储结构的最大缺点是插入和删除时需要移动大量元素,显然需要耗费时间。考虑下导致这个问题的原因。原因在于相邻像个元素的存储位置也具有邻居关系。

4933701-1009b00d430297f8.png
单线索无分支 - 线性表的链式存储结构

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存为被占用的任意位置。

线性表的顺序结构中,每个数据元素只需要存储数据元素信息即可。在线性表链式结构中,除了要存储数据元素信息外,还要存储它的后续元素的存储地址。

为了表示每个数据元素ai与其直接后续数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储器本身的信息之外,还需存储一个指示其直接后续的信息(即直接后续的存储位置)。把存储数据元素信息的域称为数据域,把存储直接后续位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素的存储映像,称为结点(node)。

n个结点(ai的存储映像)链接成了一个链表,即为线性表的链式存储结构,因为链表的每个结点中只包含一个指针域,所以叫做单链表。

4933701-70a027f7442b7e80.png
单链表

把链表中第一个节点的存储位置叫做头指针,整个链表的存取必须是从头指针开始。之后每个节点就是上一个的后续指针指向的位置。

链表中最后一个直接后继不存在,应该线性链表的最后一个节点指针为空。通常用NULL或^表示。

4933701-aa15fd957f05936b.png
头指针

为了方便地对链表操作,在单链表的地一个节点前附近设一个结点称为头结点。头节点的数据域可以不存储任何信息,也可以存储线性表长度等附加信息,头节点的指针域存储指向第一个节点的指针。

4933701-1267bf812e95e896.png
头结点

头指针和头结点的异同


4933701-d0f5dafab0ae4ca0.png
头指针和头结点异同点

若线性表为空表,则头结点的指针域为空。

4933701-9c4632090ea8814b.png
空链表

线性表链式存储结构代码描述

4933701-724a5898644ba50d.png
存储示意图
4933701-d88b1da8e555c812.png
单链表
4933701-d9c31b51e1a2ceac.png
空链表

C语言中使用结构指针来描述单链表

// 线性表的单链表存储结构
typedef struct Node
{
    //存放数据元素的数据域
    ElemType data;
    //存放后续节点地址的指针域
    struct Node *next;
} Node;

// 定义单链表 LinkList
typedef struct Node *LinkList;

单链表的读取

4933701-d0c1f2666ff84249.png
单链表
// 读取单链表的元素:用e返回l中第i个数据元素的值
Status GetItem(LinkList ll, int i, ElemType *et)
{
    // 声明某节点
    LinkList p;
    // 让p指向链表ll的第一个结点
    p = ll->next;

    // 计数器
    int j;
    j = 1;
    //p不为空或计数器j小于i时循环继续
    while(p && j<i){
        // 让p指向下一个结点
        p = p->next;
        ++j;
    }

    // 第i个元素不存在
    if(!p || j>i){
        return ERROR;
    }

    //获取第i个元素的数据
    *et = p->data;

    return OK;
}

由于单链表的结构没有定义表长,因此不方便使用for来控制循环。其主要核心思想是“工作指针后移”。

单链表的插入

4933701-8ef813f84e44d5f9.png
单链表的插入
4933701-17e1e23f09f976d2.png
单链表的表头和表尾的插入
/*单链表插入:在链表l中第i个位置之前插入新数据元素e,链表l长度加1.*/
Status ListInsert(LinkList *l, int i, ElemType e)
{
    LinkList p, s;
    p = *l;
    //计数器
    int j;
    j = 1;
    //寻找第i个结点
    while(p && j<i)
    {
        p = p->next;
        ++j;
    }
    //第i个元素不存在
    if(!p || j>i){
        return ERROR;
    }
    //生成新结点
    s = (LinkList)malloc(sizeof(Node));
    s->data = e;
    s->next = p->next;//将p的后续结点赋值给s的后续

    p->next = s;//将s赋值给p的后续

    return OK;
}

单链表的删除

4933701-4717b4e738b2a8e7.png
单链表的删除
/*单链表的删除:删除链表l的第i个元素,并用元素e返回其值,同时链表l长度减1*/
Status ListDelete(LinkList *l, int i, ElemType *e)
{
    LinkList p,q;
    //计数器
    int j;
    j = 1;
    //遍历寻找第i个元素
    while(p->next && j<i){
        p = p->next;
        ++j;
    }
    //第i个元素不存在
    if(!(p->next) || j>i){
        return ERROR;
    }

    q = p->next;
    p->next = q->next;//将q的后续赋值给p的后续
    *e = q->data;//将q节点中的数据给*e
    free(q);//让系统回收此节点释放内存

    return OK;
}

效率PK: 对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。

单链表的整表创建

  • 对于顺序存储结构的线性表的整表创建,可以使用数组的初始化来直观理解。
  • 单链表和顺序存储结构不一样,不像顺序存储结构数据那样集中,它的数据可以是分散在内存各个角落中,它的增长也是动态的。
  • 对于每个链表来说,所占用空间的大小和位置是无需预先分配划定的,可以根据系统的情况和实际的需求即时生成。
  • 创建单链表的过程是一个动态生成链表的过程,从空表的初始状态开始,依次建立各元素节点并插入链表。

单链表整表创建的算法思路

  • 声明一节点p和计数器变量i
  • 初始空链表L
  • 让L的头节点的指针指向NULL,即建立一个带头节点的单链表。
  • 循环实现后继节点的赋值和插入

头插法建立单链表

  • 从一个空表开始,生成新节点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束为止。
  • 简单来说就是把新加入的元素放在表头后的第一个位置,先让新节点的next指向头节点之后。然后让表头的next指向新节点。
4933701-9a08d7dba50d9a37.png
单链表整表创建(头插法)
/*单链表整表创建:随机产生n个元素的值,建立带表头结点的单链线性表l(头插法)*/
void CreateListHead(LinkList *l, int n)
{
    //声明结点
    LinkList p;
    //声明计数器变量
    int i;

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

    //初始化空链表,建立带头结点的单链表。
    *l = (LinkList)malloc(sizeof(Node));
    (*l)->next = NULL;//让链表的头结点指向NULL即创建带头结点的单链表

    for(i=0; i<n; i++){
        //生成新结点赋值给p
        p = (LinkList)malloc(sizeof(Node));
        p->data = rand()%100 + 1;//随机生成100以内的数字,赋值给p的数据域
        //将p插入到头结点与前一结点之间
        p->next = (*l)->next;
        (*l)->next = p;//插入到表头
    }
}

尾插法

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

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

    for(i=0; i<pos; i++){
        p = (Node *)malloc(sizeof(Node));
        p->data = rand()%100+i;
        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;//p指向第一个节点
    //没有表尾
    while(p){
        q = p->next;
        free(p);
        p = q;
    }
    (*L)->next = NULL;//头节点指针域为空
    return OK;
}

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

  • 存储分配方式
    • 顺序存储结构用一段连续的存储单元一次存储线性表的数据元素
    • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
  • 时间性能
    • 查找:顺序存储结构为O(1),单链表为O(n)。
    • 插入和删除:顺序存储结构需平均移动表长一半的元素,时间为O(n)。单链表在计算出某位置的指针后,插入和删除时间仅为O(1)。
  • 空间性能
    • 顺序存储结构需要预先分配存储空间,分大了容易造成空间浪费,分小了容易发生溢出。
    • 单链表不需要分配存储空间,只要有就可以分配,元素个数不受限制。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值