数据结构 -- 线性表

166 阅读8分钟

线性表

线性表的顺序表示和实现

顺序存储定义:在物理上相邻的存储单元中的存储结构,地址连续,即占用一片连续的存储空间

顺序表中元素存储位置的计算:

如果每个元素占用8个内存空间,ai的存储位置是2000单元,则ai+1存储位置是2008 ai+2 = 2000 + 2 * 8,ai+3 = 2000 + 3 * 8

顺序表的特点: 以物理位置相邻,任一元素均可随机存取

线性表长可变(删除),而数组长度不可动态定义 用一变量表示顺序表的长度属性

#define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
typedef struct{
	ElemType elem[LIST_INIT_SIZE];
	int length;  // 当前长度 
}SqList; 

例:

图书表的顺序存储结构类型定义

#define MAXSIZE 10000
typedef struct
{
	char no[20];
	char name[50];
	float price;
}Book;

typedef struct 
{
	Book *elem;
	int length;
}Sqlist;

类c语言操作的补充

数组补充:

数组静态分配:

tpyedef struct 
{
	ElemType data[Maxsize];
	int length;
}Sqlist;

动态分布:

tpyedef struct 
{
	ElemType *data;
	int length;
}Sqlist;
// 然后进行动态内存的分配
Sqlist L;
L.data = (ElemType *)malloc(sizeof(ElemType)*MAXSIZE);

c++的动态存储分配

new 类型名T (初值列表) 申请内存空间

int *p1 = new int; 或 int *p1 = new int(10);

释放内存空间

delete *p1`

线性表的基本操作

1.线性表的初始化

Status InitList_Sq(SqList &L) //构造一个空的顺序表L 
{
	L.elem = new ElemType[MAXSIZE]; // 为顺序表分配空间 
	if(!L.elem) // 存储分配失败 
		exit(OVERFLOW;
	L.length = 0;//空表的长度为0 
	return OK;
 } 

2.摧毁线性表

void DestroyList(SqList &L)
{
	if (L.elem)
		delete L.elem //释放储存空间 
}

3.清空线性表

void ClearList(SqList &L)
{
	L.length = 0;//将线性表的长度变为0 
 } 

4.求线性表的长度

int GetLength(SqList L)
{
	return L.length;
 } 

5.判断线性表是否为空

int IsEmpty(SqList L)
{
	if (L.length == 0) 
		return 1;
	else 
		return 0;
}

6.线性表的取值

int GetElem(SqList L,int i,ElemType &e)
{
	if (i < 1 || i > L.length)
		return error;//判断i值是否合理 
	e = L.elem[i-1];//第i-1的存储单元存储着第i个数据 
	return OK;
}

7.顺序表的查找

int LocateElem(SqList L,ElemType e)
{//在线性表L中查找值为e的数据元素,返回其序号 
	for (i = 0;i < L.length;i++)
            {
		if(L.elem[i] == e) 
			return i+1;//查找成功,返回序号 
		return 0;//查找失败,返回0 
            }
}

平均查找长度(n + 1) / 2

8.顺序表的插入

算法思想: 1.插入位置是否合法,假设有n个元素,则他可插入的位置为1~n+1

2.判断顺序表的储存空间是否已满,若已满返回ERRPOR

3.将第n至第i位的元素依次向后移动一个位置,空出第i个位置

4.将要插入的新元素e放入第i个位置

5.表长加一,成功返回OK

Status ListInsert_Sq(SqList &L,int i,ElemType e)
{
	if (i < 1 || i > L.length + 1)
		return ERROR;
	if (L.length == MAXSIZE)
		return ERROR;
	for (j = L.length - 1;j >= i - 1;j--)
		L.elem[j+1] = L.elem[j];
	L.elem[i-1] = e;
	L.length++;
	return OK;
 } 

9.顺序表的删除

算法思想:

1.判断删除位置是否合法,合法位置为1~n

2.将欲删除的元素保留在e中

3.将第i+1至第n位的元素依次向前移动一个位置

4.表长减一

Status ListDelete Sq(SqList &L,int i)
{
	if (i<1 || i >L.length)
		return ERROR
	for (j=i;j<=L.length-1;j++)
		L.elem[j-1] = L.elem[j];
	L.length--;
	return OK;
}

线性表的链式表示和实现

单链表

结点只有一个指针域的链表

双链表

结点有两个指针域的链表

循环链表

首尾相接的链表

链表的存储结构示意图有两种:

1.不带头节点

image.png 2.带头结点

image.png

好处: 1.便于首元结点的处理

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

2便于空表与非控表的统一处理

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

头节点的数据域内装的是什莫?

可以为空,也可以为表长,头节点不计入表长

单链表

带头结点的单链表

初始化

typedef struct Lnode{ // 声明节点类型和指向节点的指针类型 
	ElemType data;  //节点的数据域 
	struct Lnode *next; //节点的指针域 
}Lnode,*LinkList;//LinkList为指向结构体Lnode 的指针类型 

定义链表

LinkList L;

定义结点指针

LinkList p;

例:存储学生姓名学号成绩的单链表

一般情况下我们会这样定义

typedef struct student{
	char name[8];
	char no[8];
	int score;
	struct student *next;
}Lnode,*LinkList;

为了统一链表的操作,我们这样定义

typedef struct student{
	char name[8];
	char no[8];
	int score;
}ElemType;
typedef struct Lnode{
	ElemType data;
	struct Lnode *next; 
}Lnode,*LinkList;

1.初始化函数

算法步骤:

1.生成新节点做头节点,用头指针L指向头节点

2.将头结点的指针域置空

Status LnitList_L(LinkList &L){
	L = new LNode ;
	L->next = NULL;
	return OK;
}

2.判断链表是否为空(头节点和头指针仍然在)

int ListEmpty(LinkList L){
	if (L->next)
		return 0;
	else
		return 1;
}

3.单链表的销毁

Status DestroyList_L(LinkList &L){
	LinList p;
	while(L){
		p = L;
		L = L->next;
		delete p;
	}
	return OK;
}

4.清空链表

Status ClearList(LinkList &L){
	LinkList *p,*q;
	p = L->next;
	while(p){ //没到表尾
		q = p->next;
		delete p;
		p = q;
	}
	L->next = NULL;// 头结点的指针域为空 
	return OK;
}

5.计算单链表的表长

从首元结点开始,依次计算所有结点


cong'shou'yuan
int ListLength_L(LinkList L)
{
	int i = 0
	LinkList p;
	p = L->next;
	while(p)
	{
		i++;
		p = p-next;
	}
	return i;
}

重要操作

1.p指向头结点

p = L;

2.s指向首元结点

s = L -> next

3.p指向下一节点

p = p ->next

6.取值--去单链表中第i个元素

链表不是随机存取结构

算法步骤

1.从第一个结点开始(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p = L->next

2.j做计数器,累计当前扫描过的结点数,j初值为1

3.当p指向扫描到的下一节点时,计数器j加1

4.当j==i时,p所指的结点就是要找的第i个结点

具体实现:

Status GetElem_L(LinkList L;int i;Elem Type &e)//获取线性表L中的某个数据元素的内容,通过变量e返回 
{
	p = L->next;//进行初始化 
	j = 1;
	while(p&&j<i){
		p = p->next;//向后扫描,直到p指向第i个元素或p为空 
		j++;
	}
	if (!p || j > i)//第i个元素不存在 
	{
		return ERROR;
	}
	e = p->data;
	return OK;
}

7.单链表的查找--按值查找

  1. 返回地址

算法步骤

1 .从第一个结点起,依次和e相比较

  1. 如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或地址;

3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或NULL

Lnode *LocateElem_L(LinkList L,ELemtype e){
	p = L->next;
	while(p&&p->data != e)
	{
		p = p->next;
	}
	return p;
}
 

2.返回位置序号

Lnode *LocateElem_L(LinkList L,ELemtype e){
	p = L->next;
	while(p&&p->data != e)
	{
		p = p->next;
		j++;
	}
	if(p)
	{
		return j;
	}
	else
	{
		return 0;//查找失败返回0
	}
	return p;
}

7.插入--在第i个结点前插入值为e的新结点

算法步骤:

1.首先找到ai-1的储存位置p

2.生成一个数据域为e的新节点s

3.插入新节点:新节点的指针域指向结点ai;结点ai-1的指针域指向新节点

image.png s -> next = p -> next

p -> next = s;//s里存的是新插入结点的地址

先执行下面一句可以吗?

可以,但是需要一个中间变量

实现

Status ListLnsert_L(LinkList &L, int i,ElemType e)
{
	p = L;
	j = 0;
	while(p && j < i -1){//寻找第i-1个结点,p指向i-1结点 
		p = p-next;
		++j;
	}
	if (!p || j > i -1)//i大于表长+1或者小于1,插入位置非法 
		return ERROR;
	s = new LNode;
	s->data = e;
	s->next = p->next;
	p->next = s;
	return OK; 
}
 

8.删除--删除第i个结点

算法步骤

1.首先找到ai-1的存储位置p,保存要删除的ai的值

2.令p->next指向ai+1

p->next = p->next->next

3.释放结点ai的空间

Status ListDelete_L(LinkList &L, int i; ElemType &e)
{
	p = L;
	j = 0;
	while(p->next && j < i-1)
	{
		p = p->next;
		++j;
	}
	if(!(p->next) || j > i-1)
		return ERROR;
	q = p->next;//临时保存被删结点的地址以备释放 
	p->next = q->next;	//改变删除节点前驱结点的指针域 
	e = q->data;//保存删除节点的数据域 
	delete q;
	return OK;
 } 

9.单链表的建立

1.头插法--元素插入在链表头部

void CreateList_H(LinkList &L,int n){
	L = new Lnode;
	L->next = NULL;//先建立一个带头结点的单链表 
	for (i = n;i > 0;--i)
	{
		p = new Lnode;
		scanf("%d",&p->data);
		p->next = L-next;//插入表头 
		L->next = p;
	}
}

2.尾插法

从一个空表开始,将新节点逐个插入到链表的尾部

void Create_R(LinkkList &L, int n);
{
	L = new Lnode;
	L->next = NULL;
	r = L;//尾指针r指向头结点
	
	for (i = 0;i < n;i++)
	{
		p = new Lnode;
		scanf("%d",&p->data);
		p->next = NULL;
		r-next = p; //插入到表尾 
		r = p; //r指向新的尾结点
	 } 
}

循环链表

表中最后一个节点的指针域指向头结点,整个链表形成一个环

循环链表不像单链表一样有NULL,故遍历循环链表时,判断依据是判断他们是否等于头指针

将两个循环链表合并(将Tb合并在Ta之后)

image.png

image.png p存表头结点

Tb表头连接到Ta表尾

释放Tb表头结点

修改指针

LinkList Connect(LinkList Ta,LinkList Tb){
	p = Ta->next;
	Ta->next = Tb->next->next;
	delete Tb->next;
	Tb->next = p;
	return Tb;
} 

双链表

在单链表的每一个节点里再增加一个指向其直接前驱的指针域proior,这样链表中就形成了有两个方向不同的链

结构定义

typedef struct DuLNode{
	ElemType data;
	struct DuLNode *proior,*next;
}DuLnode,*DuLinkList;

双链表头结点的proior == NULL

尾结点的next==NULl

而双向循环链表 头结点的proior指向链表的最后一个结点 最后一个结点的next指向头结点

image.png

1.插入

image.png

void ListInsert_DuL(DuLinkList &L,int i,ElemType e)
{//在带头结点的双向循环链表L中第i个位置之前插入元素e 
	if(!p = GetElemP_DuL(L,i))
		return ERROR;
	s = new DuLNode;
	s-data = e;
	s->prior = p->prior;
	p->prior->next = s;
	s->next = p;
	p->prior = s;
	
	return OK;
	
}

2.删除

image.png

void ListDelete_DuL(DuLink &L,int i,ElemType &e){
	//删除带头结点的双向循环链表L的第i个元素,并用e返回
	if (!(p=GetElemP_DuL(L,i))) 
		return ERROR;
		e = p->data;
		p->prior->next = p->next;
		p->next->prior = p->prior;
		free(p);
		return;
} 

顺序表和链表的比较

链式存储结构的优点:

1.节点空间可以动态申请和释放

2.数据元素的逻辑次序靠结点来指示,插入和删除时不需要移动数据元素

缺点:

1.存储密度小。每个结点的指针域需额外占用储存空间,当每个结点的数据域所占字节不多时,指针域所占储存空间的比重显得很大。

2.非随机存取

image.png

存储密度越大,存储空间的利用率就越高

image.png

线性表的应用

线性表的合并

假设利用连哥哥线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A = A并B

image.png

两个集合中也会有相同的,新集合中没有相同的元素

void union(List &La,List Lb)
{
	La_len = ListLength(La);
	lb_len = ListLength(lb);
	for (i = 1;i <= Lb_len;i++)
	{
		GetElem(Lb,i,e);
		if (!LocateElem(La,e))
			ListInsert(&La,++La_len,e);
	}
}

有序表的合并

image.png 非递减:表中有相同的元素

image.png

合并完成后可能有相同的元素

image.png

从两个线性表中比较较小的元素加入到新的线性表Lc中 算法步骤 1.创建一个空表Lc 2.依次从La或Lb中摘取元素值较小的结点插入到Lc表的最后,直至其中一个表表为空为止 3.继续将La或Lb其中一个表的剩余节点插入在Lc表的最后