线性表

120 阅读6分钟

定义(逻辑结构)

线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为L=(a****1 ,a2…,a****i ,ai+1…,a****n )

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

基本操作(运算)

创销

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(L,i):按位查找操作。获取表L中第个位置的元素的值。

其他常用操作:

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

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

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

&位取地址符号,在传参时,除了查,增删改这种真实传值都要加上。

数据元素大小:sizeof()函数

存储结构(物理结构)

主要分为顺序表和链表,顺序表要求物理上存储也相邻,链表物理上存储可以不相邻。

顺序表(顺序存储)

定义

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

优点:

1、可以随机存取

2、存储密度高

缺点:

3、扩展容量不方便

4、增删数据不方便

静态分配定义

#define ListMaxSize 10	//定义最大长度
typedef struct {		
	int data[ListMaxSize];	//使用静态数组存放数据
	int length;				//顺序表当前长度
}SqList;				//顺序表类型定义(静态分配)
//初始化顺序表
void InitList(SqList &L) {
	for (int i = 0; i < ListMaxSize; i++)
	{
		L.data[i] = 0;
	}
	L.length = 0;
}

int main() {
	//初始化顺序表
	SqList L;
	InitList(L);

	//违规打印所有值
	for (int i = 0; i < ListMaxSize; i++)
	{
		printf("%d", L.data[i]);
	}
	return 0;
}

动态分配定义

#define ListMaxSize 10	//定义初始长度
typedef struct {		
	int* data;		//指示动态分配数组的指针
	int MaxSize;	//顺序表的最大容量
	int length;		//顺序表当前长度
}SqList;				//顺序表类型定义(动态分配)

动态申请和释放空间malloc函数和free函数

#include<stdio.h>
#include<stdlib.h>

void InitList(SqList& L) {
	//用malloc函数申请一片连续的空间
	L.data = (int*)malloc(sizeof(int) * InitSize);
	L.length = 0;
	L.MaxSize = InitSize;
}

void IncreaseSize(SqList& L, int len) {
	int* temp = L.data;
	L.data = (int*)malloc(sizeof(int) * (InitSize + len));
	for (int i = 0; i < L.length; i++)
	{
		L.data[i] = temp[i];
	}
	L.MaxSize = L.MaxSize + len;
	free(temp);
}

int main() {
	//初始化顺序表
	SqList L;
	InitList(L);
	IncreaseSize(L, 3);
	//违规打印所有值
	for (int i = 0; i < L.MaxSize; i++)
	{
		printf("%d", L.data[i]);
	}
	return 0;
}

插入操作

//插入操作
bool insertElement(SqList& L, int index, int element) {
	if (index<1 || index>L.length + 1) {
		return false;
	}
	if (L.length == ListMaxSize) {
		return false;
	}
	for (int j = L.length; index <= j; j--)
	{
		L.data[j] = L.data[j - 1];
	}
	L.data[index - 1] = element;
	L.length++;
	return true;
}

最好时间复杂度:O(1)

最坏时间复杂度:O(n)

平均时间复杂度:O(n)

删除操作

//删除操作
bool deleteElement(SqList& L, int index, int element) {
	if (index<1 || index>L.length) {
		return false;
	}
	if (L.length == 0) {
		return false;
	}
	for (int j = index; j < L.length; j++)
	{
		L.data[j - 1] = L.data[j];
	}
	L.data[L.length - 1] = 0;//可以不写
	L.length--;
	return true;
}

最好时间复杂度:O(1)

最坏时间复杂度:O(n)

平均时间复杂度:O(n)

查找操作(按位置查找)

//按位查找
int GetElementByIndex(SqList L, int i) {
	return L.data[i - 1];
}

最好时间复杂度:O(1)

最坏时间复杂度:O(1)

平均时间复杂度:O(1)

查找操作(按值查找)

//按值查找
int GetElementByData(SqList L, int element) {
	for (int i = 0; i < L.length; i++){
		if (L.data[i] == element){
			return i + 1;
		}
		return 0;
	}
}

最好时间复杂度:O(1)

最坏时间复杂度:O(n)

平均时间复杂度:O(n)


链表(链式存储)

优点:

可随机存取,存储密度高

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

缺点:

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

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

单链表

两种定义方式:

typedef struct LNode {		//定义单链表节点类型
	int data;				//每个节点存放一个数据元素
	struct LNode* next;		//指针指向下一个节点
}LNode, *LinkList;

struct LNode {
	int data;				//每个节点存放一个数据元素
	struct LNode* next;		//指针指向下一个节点
};
typedef struct LNode LNode;
typedef struct LNode* LinkList;
//单链表后插入节点
p=GetElem(L,i-1);//查找插入位置的前驱结点
s->next=p->next;//令新节点s指向p的后继
p->next=s;//s成为p新的后继

//单链表删除节点
p=GetElem(L,i-1);//查找删除位置的前驱结点
q=p->next;//令q指向被删除结点
p->next=q->next;//将*g结点从链中“断开”
free(q);/释放结点的存储空间

双链表

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

新增需要修改4个指针,根据已知指针的节点的前后,优先修改将要被覆盖的地址,否则会发生地址丢失。

//双链表新增节点
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
//双链表删除节点
p->next=q->next;
q->next->prior=p;
free(q);

循环单链表

p->rear = L;//表尾节点的尾指针p指向头部形成环

判空条件:L->next = L

循环双链表

p->next = L;//表尾节点的尾指针p指向头部形成环
L->prior = p;//头节点的前驱指向表尾节点

判空条件:L->next = L && L->prior = L

静态链表

#define ListMaxSize 50
typedef struct {		
	int data[ListMaxSize];	//使用静态数组存放数据
	int next;
}SqList[ListMaxSize];


顺序表和链表的比较

1.存取方式

顺序表可以顺序存取,也可以随机存取。

链表只能从表头顺序存取元素。

2.逻辑结构与物理结构

采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。

采用链式存储时,逻辑上相邻的元素,其物理存储位置则不一定相邻,其对应的逻辑关系是通过指针链接来表示的。

3.查找、插入和删除操作

对于按值查找,当顺序表在无序的情况下,两者的时间复杂度均为O(n);而当顺序表有序时,可采用折半查找,此时时间复杂度为O(logzn)。

对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1),而链表的平均时间复杂度为o(n)。

顺序表的插入、删除操作,平均需要移动半个表长的元素。

链表的插入、删除操作,只需要修改相关结点的指针域即可。由于链表每个结点带有指针域,因而在存储空间上比顺序存储要付出较大的代价,存储密度不够大。

4.空间分配

顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,如果再加入新元素将出现内存溢出,需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间将导致分配失败。

链式存储的结点空间只在需要的时候申请分配,只要内存有空间就可以分配,操作灵活、高效。

在实际中应该怎样选取存储结构呢?

1.基于存储的考虑

对线性表的长度或存储规模难以估计时,不宜采用顺序表;链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。

2.基于运算的考虑

在顺序表中按序号访问a的时间复杂度为O(1),而链表中按序号访问的时间复杂度为0(m),所以如果经常做的运算是按序号访问数据元素,显然顺序表优于链表。

在顺序表中做插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;在链表中做插入、删除操作时,虽然也要找插入位置,但操作主要是比较操作,从这个角度考虑显然后者优于前者。

3.基于环境的考虑

顺序表容易实现,任何高级语言中都有数组类型;链表的操作是基于指针的,相对来讲,前者实现较为简单,这也是用户考虑的一个因素。

总之,两种存储结构各有长短,选择哪一种由实际问题的主要因素决定。通常较稳定的线性表选择顺序存储,而频繁做插入、删除操作的线性表(即动态性较强)宜选择链式存储。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情