数据结构复习——线性表

尚未完成awa
所有代码均来自《2023年数据结构考研复习指导》

线性表

在这里插入图片描述

线性表的基本操作

//非正确写法
InitList(&L);//初始化表
Length(L);//求表长
LocateElem(L,e);//按值查找操作
GetElem(L,i);//按位查找
ListInsert(&L,i,e);//插入操作
ListDelete(&L,i,&e);//删除操作
PrintList(L);//输出操作
Empty(L);//判空操作
DestroyList(L);//销毁操作

顺序表

顺序表的定义

1.静态分配

//静态分配 
#define MaxSize 50			//定义线性表的最大长度 
typedef struct{
	ElemType data[MaxSize];	//顺序表的元素 
	int length;				//顺序表的当前长度 
}SqList;					//顺序表的类型定义 

2.动态分配

//动态分配
#define InitSize 100		//表长度的初始定义 
typedef struct{
	ElemType *data;			//指示动态分配数组的指针 
	int MaxSize,length;		//数组的最大容量和当前个数 
}SeqList;					//动态分配数组顺序表的类型定义 

C的动态初始分配语句

L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);

C++的动态初始分配语句

L.data = new ElemType[InitSize];

顺序表上基本操作的实现(使用静态分配)

/*************************************************/
//定义的是静态分配链表 
#define SqListMaxSize 50			//定义线性表的最大长度 
typedef struct {
	int data[SqListMaxSize];		//顺序表的元素 
	//ElemType data[MaxSize];
	int length;				//顺序表的当前长度 
}SqList;					//顺序表的类型定义 

//初始化
void InitList(SqList& L) {
	memset(L.data, 0, sizeof(L));
	L.length = 0;
	return ;
}

//线性表长度
int Length(SqList& L) {
	return L.length;
}

//判空
bool Empty(SqList& L) {
	return Length(L) == 0;
}

//插入操作
bool ListInsert(SqList& L, int i, int e) {
	if (i<1 || i>L.length + 1)				//判断i的范围是否有效
		return false;
	if (L.length >= SqListMaxSize)				//当前存储空间已满,不能插入
		return false;
	for (int j = L.length; j >= 1; j--)		//将第i个元素及之后的元素后移
		L.data[j] = L.data[j - 1];
	L.data[i - 1] = e;						//在位置i处放入e
	L.length++;								//线性表长度加1
	return true;
}

//删除操作
bool ListDelete(SqList& L, int i, int& e) {
	if (i<1 || i>L.length)					//判断i的范围是否有效
		return false;
	e = L.data[i - 1];						//将被删除的元素赋值给e
	for (int j = 1; j < L.length; j++)		//将第i个位置后的元素前移
		L.data[j - 1] = L.data[j];
	L.length--;								//线性表长度减1
	return true;
}

//按值查找
int LocateElem(SqList L, int e) {
	int i;
	for (i = 0; i < L.length; i++)
		if (L.data[i] == e)					//下表为i的元素值等于e,返回其位序i+1
			return i + 1;					//退出循环,说明查找失败
	return 0;
}

//位序查找
int GetElem(SqList L, int i) {
	return L.data[i];
}

//打印
void PrintList(SqList L) {

}

//销毁
void DestroyList(SqList& L) {
	//使用的静态链表,由系统自动回收
}

链表

单链表

单链表的定义

1.表结点的定义

typedef struct LNode {
	ElemType data;	//数据域
	struct LNode* next;	//指针域
}LNode,*LinkList;

单链表是非随机存取的存储结构,即不能直接找到表中某个特定的结点。
在查找某个特定的结点时,需要从表头开始遍历。

2.分类
1)不带头结点
2)带头结点

引入头结点带来两个优点:
① 由于第一个数据节点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置的操作一致,无需进行特殊处理
② 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为NULL),因此空表和非空表的处理也得到了统一

单链表上基本操作的实现

1.采用头插法建立单链表

LinkList List_HeadInsert(LinkList& L) {
	//头插法建立单链表
	LNode* s;
	int x;
	L = (LinkList)malloc(sizeof(LNode));	//头结点
	L->next = NULL;
	scanf("%d", &x);
	while (x != 9999) {						//此处9999为结束标志,实际使用中根据情况自行设置
		s = (LNode*)malloc(sizeof(LNode));	//新结点
		s->data = x;
		s->next = L->next;	
		L->next = s;
		scanf("%d", &x);
	}
	return L;
 }

设所建立的单链表表长为 n n n,则头插法建立单链表的时间复杂度为 O ( n ) O(n) O(n)

2.采用尾插法建立单链表

LinkList List_TailInsert(LinkList& L) {
	//尾插法建立单链表
	int x;
	L = (LinkList)malloc(sizeof(LNode));	
	LNode* s, * r = L;	//r为表尾指针
	scanf("%d", &x);
	while (x != 9999) {
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		r->next = s;
		r = s;
		scanf("%d", &x);
	}
	r->next	= NULL;	//尾结点指针置空
	return L;
}

设所建立的单链表表长为 n n n,则尾插法建立单链表的时间复杂度为 O ( n ) O(n) O(n)

3.按序号查找结点值

LNode* GetElem(LinkList L, int i) {
	//按序号查找结点值
	int j = 1;
	LNode* p = L->next;
	if (i == 0)
		return L;
	if (i < 1)
		return NULL;
	while (p && j < i) {	
		p = p->next;
		j++;
	}
	return p;	//若i大于表长,则返回NULL
}

按序号查找结点值的时间复杂度为 O ( n ) O(n) O(n)

4.按值查找表结点

LNode* LocateElem(LinkList L, ElemType e) {
	//按值查找表结点
	LNode* p = L->next;
	while (p != NULL && p->data != e)
		p = p->next;
	return p;
}

按值查找结点值的时间复杂度为 O ( n ) O(n) O(n)

5.插入结点操作
插入节点操作是将值为 x x x的新结点插入到单链表的第 i i i个位置上。

应先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第 i − 1 i-1 i1个结点,再在其后插入新结点。
算法首先调用按序号查找算法,查找第 i i i个结点。

	//s是指向新结点的指针
	//p是指向第i-1个结点的指针
	p = GetElem(L, i - 1);
	s->next = p->next;
	p->next = s;

本算法主要的时间开销在于查找第i-1个元素,时间复杂度为 O ( n ) O(n) O(n)
若在给定的结点后面插入新结点,则时间复杂度为 O ( 1 ) O(1) O(1)

6.对某一节点进行前插操作
前插操作:在某结点的前面插入新结点
后插操作:在某结点的后面插入新结点

对结点的前插操作均可转化为后插操作,前提是从单链表的头结点开始遍历,找到其前驱结点,时间复杂度为 O ( n ) O(n) O(n)

此外,可以采用另一种方式:

	//待插入结点为*s,欲将*s插入到*p的前面
	//仍然将*s插入到*p后面,然后将p->data与s->data交换
	s->next = p->next;
	p->next = s;
	temp = p->data;
	p->data = s->data;
	s->data = temp;

该算法,既满足了逻辑关系,又能使得时间复杂度为 O ( 1 ) O(1) O(1)

7.删除结点操作
删除节点操作是将单链表的第 i i i个结点删除

应先检查删除位置的合法性,后查找表中第 i − 1 i-1 i1个结点,即被删除结点的前驱结点,再将其删除

	//结点*p为找到的被删除结点的前驱结点
	//仅需修改*p的指针域,将*p的指针域next指向*q的下一个结点
	p = GetElem(L, i - 1);
	q = p->next;
	p->next = q->next;
	free(q);

该算法的主要时间耗费在查找操作上,时间复杂度为 O ( n ) O(n) O(n)

还有另一种实现方式

	p = GetElem(L, i - 1);
	q = p->next;
	p->data = p->next->data;
	p->next = q->next;
	free(q);

个人感觉没啥用
8.求表长操作
设置一个计数器,从表头开始遍历,每访问一个结点,计数器值+1,直至访问到空结点。
算法时间度为 O ( n ) O(n) O(n)

双链表

双链表的定义

1.为什么要使用双链表
单链表只有后继指针,限制了单链表的遍历操作只能从头结点进行
在获取特定结点的前驱结点时,只能从头遍历
为解决单链表的该缺点,双链表设置了前驱指针和后继指针。

2.双链表表结点的定义

typedef struct DNode {
	ElemType data;						//数据域
	struct DNode* prior, * next;	//前驱指针和后继指针
}DNode, * DLinkList;
双链表上基本操作的实现

1.头插法建立双链表

DLinkList DList_HeadInsert(DLinkList& L) {
	//头插法建立双链表
	DNode* s;
	int x;
	L = (DLinkList)malloc(sizeof(DNode));	//头结点
	L->next = NULL;
	L->prior = NULL;
	scanf("%d", &x);
	while (x != 9999) {						//此处9999为结束标志,实际使用中根据情况自行设置
		s = (DNode*)malloc(sizeof(DNode));	//新结点
		s->data = x;
		s->next = L->next;
		s->next->prior = s;
		L->next = s;
		s->prior = L;
		scanf("%d", &x);
	}
	return L;
}

2.尾插法建立双链表

DLinkList DList_TailInsert(DLinkList& L) {
	//尾插法建立双链表
	int x;
	L = (DLinkList)malloc(sizeof(DNode));
	DNode* s, * r = L;	//r为表尾指针
	scanf("%d", &x);
	while (x != 9999) {
		s = (DNode*)malloc(sizeof(DNode));
		s->data = x;
		r->next = s;
		s->prior = r;
		//->next = NULL;
		//由于最后有尾结点指针置空,这步可以省略
		r = s;
		scanf("%d", &x);
	}
	r->next = NULL;	//尾结点指针置空
	return L;
}

3.按序号查找结点值

DNode* GetElem(DLinkList L, int i) {
	//按序号查找结点值
	int j = 1;
	DNode* p = L->next;
	if (i == 0)
		return L;
	if (i < 1)
		return NULL;
	while (p && j < i) {
		p = p->next;
		j++;
	}
	return p;	//若i大于表长,则返回NULL
}

按序号查找结点值的时间复杂度为 O ( n ) O(n) O(n)

4.按值查找表结点

DNode* LocateElem(DLinkList L, ElemType e) {
	//按值查找表结点
	DNode* p = L->next;
	while (p != NULL && p->data != e)
		p = p->next;
	return p;
}

按值查找表结点的时间复杂度为 O ( n ) O(n) O(n)

5.双链表的插入操作(后插)
双链表的插入操作是指在双链表中p所指的结点之后插入结点*s

	s->next = p->next;		//1
	p->next->prior = s;	//2
	s->prior = p;				//3
	p->next = s;				//4
	//1,2两步必须在4之前

6.双链表的删除操作
双链表的删除操作是指删除双链表中结点p的后继结点q

	p->next = q->next;
	q->next->prior = p;
	free(q);

循环链表

循环单链表

1.循环单链表
循环单链表和单链表的区别在于:
单链表的尾结点的后继指针是NULL
循环单链表的尾结点的指针改为指向头结点,从而使整个链表形成一个环

2.循环单链表和单链表在操作上的差异
[1]判空
单链表的判空实现为

bool IsEmpty(LinkList L) {
    return (L->next == NULL);
}

循环单链表的判空实现为

bool IsEmpty(LinkList L) {
    return (L->next == L);
}

[2]插入、删除
循环单链表的插入、删除操作与单链表的几乎一样
对于循环单链表需要注意的是,如果插入、删除的位置在表尾,则要注意操作后仍要使得该单链表具有循环的性质,即表尾结点的next域指向表头

[3]遍历
单链表只能从表头结点开始遍历整个链表
而循环单链表可以从任意一个结点开始遍历整个链表
此外,建议在使用循环单链表的时候,不仅设置头指针,最好还设置一个尾指针,以方便对表尾处进行操作

循环双链表

1.循环双链表
循环双链表和双链表的区别在于:
双链表的尾结点的后继指针是NULL,双链表的头结点的前驱指针是NULL
循环双链表的尾结点的指针改为指向头结点,循环双链表的头结点的指针改为指向尾结点,从而使整个链表形成一个环
2.循环双链表和双链表在操作上的差异
[1]判空
循环双链表的判空实现为

bool IsEmpty(DLinkList L) {
    return (L->next == L && L->prior == L);
}

静态链表

静态链表的定义

1.静态链表是如何实现的
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与之前所涉及的指针的概念相区分的是,静态链表的指针值得是结点的相对地址(即为数组下表)。
静态链表需要预先分配一块连续的内存空间
2.静态链表结构类型

#define MaxSize 50	//静态链表的最大长度
typedef struct {
	ElemType data;	
	int next;		//下一个元素的数组下标
}SLinkList[MaxSize];
静态链表上的基本操作

1.静态链表的尾结点
静态链表以

	SLinkList A[MaxSize]; 
	A[k]->next == -1;

作为结束的标志
2.静态链表的插入、删除
静态链表的插入、删除操作与动态链表的相同,只需要修改指针,不需要移动元素。
静态链表并没有单链表使用起来方便,因此更适合于在不支持指针的高级语言中实现。

顺序表和链表的比较

1.存取(读写)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。
例如在第i个位置上执行存或取得操作,顺序表仅需一次访问,而链表则需要从表头开始依次访问i次。
2.逻辑结构和物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻
而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的复杂度均为 O ( n ) O(n) O(n)
顺序表有序时,可采用折半查找,此时的时间复杂度为 l o g 2 n log_2{n} log2n
对于按序号查找,顺序表支持随机访问,时间复杂度为 O ( 1 ) O(1) O(1),而链表的平均时间复杂度为 O ( n ) O(n) O(n)
顺序表的插入、删除操作,平均需要移动半个表长的元素
链表的插入、删除操作,只需要膝盖相关结点的指针域即可。
由于链表的每个结点都带有指针域,故存储密度不够大。
4.空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。
动态存储分配可以对存储空间进行扩充,但是需要移动大量元素,导致操作效率降低,而分配的成功与否则与内存中的情况相关。
链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。

选择存储结构

1.基于存储的考虑
难以估计线性表的长度或存储规模时,不宜采用顺序表
链表不用实现估计存储规模,但是链表的存储密度较低
2.基于运算的考虑
在顺序表中按序号访问 a i a_i ai的时间复杂度为 O ( 1 ) O(1) O(1),而链表中按序号访问的时间复杂度为 O ( n ) O(n) O(n)。若是经常按序号访问数据元素,那么顺序表优于链表。
在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素信息量较大时且表较长时,会很耗时;在链表中进行插入、删除操作时,主要耗时在寻找插入、删除的位置。若是经常进行插入、删除操作,那么链表优于顺序表。

难免有错误之处,请大佬们在评论区指出!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值