第二章 线性表

线性表的定义和基本操作

线性表的定义

线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表,若当L命名线性表,则其一般表示为L=(a1,a2,.…,an)

ai 是线性表中“第i个”元素线性表中的位序

a1 是表头元素,an 是表尾元素

出第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

线性表的基本操作

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

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

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

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

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

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

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

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

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

顺序表的定义

顺序表——用顺序存储的方式实现线性表

顺序表的特点

  1. 随机访问,可以在O(1)时间内找到第i个元素
  2. 存储密度高,每个节点只存储数据元素
  3. 拓展容量不方便,静态不可以拓展,动态分配拓展时间复杂度高
  4. 插入,删除操作不方便,需要移动大量元素

顺序表的实现

顺序表的实现—静态分配(存储空间是静态的,无法更改)

#include<stdio.h>
#define MaxSize 10 //定义最大长度
//创建静态顺序表的结构
typedef struct{
	int data[MaxSize];//用静态的数组存放数据元素
	int lengeh;//顺序表的长度
}SqList;//顺序表的类型定义
//初始化静态顺序表
void InitList(SqList &L){
	for(int i=0;i<MaxSize;i++){
		L.data[i]=0;
	}
	L.lengeh=0;
}
int main(){
	SqList L;
	InitList(L);
	return 0;
}

顺序表的实现—动态分配

//动态 顺序表
#include<stdio.h>
#include<stdlib.h>
#define InitSize 10//顺序表的初始长度
struct SqList {
	int* data;//只是动态分配数组的指针
	int MaxSize;//顺序表的最大容量
	int lengh;//顺序表的当前长度
};
void InitList(struct SqList* L) {
	L->data = (int*)malloc(sizeof(int) + InitSize);
	for (int i = 0; i < L->MaxSize; i++)
	{
		L->data[i] = 0;
	}
	L->lengh = 0;
	L->MaxSize = InitSize;
}
//增加动态数组的长度
void IncreaseList(struct SqList* L, int len) {
	//用malloc函数申请一片连续的存储空间
	int* p = L->data;
	L->data = (int*)malloc(sizeof(int) * (L->MaxSize + len));
	//将数据复制到新区域中
	for (int i = 0; i < L->lengh; i++)
	{
		L->data[i] = p[i];
	}
	L->MaxSize = L->MaxSize + len;//顺序表最长长度增加len
}
void ListInsert(struct SqList *L,int i ,int e) {
	for (int j = L->lengh; j >=i; j--)
	{
		L->data[j] = L->data[j - 1];
	}
	L->data[i - 1] = e;
	L->lengh++;
}
int main() {
	struct SqList L = { 0 };
	InitList(&L);
	printf("%d\n", L.lengh);
	printf("%d\n", L.MaxSize);
	for (int i = 0; i < L.MaxSize; i++)
	{
		(&L)->data[i] = i;
	}
	(&L)->lengh = L.MaxSize;
	for (int i = 0; i < L.lengh; i++)
	{
		printf("%d ", L.data[i]);
	}
	printf("\n");
	IncreaseList(&L, 10);
	printf("%d\n", L.lengh);
	printf("%d\n", L.MaxSize);
	for (int i = 0; i < L.lengh; i++)
	{
		printf("%d ", L.data[i]);
	}
	ListInsert(&L, 2, 2);
	printf("%d\n", L.lengh);
	printf("%d\n", L.MaxSize);
	for (int i = 0; i < L.lengh; i++)
	{
		printf("%d ", L.data[i]);
	}
	return 0;
}

顺序表的基本操作

顺序表元素的插入

void ListInsert(struct SqList *L,int i ,int e) {
	for (int j = L->lengh; j >=i; j--)
	{
		L->data[j] = L->data[j - 1];
	}
	L->data[i - 1] = e;
	L->lengh++;
}

最好情况 :新元素插入到表尾,不需要移动其他元素,循环0次,时间复杂度=O(1)

最坏情况 :新元素插入到表头,需要将原有的n个元素全部向后移动,循环n次,最坏时间复杂度=O(n)

平均情况 :假设新元素插入到任何一个位置的概率相同,即i=1,2,3,...length的概率都是p=1/(n+1),则平均循环次数=np+(n-1)p+....+p=n/2,平均时间复杂度=O(n)

顺序表元素的删除

int ListDelete(struct SqList *L,int i,int *e) {
	//判断
	if (i<1||i>L->lengh)//判断i的值是否有效
	{
		return 0;
	}
	else
	{
		*e = L->data[i - 1];//将删除的元素赋值给e
		for (int j = i; j < L->lengh; j++)//将第i个位置的元素前移
		{
			L->data[j - 1] = L->data[j];
		}
		L->lengh--;//有效长度减一
		return 1;
	}
}

最好情况 :删除表尾元素,不需要移动其他元素,循环0次,时间复杂度=O(1)

最坏情况 :删除表头元素,需要将后续的n-1个元素全部向前移动,循环n-1次,最坏时间复杂度=O(n)

平均情况 :假设删除任何一个元素的概率相同,即i=1,2,3,...length的概率都是p=1/n,则平均循环次数=(n-1)p+(n-2)p+....+p=(n-1)/2,平均时间复杂度=O(n)

顺序表的按位查找

GetElem(L,i)按位查找操作,获取表L中的第i位序的元素的值

ElemType GetElem(SeqList L,int i){
    return L.data[i - 1];
}

顺序表的按值查找

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

ElemType LocateElem(SeqList L, ElemType e) {
	for (int i = 0; i <= L.length; i++) {
		if (L.data[i] == e) {
			return i + 1;
		}
	}
	return -1;
}

在注重考察语法的时候,基本数据类型可以用“==”比较

时间复杂度O(n)

单链表的定义

每个结点除了数据元素外,还可以存储指向下一个结点的指针

头结点和头指针的关系: 不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表的第一个结点,结点内通常不存储信息。

引入头结点后,可以带来两个优点:

  1. 由于第一个数据节点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作在表的其他位置上的操作一致,无须进行特殊处理
  2. 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和费控表的处理也就得到了统一

单链表的特点

优点:不要求大片连续空间,改变容量方方便

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

单链表的实现

点击查看代码
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct LNode {
	ElemType data;
	struct LNode *next;
} LNode, *LinkList;
bool InitList(LinkList &L) {
	L = NULL;
	return true;
}
bool InitList_h(LinkList &L) {
	L = (LNode*)malloc(sizeof(LNode));
	if (L == NULL) {
		return false;
	}
	L->next = NULL;
	return true;
}
bool ListInsert_h(LinkList &L, int i, ElemType e) {
	if (i < 1) {
		return false;
	}
	LNode *p;
	int j = 0;
	p = L;
	while (p != NULL && j < i - 1) {
		p = p->next;
		j++;
	}
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}
bool ListInsert(LinkList &L, int i, ElemType e) {
	if (i < 1) {
		return false;
	}
	if (i == 1) {
		LNode *s = (LNode*)malloc(sizeof(LNode));
		s->data = e;
		s->next = L;
		L = s;
		return true;
	}
	LNode *p;
	int j = 1;
	p = L;
	while (p != NULL && j < i - 1) {
		p = p->next;
		j++;
	}
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}
bool InsertNextNode(LNode *p, ElemType e) {
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	if (s == NULL) {
		return false;
	}
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}
bool InsertPriorNode(LNode *p, ElemType e) {
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	if (s == NULL) {
		return false;
	}
	s->next = p->next;
	p->next = s;
	s->data = p->data;
	p->data = e;
	return true;
}
//按位序删除(带头节点)
bool ListDelete(LinkList &L, int i, ElemType &e) {
	if (i < 1) {
		return false;
	}
	LNode *p;
	int j = 0;
	p = L;
	while (p != NULL && j < i - 1) {
		p = p->next;
		j++;
	}
	if (p == NULL) {
		return false;
	}
	if (p->next == NULL) {
		return false;
	}
	LNode *q = p->next;
	e = q->data;
	p->next = q->next;
	free(q);
	return true;
}
bool DeleteNode(LNode *p) {
	if (p == NULL) {
		return false;
	}
	LNode *q = p->next;
	p->data = p->next->data;
	p->next = q->next;
	free(q);
	return true;
}
LNode* GetElem_h(LinkList L, int i) {
	if (i < 0) {
		return NULL;
	}
	LNode *p;
	int j = 0;
	p = L;
	while (p != NULL && j < i) {
		p = p->next;
		j++;
	}
	return p;
}
LNode* LocateElem(LinkList L, ElemType e) {
	LNode *p = L->next; //指向头结点的下一个节点
	while (p != NULL && p->data != e) {
		p = p->next;
	}
	return p;
}
int Length_h(LinkList L) {
	int len = 0;
	LNode *p = L;
	while (p->next != NULL) {
		p = p->next;
		len++;
	}
	return len;
}
LinkList List_HeadInsert(LinkList &L) {
	LNode *s;
	int x;
	L = (LNode*)malloc(sizeof(LNode));
	L->next = NULL;
	scanf("%d", &x);
	while (x != 9999) {
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		s->next = L->next;
		L->next = s;
		scanf("%d", &x);
	}
}
LinkList List_TailInsert(LinkList &L) {
	int x;
	L = (LNode*)malloc(sizeof(LNode));
	LNode *s, *r = L;
	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;
}

int main() {
	LinkList L;

	return 0;
}

结构体创建

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

初始化

带头结点

bool InitList_h(LinkList &L) {
	L = (LNode*)malloc(sizeof(LNode));
	if (L == NULL) {
		return false;
	}
	L->next = NULL;
	return true;
}

带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点。头结点的next域初始化为NULL

不带头结点

bool InitList(LinkList &L) {
	L = NULL;
	return true;
}

不带头结点的单链表初始化时,只需将头指针L初始化为NULL

单链表的基本操作

按位序插入(带头节点)

bool ListInsert_h(LinkList &L, int i, ElemType e) {
	if (i < 1) {
		return false;
	}
	LNode *p;
	int j = 0;
	p = L;
	while (p != NULL && j < i - 1) {
		p = p->next;
		j++;
	}
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

本算法主要的时间开销在于查找第i-1个元素,,时间复杂度为O(n)

按位序插入(不带头结点)

bool ListInsert(LinkList &L, int i, ElemType e) {
	if (i < 1) {
		return false;
	}
	if (i == 1) {
		LNode *s = (LNode*)malloc(sizeof(LNode));
		s->data = e;
		s->next = L;
		L = s;
		return true;
	}
	LNode *p;
	int j = 1;
	p = L;
	while (p != NULL && j < i - 1) {
		p = p->next;
		j++;
	}
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

当链表不带头结点时,需要判断插入位置i是否为1,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,插入位置i为1时不做特殊处理。

指定结点的后插操作

bool InsertNextNode(LNode *p, ElemType e) {
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	if (s == NULL) {
		return false;
	}
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

在指定结点后插入新结点,时间复杂度为O(1)

指定结点的前插操作

bool InsertPriorNode(LNode *p, ElemType e) {
	if (p == NULL) {
		return false;
	}
	LNode *s = (LNode*)malloc(sizeof(LNode));
	if (s == NULL) {
		return false;
	}
	s->next = p->next;
	p->next = s;
	s->data = p->data;
	p->data = e;     //结点放后面,交换值
	return true;
}

在指定结点后插入新结点,时间复杂度为O(1)

按位序删除(带头节点)

bool ListDelete(LinkList &L,int i,ElemType &e){
	if(i<1){
		return false;
	}
	LNode *p;
	int j=0;
	p=L;
	while(p!=NULL&&j<i-1){
		p=p->next;
		j++;
	}
	if(p==NULL){
		return false;
	}
	if(p->next==NULL){//确定第i个节点存在
		return false;
	}
	LNode *q=p->next;
	e=q->data;
	p->next=q->next;
	free(q);
	return true;
}

时间复杂度为O(n)

指定结点的删除

bool DeleteNode(LNode *p){
	if(p==NULL){
		return false;
	}
	LNode *q=p->next;
	p->data=p->next->data;
	p->next=q->next;
	free(q);
	return true;
}

实质就是将其后继的值赋予其自身,然后再删除后继,也能使得时间复杂度为O(1).

这种算法存在缺陷,当要删除的结点是最后一个结点的时候,无法顺利删除,因为无法找到尾结点的后继

按位查找

LNode* GetElem_h(LinkList L, int i) {
	if (i < 0) {
		return NULL;
	}
	LNode *p;
	int j = 0;   //头结点是第0个结点
	p = L;
	while (p != NULL && j < i) {
		p = p->next;
		j++;
	}
	return p;
}

时间复杂度O(n)

按值查找

LNode* LocateElem(LinkList L,ElemType e){
	LNode *p=L->next;//指向头结点的下一个节点
	while(p!=NULL&&p->date!=e){
		p=p->next;
	}
	return p;
}

时间复杂度O(n)

求表的长度

int Length_h(LinkList L) {
	int len = 0;
	LNode *p = L;
	while (p->next != NULL) {
		p = p->next;
		len++;
	}
	return len;
}

带头结点的单链表中求表长len的初始长度为0,因为头结点是第0个结点。不带头结点的单链表中求表长len的初始长度为1,因为直接从第一个结点开始计数。

求表长操作的时间复杂度为O(n)

单链表的建立

尾插法

该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点

LinkList List_TailInsert(LinkList &L){
	int x;
	L=(LNode*)malloc(sizeof(LNode));
	LNode *s,*r=L;
	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;
}

头插法

该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后

LinkList List_TailInsert(LinkList &L){
	int x;
	L=(LNode*)malloc(sizeof(LNode));
	LNode *s,*r=L;
	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;
}

采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实现链表的逆置

双链表

双链表结点中有两个指priornext,分别指向其直接前驱和直接后继

双链表的特点

双链表在单链表结点中增加了一个指向前驱的指针prior,因此双链表的按值查找和按位查找的操作与单链表相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同,这是因为“链”变化时也需要对指针prior做出修改,其关键是保证在修改的过程中不断链

双链表的实现

结构体创建

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

初始化

带头结点

bool InitLinkList_h(DLinkList &L){
	L=(DNode*)malloc(sizeof(DNode));
	if(L==NULL){
		return false;
	}
	L->prior=NULL;
	L->next=NULL;
}

不带头结点

bool IinitLinkList(DLinkList &L){
	L=NULL;
	return true;
}

双链表的基本操作

给定结点的后插操作

bool InsertNextDNode(DNode *p, DNode *s) {
	if (p == NULL || s == NULL) {
		return false;
	}
	s->next = p->next;
	if (p->next != NULL) {
		p->next->prior = s;
	}
	s->prior = p;
	p->next = s;
	return true;
}

给定结点的前插操作

bool InsertPriorDNode(DNode *p, DNode *s) {
	if (p == NULL || s == NULL) {
		return false;
	}
	s->next = p;
	s->prior = p->prior;
	if (p->prior != NULL) {
		p->prior->next = s;
	}
	p->prior = s;
	return true;
}

删除给定结点的后继结点

bool DeleteNextDNode(DNode *p) {
	if (p == NULL) {
		return false;
	}
	DNode *q = p->next;
	if (q == NULL) {
		return false;
	}
	p->next = q->next;
	if (q->next != NULL) {
		q->next->prior = p;
	}
	free(q);
	return true;
}

双链表的销毁

void DestroyList(DLinkList &L) {
	while (L->next != NULL) {
		DeleteNextDNode(L);
	}
	free(L);
	L = NULL;
}

循环链表

循环单链表

在循环单链表中,表尾结点*r的next指针域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针L

初始化

bool InitList(LinkList &L) {
	L = (LNode*)malloc(sizeof(LNode));
	if (L == NULL) {
		return false;
	}
	L->next = L;//头结点的next指向头结点
	return true;
}

判断给定结点是否为循环单链表的表尾结点

bool isTail(LinkList L, LNode *p) {
	if (p->next == L) {
		return true;
	} else {
		return false;
	}
}

循环双链表

在循环双链表中,头结点的prior指针还要指向表尾结点,当某结点*p为尾结点时,p->next==L;当循环双链表为空时,其头结点的prior域和next域都等于L

初始化

bool InitDLinkList(DLinkList &L) {
	L = (DNode*)malloc(sizeof(DNode));
	if (L == NULL) {
		return false;
	}
	L->next = L;
	L->prior = L;
	return true;
}

静态链表

静态链表是用数组来描述线性表的链式存储结构,结点也有数据域和指针域,这里的指针是结点在数组中的相对位置,又称游标,指针指向下一个结点的位置。
静态链表也要预先分配一块连续的内存空间。
image

typedef struct {
	ElemType data;
	int next;
}SLinkList[MaxSize];//一个长度为MaxSize的数组

插入位序为i的结点

  1. 找到一个空的结点,存入输入元素
  2. 从头结点出发找到位序为i-1的结点
  3. 修改新结点的next
  4. 修改i-1号结点的next

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

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

顺序表和链表的比较

逻辑结构

都属于线性表,都是线性结构

存储结构

顺序表

优点:支持随机存取,存储密度高

缺点:大片连续空间分配不方便,改变容量不方便

链表

优点:离散的小空间分配方便,改变容量方便

缺点:不可随机存取,存储密度小

基本操作

顺序表

创建:需要预分配大片连续空间,若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存空间

插入、删除:要将后续元素都后移或前移,时间复杂度为O(n),主要开销是移动元素,若移动元素过大,则移动的时间代价很高

查找:按位查找:O(1) 按值查找O(n) 若表中元素有序,可在O(ln(n))时间内找到

链表

创建:只需分配一个头结点(也可以不用头结点,只声明一个头指针),之后方便拓展

插入,删除:只需修改指针,时间复杂度为O(n),时间开销主要来自查找目标元素,查找元素的时间代价更低

查找:按位查找O(n) 按值查找O(n)

表长难以估计,经常需要增加、删除元素 ——链表

表长可预估,查询操作较多 ——顺序表

posted @ 2024-08-06 14:23  Eulbo_1018  阅读(46)  评论(0)    收藏  举报