考研数据结构-线性表(超详细)

2. 线性表

2.1 线性表的定义和操作

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

  2. 操作:

    InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。

    DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。

    ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。

    ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。

    LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。

    GetElem(L, i, &e):按位查找操作。获取表L中第i个位置的元素的值。

    其他常用操作:

    Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。

    PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。

    Empty(L):判空操作。若L为空表,则返回true,否则返回false。

2.2 线性表的顺序表示

2.2.1 顺序表的定义
  1. 顺序表的定义:用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

  2. 顺序表的实现——静态分配

    #include <stdio.h>
    #define MAXSIZE 10 // 定义最大长度
    // 顺序表的静态实现
    typedef struct {
    	int data[MAXSIZE]; // 用静态的“数组”存放数据元素
    	int length; // 顺序表的当前长度
    } SqList;
    
    // 基本操作,初始化一个顺序表
    void InitList(SqList* list)
    {
    	for (int i = 0; i < MAXSIZE; i++)
    	{
    		list->data[i] = 0; // 将所有元素设置为默认初始值
    	}
    	list->length = 0; // 顺序表的原始长度为0
    }
    int main()
    {
    	SqList list; // 声明一个顺序表
    	InitList(&list); // 初始化一个顺序表
    	// 尝试违规打印数据表, 正常情况下是 i < list.length
    	for (int i = 0; i < MAXSIZE; i++)
    	{
    		printf("data[%d] = %d\n", i, list.data[i]);
    	}
    	return 0;
    }
    

    注意:顺序表静态分配实现的方式如果这个“数组”存满了,没有办法解决扩容问题,放弃治疗。

  3. 顺序表的实现——动态分配

    #include <stdio.h>
    #include <stdlib.h>
    #define InitSize 5 // 默认的最大长度
    // 顺序表的动态实现(动态数组)
    typedef struct {
    	int* data; // 指示动态数组分配的指针
    	int MaxSize; // 顺序表的最大容量
    	int length; // 顺序表的当前长度
    }SeqList;
    
    // 初始化一个顺序表
    void InitList(SeqList* list)
    {
    	// 用 malloc 函数在堆内存中申请一片连续的存储空间
    	list->data = (int*)malloc(sizeof(int) * InitSize);
    	list->length = 0;
    	list->MaxSize = InitSize;
    	// 将所有元素设置为默认初始值
    	for (int i = 0; i < InitSize; i++)
    	{
    		list->data[i] = 0;
    	}
    }
    /*
    * 扩容(增加动态数组的长度)
    * SeqList* list: 需要被扩容的线性表
    * int len: 将线性表在原有的基础上加上len个长度
    */
    void IncreaseSize(SeqList* list, int len)
    {
    	int *p = list->data;
    	list->data = (int*)malloc(sizeof(int) * (list->MaxSize + len));
    	for (int i = 0; i < list->length; i++)
    	{
    		list->data[i] = p[i]; // 将数据复制到新的区域
    	}
    	list->MaxSize = list->MaxSize + len; // 顺序表的最大长度增加了 len
    	free(p); // 释放原来的内存空间
    }
    int main()
    {
    	SeqList list;
    	InitList(&list);
    	// 模拟往链表中插入三个数据
    	list.data[0] = 11;
    	list.length = 1;
    	list.data[1] = 22;
    	list.length = 2;
    	list.data[2] = 33;
    	list.length = 3;
    	for (int i = 0; i < list.length; i++)
    	{
    		printf("before--list.data[%d] = %d\n", i, list.data[i]);
    	}
    	// 将容量扩大5个单位
    	IncreaseSize(&list, 5);
    	for (int i = 0; i < list.length; i++)
    	{
    		printf("after---list.data[%d] = %d\n", i, list.data[i]);
    	}
    	return 0;
    }
    
  4. 顺序表的特点:

    ① 随机访问,即可以在 O(1) 时间内找到第 i 个元素。

    ② 存储密度高,每个节点只存储数据元素

    ③ 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)

    ④ 插入、删除操作不方便,需要移动大量元素

2.2.2_1 顺序表的插入删除
  1. 顺序表的基本操作——插入

    注:代码建立在顺序表的“静态分配”实现方式之上

    最差时间复杂度O(n)
    最好时间复杂度O(1)
    平均时间复杂度O(n)
    #include <stdio.h>
    #define MAXSIZE 10
    #define OK 1 // 表示操作成功
    #define ERROR 0 // 表示操作失败
    typedef int Status; // 状态码,其值有OK 和 ERROR
    typedef int ElemType;
    typedef struct {
    	ElemType data[MAXSIZE]; 
    	int length; 
    } SqList;
    
    // 基本操作,初始化一个顺序表
    void InitList(SqList* list)
    {
    	for (int i = 0; i < MAXSIZE; i++)
    	{
    		list->data[i] = 0;
    	}
    	list->length = 0; 
    }
    // 在表中的第i个位置(位序)插入指定元素e
    Status ListInsert(SqList *list, int i, ElemType e)
    {
    	if (i < 0 || i > list->length + 1) // 判断 i 的范围是否有效
    		return ERROR;
    	if (i >= MAXSIZE) // 当前存储空间已满,不能插入
    		return ERROR;
    	// i 位置及 i 位置以后的所有元素往后移
    	for (int j = list->length - 1; j >= i - 1; j--)
    	{
    		list->data[j + 1] = list->data[j];
    	}
    	list->data[i - 1] = e; // 在第i个位置插入元素 e
    	list->length++; // 顺序表的长度加一
    	return OK;
    }
    int main()
    {
    	SqList list; 
    	InitList(&list); // 初始化一个顺序表
    	ListInsert(&list, 1, 66) ? printf("insert OK\n") : printf("insert ERROR\n");
    	ListInsert(&list, 2, 77) ? printf("insert OK\n") : printf("insert ERROR\n");
    	ListInsert(&list, 1, 88) ? printf("insert OK\n") : printf("insert ERROR\n");
    	// 打印顺序表
    	for (int i = 0; i < list.length; i++)
    	{
    		printf("data[%d] = %d\n", i, list.data[i]);
    	}
    	return 0;
    }
    
  2. 顺序表的基本操作——删除

    最差时间复杂度O(n)
    最好时间复杂度O(1)
    平均时间复杂度O(n)
    #include <stdio.h>
    #define MAXSIZE 10
    #define OK 1 // 表示操作成功
    #define ERROR 0 // 表示操作失败
    typedef int Status; // 状态码,其值有OK 和 ERROR
    typedef int ElemType;
    typedef struct {
    	ElemType data[MAXSIZE]; 
    	int length; 
    } SqList;
    
    // 基本操作,初始化一个顺序表
    void InitList(SqList* list)
    {
    	for (int i = 0; i < MAXSIZE; i++)
    	{
    		list->data[i] = 0;
    	}
    	list->length = 0; 
    }
    
    // 基本操作,删除第i(位序)个元素
    Status ListDelete(SqList* list, int i, ElemType* e)
    {
    	if (i < 0 || i > list->length) // 判断 i 的位置是否合法
    		return ERROR;
    	*e = list->data[i - 1]; // 将被删除的元素赋值给e
    	for (int j = i; j < list->length; j++) // 第i个元素及后面的元素向前移动一个位置
    	{
    		list->data[j - 1] = list->data[j];
    	}
    	list->length--; // 链表长度减一
    	return OK;
    }
    int main()
    {
    	SqList list; 
    	InitList(&list); // 初始化一个顺序表
    	// 模拟往链表中插入三个数据
    	list.data[0] = 11;
    	list.length = 1;
    	list.data[1] = 22;
    	list.length = 2;
    	list.data[2] = 33;
    	list.length = 3;
    	// 打印顺序表
    	for (int i = 0; i < list.length; i++)
    	{
    		printf("data[%d] = %d\n", i, list.data[i]);
    	}
    	ElemType e;
    	ListDelete(&list, 3, &e) ? printf("delete OK,e = %d\n", e) : printf("delete ERROR\n");
    	ListDelete(&list, 1, &e) ? printf("delete OK, e = %d\n", e) : printf("delete ERROR\n");
    	for (int i = 0; i < list.length; i++)
    	{
    		printf("data[%d] = %d\n", i, list.data[i]);
    	}
    	return 0;
    }
    
2.2.2_2 顺序表的查找
  1. 顺序表的基本操作——按位查找

    时间复杂度O(1)
    #include <stdio.h>
    #define MAXSIZE 10
    #define OK 1 // 表示操作成功
    #define ERROR 0 // 表示操作失败
    typedef int Status; // 状态码,其值有OK 和 ERROR
    typedef int ElemType;
    typedef struct {
    	ElemType data[MAXSIZE];
    	int length;
    } SqList;
    
    // 基本操作,初始化一个顺序表
    void InitList(SqList* list)
    {
    	for (int i = 0; i < MAXSIZE; i++)
    	{
    		list->data[i] = 0;
    	}
    	list->length = 0;
    }
    // 基本操作,获取表L中第i个位置的元素的值。
    Status GetElem(SqList list, int i, ElemType *e)
    {
    	if (i < 0 || i > list.length)
    		return ERROR;
    	*e = list.data[i - 1]; // 将位序位 i 的元素赋值给e
    	return OK;
    }
    int main()
    {
    	SqList list;
    	InitList(&list); // 初始化一个顺序表
    	// 模拟往链表中插入三个数据
    	list.data[0] = 11;
    	list.length = 1;
    	list.data[1] = 22;
    	list.length = 2;
    	list.data[2] = 33;
    	list.length = 3;
    	ElemType e;
    	GetElem(list, 1, &e) ? printf("GetElem OK, e = %d\n", e) : printf("GetElem ERROR\n", e);
    	return 0;
    }
    
  2. 顺序表的基本操作——按值查找

    最差时间复杂度O(n)
    最好时间复杂度O(1)
    平均时间复杂度O(n)
    #include <stdio.h>
    #define MAXSIZE 10
    #define OK 1 // 表示操作成功
    #define ERROR 0 // 表示操作失败
    typedef int Status; // 状态码,其值有OK 和 ERROR
    typedef int ElemType;
    typedef struct {
    	ElemType data[MAXSIZE];
    	int length;
    } SqList;
    
    // 基本操作,初始化一个顺序表
    void InitList(SqList* list)
    {
    	for (int i = 0; i < MAXSIZE; i++)
    	{
    		list->data[i] = 0;
    	}
    	list->length = 0;
    }
    // 基本操作,获取表L中第i个位置的元素的值。
    Status GetElem(SqList list, int i, ElemType *e)
    {
    	if (i < 0 || i > list.length)
    		return ERROR;
    	*e = list.data[i - 1]; // 返回位序位 i 的元素
    	return OK;
    }
    /*基本操作,按值查找,*/
    /*如果找到返回L中第1个与e满足关系的数据元素的位序。*/
    /*如果未找到返回 -1 */
    int LocateElem(SqList list, ElemType e)
    {
    	for (int i = 0; i < list.length; i++)
    	{
    		if (e == list.data[i])
    			return i + 1; // 返回与e满足关系的数据元素的位序。
    	}
    	return -1; // 表示未找到
    }
    int main()
    {
    	SqList list;
    	InitList(&list); // 初始化一个顺序表
    	// 模拟往链表中插入三个数据
    	list.data[0] = 11;
    	list.length = 1;
    	list.data[1] = 22;
    	list.length = 2;
    	list.data[2] = 33;
    	list.length = 3;
    	ElemType e;
    	GetElem(list, 1, &e) ? printf("GetElem OK, e = %d\n", e) : printf("GetElem ERROR\n", e);
    	int i = LocateElem(list, 22);
    	if (i != -1)
    		printf("Your element in the list and rank is %d\n", i);
    	else
    		printf("Your element not in the list!\n");
    	return 0;
    }
    

2.3 线性表的链式表示

2.3.1 单链表的定义
  1. 单链表的定义:线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。-优点:不要求大片连续空间,改变容量方便

    缺点:不可随机存取,要耗费一定空间存放指针

  2. 不带头结点的单链表的定义及判空操作

    #include <stdio.h>
    #define OK 1
    #define ERROR 0
    typedef int Status;
    typedef int ElemType;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    } LNode, *LinkList;
    
    /*初始化一个不带头结点的空链表*/
    // 注意:此时L必须是一个二级指针,因为我们要在被调函数中修改调用函数中指针变量的值
    Status InitList(LinkList* L)
    {
    	*L = NULL; // 空表,暂时还没有任何节点
    	return OK;
    }
    /*不带头结点的链表的判空操作*/
    Status isEmpty(LinkList L)
    {
    	return L == NULL;
    }
    int main()
    {
    	LinkList L; // 声明一个指向单链表的指针
    	// 初始化一个单链表
    	InitList(&L) ? printf("init is OK\n") : printf("init is ERROR\n");
    	// 判断链表是否为空
    	isEmpty(L) ? printf("L is NULL\n") : printf("L is not NULL\n");
    	return 0;
    }
    
  3. 带头结点的单链表的定义及判空

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int Status;
    typedef int ElemType;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    } LNode, * LinkList;
    
    /*初始化一个带头结点的空链表*/
    // 注意:此时L必须是一个二级指针,因为我们要在被调函数中修改调用函数中指针变量的值
    Status InitList(LinkList* L)
    {
    	*L = (LNode*)malloc(sizeof(LNode));
    	if (*L == NULL)
    		return ERROR; // 表示内存不足,malloc申请空间失败
    	(*L)->next = NULL; // 头节点之后暂时还没有节点
    	return OK;
    }
    /*带头结点的链表的判空操作*/
    Status isEmpty(LinkList L)
    {
    	return L->next == NULL;
    }
    int main()
    {
    	LinkList L; // 声明一个指向单链表的指针
    	// 初始化一个单链表
    	InitList(&L) ? printf("init is OK\n") : printf("init is ERROR\n");
    	// 判断链表是否为空
    	isEmpty(L) ? printf("L is NULL\n") : printf("L is not NULL\n");
    	return 0;
    }
    
2.3.2_1 单链表的插入删除(带头节点)
  1. Status ListInsert(LinkList* L, int i, ElemType e)

    在表中第i个位置插入元素 e

    时间复杂度O(n)
    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    /*初始化一个带头结点的空链表*/
    Status InitList(LinkList* L)
    {
    	*L = (LNode*)malloc(sizeof(LNode));
    	if (*L == NULL)
    		return ERROR;
    	(*L)->next = NULL;
    	return OK;
    }
    /*在表中的第i个位置插入指定元素 e */
    Status ListInsert(LinkList* L, int i, ElemType e)
    {
    	if (i < 1) // 判断插入位置是合法
    		return ERROR;
    	LNode* p = *L; // 指针p指向头节点,第0个节点,不存储数据
    	int j = 0;
    	while (j < i - 1 && p != NULL) // 循环找到第 i - 1个节点
    	{
    		p = p->next;
    		j++;
    	}
    	if (p == NULL) // i值不合法
    		return ERROR;
    	LNode* s = (LNode*)malloc(sizeof(LNode)); // 申请要新插入的节点
    	s->data = e;
    	s->next = p->next;
    	p->next = s; // 将节点s连到p原来的后继
    	return OK; // 插入成功
    }
    
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	ListInsert(&L, 1, 66);
    	ListInsert(&L, 2, 77);
    	ListInsert(&L, 1, 88);
    	LNode* p = L->next;
    	while (p)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    
  2. Status InsertNextNode(LNode* p, ElemType e)

    后插操作:在p节点元素之后插入 e

    注意:封装了该操作之后会简化ListInsert()函数的实现

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    /*初始化一个带头结点的空链表*/
    Status InitList(LinkList* L)
    {
    	*L = (LNode*)malloc(sizeof(LNode));
    	if (*L == NULL)
    		return ERROR;
    	(*L)->next = NULL;
    	return OK;
    }
    /*后插操作:在p节点元素之后插入 e */
    Status InsertNextNode(LNode* p, ElemType e)
    {
    	if (p == NULL)
    		return ERROR;
    	LNode* s = (LNode*)malloc(sizeof(LNode));
    	if (s == NULL)
    		return ERROR; // 内存分配失败
    	s->data = e; // 用节点s保存数据e
    	s->next = p->next;
    	p->next = s; // 将节点s连到 p原来的后继
    	return OK;
    }
    /*在表中的第i个位置插入指定元素 e */
    Status ListInsert(LinkList* L, int i, ElemType e)
    {
    	if (i < 1) // 判断插入位置是合法
    		return ERROR;
    	LNode* p = *L; // 指针p指向头节点,第0个节点,不存储数据
    	int j = 0;
    	while (j < i - 1 && p != NULL) // 循环找到第 i - 1个节点
    	{
    		p = p->next;
    		j++;
    	}
    	return InsertNextNode(p, e); // 插入成功
    }
    
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	ListInsert(&L, 1, 66);
    	ListInsert(&L, 2, 77);
    	ListInsert(&L, 1, 88);
    	LNode* p = L->next;
    	while (p)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    
  3. Status InsertPriorNode(LNode* p, ElemType e)

    在p节点元素之前插入元素 e

    ​ 两种解决方案:

    ​ (a)我们需要把头指针传入这个函数,循环遍历找到指定节点的前一个节点,然后在该位置插入新节 点。时间复杂度为O(n)

    ​ (b)我们直接在指定节点后方插入一个新节点,然后让这两个节点中的值互换,营造一种在指定节点前 插入新节点的假象。(代码实现我们采用的是这种)

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    /*初始化一个带头结点的空链表*/
    Status InitList(LinkList* L)
    {
    	*L = (LNode*)malloc(sizeof(LNode));
    	if (*L == NULL)
    		return ERROR;
    	(*L)->next = NULL;
    	return OK;
    }
    
    /*在p节点元素之前插入元素 e */
    Status InsertPriorNode(LNode* p, ElemType e)
    {
    	if (p == NULL)
    		return ERROR;
    	LNode* s = (LNode *)malloc(sizeof(LNode));
    	if (s == NULL)
    		return ERROR;
    	s->next = p->next;
    	p->next = s; // 新节点s连接到p的后面
    	s->data = p->data; // 将p中元素复制到s中
    	p->data = e; // p中元素覆盖为e
    	return OK;
    }
    
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	// 模拟向链表中添加一个值为999的节点
    	LNode* s = (LNode*)malloc(sizeof(LNode));
    	s->data = 999;
    	s->next = NULL;
    	L->next = s;
    	// 测试 InsertPriorNode 函数是否可用
    	InsertPriorNode(s, 66);
    	InsertPriorNode(s, 77);
    	LNode* p = L->next;
    	while (p)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    	// 预计输出结果:66 77 999
    	// 但是实际输出结果是:77 66 999
    	// 为什么呢?因为链表中本身就有s(999)这个节点 我们向 999 前面 插入 66
    	// 其实本质上是向999 后面插入了 一个 66,然后让66 和 999 这两个值互换但是节点没换
    	// 此时s节点保存的值是66,我们向s节点之前插入77 其实是在66 后面插入77 然后互换
    	// 所以输出结果是 77 66 999
    	return 0;
    }
    
  4. Status ListDelete(LinkList L, int i, ElemType* e)

    按位序删除

    最坏时间复杂度O(n)
    平均时间复杂度O(n)
    最好时间复杂度O(1)
    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    /*初始化一个带头结点的空链表*/
    Status InitList(LinkList* L)
    {
    	*L = (LNode*)malloc(sizeof(LNode));
    	if (*L == NULL)
    		return ERROR;
    	(*L)->next = NULL;
    	return OK;
    }
    
    /*按位序删除*/
    Status ListDelete(LinkList L, int i, ElemType* e)
    {
    	if (i < 0)
    		return ERROR;
    	LNode* p = L; // 让p节点指向链表的头节点
    	int j = 0;
    	while (j < i - 1 && p != NULL) // 循环找到第 i - 1个节点
    	{
    		j++;
    		p = p->next;
    	}
    	if (p == NULL || p->next == NULL) // i值不合法
    		return ERROR;
    	LNode* q = p->next;
    	*e = q->data; // 令q指向被删除的节点
    	p->next = q->next; // 将 *q节点从链中断开
    	free(q); // 释放节点的存储空间
    	return	OK; // 删除成功
    }
    
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	// 模拟向链表中添加一个值为999的节点
    	LNode* s1 = (LNode*)malloc(sizeof(LNode));
    	s1->data = 999;
    	s1->next = NULL;
    	L->next = s1;
    	// 模拟向链表中添加一个值为888的节点
    	LNode* s2 = (LNode*)malloc(sizeof(LNode));
    	s2->data = 888;
    	s2->next = NULL;
    	s1->next = s2;
    	// 模拟向链表中添加一个值为777的节点
    	LNode* s3 = (LNode*)malloc(sizeof(LNode));
    	s3->data = 777;
    	s3->next = NULL;
    	s2->next = s3;
    	LNode* p = L->next;
    	while (p != NULL)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    	ElemType e;
    	// 按位序删除
    	ListDelete(L, 1, &e) ? printf("delete OK, and e = %d\n", e) : printf("delete ERROR!\n");
    	ListDelete(L, 3, &e) ? printf("delete OK, and e = %d\n", e) : printf("delete ERROR!\n");
    	ListDelete(L, 2, &e) ? printf("delete OK, and e = %d\n", e) : printf("delete ERROR!\n");
    	p = L->next;
    	while (p != NULL)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    
  5. Status DeleteNode(LinkList L, LNode* p)

    指定节点的删除

    ​ 两种解决方案:

    ​ (a)我们使用类似于在指定节点前插的操作,只需传入需要删除的节点的指针,然后将指定节点后一个 节点保存的值赋值给指定删除的节点,然后将指定删除的节点的后继给删了,营造了一种删除指定节 点的假象。优点是时间复杂度低只需O(1),缺点是在删除最后一个元素时此种方法不具有通用性。

    ​ (b)直接将头指针和指定删除节点的指针传给此函数,此函数拿着头指针直接循环遍历找到指定删除节 点的前驱节点,然后让前驱节点直接指向指定删除的节点的后继,再释放被删除节点的内存即可。**优 点是该方法具有通用性,无论删除哪个节点都可以使用,缺点是时间发复杂度高:最坏和平均时间复 杂度都达到了O(n)级别。**我们代码实现采用的是这种。

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    /*初始化一个带头结点的空链表*/
    Status InitList(LinkList* L)
    {
    	*L = (LNode*)malloc(sizeof(LNode));
    	if (*L == NULL)
    		return ERROR;
    	(*L)->next = NULL;
    	return OK;
    }
    
    // 比较两个节点是否相同
    Status isEquals(LNode* p1, LNode* p2)
    {
    	return p1->next == p2->next && p1->data == p2->data;
    }
    /*指定节点删除*/
    Status DeleteNode(LinkList L, LNode* p)
    {
    	if (p == NULL || L == NULL)
    		return ERROR;
    	LNode* s = L; // 让s节点指向头节点
    	while (!isEquals(s->next, p) && s != NULL) // 循环遍历找到指定节点的前一个节点
    	{
    		s = s->next;
    	}
    	if (s == NULL || s->next == NULL) // 指定节点未找到
    		return ERROR;
    	LNode* q = s->next;
    	s->next = q->next; // 让s指向指定删除节点的后继节点
    	free(q); // 释放被删除的节点
    	return	OK;
    }
    
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	// 模拟向链表中添加一个值为999的节点
    	LNode* s1 = (LNode*)malloc(sizeof(LNode));
    	s1->data = 999;
    	s1->next = NULL;
    	L->next = s1;
    	// 模拟向链表中添加一个值为888的节点
    	LNode* s2 = (LNode*)malloc(sizeof(LNode));
    	s2->data = 888;
    	s2->next = NULL;
    	s1->next = s2;
    	// 模拟向链表中添加一个值为777的节点
    	LNode* s3 = (LNode*)malloc(sizeof(LNode));
    	s3->data = 777;
    	s3->next = NULL;
    	s2->next = s3;
    	LNode* p = L->next;
    	while (p != NULL)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    
    	DeleteNode(L, s3); // 删除 s3 节点
    	DeleteNode(L, s1); // 删除 s1 节点
    	printf("------------------\n");
    	p = L->next;
    	while (p != NULL)
    	{
    		printf("%d\n", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    
2.3.2_2 单链表的查找
  1. LNode* GetElem(LinkList L, int i)

    按位查找——返回第i个元素

    最坏时间复杂度O(n)
    平均时间复杂度O(n)
    最好时间复杂度O(1)
    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, *LinkList;
    
    /*带头链表的初始化操作*/
    Status InitList(LinkList *L)
    {
    	LNode *s = (LNode*)malloc(sizeof(LNode));
    	if (s == NULL)
    		return ERROR;
    	s->next = NULL;
    	(*L) = s;
    }
    /*按位查找——返回第i个元素*/
    LNode* GetElem(LinkList L, int i)
    {
    	if (i < 0)
    		return NULL;
    	int j = 0;
    	LNode* p = L; // p指向被扫描的节点,现在指向L的头节点
    	while (j < i && p != NULL) // 循环遍历找到第 i 的节点
    	{
    		p = p->next;
    		j++;
    	}
    	return p;
    }
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	// 模拟向链表中添加一个值为999的节点
    	LNode* s1 = (LNode*)malloc(sizeof(LNode));
    	s1->data = 999;
    	s1->next = NULL;
    	L->next = s1;
    	// 模拟向链表中添加一个值为888的节点
    	LNode* s2 = (LNode*)malloc(sizeof(LNode));
    	s2->data = 888;
    	s2->next = NULL;
    	s1->next = s2;
    	// 模拟向链表中添加一个值为777的节点
    	LNode* s3 = (LNode*)malloc(sizeof(LNode));
    	s3->data = 777;
    	s3->next = NULL;
    	s2->next = s3;
    	
    	LNode* res1 = GetElem(L, 1);
    	LNode* res2 = GetElem(L, 2);
    	LNode* res3 = GetElem(L, 3);
    
    	printf("res1->data = %d\n", res1->data);
    	printf("res2->data = %d\n", res2->data);
    	printf("res3->data = %d\n", res3->data);
    	return 0;
    }
    
  2. LNode* LocateElem(LinkList L, ElemType e)

    按值查找,找到数据域等于 e 的节点

    最坏时间复杂度O(n)
    平均时间复杂度O(n)
    最好时间复杂度O(1)
    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, *LinkList;
    
    /*带头链表的初始化操作*/
    Status InitList(LinkList *L)
    {
    	LNode *s = (LNode*)malloc(sizeof(LNode));
    	if (s == NULL)
    		return ERROR;
    	s->next = NULL;
    	(*L) = s;
    }
    /*按值查找,找到数据域等于 e 的节点*/
    LNode* LocateElem(LinkList L, ElemType e)
    {
    	LNode* p = L->next;// 从第一个节点开始查找数据为 e 的节点
    	while (p != NULL && p->data != e)
    		p = p->next;
    	return p; // 找到了则返回 p, 找不到则返回NULL
    
    }
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	// 模拟向链表中添加一个值为999的节点
    	LNode* s1 = (LNode*)malloc(sizeof(LNode));
    	s1->data = 999;
    	s1->next = NULL;
    	L->next = s1;
    	// 模拟向链表中添加一个值为888的节点
    	LNode* s2 = (LNode*)malloc(sizeof(LNode));
    	s2->data = 888;
    	s2->next = NULL;
    	s1->next = s2;
    	// 模拟向链表中添加一个值为777的节点
    	LNode* s3 = (LNode*)malloc(sizeof(LNode));
    	s3->data = 777;
    	s3->next = NULL;
    	s2->next = s3;
    	
    	LNode* res1 = LocateElem(L, 777);
    	res1 != NULL ? printf("%d\n", res1->data) : printf("not found\n");
    	LNode* res2 = LocateElem(L, 222);
    	res2 != NULL ? printf("%d\n", res2->data) : printf("not found\n");
    	return 0;
    }
    
  3. int Length(LinkList L)

    求表的长度

    时间复杂度O(n)
    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    typedef int ElemType;
    typedef int Status;
    
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, *LinkList;
    
    /*带头链表的初始化操作*/
    Status InitList(LinkList *L)
    {
    	LNode *s = (LNode*)malloc(sizeof(LNode));
    	if (s == NULL)
    		return ERROR;
    	s->next = NULL;
    	(*L) = s;
    }
    /*求表的长度*/
    int Length(LinkList L)
    {
    	int length = 0;
    	LNode* p = L;
    	while (p->next != NULL)
    	{
    		p = p->next;
    		length++;
    	}
    	return length;
    }
    int main()
    {
    	LinkList L;
    	InitList(&L);
    	// 模拟向链表中添加一个值为999的节点
    	LNode* s1 = (LNode*)malloc(sizeof(LNode));
    	s1->data = 999;
    	s1->next = NULL;
    	L->next = s1;
    	
    	int len = Length(L);
    	printf("L len is %d\n", len);
    	return 0;
    }
    
2.3.2_3 单链表的建立
  1. 尾插法建立单链表

    两种实现思路:

    ​ (a)初始化单链表

    ​ 设置变量 length 记录链表长度

    ​ While 循环 {

    ​ 每次取一个数据元素 e;

    ListInsert (L, length+1, e) 插到尾部;

    ​ length++;

    ​ }

    这种实现思路时间复杂度为O(n²)

    ​ (b)初始化单链表

    ​ 设置一个rear尾指针,一直指向链表尾节点

    ​ While 循环 {

    ​ 每次取一个数据元素 e;

    Status InsertNextNode(r, e)插到尾部

    ​ rear指向新的尾节点

    ​ }

    这种实现思路的时间复杂度为O(n)

    #define  _CRT_SECURE_NO_WARNINGS 1
    #include <stdio.h>
    #include <stdlib.h>
    typedef int ElemType;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    
    /*尾插法建立单链表*/
    LNode* List_TailInsert(LinkList* L)
    {
    	int x; // 设 ElemType为整型
    	(*L) = (LNode*)malloc(sizeof(LNode)); // 建立头节点
    	(*L)->next = NULL;
    	LNode *s, *r = *L; // r为表尾指针
    	scanf("%d", &x);
    	while (x != 9999) // 输入9999表示结束
    	{
    		s = (LNode*)malloc(sizeof(LNode));
    		s->data = x;
    		s->next = NULL;
    		r->next = s; // 将新节点插入表尾
    		r = s; // 保证表尾指针一直指向表尾节点
    		scanf("%d", &x);
    	}
    	return L;
    }
    
    int main()
    {
    	LinkList L;
    	List_TailInsert(&L); // 尾插法建立单链表
    	LNode* p = L->next;
    	while (p != NULL) // 循环遍历输出单链表
    	{
    		printf("%d\t", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    
  2. 头插法建立单链表

    实现思路:

    ​ 初始化单链表

    ​ While 循环 {

    ​ 每次取一个数据元素 e;

    InsertNextNode (L, e);

    ​ }

    时间复杂度O(1)
    #define  _CRT_SECURE_NO_WARNINGS 1
    #include <stdio.h>
    #include <stdlib.h>
    typedef int ElemType;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    
    /*头插法建立单链表*/
    LNode* List_HeadInsert(LinkList* L)
    {
    	int x; // 设 ElemType为整型
    	(*L) = (LNode*)malloc(sizeof(LNode)); // 建立头节点
    	(*L)->next = NULL;
    	LNode* s;
    	scanf("%d", &x);
    	while (x != 9999) // 输入9999表示结束
    	{
    		s = (LNode*)malloc(sizeof(LNode));
    		s->data = x;
    		s->next = NULL;
    		s->next = (*L)->next; // 将 s 的后继设置为原来头节点的后继
    		(*L)->next = s; // 将新节点插入到头节点的后面,做头节点的后继
    		scanf("%d", &x);
    	}
    	return L;
    }
    int main()
    {
    	LinkList L;
    	List_HeadInsert(&L); // 头插法建立单链表
    	LNode* p = L->next;
    	while (p != NULL) // 循环遍历输出单链表
    	{
    		printf("%d\t", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    
  3. 头插法重要应用——链表逆置

    实现思路:

    ​ 循环遍历整个链表

    ​ 每循环到一个节点,就把这个节点从链表中断开,用头插法的思想插入到链表中

    #define  _CRT_SECURE_NO_WARNINGS 1
    #include <stdio.h>
    #include <stdlib.h>
    typedef int ElemType;
    typedef struct LNode {
    	ElemType data;
    	struct LNode* next;
    }LNode, * LinkList;
    
    /*链表逆置*/
    LinkList List_Reverse(LinkList L)
    {
    	LNode* p, * r; // 两个都是用来遍历链表的
    	p = L->next; // p指向第一个节点
    	L->next = NULL; // 直接断开链表
    	while (p != NULL) // 循环遍历L链表
    	{
    		r = p->next; // 存储p节点的下一个节点,因为下面要修改p->next
    		p->next = L->next; // 让插入节点指向原来头节点的后继
    		L->next = p; // 让链表头节点指向要插入的节点
    		p = r; // 把 p的下一个节点赋值给p,来完成循环遍历整张链表的目的
    	}
    	return L;
    }
    
    
    /*尾插法建立单链表*/ // 辅助建立单链表,用来测试List_Reverse()
    LNode* List_TailInsert(LinkList* L)
    {
    	int x; // 设 ElemType为整型
    	(*L) = (LNode*)malloc(sizeof(LNode)); // 建立头节点
    	(*L)->next = NULL;
    	LNode* s, * r = *L; // r为表尾指针
    	scanf("%d", &x);
    	while (x != 9999) // 输入9999表示结束
    	{
    		s = (LNode*)malloc(sizeof(LNode));
    		s->data = x;
    		s->next = NULL;
    		r->next = s; // 将新节点插入表尾
    		r = s; // 保证表尾指针一直指向表尾节点
    		scanf("%d", &x);
    	}
    	return L;
    }
    
    int main()
    {
    	LinkList L;
    	List_TailInsert(&L); // 尾插法建立单链表
    	LNode* p = L->next;
    	while (p != NULL) // 循环遍历输出单链表
    	{
    		printf("%d\t", p->data);
    		p = p->next;
    	}
    	printf("\n");
    	List_Reverse(L);
    	p = L->next;
    	while (p != NULL) // 循环遍历输出单链表
    	{
    		printf("%d\t", p->data);
    		p = p->next;
    	}
    	return 0;
    }
    

2.4 双链表

在这里插入图片描述

2.4.1 双链表的初始化和判空
#include <stdio.h>
#include <stdlib.h>
#define true 1
#define false 0
typedef int bool;
typedef int ElemType;
typedef struct DNode { // 定义双链表节点类型
	ElemType data; // 数据域
	struct DNode *prior, *next; // 分别带票前驱指针和后继指针
}DNode, *DLinkList;
/*初始化一个双链表*/
bool InitDList(DLinkList* L)
{
	(*L) = (DNode *)malloc(sizeof(DNode)); // 申请头结点,并让头指针指向头节点
	if (*L == NULL) // 内存不足,分配失败
		return false;
	(*L)->next = NULL; // 让头节点的后继指针指向NULL
	(*L)->prior = NULL; // 让头节点的前驱指针指向NULL
	return true;
}

/*判断双链表是否为空*/
bool isEmpty(DLinkList L)
{
	if (L->next == NULL)
		return true; // 双链表为空,返回 true
	return false; // 双链表不为空,返回 false
}
int main()
{
	DLinkList L;
	InitDList(&L) ? printf("init OK\n") : printf("init ERROR\n");
	isEmpty(L) ? printf("L is empty\n") : printf("L not is empty\n");
	return 0;
}
2.4.2 双链表的插入&删除&销毁
#include <stdio.h>
#include <stdlib.h>
#define true 1
#define false 0
typedef int bool;
typedef int ElemType;
typedef struct DNode { // 定义双链表节点类型
	ElemType data; // 数据域
	struct DNode* prior, * next; // 分别带票前驱指针和后继指针
}DNode, * DLinkList;
/*初始化一个双链表*/
bool InitDList(DLinkList* L)
{
	(*L) = (DNode*)malloc(sizeof(DNode)); // 申请头结点,并让头指针指向头节点
	if (*L == NULL) // 内存不足,分配失败
		return false;
	(*L)->next = NULL; // 让头节点的后继指针指向NULL
	(*L)->prior = NULL; // 让头节点的前驱指针指向NULL
	return true;
}

/*判断双链表是否为空*/
bool isEmpty(DLinkList L)
{
	if (L->next == NULL)
		return true; // 双链表为空,返回 true
	return false; // 双链表不为空,返回 false
}

/*在p节点之后插入s节点*/
bool InsertNextDNode(DNode* p, DNode* s)
{
	if (p == NULL || s == NULL) // 非法参数
		return false;
	s->next = p->next;
	if(p->next != NULL) // 如果p节点有后继节点
		p->next->prior = s;
	p->next = s;
	s->prior = p;
	return true;
}
/*删除p节点的后继节点*/
bool DeleteNextNode(DNode* p)
{
	if (p == NULL || p->next == NULL) // 参数不合法
		return false;
	DNode* q = p->next;
	p->next = q->next;
	if (q->next != NULL) // 如果被删除的节点有后继节点
		q->next->prior = p;
	free(q); // 释放被删除节点的空间
	return true;
}

/*销毁链表*/
bool DestoryDList(DLinkList *L)
{
	while ((*L)->next != NULL)
		DeleteNextNode(*L);
	free(*L);
	*L = NULL;
}

int main()
{
	DLinkList L;
	InitDList(&L);
	DNode* s1 = (DNode*)malloc(sizeof(DNode));
	s1->data = 666;
	InsertNextDNode(L, s1); // 插入节点 s1
	DNode* s2 = (DNode*)malloc(sizeof(DNode));
	s2->data = 777;
	InsertNextDNode(s1, s2); // 向 s1 后面插入 s2
	DeleteNextNode(s1); // 删除 s1 后面的节点
	DestoryDList(&L); // 销毁 双向链表L
	return 0;
}

注意:双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度 O(n)

2.5 循环链表

2.5.1 循环单链表

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef int Status;
typedef int ElemType;

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

/*循环单链表的初始化*/
Status InitList(LinkList* L)
{
	*L = (LNode*)malloc(sizeof(LNode));
	if (*L == NULL) // 内存不足,分配失败
		return ERROR;
	(*L)->next = *L; // 头节点的后继指针指向头节点自身
	return OK;
}
/*判断单链表是否为空*/
Status isEmpty(LinkList L)
{
	if (L->next == L)
		return OK;
	return ERROR;
}
/*判断p节点是否为链表的表尾节点*/
Status isTail(LinkList L,LNode* p)
{
	if (p->next == L)
		return OK;
	return ERROR;
}

int main()
{
	LinkList L;
	InitList(&L) ? printf("init OK\n") : printf("init ERROR\n");
	// 模拟像链表中添加一个节点s
	LNode* s = (LNode*)malloc(sizeof(LNode));
	s->data = 666;
	L->next = s;
	s->next = L;
	// 判空
	isEmpty(L) ? printf("The List is NULL\n") : printf("The List not is NULL\n");
	// 判断是否是尾节点
	isTail(L, s) ? printf("is Tail\n") : printf("not is Tail\n");
	isTail(L, L) ? printf("is Tail\n") : printf("not is Tail\n");
	return 0;
}
2.5.2 循环双链表

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
typedef int Status;
typedef int ElemType;

typedef struct DNode {
	ElemType data;
	struct DNode *prior, *next;
}DNode, * DLinkList;

/*初始化空的循环双链表*/
Status InitDLinkList(DLinkList* L)
{
	*L = (DNode*)malloc(sizeof(DNode));
	if (*L == NULL) // 内存不足,分配失败
		return ERROR;
	(*L)->prior = *L; // 让头节点的前驱指针只想头节点自身
	(*L)->next = *L; // 让头节点的后继指针只想头节点自身
	return OK;
}
/*判断循环双链表是否为空*/
Status isEmpty(DLinkList L)
{
	if (L->next == L)
		return OK;
	return ERROR;
}
/*判断节点p是否为循环双链表的尾节点*/
Status isTail(DLinkList L, DNode* p)
{
	if (p->next == L)
		return OK;
	return ERROR;
}
/*在p节点之后插入s节点*/
Status InsertNextNode(DNode* p, DNode* s)
{
	s->next = p->next;
	p->next->prior = s;
	s->prior = p;
	p->next = s;
	return OK;
}
/*删除p的后继节点*/
Status DeleteNextNode(DLinkList L, DNode* p)
{
	DNode* q = p->next;
	if (q == L) // 说明要删除的节点是头节点
		return ERROR;
	p->next = q->next;
	q->next->prior = p;
	free(q);
	return OK;
}
int main()
{
	DLinkList L;
	InitDLinkList(&L);
	isEmpty(L) ? printf("L is empty\n") : printf("L not is empty\n");
	DNode* s = (DNode*)malloc(sizeof(DNode));
	s->data = 666;
	InsertNextNode(L, s);
	isTail(s, L) ? printf("it is tail\n") : printf("it not is tail\n");
	DeleteNextNode(L, L);
	return 0;
}

2.6 静态链表

静态链表:用数组的方式实现的链表

优点:增、删 操作不需要大量移动元素

缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变

适用场景:

​ ①不支持指针的低级语言;

​ ②数据元素数量固定不变的场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ghh9hHmV-1691169141957)(C:\Users\28131\AppData\Roaming\Typora\typora-user-images\image-20230805001317224.png)]

#include <stdio.h>
#include <stdlib.h>
#define true 1
#define false 0
#define MaxSize 10
typedef int bool;
typedef int ElemType;
typedef struct {
	ElemType data; // 存储数据元素
	int next; // 下一个元素的数组下标
}SLinkList[MaxSize]; // 可用 SLinkList 定义“一个长度为 MaxSize 的Node 型数组”

/*初始化静态链表*/
bool InitSLinkList(SLinkList L)
{
	L[0].data = -1; // L[0]充当头节点,不存储数据
	for (int i = 1; i < MaxSize; i++)
	{
		L[i].data = -2; // 让除了头节点以外的节点的指针域都存储-2,目的是为了区分节点是否可用
	}
}

/*插入位序为i的节点*/
ListInsert(SLinkList L, int i, ElemType e)
{
	// 1. 找到一个可用节点,把数据给存下来
	// 2. 找到位序为 i - 1的节点
	// 3. 修改新节点的 next
	// 4. 修改 i-1 号节点的next
}
/*删除某个节点*/
void DeleteNode()
{
	// 1. 从头结点出发找到前驱节点
	// 2. 修改前驱节点的游标
	// 3. 被删除的节点的 next 设为 -2
}
int main()
{
	SLinkList L;
	InitSLinkList(L);
	return 0;
}

2.7 顺序表和链表的比较

  1. 逻辑结构:都属于线性表,都是线性结构

  2. 存储结构:

    1. 顺序表是顺序存储
      1. 优点:支持随机存取,存储密度高。
      2. 缺点:大片连续空间分配不方便,改变容量不方便
    2. 链表是链式存储
      1. 优点:离散的小空间分配方便,改变容量方便
      2. 缺点:不可随机读取,存储密度低。
  3. 基本操作:

      1. 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
        1. 采用静态分配时,容量不可改变。
        2. 采用动态分配时,容量可改变,但需要移动大量元素,时间代价高。
      2. 链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展。
    1. 增 / 删
      1. 顺序表:插入/删除元素要将后续元素都后移/前移,时间复杂度 O(n),时间开销主要来自移动元素
      2. 链表:插入/删除元素只需修改指针即可,时间复杂度 O(n),时间开销主要来自查找目标元素
      1. 顺序表:按位查找:O(1);按值查找:O(n)若表内元素有序,可在O(lo₂n) 时间内找到
      2. 链表:按位查找:O(n);按值查找:O(n)
  4. 综上可知:顺序表和链表各适用于什么样的场景

    1. 表长难以预估、经常要增加/删除元素 ——链表

    2. 表长可预估、查询(搜索)操作较多 ——顺序表

  5. 开放式问题的答题思路:

    问题: 请描述顺序表和链表的 bla bla bla…实现线性表时,用顺序表还是链表好?
    答:顺序表和链表的逻辑结构都是线性结构,都属于线性表。但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。`
    由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元
    素时…;当删除一个数据元素时…;当查找一个数据元素时…

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别云超

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值