数据结构与算法(2)- 线性表

407 阅读10分钟

线性表

一对一的逻辑结构

对于非空的线性表和线性结构,特点如下:

  • 存在唯⼀的一个被称作”第⼀个”的数据元素;
  • 存在唯⼀的一个被称作”最后⼀个"的数据元素
  • 除了第一个之外,结构中的每个数据元素均有一个前驱
  • 除了最后一个之外,结构中的每个数据元素都有一个后继

1.线性表的顺序存储

逻辑相邻,物理存储地址相邻

1.0 结构体定义

/* ElemType类型根据实际情况而定,这里假设为int */
typedef int ElementType;
/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Status;

//顺序表结构设计
typedef struct {
    //    指向一块连续的内存空间来存储数据
    ElementType *data;
    //    表的长度
    int length;
} Sqlist;

1.1 顺序表初始化

// 修改链表本身,要传链表的地址 即*L。 只是打印,可以传L
// 函数入参 *L,则其属性 L->data。 函数入参 L (只读),则访问其属性为L.data
Status InitList(Sqlist *L){
    //为顺序表分配一个大小为MAXSIZE 的数组空间
    L->data = malloc(sizeof(ElementType) * MAXSIZE);
    //存储分配失败退出
    if (!L->data) return ERROR;
    //空表长度为0
    L->length = 0;
    return OK;
}

1.2 顺序表的插入

Status ListInsert(Sqlist *L, int i, ElementType e) {
    //i值不合法判断
    if((i<1)||(i>L->length+1)) return ERROR;
    //存储空间已满
    if(L->length == MAXSIZE) return ERROR;
    //插入位置不在表尾,则先移动出空余位置
    if(i<=L->length) {
        for (int j=L->length-1; j>=i-1; j--) {
            L->data[j+1] = L->data[j];
        }
    }
    //将新元素e放到第i个位置
    L->data[i-1] = e;
    //长度+1
    L->length++;
    return OK;
}

1.3 顺序表的取值

Status GetElem(Sqlist L, int i, ElementType *e) {
    //判断i值是否合理, 若不合理,返回ERROR
    if((i<1) || (i>L.length)) return ERROR;
    //data[i-1]单元存储第i个数据元素.
    *e = L.data[i-1];
    return OK;
}

1.4 顺序表删除

/*
 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
 操作结果: 删除L的第i个数据元素,L的长度减1
 */
Status ListDelete(Sqlist *L, int i) {
    //线性表为空
    if(L->length==0) return ERROR;
    //i值不合法判断
    if((i<1)||(i>L->length)) return ERROR;
    
    for(int j = i; j < L->length;j++){
        //被删除元素之后的元素向前移动即可,在顺序存储时可以这样操作。
        //不用清除内存,只需把可访问的区域减少即可(length-1)
        //对于频繁添删元素的场景下,这种方式能够减少频繁申请释放内存的性能损耗
        //注意⚠️在链表存储时还需要释放该元素所占空间,否则野指针。
        L->data[j-1] = L->data[j];
    }
    //表长度-1;
    L->length--;
    
    return OK;
}

1.5 清空顺序表(区别于销毁)

/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(Sqlist *L)
{
    //⚠️ 顺序表创建时,连续内存空间已经分配好了,只需要将length置为0,后面就不会被访问到了。
    // 因为插入、删除、查找等操作都会对参数有一个类似限制条件:if(i>L.length || i<1),这样就保证了在把length设为0后,其他操作就不能访问到原来的那些元素,那么就可以认为length=0的表是空表
    //(实质上原来的元素还是在内存空间里的,如果直接用索引去访问,任然可以把元素读取出来的)
    L->length=0;
    return OK;
}

1.6 销毁顺序表

// 销毁操作,则是把表的整个结构给消灭掉,把原来所占有的内存空间都给释放出来
Status DestroyList(Sqlist *L)
{
    if(L->data) {
        free(L->data);
        L->data = NULL;
        L->length = 0;
    } else {
        return ERROR;
    }
    return OK;

}

1.7 判断顺序表清空

/* 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
Status ListEmpty(Sqlist L)
{
    if(L.length==0)
        return TRUE;
    else
        return FALSE;
}

1.8 顺序表查找元素并返回位置

/* 初始条件:顺序线性表L已存在 */
/* 操作结果:返回L中第1个与e满足关系的数据元素的位序。 */
/* 若这样的数据元素不存在,则返回值为0 */
int LocateElem(Sqlist L, ElementType e) {
    if(L.length == 0) return 0;
    
    int i;
    for (i=0; i<L.length; i++) {
        if(L.data[i]==e) break;
    }
    
    if(i>=L.length) return 0;
    
    return i+1;
}

2.线性表的链式存储

线性表在链式存储时,数据是不连续的,需要额外的指针域来记录结点间的关系。

一系列的存储数据元素的单元通过指针串接起来形成链表,每个单元至少有两个域, 一个用于数据元素的存储区域,一个指向其他单元的地址(位置)的指针区域。 这里具有一个数据域和多个指针域的存储单元通常称为 结点(node)。

2.1 单链表的逻辑状态

  • 首元结点:链表中首个带有值的结点。
  • 头结点:只是一个标志,其后继是首元结点。头结点的数据域可以不存储任何信息,头结点的指针域存储指向首元结点的指针。
  • 首指针:有头结点则指向头结点,没有头结点则指向首元结点。

单链表为什么要增加头结点

  1. 便于首元结点处理
  2. 便于空表和非空表的统⼀处理

设置头结点是为了方便单链表的特殊操作,能有效减少代码量,在插入在表头或者删除首元结点时不用考虑特殊情况(不用考虑首指针的指向问题),删除或插入用户的首元节点的值和删除或插入中间的值用一样的代码,这样就保持了单链表操作的统一性。

2.2 单链表的初始化,即建立一个空链表

// 定义结点
typedef struct Node{
    ElemType data;// 数据域
    struct Node *next;// 指针域
}Node;

// 方便写法,LinkList和Node等价
typedef struct Node * LinkList;

//2.2 初始化单链表线性表(带头结点)
Status InitList(LinkList *L){
    //产生头结点,并使用L指向此头结点
    // 头结点主要作用是辅助增删改查,标志位结点。其后继为首元结点
    *L = (LinkList)malloc(sizeof(Node));
    //存储空间分配失败
    if(*L==NULL) return ERROR;
    //将头结点的指针域置空
    (*L)->next = NULL;
    
    return OK;
}
//2.2 初始化单链表线性表(不带头结点)
Status InitList2(LinkList L){
    L = NULL;
    return OK;
}

2.3 单链表插入

  1. 创建一个新的结点q。
  2. 将此结点的数据域赋值为b,并将它的next指针指向p。
  3. 查找到p的前驱结点pre。
  4. 将pre的next指针指向新创建的结点q。
// 单链表插入
/*
 初始条件:链式线性表L已存在,1≤i≤ListLength(L);
 操作结果:在L中第i个位置之后插入新的数据元素e,L的长度加1;
 */
Status ListInsert(LinkList *L, int i, ElemType e) {
    // 寻找第i个结点
    LinkList p, s;
    p = *L;
    for (int j = 1; j<i; j++) {
        p = p->next;
    }
    //第i个元素不存在
    if (!p) return ERROR;
    //生成新结点s
    s = (LinkList)malloc(sizeof(Node));
    //将e赋值给s的数值域
    s->data = e;
    //将p的后继结点赋值给s的后继
    s->next = p->next;
    //将s赋值给p的后继
    p->next = s;
    
    return OK;
}

2.4 单链表节点删除

  1. 找到被删除节点p的前面一个节点pre。
  2. 将pre节点的next指向p的next节点。
  3. 将被删除节点p的内存释放。
// 单链表删除元素
/*
 初始条件:链式线性表L已存在,1≤i≤ListLength(L)
 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
 */

Status ListDelete(LinkList *L,int i,ElemType *e){
    int j;
    LinkList p,q;
    
    p = (*L)->next;
    j = 1;
    //查找第i-1个结点,p指向该结点
    while (p->next && j<i-1) {
        p = p->next;
        ++j;
    }
    //当i>n 或者 i<1 时,删除位置不合理
    if (!(p->next) || j>i-1) return ERROR;
    //q指向要删除的结点
    q = p->next;
    //将q的后继赋值给p的后继
    p->next = q->next;
    //将q结点中的数据给e
    *e = q->data;
    //让系统回收此结点,释放内存;
    free(q);
    return OK;
}

2.5 清空链表

// 清空链表
/* 初始条件:链式线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(LinkList *L) {
    LinkList p, q;
    // p指向第一个节点
    p = (*L)->next;
    // 若为空表
    if(p == NULL) return OK;
    // 没到表尾
    while (p) {
        q = p->next;
        free(p);
        p = q;
    }
    
    // 头结点指针域为空
    (*L)->next = NULL;
    
    return OK;
}

2.6 单链表头插法

  1. 定义一个链表类型的指针L,L指向的是链表的首地址(头结点),头结点->next 为首元节点
  2. 每次创建新节点,都加到头结点后面。即新节点置为首元节点。

用这种方法建立起来的链表的实际顺序与输入顺序刚好向反,输出时为倒序

//2.6 单链表头插法
/* 随机产生n个元素值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L, int n) {
    // 创建头结点
    *L = (LinkList)malloc(sizeof(Node));
    // 头结点后继为NULL
    (*L)->next = NULL;
    
    LinkList p;
    for (int i=1; i<=n; i++) {
        //生成新结点
        p = (LinkList)malloc(sizeof(Node));
        //i赋值给新结点的data
        p->data = i;
        //p->next = 头结点的L->next
        p->next = (*L)->next;
        //头结点后继指向该节点p
        (*L)->next = p;
    }
}

2.7 单链表尾插法

  1. 定义一个链表类型的指针L指向链表的头结点址
  2. 定义一个q指针,保证q指针始终指向链表的最后一个位置上的节点
  3. 每次新创建的节点p加入到q指针,即q->next = p

这种方法建立起来的链表的实际顺序与输入顺序相同,在对链表顺序有严格要求时,建议使用尾插法

//2.7 单链表尾插法
/* 随机产生n个元素值,建立带表头结点的单链线性表L(后插法)*/
void CreateListTail(LinkList *L, int n)  {
    // 创建头结点
    *L = (LinkList)malloc(sizeof(Node));
    // 头结点后继首元节点指向NULL
    (*L)->next = NULL;
    
    LinkList p, q;
    // 辅助节点 q = 头结点
    q = (*L);
    for (int i = 1; i<=n; i++) {
        // 生成新节点
        p = (LinkList)malloc(sizeof(Node));
        // 新节点赋值
        p->data = i;
        // 新节点后继指向NULL
        p->next = NULL;
        // 辅助节点q 后继指向新节点p
        q->next = p;
        // 辅助节点后移,即q=p
        q = p;
    }
}

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

存储分配方式

  • 顺序存储结构用⼀段连续的存储单元依次存储线性表的数据元素
  • 单链表采⽤链式存储结构,用一组任意的存储单元存放线性表的元素

时间性能

  • 查找
    • 顺序存储 O(1)
    • 单链表O(n)
  • 插入和删除
    • 顺序存储结构需要平均移动⼀个表⻓一半的元素,时间O(n)
    • 单链表查找某位置后的指针后,插入和删除为 O(1)

空间性能

  • 顺序存储结构需要预先分配存储空间,分太大,浪费空间;分⼩了,发⽣上 溢出
  • 单链表不需要分配存储空间,只要有就可以分配, 元素个数也不受限制

源码地址,欢迎Star github数据结构与算法