数据结构学习笔记(二:线性表)

线性表的定义和基本操作

1. 线性表的定义

  • 线性表是具有相同数据结构类型的n(n≥0)个数据元素的有限序列,其中表长为n,当n=0时线性表是一个空表。

  • 线性表的特点:

    • 表中的元素有限
    • 表中的元素具有有逻辑上的顺序性,表中的元素有其先后次序。
    • 表中的元素都是数据元素,每个元素都是单个元素。
    • 表中元素的数据类型都相同,这意味着每个元素都占有相同大小的存储空间。
    • 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。
  • 注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构。

2.线性表的基本操作

  • InitList(&L):
  • Length(L):
  • LocateElem(L,e):
  • GetElem(L,i):
  • ListInsert(&L,i,e):
  • ListDelete(&L,i,&e):
  • PrintList(L):
  • Empty(L):
  • DestroyList(&L):

线性表的顺序表示

1. 顺序表的定义

  • 顺序表是用一组地址连续的存储单元依次存储线性表中的数据元素,从而是的逻辑相邻的两个元素在物理位置上也相邻。
  • 称 i 为元素 ai 在线性表中的位序
  • 顺序表特点:
    • 逻辑顺序与其物理顺序相同,插入和删除操作需要移动大量元素
    • 随机访问,即通过首地址和元素序号可在时间O(1)内找到指定元素
    • 存储密度高,每个节点只存储数据元素
    • 除第一个元素外,每个元素有且仅有一个直接前驱。
    • 除最后一个元素外,每个元素有且仅有一个直接后继。
  • 注意:线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始的。
  • 静态顺序表存储类型表述:
    #define MaxSize 50                //定义线性表的最大长度
    typedef struct {                  
        ElemType data[MaxSize];       //顺序表的元素
        int length;                   //顺序表的当前长度
    } SqList;                         //顺序表的类型定义
  • 动态顺序表存储类型表述:
    #define InitSize 50               //定义线性表的初始长度
    typedef struct {                  
        ElemType *data;               //指示动态分配数组的指针
        int MaxSize, length;          //数组的最大容量和当前个数
    } SqList;                         //动态分配数组顺序表的类型定义

C的初始动态分配语句:

L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);  

2. 顺序表的基本操作

  • 1.插入
    • 算法思路:
      • 1.判断i的值是否正确
      • 2.判断表长是否超过数组长度
      • 3.从后向前到第i个位置,分别将这些元素都向后移动一位
      • 4.将该元素插入位置i 并修改表长
    • 代码
    • 分析:
      • 最好情况:在表尾插入(即i=n+1),元素后移语句将不执行,时间复杂度为O(1)。
      • 最坏情况:在表头插入(即i=1),元素后移语句将执行n次,时间复杂度为O(n)。
      • 平均情况:假设pi(pi=1/(n+1) )是在第i个位置上插入一个结点的概率,则在长度为n的线性表中插入一个结点时所需移动结点的平均次数为 n/2,时间复杂度为O(n)

  • 2.删除
    • 算法思路:
      • 1.判断i的值是否正确
      • 2.取删除的元素
      • 3.将被删元素后面的所有元素都依次向前移动一位
      • 4.修改表长
    • 代码
    • 分析
      • 最好情况:删除表尾元素(即i=n),无须移动元素,时间复杂度为O(1)。
      • 最坏情况:删除表头元素(即i=1),需要移动除第一个元素外的所有元素,时间复杂度为O(n)。
      • 平均情况:假设pi(pi=1/n)是删除第i个位置上结点的概率,则在长度为n的线性表中删除一个结点时所需移动结点的平均次数为 (n-1)/2,时间复杂度为O(n)

    3.顺序表的优缺点

    • 优点:存取速度快
    • 缺点:插入和删除操作效率低

    线性表的链式存储

    1.单链表的定义

    • 单链表是指通过一组任意的存储单元来存储线性表中的数据元素。
    • 单链表每个结点由数据与和指针域两部分构成。
    • 单链表节点类型的描述:
        typedef struct LNode {        //定义单链表节点类型
            ElemType data;            //数据域
            struct LNode *next;       //指针域
        }LNode, *LinkList;            //LNode是struct LNode的别名,*LinkList是该结构体的指针类型   
    
    • 单链表是非随机存取的存储结构,即不能直接找到表中的某个特定的结点。
    • 通常用头指针来表示一个单链表,头指针为NULL时表示一个空表。
    • 为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点,头结点的数据域可为空,也可记录表长等信息,指针域指向线性表的第一个元素结点。
    • 头指针和头结点的区分:
      • 不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点链表中的第一个结点,结点内通常不存储信息。
    • 引入头结点的优点:
      1. 第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
      2. 无论链表是否为空,其头指针都是指向头结点的非空指针,因此空表和飞控表的处理也就得到了统一。

    2.单链表的操作

    • 访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。
    • 1.头插法建立单链表:
      • 建立新的结点分配内存空间,将新结点插入到当前链表的表头。
      • 读入数据的顺序与生成的链表中的元素的顺序是相反的
      • 每个结点插入的时间为O(1),设单链表长度为n,则总时间复杂度为O(n)
      • 代码
    • 2.尾插法建立单链表:
      • 建立新的结点分配内存空间,将新结点插入到当前链表的表尾
      • 读入数据的顺序和生成的链表中节点的次序一致
      • 时间复杂的与头插法相同
      • 代码
    • 3.按序号查找结点
      • 在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL。
      • 按序号查找操作的时间复杂度为O(n)
      • 代码
    • 4.按值查找结点
      • 从单链表第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL。
      • 按值查找操作的时间复杂度为O(n)
      • 代码
    • 5.插入
      • 插入操作是将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i−1个结点,再在其后插入新结点。
      • 算法思路:
        1.取指向插入位置的前驱结点的指针
        ① p=GetElem(L,i-1);
        2.令新结点s的指针域指向p的后继结点
        ② s->next=p->next;
        3.令结点p的指针域指向新插入的结点s
        ③ p->next=s;
      • 本算法主要的时间开销在于查找第i-1个元素,时间复杂度为O(n),之后插入新节点的时间复杂度仅为O(1)
      • 对某一结点的前插操作:
        1. 找到第i-1个结点,转化为后插操作,时间复杂度为O(n) 2. 将待插入的结点*s插入到*p前面,我们仍然将*s插入到*p的后面,然后交换它们的数据域,时间复杂度为O(1)
    •     //将*s结点前插到*p的主要代码片段
          s->next = p->next;
          p->next = s;
          temp = p->data;
          p->data = s->data;
          s->data = temp;
      
    • 6.删除
      • 删除操作是将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i−1个结点,即被删结点的前驱结点,再将其删除。
      • 算法思路:
        1.取指向删除位置的前驱结点的指针 p=GetElem(L,i-1);
        2.取指向删除位置的指针 q=p->next;
        3.p指向结点的后继指向被删除结点的后继 p->next=q->next
        4.释放删除结点 free(q);
      • 时间复杂度为O(n)
      • 删除给定结点*p:
        删除结点*p的操作可以用删除*p的后继结点操作来实现,实质就是将其后继结点的值赋予自身,然后删除后继结点,也能使得时间复杂度为O(1)
          //主要代码片段
          q = p->next;
          p->data = q->data;
          p->next = q->next;
          free(q);
      

    3.双链表

    • 为克服单链表访问前驱结点只能从头开始遍历的缺点,引入了双链表
    • 双链表有两个指针prior和next,分别指向其前驱结点和后继结点
    • 双链表中结点类型的描述如下:
        typedef struct DNode {
            ElemType data;
            struct DNode *prior, *next;
        } DNode, *DLinklist;
    
      1. 插入:(方法不唯一)
        ① s->next = p->next;
        ② p->next->prior = s;
        ③ s->prior = p;
        ④ p->next = s;
      1. 删除:
        ① p->next=q->next;
        ② q->next->prior=p;
        ③ free(q);

    4.循环链表

    1. 循环单链表
      • 表中的最后一个结点的指针不是NULL,而是改为指向头结点。从而整个链表形成一个环
      • 循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针
      • 循环单链表可以从表中的任意一个节点开始遍历整个链表。
      • 循环单链表的插入、删除操作无需判断是否为表尾。
      • 循环单链表不设头指针而仅设尾指针,从而使得操作效率更高。因为若设尾指针r,则r->next即为头指针,对于表头与表尾进行操作都只需要O(1)的时间复杂度。
    2. 循环双链表
      • 表中最后一个结点的next指针指向头结点,头结点的prior指针指向尾结点
      • 当循环双链表为空表时,其头结点的prior和next都等于头指针L

    5,静态链表

    • 借助数组来表述线性表的链式存储结构,与顺序表一样须要预先分配一块连续的内存空间。
    • 指针域存放数组下标
    • 以next==-1作为结束标志
    • 没有单链表好用,主要应用于不支持指针的高级语言

    顺序表和链表的比较

    1. 存取(读写)方式

    顺序表可以顺序存储,也可以随机存取,链表只能从表头顺序存取元素。例如在第i个位置上执行存或取的操作,顺序表仅需一次访问,而链表则需要从表头开始依次访问i次。

    2. 逻辑结构与物理结构

    采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系时通过指针链接来表示的。

    3. 查找、插入和删除操作

    • 对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n).
    • 对于按序查找,顺序表支持随机访问,时间复杂度为O(1);链表的平均时间复杂度为O(n)。
    • 对于插入和删除操作,顺序表平均需要移动半个表长的元素,而链表只需修改相关结点的指针域即可。
    • 由于链表的每个结点都带有指针域,故存储密度不够大

    4. 空间分配

    • 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入元素则会内存溢出,因此需要预先分配足够大的存储空间。分配过大会造成浪费,分配过小会溢出。
    • 顺序存储再动态存储分配情形下,虽春初空间可以扩充,但需要移动大量元素,导致操作效率降低。而若内存中没有更大块的连续存储空间,则会导致分配失败。
    • 链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。

    总结

    1. 难以估计线性表的长度或存储规模时,不宜采用顺序表

    2. 存储密度要求高,不用链表

    3. 经常做按序查找操作,顺序表优于链表

    4. 经常做插入、删除操作,链表优于顺序表

    5. 顺序表实现较为简单,任何高级语言都有数组类型;链表的操作基于指针,较复杂,有些高级语言无指针类型

    posted @ 2020-04-02 17:32  枣子今天不吃枣  阅读(358)  评论(0编辑  收藏  举报