当前位置:   article > 正文

数据结构与算法笔记手册(C/C++)_数据结构王卓编程采用什么

数据结构王卓编程采用什么


前提说明:

1、笔记基于 数据结构与算法基础(青岛大学-王卓)_哔哩哔哩_bilibili 整理,老师讲得很通彻,可观看视频学习后,若有遗忘,将本笔记当手册使用。

2、编程语言使用C/C++语言,存在混用情况,部分为伪代码,可能存在直接粘贴代码报错的情况,但不影响理解数据结构本身。数据结构看别人的代码主要是为了理解算法的思想,编程要自己动手实践才能把知识真正变成自己的东西。ヾ(◍°∇°◍)ノ゙

3、本笔记内容尚未完善,后面有时间的时候会不断修改更新,若发现问题,希望大家多多指正Thanks♪(・ω・)ノ


线性表

顺序表

(随机存取)

顺序表的顺序存储表示
#define LIST_INIT_SIZE 100  //线性表存储空间的初始分配量
typedef struct{
	ElemType elem[LIST_INIT_SIZE];  //存放的元素(其中ElemType是数据类型,根据需要选择int等)
	int length;  //当前长度
}SqList;
  • 1
  • 2
  • 3
  • 4
  • 5
数组静态分配
typedef struct{
	ElemType data[MaxSize];  
	int length;  
}SqList;
  • 1
  • 2
  • 3
  • 4
数组动态分配
typedef struct{
	ElemType *data;  
	int length;  
}SqList;
SqList L;
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
//其中   (ElemType*)  的作用是将开辟的空间 强制转换 为 对应数据类型 的 指针
/*需要加载头文件:<stdlib.h>
malloc(m)函数:开辟m个字节长度的地址空间,并返回这段空间的首地址
sizeof(x)运算,计算变量x的长度
free(p)函数,释放指针p所指变量的存储空间,即彻底删除一个变量
*/

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
线性表的基本操作
InitList(&L);  //初始化操作,建立一个空的线性表L
DestroyList(&L);  //销毁已存在的线性表L
ClearList(&L);  //将线性表L清空
ListInsert(&L,i,elem);  //在线性表L中第i个位置插入新元素elem
ListDelte(&L,i,&e);  //删除线性表L中第i个位置元素,用e返回
IsEmpty(L);  //若线性表为空,返回true,否则返回false
ListLength(L);  //返回线性表L的元素个数
LocateElem(L,e);  //L中查找与给定值e相等的元素,若成功返回该元素在表中的序号,否则返回0
GetElem(L,i,&e);  //将线性表L中的第i个元素返回给e
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

PS:

//函数结果状态代码
#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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. 线性表的初始化(参数用引用)

    Status InitList_Sq(SqList &L){  //构造一个空的顺序表L
    	L.elem = new ElemType[MAXSIZE];  //为顺序表分配空间
    	if(!L.elem){
    		exit(OVERFLOW);  // 存储分配失败
    	}else{
    		L.length = 0;  //空表长度为0
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  2. 销毁线性表

    void DestroyList(SqList &L){
    	if(L.elem){
    		free(L.elem);  //释放存储空间
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
  3. 清空线性表

    void ClearList(SqList &L){
    	L.length = 0;  //将线性表的长度置为0(逻辑清空)
    }
    
    • 1
    • 2
    • 3
  4. 求线性表长度

    int GetLength(SqList L){
    	return (L.length);
    }
    
    • 1
    • 2
    • 3
  5. 判断线性表是否为空

    int IsEmpty(SqList L){
    	if(L.length == 0){
    		return 1;
    	}else{
    		return 0;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  6. 顺序表的取值(根据位置 i 获取相应位置元素的内容)

    int GetElem(SqList L, int i, ElemType &e){
    	if(i < 1 || i > L.length){  //判断i值合不合理
    		return ERROR;
    	}else{
    		e = L.elem[i-1];  //第i-1的单元存储着第i个元素
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  7. 顺序表的查找(从表的一端开始,逐个进行记录的关键字和给定值的比较)

    int LocateElem(SqList L, ElemType e){
    	int i;
    	for(i = 0; i < L.length; i++){  //逐个遍历
    		if(L.elem[i] == e){
    			return i+1;  //查找成功返回序号
    		}
    	}
    	return 0;  //查找失败返回0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  8. 顺序表的插入(整体考虑插入位置在 首,尾,中间 的情况)

    Status ListInsert_Sq(SqList &L, int i, ElemType e){
    	if(i < 1 || i > L.length + 1){  //i值不合法
    		return ERROR;
    	}
    	if(L.length == MAXSIZE){  //当前存储空间已满
    		return ERROR;
    	}
    	int j;
    	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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  9. 顺序表的删除(整体考虑删除位置在 首,尾,中间 的情况)

    Status ListDelete_Sq(SqList &L, int i){
    	if(i < 1 || i > L.length + 1){  //i值不合法
    		return ERROR;
    	}
    	int j;
    	for(j = i; j <= L.length - 1; j++){
    		L.elem[j-1] = L.elem[j];  //被删除元素之后的元素前移
    	}
    	L.length--;  //表长减1
    	return OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

顺序表小结:

  • 优点:
    • 存储密度大(结点本身所占存储量 / 结点结构所占存储量 = 1)
    • 可以随机存取表中任一元素
  • 缺点: 为克服这一缺点 => 链表
    • 在插入、删除某一元素时,需要移动大量元素
    • 浪费存储空间,存储空间不灵活
    • 属于静态存储形式,数据元素的个数不能自由扩充

链表

(顺序存取)

单链表、双链表、循环链表:

  • 结点只有一个指针域的链表,称为单链表
  • 结点有两个指针域(前驱,后继)的链表称为双链表
  • 首尾相连的链表称为循环链表

讨论1:如何表示空表?

无头结点:头指针为空时表示空表

有头结点:头结点的指针域为空时表示空表

讨论2:链表设置头结点有什么好处?

1、便于首元结点的处理

首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无需特殊处理。

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

无论链表是否为空,头指针都是指向头结点的非空指针,因此,空表和非空表的处理也就统一了。

讨论3:头结点的数据域内装的是什么?

头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。

链表(链式存储结构)的特点

  • 结点在存储器中的位置是任意的,及逻辑上相邻的数据元素在物理上不一定相邻(存储地址不连续,任意)
  • 访问时,只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
单链表的存储结构
typedef struct Lnode{  //声明结点的类型和指向结点的指针类型
	ElemType data;  //结点的数据域
	struct Lnode *next;  //结点的指针域
}LNode, *LinkList;  //LinkList为指向结构体Lnode的指针类型
 
/*
定义链表: LinkList L;  (或:LNode *L)
定义结点指针p: LNode *p;  (或:LinkList p)
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

例:存储学生学号、姓名、成绩的单链表结点类型定义

typedef struct student{
	char num[8];  //数据域
	char name[8];  //数据域
	int score;  //数据域
	struct student *next;  //指针域
}
--------------------------------------------------------------------------
为了统一链表操作,通常这样定义:
typedef struct{
	char num[8];
	char name[8];
	int score;
}ElemType;
typedef struct Lnode{
	ElemType data;  //数据域
	struct Lnode *next;  //指针域
}LNode, *LinkList;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
单链表的基本操作
  • 单链表的初始化
  • 判断单链表是否为空
  • 销毁单链表
  • 清空单链表
  • 求单链表表长
  • 取单链表第i个元素内容
  • 查找:根据指定数据查找所在位置(地址)/ 位置序号
  • 在第i个结点前插入新结点
  • 删除第i个结点
  1. 单链表的初始化

    Status InitList_L(LinkList &L){
    	L = new LNode;  // L = (LinkList)malloc (sizeof(LNode));
        L -> next = NULL;  //头结点指针域置空
    }
    
    • 1
    • 2
    • 3
    • 4
  2. 判断链表是否为空

    int ListEmpty(LinkList L){
    	if(L->next != NULL){  //非空
    		return 0;
    	}else{
    		return 1;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  3. 单链表的销毁(清除所有结点(包括头结点))

    Status DestroyList_L(LinkList &L){
    	LNode *p;  //或LinkList p;
    	while(L != NULL){
    		p = L;
    		L = L->next;
    		delete p;  // free(p);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  4. 清空链表(清除除头结点外的所有结点)

    Status ClearList(LinkList &L){
    	LNode *p,*q;
    	p = L->next;
    	while(p != NULL){
    		q = p->next;
    		delete p;
    		p = q;
    	}
    	L->next = NULL;  //头结点指针域置为空
    	return OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  5. 单链表的表长

    int ListLength(LinkList L){
    	LNode *p;
    	p = L->next;
    	int i = 0;
    	while(p != NULL){
    		i++;
    		p = p->next;
    	}
    	return i;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  6. 单链表的取值(获取第 i 个数据元素的内容)

    Status GetElem_L(LinkList L, int i, ElemType &e){
    	LNode *p;
    	p = L->next;
    	int j = 1;
    	while(p != NULL && j < i){
    		p = p->next;
    		++j;
    	}
    	
    	if(p == NULL || j > i){
    		return ERROR;  //第i个元素不存在
    	}else{
    		e = p->data;
    		return OK;
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  7. 根据指定数据查找所在位置(地址)

    Lnode *LocateElem_L(LinkList L, ElemType e){
    	LNode *p = L->next;
    	while(p != NULL && p->data != e){
    		p = p->next;
    	}
    	return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  8. 根据指定数据查找所在位置序号

    int LocateElem_L(LinkList L, ElemType e){
    	LNode *p = L->Next;
    	int j = 1;
    	while(p != NULL && p->data != e){
    		p = p->next;
    		j++;
    	}
    	
    	if(p != NULL){
    		return j;
    	}else{
    		return 0;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  9. 在第i个结点前插入新结点

    Status ListInsert_L(LinkList &L, int i, ElemType e){
    	LNode *p = L;
    	int j = 0;
    	while(p != NULL && j < i-1){  //寻找第i-1个结点
    		p = p->next;
    		j++;
    	}
    	
    	if(p == NULL || j > i-1){
    		return ERROR;
    	}else{
    		LNode *s = new LNode;
    		s->data = e;
    		s->next = p->next;
    		p-next = s;
    		return OK;
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  10. 删除第i个数据元素

    Status ListDelete_L(LinkList &L, int i, ElemType &e){
    	LNode *p = L;
    	int j = 0;
    	while(p->next != NULL && j < i-1){
    		p = p->next;
    		j++;
    	}
    	
    	if(p->next == NULL || j > i-1){
    		return ERROR;
    	}else{
    		LNode *q = p->next;
    		p->next = q->next;
    		e = q->data;  //保存删除结点的数据域
    		delete q; //free(q);
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
  11. 头插法建立单链表

    void CreateList_H(LinkList &L, int n){
    	L = new LNode;  // L = (LinkList)malloc (sizeof(LNode));
    	L->next = NULL;  //先建立一个带头结点的单链表
    	int i;
    	for(i = n; i > 0; i--){
    		LNode *p = new LNode;  //p = (LNode*)malloc(sizeof(LNode));
    		cin >> p->data;  //scanf(&p->data);
    		p->next = L->next;
    		L->next = p;
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  12. 尾插法建立单链表

    void CreateList_R(LinkList &L, int n){
    	L = new LNode;  // L = (LinkList)malloc (sizeof(LNode));
    	L->next = NULL;
        LNode*p,*r;
        r = L;
       	int i;
       	for(i = 0; i < n; i++){
       		p = new LNode;
       		cin >> p->data;
       		p->next = NULL;
       		r->next = p;
       		r = p;  //r指向新的尾结点
       	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

存储方式:

同一般线性表的顺序存储结构相同,

利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端。

  • 附设top指针,指示栈顶元素在顺序栈中的位置。
  • 另设base指针,指示栈底元素在顺序栈中的位置。

栈的顺序存储结构:顺序栈

栈的链式存储结构:链栈

####顺序栈的表示

#define MAXSIZE 100
typedef struct{
	SElemType *base;  //栈底指针
	SElemType *top;  //栈顶指针
	int stacksize;  //栈可用最大容量
}SqStack;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
顺序栈的基本操作
  1. 顺序栈的初始化

    Status InitStack(SqStack &S){  //构造一个空栈 
    	S.base = new SElemType[MAXSIZE];
    	if(!S.base){
    		exit(OVERFLOW);  //存储分配失败
    	}else{
    		S.top = S.base;  //栈顶指针等于栈底指针
    		S.stacksize = MAXSIZE;
            return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  2. 判断栈是否为空

    Status StackEmpty(SqStack S){
    	if(S.top == S.base){  //空栈
    		return TRUE;
    	}else{
    		return FALSE;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  3. 求顺序栈的长度

    int StackLength(SqStack S){
    	return S.top - S.base;
    }
    
    • 1
    • 2
    • 3
  4. 清空顺序栈

    Status ClearStack(SqStack S){
    	if(S.base){  //栈存在
    		S.top = S.base;
    	}
    	return OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  5. 销毁顺序栈

    Status DestroyStack(SqStack &S){
    	if(S.base){  //栈存在
    		delete S.base;
    		S.stacksize = 0;
    		S.base = S.top = NULL;
    	}
    	return OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  6. 顺序栈的入栈

    Status Push(SqStack &S, SElemType e){
    	if(S.top - S.base == S.stacksize){  //栈满,无法入栈
    		return ERROR;
    	}else{
    		*S.top = e;
    		S.top++;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  7. 顺序栈的出栈

    Status Pop(SqStack &S, SElemType e){
    	if(S.top == S.base){ //栈为空栈
    		return ERROR;
    	}else{
    		e = *S.top;
    		S.top--;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
链栈的表示

链栈是运算受限的单链表,只能在链表头部进行操作

typedef struct StackNode{
	SElemType data;
	struct StackNode *next;
}StackNode, *LinkStack;
LinkStack S;
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 链栈的入栈

    Status Push(LinkStack &S, SElemType e){
    	stackNode *p;
    	p = new StackNode;  //生成新结点p
    	p->data = e;  
    	p->next = S;  //将新结点插入栈顶
    	S = p;  //修改栈顶指针
    	return OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  2. 链栈的出栈

    Status Pop(LinkStack &S, SElemType &e){
    	if(S == NULL){
    		return ERROR;
    	}else{
    		e = S->data;
    		stackNode *p;
    		p = S;
    		S = S->next;
    		delete p;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  3. 取栈顶元素

    SElemType GetTop(LinkStack S){
    	if(S != NULL){
    		return S->data;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
栈和递归
分治法求解递归问题算法的一般形式
void p(参数表){
	if(递归结束条件)可直接求解步骤;  //-----基本项
	else p(较小的参数);  //-----归纳项
}
  • 1
  • 2
  • 3
  • 4
函数的调用过程

(遵循后调用,先返回----->栈)

调用前,系统完成:

  1. 将实参,返回地址等传递给被调用函数
  2. 为被调用函数的局部变量分配存储区
  3. 将控制转移到被调用函数的入口

调用后,系统完成:

  1. 保存被调用函数的计算结果
  2. 释放被调用函数的数据区
  3. 依照被调用函数保存的返回地址,将控制转移到调用函数

队列

  • 队列的物理存储方式可以用顺序存储结构,也可以用链式存储结构。相应的,队列的存储方式也分为两种,即顺序队列和链式队列。
顺序队列的表示
  • 队列的顺序表示——用一维数组base[MAXQSIZE]
#define MAXQSIZE 100  //最大队列长度
Typedef struct{
	QElemType *base;  //初始化的动态分配存储空间
    int front;  //数组的头下标
    int rear;  //数组的尾下标
}SqQueue;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

问题:rear == MAXQSIZE且front != 0(第一个位置下标为0)时,再入队会发生“溢出”,但base[0]~base[front-1]的存储位置是空的(之前的元素已出队),这被称为“假溢出”

解决假溢出的方法

  1. 将队列中的元素依次向队头方向移动。

    缺点:浪费时间。每移动一次,队列中的元素都要移动。

  2. 将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为MAXQSIZE时,若向量的开始端为空,又可以从头使用空着的空间。当front为MAXQSIZE时也一样。——引入循环队列

循环队列

核心思路:base[0]接在base[MAXQSIZE-1]之后,若rear+1==M,则令rear=0;

实现方法:利用mod(%)运算;

每次插入元素:Q.base[Q.rear]=element; Q.rear = (Q.rear+1)%MAXQSIZE;

每次删除元素:element = Q.base[s.front]; Q.front = (Q.front+1)%MAXQSIZE;

队空:front == rear; 队满:front == rear; -------->? 无法判断队空还是队满

解决方案:

  1. 另外设置一个标志已区别队空、队满
  2. 另设一个变量,记录元素个数
  3. 少用一个元素空间–>(太绕) 此时队满表示:(rear+1)%MAXQSIZE == front;
循环队列的基本操作
  1. 循环队列初始化

    Status InitQueue(SqQueue &Q){
    	Q.base = new QElemType[MAXQSIZE];  //分配数组空间
    	if(!Q.base){
    		exit(OVERFLOW);  //存储分配失败
    	}else{
    		Q.front = Q.rear = 0;  //头指针尾指针置为0,队列为空
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  2. 求循环队列长度

    int QueueLength(SqQueue Q){
    	return (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE;
    }
    
    • 1
    • 2
    • 3
  3. 循环队列的入队

    Status EnQueue(SqQueue &Q, QElemType &e){
    	if((Q.rear + 1) % MAXQSIZE == Q.front){  //此法为空出一个空间判断队空队满
    		return ERROR;  //队已满
    	}else{
    		Q.base[Q.rear] = e;  //新元素加入队尾
    		Q.rear = (Q.rear + 1) % MAXQSIZE;  //队尾指针+1
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  4. 循环队列的出队

    Status DeQueue(SqQueue &Q, QElemType &e){
    	if(Q.rear == Q.front){
    		return ERROR;  //队空
    	}else{
    		e = Q.base[front];
    		Q.front = (Q.front + 1) % MAXQSIZE;  //队头指针+1
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  5. 循环队列取队头元素

    SElemType GetHead(SqQueue Q){
    	if(Q.front == Q.rear){  //队列为空
    		exit(ERROR);
    	}else{
    		return Q.base[front];
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
链队列的表示
  • 若用户无法估计所用队列的长度,宜采用链队列

  • 链队列的类型定义

    #define MAXQSIZE 100  //队列的最大长度
    typedef struct Qnode{
    	QElemType data;
    	struct Qnode *next;
    }QNode, *QueuePtr;
    
    typedef struct{
    	QueuePtr front;  //队头指针
    	QueuePtr rear;  //队尾指针
    }LinkQueue;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
链队列的基本操作
  1. 链队列的初始化

    Status InitQueue(LinkQueue &Q){
    	Q.front = Q.rear = new QNode;
    	if(!Q.front){
    		exit(OVERFLOW);
    	}else{
    		Q.front->next = NULL;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  2. 链队列的销毁

    Status DestroyQueue(LinkQueue &Q){
    //算法思想:从队头开始,依次释放所有结点
    	QNdoe *p = NULL;  //或者不新建指针,直接用尾指针
    	while(Q.front){
    		p = Q.front->next;  //Q.rear = Q.front->next;
    		delete Q.front;
    		Q.front = p;  //Q.front = Q.rear;
    	}
    	return OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  3. 链队列的入队(将元素e入队)

    Status EnQueue(LinkQueue &Q, QElemType e){
    	QNode *p = new QNode;
    	if(!p){  
    		exit(OVERFLOW);  //分配空间失败
    	}else{
    		p->data = e;
    		p->next = NULL;
    		Q.rear->next = p;
    		Q.rear = p;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  4. 链队列的出队

    Status DeQueue(LinkQueue &Q, QElemType &e){
    	QNode *p = new QNode;
    	if(Q.front == Q.rear){  //队列为空
    		return ERROR;
    	}else{
    		//PS:Q.front->next才是存放第一个元素的结点,Q.front不是
    		
    		p = Q.front->next;
    		e = p->data;
    		Q.front->next = p->next;
    		
    		if(Q.rear == p){  //此步的目的是,若出队的结点已是尾结点,则也需要修改尾指针Q.rear,使其与头结点指向相同位置
    			Q.rear = Q.front;
    		}
    		
    		delete p;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  5. 求链队列的队头元素

    Status GetHead(LinkQueue Q, QElemType e){
    	if(Q.front == Q.rear){  //队列为空
    		return ERROR;
    	}else{
    		e = Q.front->next->data;
    		return OK;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

串、数组、广义表

串:内容受限的线性表

数组、广义表:线性结构的推广

  • 串的定义:零个或任意多个任意字符组成的有限序列。
  • 子串:一个串中任意个连续字符组成的子序列(含空串)称为该串的子串。
  • 真子串:不包含自身的所有子串。
  • 字符位置:字符在序列中的序号为该字符在串中的位置。
  • 子串位置:子串的第一个字符在主串中的位置。
  • 空格串:由一个或多个空格组成的串,与空串不同。
  • 串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相等。
  • 所有空串都相等。
串的存储结构
  • 串的顺序存储结构
#define MAXLEN 255
typedef struct{
	char ch[MAXLEN + 1];  //存储串的一维数组
	int length;  //串的当前长度
}SString
  • 1
  • 2
  • 3
  • 4
  • 5
  • 串的链式存储结构-----块链结构(一个节点放多个数据,从而增大数据密度)
#define CHUNKSIZE 80  //块的大小可由用户定义
typedef struct Chunk{
	char ch[CHUNKSIZE];
	struct Chunk *next;
}Chunk;  //结点的结构

typedef struct{
	Chunk *head, *tail;  //串的头指针和尾指针
	int curlen;  //串的当前长度
}LString;  //字符串的块链结构
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
串的匹配算法

模式匹配算法

算法目的:确定主串中所含子串(模式串)第一次出现的位置(定位)

算法应用:搜索检查、拼写检查、语言翻译、数据压缩

算法种类:

  • BF算法(Brute-Force,古典、经典、朴素、穷举)
  • KMP算法(快速)
  1. BF算法(简单匹配算法)-----穷举法思路

    时间复杂度:O((n-m+1)m),,若m<<n,则为O(nm)

    int Index_BF(SString S, SStirng T){  //S为主串,T为模式串
    	int i = 1, j = 1;
    	while(i <= S.length && j <= T.length){
    		if(S.ch[i] == T.ch[j]){
    			++i;
    			++j;
    		}else{
    			i = i - (j - 1) + 1;  //i回溯到开始位置的下一处
    			j = 1;  //j回到模式串开头
    		}
    	}
    	
    	if(j > T.length){  //此时表明匹配成功
    		return i - (T.length - 1);  //返回匹配成功的第一个字符在主串中的位置
    	}else{
    		return 0;  //匹配不成功
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  2. KMP算法

树(tree)的定义:

n(n>=0)个结点的有限集。

n=0,称为空树;

n>0,则它满足如下两个条件:

​ (1)有且仅有一个特定的称为根(root)的结点;

​ (2)其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3…Tm,其中每一个 集合本身又是一棵树,并称为根的子树(Sub Tree)。

树的基本术语:

根节点:非空树中无前驱点的结点

结点的度:结点拥有的子树数

分支结点:非终端结点

内部结点:根结点以外的分支结点

叶子:终端结点

树的度:树内各结点的度的最大值

双亲、孩子:结点的子树的根称为该结点的孩子,该结点称为孩子的双亲

兄弟:双亲为同一结点的结点

堂兄弟:双亲在同一层的结点

结点的祖先:从根到该结点所经分支上的所有结点

树的深度:树中结点的最大层次

有序树:树中结点的各子树从左至右有次序(最左边的为第一个孩子)

无序树:树中结点的各子树无次序

森林:m(m>0)课互不相交的树的集合

  • 把根结点删除,树就变成了森林
  • 一棵树可以看成是一个特殊的森林
  • 给森林中的各子树加上一个双亲结点,森林就变成了树

线性结构 树结构

第一个元素:无前驱 根结点(只有一个):无双亲

最后一个元素:无后继 叶子结点(可以有多个):无孩子

其他数据元素:一个前驱,一个后继 中间结点:一个双亲,多个孩子


二叉树的定义:

n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。

特点:

  1. 每个结点最多有两个孩子(二叉树中不存在度大于2的结点)
  2. 子树有左右之分,其次序不能颠倒
  3. 二叉树可以是空集合,根可以有空的左子树或空的右子树

注:二叉树不一定是树(概念不同)


二叉树的性质和存储结构

性质
  • 在二叉树的第i层上至多有2i-1个结点(i>=1),至少1个结点

  • 深度为k的二叉树至多有2k-1个结点(k>=1),至少k个结点

  • 对任何一棵二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0=n2+1

  • 特殊形式的二叉树(满二叉树一定是完全二叉树)

    • 满二叉树:深度为k且有2k-1个结点的二叉树(每层都满,叶子结点全在最底层,编号自上而下从左到右)
    • 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应。(或:从满二叉树最后一个结点开始,连续去掉任意个结点,得到的都是完全二叉树)(特点:叶子只可能分布在层次最大的两层上;对任一结点,若其右子树最大层次为i,则其左子树最大层次为i或i+1)
  • 具有n个结点的完全二叉树的深度为[log2n+1]+1。(PS:[n]表示不大于n的最大整数)

  • 完全二叉树任一结点i,其双亲结点为[i/2],左孩子结点为[2i],右孩子结点为[2i+1]

存储结构
顺序存储

实现:按满二叉树的结点层次编号,依次存放二叉树(不管是否是完全二叉树)中的数据元素

//二叉树的顺序存储表示
#define MAXTSIZE 100
Typedef TElemType SqBiTree[MAXTSIZE]
SqBiTree bt;
  • 1
  • 2
  • 3
  • 4

缺点:最坏情况->深度为k且只有k个结点的树需要长度为2k-1的一维数组

特点:结点间关系蕴含在其存储位置中;浪费空间,仅适用于存完全二叉树

链式存储结构
//二叉树的链式存储结构表示(二叉链表)
typedef struct BiNode{
	TElemType data;
	struct BiNode *lchild,*rchild;
}BiNode,*BiTree;
//PS:若建立三叉链表,则再增添一个指向双亲的指针
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

特点:在n个结点的二叉链表中,有n+1个空指针域


遍历二叉树

  • 先序遍历(根-左-右)
  • 中序遍历(左-根-右)
  • 后序遍历(左-右-根)

已知二叉树的先序、中序或中序、后序可以推出二叉树的构造

####递归算法

//先序遍历
void PreOrderTraverse(BiTree T){
	if(T == NULL){
		exit(0);  //空二叉树
	}else{
		visit(T.data);  //访问根结点
		PreOrderTraverse(T->lchild);  //递归遍历左子树
		PreOrderTraverse(T->rchild);  //递归遍历右子树
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
//中序遍历
void InOrderTraverse(BiTree T){
	if(T == NULL){
		exit(0);
	}else{
		InOrderTraverse(T->lchild);  //递归遍历左子树
		visit(T.data);  //访问根结点
		InOrderTraverse(T->rchild);  //递归遍历右子树
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
//后序遍历
void PostOrderTraverse(BiTree T){
	if(T == NULL){
		PostOrderTraverse(T->lchild);  //递归遍历左子树
		PostOrderTraverse(T->rchild);  //递归遍历右子树
		visit(T.data);  //访问根结点
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

时间复杂度:O(n) //每个结点只访问一次

空间复杂度:O(n) //栈占用的最大辅助空间

非递归算法(压栈)
//中序遍历
void InOrderTraverse(BiTree T){
	BiTree p = T;
	stack st;
	while(p != NULL || !st.empty()){  //当首次访问时树不为空,栈不为空时,循环
		if(p != NULL){
			st.push(p);
			p = p->lchild;
		}else{
			BiTree q;
			q = st.pop();
			visit(q.data);
			p = q->rchild;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

思考:如何实现非递归的先序和后序遍历

层次遍历(队列)
void LevelOrder(BiTree T){
	BNode* p = T;
	if(p == NULL){
		exit(0); //空二叉树
	}
	queue que;
	que.push(p);
	
	while(!que.empty()){
		p = que.pop();
		visit(p);
		if(p->lchild != NULL){
			que.push(p->lchild);
		}
		if(p->rchild != NULL){
			que.push(p->rchild);
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

二叉树遍历算法应用

二叉树的建立

对于建立字符ABCDEGF二叉树,需插入特殊字符来使该序列建立的二叉树唯一。

如:ABC##DE#G##F###

先序遍历(递归构造)

Status CreateBiTree(BiTree &T){
	cin>>ch;  //scanf(&ch)
	if(ch == '#'){
		T = NULL;
	}else{
		if(!T = (BiTNode*)malloc(sizeof(BiTNode))){  //T = new BiTNode;
			exit(0);  //分配内存失败
		}
		T->data = ch;  //生成根结点
		CreateBiTree(T->lchild);  //构造左子树
		CreateBiTree(T->rchild);  //构造右子树
	}
	return OK;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
复制二叉树
int Copy(BiTree T, BiTree &NewT){
	if(T == NULL){
		NewT = NULL;
		return 0;
	}else{
		NewT = new BiTNode;
		NewT->data = T->data;
		Copy(T->lchild, NewT->lchild);
		Copy(T->rchild, NewT->rchild);
	}
	return 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
计算二叉树深度
int Depth(BiTree T){
	if(T == NULL){
		return 0;
	}else{
		int m = Depth(T->Lchild);
		int n = Depth(T->Rchild);
		if(m > n){
			return m+1;
		}else{
			return n+1;
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
计算二叉树结点数
int NodeCount(BiTree T){
	if(T == NULL){
		return 0;
	}else{
		return NodeCount(T->lChild)+NodeCount(T->rChild)+1;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
计算二叉树叶子结点数
int LeafCount(BiTree T){
	if(T == NULL){
		return 0;
	}
	if(T->lChild == NULL && T->rChild == NULL){
		return 1;
	}else{
		return LeafCount(lChild)+LeafCount(rChild);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

*线索二叉树

提出的问题:如何寻找特定遍历二叉树结点的前驱和后继?

解决方法:1、再遍历寻找---------费时间

​ 2、再 增设前驱、后继指针域---------增加存储负担

​ 3、利用二叉链表中的空指针域

二叉链表中空指针域的数量:

具有n个结点的二叉链表中,共2n个指针域,因为n个结点有n-1个孩子(n-1个边,即指针),所以剩下n+1个空指针域。

利用二叉链表中的空指针域:

如果某结点的左孩子为空,则将其指向其前驱;如果右孩子为空,则指向其后继

----------这种改变指向的指针称为**线索**

加上了线索的二叉树称为线索二叉树(Threaded Binary Tree)

//线索二叉树的结点结构
typedef struct BiThrNode{
	int data;
	int ltag;//ltag=0--->lchild指向该结点的左孩子,ltag=1则指前驱
	int rtag;//rtag=0--->rchild指向该结点的右孩子,rtag=1则指后继
	struct BiThrNode*lchild,rchild;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

树和森林

树的存储结构
*1、双亲表示法

实现:定义结构数组,存放树的结点,每个结点含两个域:

  • 数据域:存放结点本身信息
  • 双亲域:指示本结点在数组中的位置
//树结点
typedef struct PTNode{
	TElemType data;
	int parent;  //双亲位置域
}PTNode;
  • 1
  • 2
  • 3
  • 4
  • 5
//树结构
#define MAX_TREE_SIZE 100
typedef struct{
	PTNode nodes[MAX_TREE_SIZE];
	int r,n;  //根结点的位置和结点个数
}PTree;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
*2、孩子链表

把每个结点的孩子结点排列起来,看成一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子结点的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。

//孩子结点结构
typedef struct CTNode{
	int child;
	struct CTNode *next;
}*ChildPtr;
  • 1
  • 2
  • 3
  • 4
  • 5
//双亲结点结构
typedef struct{
	TElemType data;
	ChildPtr firstchild;  //孩子链表头指针
}CTBox;
  • 1
  • 2
  • 3
  • 4
  • 5
//树结构
typedef struct{
	CTBox nodes[MAX_TREE_SIZE];
	int n,r;  //结点数和根结点的位置
}
  • 1
  • 2
  • 3
  • 4
  • 5
3、孩子兄弟表示法

(二叉树表示法,二叉链表表示法)

实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点下一个兄弟结点

//结点结构
typedef struct CSNode{
	ElemType data;
	struct CSNode *firstchild,*nextchild;
}CSNode,*CSTree;
  • 1
  • 2
  • 3
  • 4
  • 5

树与二叉树的转换
  • 将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。
  • 由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表为媒介,可以导出树与二叉树之间的一个对应关系。
将树转换成二叉树
  1. 加线:在兄弟之间加一条线
  2. 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
  3. 旋转:以树的根结点为轴心,将整树顺时针转45o

口诀:兄弟相连留长子

将森林转化为二叉树
  1. 将各棵树分别转化为二叉树
  2. 将每棵树的根结点用线相连
  3. 旋转

口诀:树变二叉根相连

将二叉树转化为森林

口诀:去掉全部右孩线,孤立二叉再还原

森林的遍历

先序遍历:若树不空,则

1、访问森林中第一棵树的根结点

2、先序遍历森林中第一棵树的子树森林

3、先序遍历森林中(除第一棵树之外)其余树构成的森林

即:依次从左至右对森林中的每一棵树进行先根遍历

中序遍历:若树不空,则

1、中序遍历森林中第一棵树的子树森林

2、访问森林中第一棵树的根结点

3、中序遍历森林中(除第一棵树之外)其余树构成的森林

即:依次从左至右对森林中的每一棵树进行后根遍历


哈夫曼树及其应用

哈夫曼树(最优二叉树)的基本概念:

路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径

结点的路径长度:两结点路径上的分支数

树的路径长度:从树根到每一个结点的路径长度之和,记作:TL

(结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树)

权(weight):将树中结点赋给一个有着某种含义的数值,这个数值称为该结点的权

结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积

树的带权路径长度:树中所有叶子结点的带权路径长度之和

->哈夫曼树:最优树(带权路径长度(WPL)最短的树)

注:“带权路径长度最短”是在“度相同”的树中比较而得到的结果


图的存储——邻接矩阵

邻接矩阵的存储表示:用两个数组分别存储顶点表邻接矩阵

#define MVNum 100 //最大顶点数
#define INF 0xff
typedef struct{
	int vexs[MVNum]; //顶点表
	int arcs[MVNum][MVNum]; //邻接矩阵
    int vexnum,arcnum; //图当前的顶点数和边数
}AMGraph;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

创建无向网

void CreateUDN(AMGraph &G){
	cin >> G.vexnum >> G.arcnum;
	for(int i=0;i<G.vexnum;++i){
		cin>>G.vexs[i]; //输入顶点信息
	}
	for(int i=0;i<G.vexnum;++i){
		for(int j=0;j<G.vexnum;++j){
			G.arcs[i][j]=INF; //边的初始值设为最大值
		}
	}
    for(int k=0;k<G.arcnum;++k){
        cin >> v1 >> v2 >> w; //v1,v2代表的是顶点表中的顶点
        int i = LocateVex(G,v1);//查找v1顶点的下标
        int j = LocateVex(G,v2);//查找v2顶点的下标
        
        G.arcs[i][j]=w;
        G.arcs[j][i]=G.arcs[i][j];//无向图对称
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

图的存储——邻接表

顶点的顶点结构

typedef struct VNode{
	int data; //顶点信息
	ArcNode* firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum]; //AdjList表示邻接表类型
//PS:  AdjList v <=> VNode v[MVNum]
  • 1
  • 2
  • 3
  • 4
  • 5

弧(边)的结点结构

typedef struct ArcNode{
	int adjvex; //该边所指向的顶点的位置(即哪一个顶点的边,adjList的下标)
	struct ArcNode* next; //指向下一条边的指针
	int weight; //和边相关的信息,如权值
}ArcNode;
  • 1
  • 2
  • 3
  • 4
  • 5

图的结构定义

typedef struct{
	AdjList vertices; //即头顶点数组
	int vexnum,arcnum; //图的当前顶点数和边数
}ALGraph;
  • 1
  • 2
  • 3
  • 4

创建无向网

void CreateUDG(ALGraph &G){
	cin >> G.vexnum >> G.arcnum;
	for(int i=0;i<G.vexnum;++i){
		cin>>G.vertices[i].data; //输入顶点值
		G.vertices[i].firstarc = NULL; //初始化表头指针域
	}
	
	for(int k=0;k<G.arcnum;++k){
		cin >> v1 >> v2;
		i = LocateVex(G,v1);
		j = LocateVex(G,v2); //找到下标
		
		p1 = new ArcNode;  //生成一个新的边结点p1
		p1->adjvex = j;  //邻点序号为j
		p1->next = G.vertices[i].firstarc;
		G.vertices[i].firstarc = p1;  //头插法
		
		p2 = new ArcNode; //生成另一个对称的新的边界点p2
		p2->adjvex = i;
		p2->next = G.vertices[j].firstarc;
		G.vertices[j].firstarc = p2; //无向图对称操作
		
		
		
		
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

图的遍历——深度优先

邻接矩阵表示
void DFS(AMGraph G, int v){
	cout << v;
	visited[v] = true; //先访问第v个结点
	for(int w=0;w<G.vexnum;++w){
		if((G.arcs[v][w]!=0) && (!Visited[w])){
			DFS(G,w);
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

图的遍历——广度优先

void BFS(Graph G, int v){
	cout << v;
	visited[v] = true; //先访问第v个结点
	
	InitQueue(Q); //队列初始化
	EnQueue(Q,v); //v进队
	while(!QueueEmpty(Q)){ //队列非空循环
		DeQueue(Q,u);  //队头元素出队
		for(int w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w)){
			if(!visited[w]){
				cout << w;
				visited[w] = true;
				EnQueue(Q,w);  //w进队
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/513821
推荐阅读
相关标签
  

闽ICP备14008679号