【数据结构】线性表的基础知识


线性表按照存储结构分为 顺序表链表

顺序表的特点:逻辑上相连的数据元素,其物理次序也是相邻的。

链表的特点:用任意的存储单元存储线性表的数据元素(可以连续,也可以不连续)

1. 顺序表

存储结构的分类:

  1. 随机存取(任意存取):知道起始位置就可以通过下标直接访问到元素的位置,与存储位置无关。
  2. 顺序存取:不能通过下标访问,在存取第N个数据时,必须先访问前(N-1)个数据 。

顺序表属于随机存取。

1.1 存储结构表示

顺序表存储结构
顺序表的数据元素是按照数组一个接一个紧密排列的。

对顺序表进行操作时,需要一个“操作台”,使用这个操作台能够访问到顺序表中的任意一个元素,同时也要保存该顺序表的大小,方便用户了解顺序表的信息,确保不会越界操作。

上图的顺序表结构类型就是这个操作台。

typedef struct
{
    ElemType *elem; //顺序表的基地址
    int length;		//顺序表当前总长度
}SqList;//顺序表结构类型

其中ElemType代表顺序表存放的数据类型。


用顺序表的方式来存储一下图书数据:

ISBN书名定价
9787302257646程序设计基础25
9787302219972单片机技术与应用32
9787302203513编译原理46

每个图书的信息有ISBN、书名和定价3个元素组成,因此定义图书信息的结构体来包含这3个元素:

typedef struct
{
    char no[20];	//ISBN
    char name[50];	//书名
    float price;	//定价
}Book;

这个线性表的数据元素是Book,因此可以确定对应“操作台”里面的数据元素:

typedef struct
{
    Book *elem; //顺序表的基地址
    int length;		//顺序表当前总长度
}SqList;

将上面顺序表结构类型的ElemType换成Book即可。

1.2 初始化

顺序表的初始化操作就是构造1个空的顺序表。

算法步骤:

  1. 为顺序表动态分配1个预定义大小的数组空间,再使“操作台”的elem指向这块空间的基地址
  2. 将“操作台”的length设置为0,因为空表还没有元素
#define MAXSIZE 100 //数据可能到达的长度
Status InitList(SqList *L)
{
    L->elem = (ElemType*)malloc(sizeof(ElemType)*MAXSIZE); //为顺序表分配指定大小的空间
    if (L->elem == NULL) exit(OVERFLOW); //内存分配失败就退出
    L->length = 0; //空表长度为0
    return OK;
}

其中,ElemType是数据元素的类型,SqList是操作台的类型,Status是函数的返回值,代表函数的结果状态。

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;

1.3 取值

获取顺序表第i个数据元素的值。

时间复杂度O(1)

算法步骤:

  1. 判断序号i是否合理(1≤i≤length),不合理就返回ERROR
  2. 合理就取出第i个数据元素的值,放到参数e
Status GetElem(const SqList *L, int i, ElemType *e)
{
    if ( (i<1) || (i>(L->length)) ) return ERROR; //判断i是否合理,不合理返回ERROR
    *e = (L->elem)[i-1]; //将elem[i-1]单元的数据赋给e   L->elem代表数组首元素地址
    return OK;
}

1.4 查找

查找指定元素e。若查找成功,则返回该元素在顺序表的位置序号;若查找失败,则返回0。

时间复杂度O(n)

算法步骤:

  1. 从第1个元素起,依次和指定元素e比较,若查找成功,返回元素的序号+1
  2. 整个顺序表找完都没有找到指定元素e,则返回0
Status LocateElem(const SqList *L, ElemType e)
{
    int i = 0;
    for (i=0; i<L->length; i++)
    {
        if ((L->elem)[i] == e)
            return i+1; //查找成功,返回序号+1
    }
    return 0; //找遍整个顺序表都没有找到,查找失败,返回0
}

1.5 插入

在顺序表的第i个位置插入新的数据元素e,同时顺序表的长度要+1。

时间复杂度O(n)
顺序表插入

算法步骤:

  1. 判断插入的位置i是否合法(1≤i≤length+1),不合理就返回ERROR
  2. 判断顺序表的存储空间是否已满,若满的话返回ERROR
  3. 将原先的第n个至第i个元素依次后移1个位置(i=n+1时无需移动)
  4. 在第i个位置插入新的数据元素e
  5. 顺序表的数据长度+1
Status ListInsert(SqList *L, int i, ElemType e)
{
    int j = 0;
    if ( (i<1) || (i>(L->length)+1) ) return ERROR; //1. 判断i是否合理,不合理返回ERROR
    if (L->length == MAXSIZE) return ERROR; //2. 判断顺序表的存储空间是否已满
    for (j=(L->length)-1; j>=i-1; j--)
    {
       (L->elem)[j+1] = (L->elem)[j]; //3. 插入位置及之后的元素后移
    }
    (L->elem)[i-1] = e; //4. 将新的数据元素e放到第i个位置
    L->length++; //5. 表长加1
    return OK;
}

1.6 删除

将顺序表的第i个位置的数据元素删除,同时顺序表的长度要-1。

时间复杂度O(n)

顺序表删除

算法步骤:

  1. 判断插入的位置i是否合法(1≤i≤length),不合理就返回ERROR
  2. 将原先的第i+1个至第n个元素依次前移1个位置(i=n时无需移动)
  3. 顺序表的数据长度-1
Status ListDelete(SqList *L, int i)
{
    int j = 0;
    if ( (i<1) || (i>(L->length)) ) return ERROR; //判断i是否合理,不合理返回ERROR
    for (j=i; j<(L->length); j++)
    {
       (L->elem)[j-1] = (L->elem)[j]; //被删除元素之后的元素前移
    }
    L->length--; //表长减1
    return OK;
}

顺序表缺点:在做插入或删除的操作时,需移动大量元素。

1.7 案例

给出一个简单的案例,将数据元素类型ElemType设置为int,所以这是一个数据元素为int的顺序表。

包含上述5种操作,便于理解、应用。

下述代码运行环境:VS2019,其他环境可移植。

#include <stdio.h>

#define MAXSIZE 100 //数据可能到达的长度

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;

typedef struct
{
    int* elem;      //顺序表的基地址
    int length;     //顺序表当前总长度
}SqList;//顺序表结构类型

// 顺序表的初始化
// 函数参数:L -> 操作台地址
// 返回值:1代表运行正常
Status InitList(SqList* L)
{
    L->elem = (int*)malloc(sizeof(int) * MAXSIZE); //为顺序表分配指定大小的空间
    if (L->elem == NULL) exit(OVERFLOW); //内存分配失败就退出
    L->length = 0; //空表长度为0
    return OK;
}

// 获取顺序表第i个数据元素的值
// 函数参数:L -> 操作台地址; i -> 获取第i个元素; e -> 存放元素的地址
// 返回值:1代表运行正常; 0代表运行有问题
Status GetElem(const SqList* L, int i, int* e)
{
    if ((i < 1) || (i > (L->length))) return ERROR; //判断i是否合理,不合理返回ERROR
    *e = (L->elem)[i - 1]; //将elem[i-1]单元的数据赋给e   L->elem代表数组首元素地址
    return OK;
}

// 查找指定元素e
// 函数参数:L -> 操作台地址; e -> 查找的指定元素
// 返回值:0代表运行有问题; 其他表示指定元素的编号
Status LocateElem(const SqList* L, int e)
{
    int i = 0;
    for (i = 0; i < L->length; i++)
    {
        if ((L->elem)[i] == e)
            return i + 1; //查找成功,返回序号+1
    }
    return 0; //找遍整个顺序表都没有找到,查找失败,返回0
}

// 在顺序表的第i个位置插入新的数据元素e
// 函数参数:L -> 操作台地址; i -> 第i个位置; e -> 插入的新元素
// 返回值:1代表运行正常; 0代表运行有问题
Status ListInsert(SqList* L, int i, int e)
{
    int j = 0;
    if ((i < 1) || (i > (L->length) + 1)) return ERROR; //判断i是否合理,不合理返回ERROR
    if (L->length == MAXSIZE) return ERROR; //判断顺序表的存储空间是否已满
    for (j = (L->length) - 1; j >= i - 1; j--)
    {
        (L->elem)[j + 1] = (L->elem)[j]; //插入位置及之后的元素后移
    }
    (L->elem)[i - 1] = e; //将新的数据元素e放到第i个位置
    L->length++; //表长加1
    return OK;
}

// 将顺序表的第i个位置的数据元素删除
// 函数参数:L -> 操作台地址; i -> 第i个位置
// 返回值:1代表运行正常; 0代表运行有问题
Status ListDelete(SqList* L, int i)
{
    int j = 0;
    if ((i < 1) || (i > (L->length))) return ERROR; //判断i是否合理,不合理返回ERROR
    for (j = i; j < (L->length); j++)
    {
        (L->elem)[j - 1] = (L->elem)[j]; //被删除元素之后的元素前移
    }
    L->length--; //表长减1
    return OK;
}

int main()
{
    SqList simpleData; //创建顺序表的操作台
    int getData = 0;   //保存第i个数据元素的值
    int checkData = 1; //存储查找到元素的编号

    //初始化操作台
    InitList(&simpleData); 


    ListInsert(&simpleData, 1, 4); //顺序表插入数据,第1个位置插入4
    ListInsert(&simpleData, 2, 7); //顺序表插入数据,第2个位置插入7
    ListInsert(&simpleData, 3, 1); //顺序表插入数据,第3个位置插入1

    GetElem(&simpleData, 1, &getData);
    printf("%d\n", getData); //打印取到的元素
    GetElem(&simpleData, 2, &getData);
    printf("%d\n", getData); //打印取到的元素
    GetElem(&simpleData, 3, &getData);
    printf("%d\n\n", getData); //打印取到的元素


    if (checkData = LocateElem(&simpleData, 4)) //在顺序表中查找元素,并记录其位置信息
    {
        GetElem(&simpleData, checkData, &getData); //获取对应位置信息的元素
        printf("GetElem : %d\n\n", getData); //打印取到的元素
    }
    
    ListDelete(&simpleData, 2); //删除第2个元素
    GetElem(&simpleData, 2, &getData);
    printf("Change Elem : %d\n\n", getData); //打印取到的元素; 应该打印为原先第3个元素的值
    
	//获取原先第3个元素的数值,因为删掉了,所以函数运行出错
    if (GetElem(&simpleData, 3, &getData)) 
    {
        printf("GetElem Success!\n");
    }
    else
    {
        printf("Delete Success! GetElem Error!\n");
    }

    return 0;
}

2. 链表

存储结构的分类:

  1. 随机存取(任意存取):知道起始位置就可以通过下标直接访问到元素的位置,与存储位置无关。
  2. 顺序存取:不能通过下标访问,在存取第N个数据时,必须先访问前(N-1)个数据 。

链表属于顺序存取。

2.1 存储结构表示

对于链表而言,不仅要存储数据本身的信息,还需要存储一个指示其直接后继的信息,这就是结点的组成。

结点: 数据域+指针域
链表存储结构

其中上图的L为头指针,指向链表的首元素。

链表的结点:

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

其中ElemType代表顺序表存放的数据类型。

通常习惯用Linklist定义单链表的头指针(上图中的L)。

LNode *定义指向单链表中任意结点的指针变量

为了方便处理,在单链表的第一个结点前会附设一个结点,称之为头结点,下图为添加头结点的链表:

链表存储结构_头结点

明确上图中的概念:

  1. 头指针是指向链表中第一个结点的指针

    若链表有头结点,则头指针就指向链表的头结点;

    若链表没有头结点,则头指针就指向链表的首元结点

  2. 头结点是在首元结点之前附设的一个结点,其指针域指向首元结点

    头结点的数据域可以不存储任何信息,也可以存储数据元素类型相同的其他附加信息——链表的长度等。

  3. 首元结点是链表中存储第一个数据元素 a 0 a_0 a0的结点

增加头结点的好处:

  1. 首元结点的地址保存在头结点的指针域当中,此时对首元结点的操作和其他结点操作相同,无需特殊处理。

  2. 便于空表和非空表的统一处理。

    1. 无头结点时,头指针指向链表的首元结点;空链表时L == NULL,并不指向首元结点(因为没有)

    2. 增加头结点后,头指针都是指向头结点的非空指针。空表的判断条件:L->next == NULL

非空链表

上图是非空链表。

空链表

上图是空链表。

2.2 初始化

下图就是链表初始化的顺序及结果:
链表初始化顺序

算法步骤:

  1. 生成头结点,并用头指针指向头结点
  2. 头结点的指针域置空
Status InitList(LinkList* L)
{
    *L = (LinkList)malloc(sizeof(LNode)); //生成头结点,并用头指针指向头结点
    (*L)->next = NULL; //头结点的指针域置空
    return OK;
}

2.3 取值

获取顺序表第i个数据元素的值。

时间复杂度O(n)

算法步骤:

  1. 用指针p指向首元结点,用j做计数器,初值为1
  2. 从首元结点开始依次顺着链域next向下寻找。只要p不为NULL(链表结束),并且还没有到第i个结点,p就指向下一个结点,同时计数器j的值加1
  3. 退出查找后,如果指针pNULL(从头找到尾都没有找到),或者计数器j大于ii≤0),表示取值失败;否则取值成功,用e来保存取到的数值

非空链表

Status GetElem(const LinkList* L, int i, ElemType *e)
{
	LinkList p = (*L)->next; //p指向首元结点
    int j = 1; //设置计数器
    while ( (p!=NULL) && (j<i) ) //循环结束标志:p为空指针,i<1(输入非法)
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    if (p==NULL || j>i) return ERROR; //从头找到尾都没有找到,或者输入的i非法
    *e = p->data; //存储找到的数据
    return OK;
}

2.4 查找

查找指定元素e。若查找成功,则返回该结点的地址;若查找失败,则返回NULL

时间复杂度O(n)

算法步骤:

  1. 用指针p指向首元结点
  2. 从首元结点开始依次顺着链域next向下寻找。只要p不为NULL(链表结束),并且p指向结点的数据域不等于给定值ep就指向下一个结点
  3. 返回p。若查找成功,p就是该结点的地址;若查找失败,p的值就是NULL
LNode* LocateElem(const LinkList* L, ElemType e)
{
	LinkList p = (*L)->next; //p指向首元结点
    while ( (p!=NULL) && (p->data!=e) ) //循环结束标志:p为空指针; 找到了e
    {
        p = p->next; //p指向下一个结点
    }
    return p;
}

2.5 插入

在链表的第i个位置插入新的结点,其数据域为e,即插入到结点 a i − 1 a_{i-1} ai1与结点 a i a_i ai之间。下图为的过程,共分为5步。

时间复杂度O(n)
链表的插入

算法步骤:

  1. 查找到结点 a i − 1 a_{i-1} ai1,并用指针p指向该结点
  2. 生成一个新结点 s s s
  3. 将新结点 s s s的数据域设置为x
  4. 将新结点 s s s的指针域指向结点 a i a_i ai
  5. a i − 1 a_{i-1} ai1的指针域指向新结点 s s s
Status ListInsert(const LinkList* L, int i ,ElemType e)
{
	LinkList p = *L; //p指向头结点
    int j = 0;
    while ( (p!=NULL) && (j<i-1) ) //1. 查找第i-1个结点,p指向该结点
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    if (p==NULL || j>i-1) return ERROR; //i过大,或者输入的i过小
    
    LinkList s = (LinkList)malloc(sizeof(LNode)); //2. 生成新结点s
    s->data = e;		//3. 设置新结点的数据域为e
    s->next = p->next;	//4. 设置新结点的指针域指向ai
    p->next = s;		//5. 设置ai-1的指针域指向新结点s
    
    return OK;
}

2.6 删除

将链表的第i个位置的结点删除。

时间复杂度O(n)

链表的删除

算法步骤:

  1. 查找到结点 a i − 1 a_{i-1} ai1,并用指针p指向该结点
  2. 临时保存结点 a i a_i ai的地址在q中,以备释放
  3. p的指针域指向结点 a i + 1 a_{i+1} ai+1
  4. 释放掉结点 a i a_i ai
Status ListDelete(const LinkList* L, int i)
{
	LinkList p = *L; //p指向头结点
    LinkList q = NULL;
    int j = 0;
    while ( (p->next!=NULL) && (j<i-1) ) //1. 查找第i-1个结点,p指向该结点
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    if (p->next!=NULL || j>i-1) return ERROR; //i过大,或者输入的i过小
   
    q = p->next;		//2. 临时保存结点ai的地址在q中,以备释放
    p->next = q->next;  //3. 将p的指针域指向结点ai+1
    free(q);			//4. 释放掉结点ai
    
    return OK;
}

注:链表函数里面前3行的初始化和判断条件很重要!

  1. p的初始值: p=*L还是p = (*L)->next
  2. j的初始值:j = 0还是j = 1
  3. 判断条件:p != NULL还是p->next != NULL

这3者要配合着来看待,特别是针对首元结点的操作

LinkList p = *L; //p指向头结点
int j = 0;
while ((p != NULL) && (j < i - 1)) //查找第i-1个结点,p指向该结点
{
    p = p->next; //p指向下一个结点
    ++j;
}

2.7 案例

给出一个简单的案例,将数据元素类型ElemType设置为int,所以这是一个数据元素为int的链表。

包含上述5种操作,便于理解、应用。

下述代码运行环境:VS2019,其他环境可移植。

#include <stdio.h>

#define MAXSIZE 100 //数据可能到达的长度

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;

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

// 链表的初始化
// 函数参数:L -> 链表头指针地址
// 返回值:1代表运行正常
Status InitList(LinkList* L)
{
    *L = (LinkList)malloc(sizeof(LNode)); //生成头结点,并用头指针指向头结点
    (*L)->next = NULL; //头结点的指针域置空
    return OK;
}

// 获取链表第i个数据元素的值
// 函数参数:L -> 头指针地址; i -> 获取第i个元素; e -> 存放元素的地址
// 返回值:1代表运行正常; 0代表运行有问题
Status GetElem(const LinkList* L, int i, int* e)
{
    LinkList p = (*L)->next; //p指向首元结点
    int j = 1; //设置计数器
    while ((p != NULL) && (j < i)) //循环结束标志:p为空指针,i<1(输入非法)
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    if (p == NULL || j > i) return ERROR; //从头找到尾都没有找到,或者输入的i非法
    *e = p->data; //存储找到的数据
    return OK;
}

// 查找指定元素e
// 函数参数:L -> 头指针地址; e -> 查找的指定元素
// 返回值:该元素所在结点的地址
LNode* LocateElem(const LinkList* L, int e)
{
    LinkList p = (*L)->next; //p指向首元结点
    while ((p != NULL) && (p->data != e)) //循环结束标志:p为空指针; 找到了e
    {
        p = p->next; //p指向下一个结点
    }
    return p;
}

// 在链表的第i个位置插入新的数据元素e
// 函数参数:L -> 头指针地址; i -> 第i个位置; e -> 插入的新元素
// 返回值:1代表运行正常; 0代表运行有问题
Status ListInsert(const LinkList* L, int i, int e)
{
    LinkList p = *L; //p指向头结点
    int j = 0;
    while ((p != NULL) && (j < i - 1)) //1. 查找第i-1个结点,p指向该结点
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    if (p == NULL || j > i - 1) return ERROR; //i过大,或者输入的i过小

    LinkList s = (LinkList)malloc(sizeof(LNode)); //2. 生成新结点s
    s->data = e;		//3. 设置新结点的数据域为e
    s->next = p->next;	//4. 设置新结点的指针域指向ai
    p->next = s;		//5. 设置ai-1的指针域指向新结点s

    return OK;
}

// 将链表的第i个位置的数据元素删除
// 函数参数:L -> 头指针地址; i -> 第i个位置
// 返回值:1代表运行正常; 0代表运行有问题
Status ListDelete(const LinkList* L, int i)
{
    LinkList p = *L; //p指向头结点
    LinkList q = NULL;
    int j = 0;
    while ((p->next != NULL) && (j < i - 1)) //1. 查找第i-1个结点,p指向该结点
    {
        p = p->next; //p指向下一个结点
        ++j;
    }
    if (p->next != NULL || j > i - 1) return ERROR; //i过大,或者输入的i过小

    q = p->next;		//2. 临时保存结点ai的地址在q中,以备释放
    p->next = q->next;  //3. 将p的指针域指向结点ai+1
    free(q);			//4. 释放掉结点ai

    return OK;
}

int main()
{
    LinkList myList; 	//创建头指针
    int getData = 0;	//保存第i个数据元素的值
    LNode* checkData = NULL; //存储查找到的元素所在结点的地址

    //初始化操作台
    InitList(&myList);

    ListInsert(&myList, 1, 9); //链表插入数据,第1个位置插入9
    ListInsert(&myList, 2, 8); //链表插入数据,第2个位置插入8
    ListInsert(&myList, 3, 7); //链表插入数据,第3个位置插入7

    GetElem(&myList, 1, &getData);
    printf("%d\n", getData); //打印取到的元素
    GetElem(&myList, 2, &getData);
    printf("%d\n", getData); //打印取到的元素
    GetElem(&myList, 3, &getData);
    printf("%d\n\n", getData); //打印取到的元素


    if (checkData = LocateElem(&myList, 8)) //在链表中查找元素,并记录其位置信息
    {
        printf("GetElem : %d\n\n", checkData->data); //打印取到的元素
    }

    ListDelete(&myList, 2); //删除第2个元素
    GetElem(&myList, 2, &getData);
    printf("Change Elem : %d\n\n", getData); //打印取到的元素; 应该打印为原先第3个元素的值

    //获取原先第3个元素的数值,因为删掉了,所以函数运行出错
    if (GetElem(&myList, 3, &getData))
    {
        printf("GetElem Success!\n");
    }
    else
    {
        printf("Delete Success! GetElem Error!\n");
    }

    return 0;
}

3. 顺序表和链表的比较

顺序表链表
存储空间预先分配,会导致空间闲置溢出现象动态分配,不会出现存储空间闲置或溢出现象
存储密度不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1需要借助指针来体现元素间的逻辑关系,存储密度小于1
存取元素随机存取,按位置访问元素的时间复杂度为O(1)顺序存取,按位置访问元素的时间复杂度为O(n)
插入、删除平均移动约表中一半元素,时间复杂度为O(n)不需移动元素,确定插入、删除位置后,时间复杂度为O(1)
适用情况表长变化不大,且能事先确定变化的范围;很少进行插入或删除操作,经常按元素位置序号访问数据元素长度变化较大频繁进行插入或删除操作

4. 单链表的创建

4.1 头插法

将新结点逐个插入链表的头部(头结点之后)来创建链表,简而言之就是将新结点插入到头结点之后

头插法

算法步骤:

  1. 创建一个只有头结点的空链表
  2. 循环n次,插入链表的元素
    1. 生成一个新结点p
    2. 将要插入的元素放到p的数据域中
    3. 将新结点p放入到头结点之后
void CreateList_H(const LinkList* L, int n)
{
    LinkList L = (LinkList)malloc(sizeof(LNode)); //1. 创建一个只有头结点的空链表
    L->next = NULL;
	int i = 0;
    for (i=0; i<n; ++i)
    {
        LinkList p = (LinkList)malloc(sizeof(LNode));	//2.1 生成一个新结点p
        scanf("%EelemType",&(p->data));			//%ElemType是结点数据的类型,自行更改
        //cin>>p->data;							//2.2 将要插入的元素放到p的数据域中
        p->next = L->next;						
        L->next = p;							//2.3 将新结点p放入到头结点之后
    }
}

4.2 尾插法

将新结点逐个插入链表的尾部来创建链表。相较于头插法,需要增加一个尾指针来指向链表的尾结点。

尾插法

算法步骤:

  1. 创建一个只有头结点的空链表
  2. 初始化尾指针r,指向头结点
  3. 循环n次,插入链表的元素
    1. 生成一个新结点p
    2. 将要插入的元素放到p的数据域中
    3. 将新结点p放入到尾指针r指向的结点后
    4. 尾指针r指向新的尾结点p
void CreateList_R(const LinkList* L, int n)
{
    LinkList L = (LinkList)malloc(sizeof(LNode)); //1. 创建一个只有头结点的空链表
    L->next = NULL;
    LinkList r = L;
	int i = 0;
    for (i=0; i<n; ++i)
    {
        LinkList p = (LinkList)malloc(sizeof(LNode));	//2.1 生成一个新结点p
        scanf("%EelemType",&(p->data));			//%ElemType是结点数据的类型,自行更改
        //cin>>p->data;							//2.2 将要插入的元素放到p的数据域中
        r->next = p;							//2.3 将新结点p放入到尾指针r指向的结点后
        p->next = NULL;							
        r = p;									//2.4 尾指针r指向新的尾结点p
    }
}

5. 循环链表

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

由此,从表中任一结点出发均可找到表中其他结点。

下图为空的循环链表:
空循环链表

下图为非空的循环链表:

非空循环链表

循环单链表的操作和单链表基本一致,差别仅在于:当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULLp->next!=NULL,而循环单链表的判别条件为p!=Lp->next!=L。(L为头指针

6. 双向链表

双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。

双向链表的存储结构如下:

typedef struct DuLNode
{
    Elemtype data;			//数据域
    struct DuLNode *prior;	//直接前驱
    struct DuLNode *next;	//直接后继
}DuLNode, *DuLinkList;

空双向链表

空双向链表头结点的两个指针都指向自身。

非空双向链表

其中,双向链表最后一个结点的直接后继指针指向头结点,形成了循环链表。

d为循环双向链表的任意一个结点,循环双向链表的特性可表示成:

d->next->prior = d->prior->next = d

6.1 双向链表的插入

下图为插入新结点时,双向链表的指针变化情况。

双向链表的插入

Status ListInsert_DuL(const DuLinkList* L, int i ,ElemType e)
{
    DuLinkList p = NULL;
    if (!(p=GetElem_DuL(L, i)) //在L中确定第i个元素的位置指针
        return ERROR; //p为NULL时,第i个元素不存在
    
    DuLinkList s = (DuLinkList)malloc(sizeof(DuLNode)); //生成新结点s
    s->data = e;		//设置新结点的数据域为e
    s->prior = p->prior;//对应上图的第1步
    p->prior->next = s;	//对应上图的第2步
    s->next = p;		//对应上图的第3步
    p->prior = s; 		//对应上图的第4步
        
    return OK;
}

6.2 双向链表的删除

下图为删除指定结点时,双向链表的指针变化情况。

双向链表的删除

Status ListInsert_DuL(const DuLinkList* L, int i ,ElemType e)
{
    DuLinkList p = NULL;
    if (!(p=GetElem_DuL(L, i)) //在L中确定第i个元素的位置指针
        return ERROR; //p为NULL时,第i个元素不存在
    
    p->prior->next = p->next; //对应上图的第1步
    p->next->prior = p->prior;//对应上图的第2步
    free(p);	//对应上图的第3步:释放被删除的结点空间
        
    return OK;
}

7. 单链表、循环链表和双向链表的比较

L表示头指针;R表示尾指针;p表示当前结点的指针

查找表头结点查找表尾结点查找结点*p 的前驱结点
带头结点的单链表LL->next从L->next 依次向后遍历通过p->next 无法找到其前驱
带头结点仅设头指针L的循环单链表L->next从L->next 依次向后遍历通过p->next 可以找到其前驱
带头结点仅设尾指针R的循环单链表R->nextR通过p->next 可以找到其前驱
带头结点的双向循环链表LL->nextL->priorp->prior
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值