线性表

94 阅读15分钟

线性表

结点/元素

起始节点/首元

终端节点

应用举例

  1. 一元多项式运算
  2. 稀疏多项式的运算
  3. 图书信息管理系统

线性表类型定义

抽象数据类型线性表的定义 ADT

基本操作

· InitList(&L)	
    // 操作结果:构造一个空的线性表
· DestoryList(&L)
    // 初始条件:线性表L已经存在
    // 操作结果:销毁线性表L
· ClearList(&L)
    // 初始条件:线性表L已经存在
    // 操作结果:将线性表L重置为空表
· ListEmpty(L)
    // 初始条件:线性表L已经存在
    // 操作结果:若线性表L为空表,则返回TRUE,否则返回FALSE
· ListLength(L)
    // 初始条件:线性表L已经存在
    // 操作结果:返回线性表L中的数据元素个数
· GetElem(L,i,&e)
	// 初始条件:线性表L已经存在,1≤i≤ListLength(L)
	// 操作结果:用e返回线性表L中第i个数据元素的值
· LocateElem(L,e,compare())
    // 初始条件:线性表L已经存在,compare()是数据元素判定函数
    // 操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0
· PriorElem(L,cur_e,&pre_e)
    // 初始条件:线性表L已经存在
    // 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败,pre_e无意义
· NextElem(L,cur_e,&next_e)
    // 初始条件:线性表L已经存在
    // 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无意义
· ListInsert(&L,i,e)
    // 初始条件:线性表L已经存在,1≤i≤ListLength(L)+1
    // 操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一
· ListDelete(&L,i,&e)
    // 初始条件:线性表L已经存在,1≤i≤ListLength(L)
    // 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一
· ListTraverse(&L,visited())
    // 初始条件:线性表L已经存在
    // 操作结果:依次对线性表中的每个元素调用visited()
    

线性表的顺序表示和实现

顺序存储定义:将逻辑上相邻的数据元素存储到物理上相邻的存储单元中的存储结构

随机存取:所有数据元素的存储位置都可以由第一个数据元素的存储位置得到

​ LOC(ai) = LOC(a1) + (i-1) * l // l 为每一个元素的长度

顺序表:地址连续、依次存放、随机存取、类型相同 → 数组

但:数组长度不可以动态定义,线性表长度可变(删除),怎么办?
答:再用一个变量表示顺序表的长度属性

顺序表的定义

#define LIST_INIT_SIZE 100
typedef struct{
    ElemType elem[LIST_INIT_SIZE];
    int length;		//当前长度
}SqList;

**举例1:**多项式的顺序存储结构类型定义

#define MAXSIZE 1000 //多项式可能达到的最大长度

typedef struct{	//多项式非零项的定义
    float p;	//系数
    int e;		//指数
}Polynomial;

typedef struct{
    Polynomial *elem;	//存储空间的基地址
    int length;			//多项式中当前项的系数
}SqList;				//多项式的顺序存储结构类型为SqList

**举例2:**图书表的顺序存储结构定义

#define MAXSIZE 10000

typedef struct{		//图书信息定义
    char no[20];	//ISBN
    char name[50];
    float price;
}Book;

typedef struct{
    Book *elem;	//存储空间的基地址
    int length;
}SqList;

数组定义

// 数组静态分配
typedef struct{
    ElemType data[MaxSize];
    int length;
}SqList;


// 数组动态分配
typedef struct{
    ElemType *data;
    int length;
}SqList;

SqList L;
L.data=(ElemType*)malloc(sizeof(ElemType)*MaxSize); 

补充:

  • C语言内存分配函数
· malloc(m)
	开辟 m 字节长度的地址空间,并返回这段空间的首地址
· sizeof(x)
	计算变量 x 的长度
· free(p)
	释放指针 p 所指变量的存储空间,彻底删除一个变量
	
注:需要加载头结点文件 <stdlib.h>
  • 参数传递的两种方式

    1. 传值方式(参数为整型、实型、字符型等)

    2. 传地址:

      a. 参数为指针变量

      b. 参数为引用类型

      c. 参数为数组名

  • 操作算法中用到的预定义常量和类型

    // 函数结果状态码
    #define TRUE 1
    #define FALSE 0
    #define OK 1
    #define ERROR 0
    #define INFEASIBLE -1
    #define OVERFLOW -2
    
    // Status 是函数的类型,其值是函数结果的状态码
    typedef int Status;
    typedef char ElemType;
    

顺序标基本操作的实现

顺序标L的初始化

Status InitList_Sq(SqList &L){	// 构造一个空的顺序表
    L.elem = (ElemType*)malloc(sizeof(ElemType));	// 为顺序表分配空间
    if(!L.elem)
        exit(OVERFLOW);	// 存储分配失败
    L.length=0;	// 空表长度为0
    return OK;
}

销毁线性表L

void DestoryList(SqList &L){
    if(L.elem)
        free(L.elem);
}

清空线性表L

void ClearList(SqList &L){
    L.length=0;
}

求线性表L的长度

int GetLength(SqList L){
    return (L.length);
}

判断线性表L是否为空

int ListEmpty(SqList L){
    if(L.length==0)
        return 1;
    else return 0;
}

顺序表的取值

根据位置i获取相应位置数据元素的内容

int GetElem(SqList L, int i, ElemType &e){
    if(i<1 || i>L.length)
        return ERROR;
    e=L.elem[i-1];	// 第 i-1 的单元存储着第 i 个数据
    return OK;
}

// 随机存取
// 时间复杂度 O(1)

顺序表的查找

// 在线性表L中查找与指定值e相同的数据元素的位置
// 从表的一段开始,逐个进行记录的关键字和给定值的比较;找到就返回该元素的位置序号,否则返回0 

int LocateElem(SqList L, ElemType e){
    // 在线性表 L 中查找值为 e 的数据元素,返回其序号
    for(i=0;i<L.length;i++)
        if(L.elem[i]==e)
            return i+1; // 查找成功返回序号
    return 0;
}


// 或者用 while 语句实现
int LocateElem(SqList L,ElemType e){
    i=0;
    while(i<L.length && L.elem[i]!=e)
        i++;
    if(i<L.length)
        return i+1;
    return 0;
}

// 时间复杂度 O(n)
平均查找长度ASL

Average Search Length——得到结果的数学期望

**Pi:**找到第 i 个记录需要比较的次数

**Ci:**第 i 个记录被查找的概率

顺序表的插入

  1. 判断插入位置 i 是否合法
  2. 判断顺序表的存储空间是否已满,若满则返回ERROR
  3. 将第 n 至 第 i 位的元素依次向后移动一个位置,空出第 i 个位置
  4. 将要插入的新元素 e 放入第 i 个位置
  5. 将表长 +1 ,成功后返回 OK
Status ListInsert_Sq(SqList &L, int i, ElemType e){
    if(i<1 || i>L.length+1)
        return ERROR;	
    if(L.length == MAXSIZE)
        reutrn ERROR;
    for(j=L.length-1; j>=i-1; j--)
        L.elem[j+1] = L.elem[j];
    L.elem[i-1] = e;
    L.length++;	// 表长 +1
    return OK;
}

// 时间复杂度 O(n)

顺序表的删除

  1. 判断删除位置 i 是否合法(1≤ i ≤ n)
  2. 将欲删除的元素保留在 e 中
  3. 将第 i+1 至第 n 位的元素依次向前移动一个位置
  4. 表长 -1,删除成功后返回 OK
Status ListDelete_Sq(SqList &L, int i){
    if((i<1) || (i>L.length))
        return ERROR;
    for(j=i; j<=L.length-1; j++)
        L.elem[j-1] = L.elem[j];
    L.length--;
    return OK;
}

// 时间复杂度 O(n)

总结

顺序表的特点

顺序表——线性表的顺序存储结构

优点
  • 随机存取,O(1)
  • 存储密度大(节点本身所占存储量/节点结构所占存储量)
缺点
  • 插入、删除某一元素时需要移动大量元素
  • 浪费存储空间
  • 属于静态存储形式,数据元素的个数不能自由扩充

线性表的链式表示和实现

链式存储术语
	1.结点:顺序域、指针域(存储后继节点的存储位置)组成,时数据元素的存储映像;
	2.链表:n个结点由指针链组成一个链表;
	3.单链表(一个指针域)、双链表(两个指针域,前驱与后继)、循环链表(首尾相接);
	4.头结点指针:指向链表第一个结点的指针;
	5.首元结点:存储第一个数据元素的结点;
	6.头结点节点:在链表首元结点之前附设的一个结点;

单链表分带头结点结点不带头结点节点两种

单链表由头指针唯一确定,因此单链表可以用头指针的名字来命名

问:单链表如何表示空表?

· 无头结点结点时,头结点指针为空则为空表;
· 有头结点节点时,头结点节点的指针域为空则为空表;

问:在链表中设置头结点节点的好处?

· 便于首元节点的处理:
	首元结点的地址保存在头结点结点的指针域中,所以在链表的第一个位置上操作和其它位置一致,无需进行特殊处理;
· 便于空表和非空表的统一处理
	无论链表是否为空,头结点指针都是指向头结点结点的非空指针,因此空表和非空表的处理可以统一;
	
问:头结点的数据域内存储什么内容?
· 头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表的长度值;

链表特点

  1. 结点在存储器中的位置是任意的——逻辑上相邻的数据元素在物理上不一定相邻;
  2. 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点(顺序存取);

带头结点的单链表

数据域指针域
datanext

存储结构

typedef struct LNode{	// 声明结点的类型和指向结点的指针类型
    ElemType data;		// 数据域
    struct LNode *next; // 指针域
}LNode,*LinkList;	// LinkList 为指向结构体 LNode 的指针类型

LNode a; 定义一个结点 a

LNode *p; 定义一个指向这种类型的结点的指针

LinkList p; = LNode *p;

一般用法:

// 定义链表
LinkList L;

// 定义结点指针p
LNode *p;

单链表定义举例:

// 存储学生学号、姓名、成绩
typedef struct{
    char num[8];
    char name[8];
    int score;
}ElemType;

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

单链表的基本操作

单链表初始化

  1. 生成新节点作为头结点,用头指针L指向头结点;
  2. 将头结点的指针域置空;
Status InitList_L(LinkList &L){
    L = (LinkList)malloc(sizeof(LNode));
    // c++语法
    // L = new LNode;
    L->next = NULL;
    return OK;
}

判断链表是否为空

**空链表:**链表中无元素,但头指针和头结点仍然存在

int ListEmpty(LinkList L){
    // 空返回1,否则返回0
    if(L->next)
        return 0;
    else
        return 1;
}

单链表销毁

从头结点开始依次释放所有结点

Status DestoryList_L(LinkList &L){
    LNode *p; // 或者 LinkList p;
    while(L){
        p = L;
        L = L->next;
        free(p);	// 需要加载头文件 <stdlib.h>
    }
}

清空单链表

依次释放所有结点,并将头结点指针域置空

Status ClearList(LinkList &L){
    LNode *p,*q;
    p=L->next;
    while(p){
        q = p->next;
        free(p);
        p = q;
    }
    L->next = NULL;
    return OK;
}

求链表的表长

从首元结点开始,依次计数所有结点

int ListLength_L(LinkList L){
    LinkList p;
    p = L->next;
    i = 0;
    while(p){ // 或者 for 循环
        i++;
        p = p->next;
    }
    return i;
}

取值

取单链表第 i 个元素的内容

注意判断:

  • 要找的元素位置是否超过单链表的长度
  • i 是否小于1

算法思想

  1. 从第 1 个结点(L->next)顺链扫描,用指针 p 指向当前扫描到的结点,p 初值为 p = L->next;
  2. j 作计数器,累计当前扫描过的结点数,j 初值为 1;
  3. p 指向扫描到的下一结点时,计数器 j 加 1;
  4. 当 j == i 时,表示找到了第 i 个结点;
Status GetElem_L(LinkList L, int i, ElemType &e){
    p = L->next; // p 指向了首元结点
    j = 1;
    while(p && j<i){
        p = p->next;
        j++;
    }
    if(!p || j>i)	// 在这里判断初始的 i 值是否合理
        return ERROR;
    e = p->data;
    return OK;
}

查找

  • 按值查找返回地址
  • 按值查找返回序号
按值查找返回地址
  1. 从第一个结点开始,依次和 e 比较;
  2. 如果找到值与 e 相等的数据元素,则返回其在链表中的地址(返回一个指针);
  3. 如果遍历完链表也没有找到,则返回 0 或者 NULL;
LNode *LocateElem_L(LinkList L, ElemType e){
    // 这里是 *LocateElem_L 
    // 返回一个指针 p
    p = L->next;
    while(p && p->data != e)
        p = p->next;
    return p;
}
// 时间复杂度 O(n)
按值查找返回序号
int LocateElem_L(LinkList L, ElemType e){
    p = L->next;
    j = 1;
    while(p && p->data != e){
        p = p->next;
        j++;
    }
    if(p)
        return j;
    else
        return 0;
}

插入

在第 i 个结点前插入值为 e 的新结点

Status ListInsert_L(LinkList &L, int i, ElemType e){
    p = L;
    j = 0;
    while(p && j<i-1){
        p = p->next;
        ++j;
    }
    if(!p || j>i-1)
        return ERROR;
    q = (LinkList)malloc(sizeof(LNode));
    q->data = e;
    q->next = p->next;
    p->next = q;
    return OK;
}

删除

删除第 i 个结点

Status ListDelete_L(LinkList &L, int i, ElemType &e){
    p = L;
    j = 0;
    while(p->next && j<i-1){ // 删除时,考虑 p->next 是否存在
        p = p->next;
        ++j;
    }
    if(!(p->next) || j>i-1)
        return ERROR;
    q = p->next;
    p->next = q->next;
    e = q->data;
    free(q);
    return OK;
}

:链表插入和删除的时间复杂度为 O(1),但如果不知道插入或删除元素的位置,要从头查找,则时间复杂度为 O(n)

建立单链表

头插法
  1. 从一个空表开始,重复读入数据;
  2. 生成新结点,将读入的数据放到新结点的数据域中;
  3. 从最后一个结点开始,依次将各个结点插入到链表的前端;
// 倒位序输入 n 个元素的值
void CreateList_H(LinkList &L, int n){
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;	// 先建立一个带头结点的单链表
    for(i=n; i>0; --i){
        p = (LinkList)malloc(sizeof(LNode));
        // 输入元素值, C++语法为 cin>>p->next>>data;
        scanf(&p->data);
        p->next = L->next;
        L->next = p;
    }
}
// 时间复杂度为 O(n)
尾插法
  1. 从一个空表 L 开始,将新结点逐个插入到链表的尾部,尾指针 r 指向链表的尾结点;
  2. 初始时,r 与 L 均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r 指向新结点;
// 正为序输入 n 个元素的值
void CreateList_R(linkList &L, int n){
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
    r = L;	// 尾指针 r 指向头结点
    for(i=0; i<n; ++i){
        p = (LinkList)malloc(sizeof(LNode));
        scanf(&p->data);
        p->next = NULL;
        r->next = p;
        r = p;
    }
}
// 时间复杂度为O(n)

循环链表

循环链表中最后一个结点的指针域指向头结点,整个链表形成一个环

**优点:**从表中任意一个结点出发,均可以找到表中其它结点

注意:
	由于循环链表中没有 NULL 指针,所以涉及到遍历操作时,它的终止条件就不再像非循环链表那样判断 p 或者 p->next 是否为空,而是判断它们是否等于头指针

单循环链表循环条件:p != L; || p->next != L;

时间复杂度:

  • 找 a1 为 O(1);
  • 找 an 为 O(n);

由于表的操作常常是在表的首位位置上经行的,所以可以选择用尾指针来表示单循环链表,此时:

  • a1 的存储位置为 R->next->next;
  • an 的存储位置为 R

​ 它们的时间复杂度均为 O(1)

合并

合并带尾指针的循环链表

LinkList Connect(LinkList Ta, LinkList Tb){
    // 设 Ta、Tb 均为非空的单循环链表
    p = Ta->next;	// p 存储表头结点
    Ta->next = Tb->next->next;	// Tb 表头连接 Ta 表尾
    free(Tb->next);	// 释放 Tb 表头结点
    Tb->next = p;	// 修改指针
    return Tb;
}
// 时间复杂度 O(1)

双向链表

结构定义

typedef struct DuLNode{
	ElemType data;
    struct DuLNode *prior, *next;
}DuLNode, *DuLinkList;
*priordata*next

双循环链表

  • 让头结点的前驱指针指向链表的最后一个结点
  • 让最后一个结点的后继指针指向头结点

双向链表结构具有对称性

p->prior->next == p == p->next->prior

插入

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

删除

// 删除带头结点的双向循环链表 L 的第 i 个元素,并用 e 返回
void ListDelete_DuL(DuLink &L, int i, ElemType &e){
    if(!(p = GetElemP_DuL(L,i)))
        return ERROR;
    e = p->data;
    p->prior->next = p->next;
    p->next->prior = p->prior;
    free(p);
    return OK;
}
// 时间复杂度在知道要删除的元素是第几个时是 O(1),否则是 O(n)

时间效率比较

查找表头结点(首元节点)查找表尾结点查找结点 *p 的前驱结点
带头结点的单链表 LL->next,O(1)从 L->next 依次向后遍历,O(n)p->next 无法找到前驱结点
带头结点仅设头指针 L 的循环单链表L->next,O(1)从 L->next 依次向后遍历,O(n)通过 p->next 可以找到其前驱,O(n)
带头结点仅设尾指针 R 的循环单链表R->next,O(1)R,O(1)通过 p->next 可以找到其前驱,O(n)
带头结点的双向循环链表 LL->next,O(1)L->prior,O(1)p->prior,O(1)

顺序表和链表的比较

链式存储优点:

  • 结点空间可以动态申请和释放
  • 数据元素的逻辑次序靠结点的指针来指示,插入和删除元素时不需要移动大量数据元素

链式存储缺点:

  • 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重比较大

    存储密度=节点数据本身占用的空间节点占用的总空间存储密度 = \frac{节点数据本身占用的空间}{节点占用的总空间}
  • 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度

线性表的应用

线性表的合并

利用两个线性表 La 和 Lb 分别表示两个集合 A 和 B,现合并成一个新的集合

A=ABA = A \cup B

若两个线性表中出现重复的元素,合并的新的线性表中该元素只出现一次

算法步骤:
	1. 在 La 中查找该元素;
	2. 如果找不到,则将其插入到 La 的最后。
void union(List &La, List Lb){
    La_len = ListLength(La);
    Lb_len = ListLength(Lb);
    for(i=1; i<Lb_len; i++){
        GetElem(Lb, i, e);
        if(!LocateElem(La, e))
            ListInsert(&La, ++La_len, e);
    }
}
// 时间复杂度为 O( ListLength(La)*ListLength(Lb) )

有序表的合并

线性表 La 和 Lb 中的数据元素按值非递减有序排列,现要求将 La 和 Lb 归并成一个新的线性表 Lc ,且 Lc 中的数据元素仍按值非递减有序排列

算法步骤:
	1. 创建一个空表 Lc;
	2. 依次从 La 或 Lb 中“摘取”元素较小的结点插入到 Lc 表的最后,直至其中一个表变为空为止;
	3. 将 La 或 Lb 中一个表剩余结点插入在 Lc 表的最后。

用顺序表实现

void MergeList_Sq(SqList La, SqList Lb, SqList &Lc){
    // 指针 pa、pb 的初值分别指向两个表的第一个元素
    pa = La.Elem;
    pb = Lb.Elem;
    // 新的表长度为 Lc
    Lc.length = La.length + Lb.length;
    // 为合并的新表分配一个数组空间
    Lc.Elem =(LinkList)malloc(sizeof(LNode)*Lc.length);
    // 指针 pc 指向新表的第一个元素
    pc = Lc.Elem;
    // 指针 pa_last 指向 La 表的最后一个元素
    pa_last = La.Elem + La.length - 1;
    // 指针 pb_last 指向 Lb 表的最后一个元素
    pb_last = Lb.Elem + Lb.length - 1;
    // 两个表都非空,依次摘取量表中值较小的结点
    while(pa<=pa_last && pb<=pb_last){
        if(*pa <= *pb)
            *pc++ = *pa++;
        else
            *pc++ = *pb++;
    }
    // 将剩余元素加入 Lc 表的表尾
    while(pa <= pa_last)
        *pc++ = *pa++;
    while(pb <= pb_last)
        *pc++ = *pb++;
}

// 时间复杂度 O(ListLength(La)+ListLength(Lb))
// 空间复杂度 O(ListLength(La)+ListLength(Lb)) 

用链表实现

void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){
    pa = La->next;
    pb = Lb->next;
    Pc = Lc = La; 	// 用 La 的头结点作为 Lc 的头结点
    while(pa && pb){
        if(pa->data <= pb-> data){
            pc->next = pa;
            pc = pa;
            pa = pa->next;
        }
        else{
            pc->next = pb;
            pc = pb;
            pb = pb->next;
        }
    }
    pc->next = pa?pa:pb;
    free(Lb);
}

// 时间复杂度为 O(ListLength(La)+ListLength(Lb))
// 空间复杂度为 O(1)