数据结构与算法(三)——线性表

目录

一、线性表的定义

二、线性表的抽象数据类型

三、线性表的顺序存储结构

1、顺序存储定义

2、顺序存储方式

3、数据长度与线性表长度区别

4、地址计算方法

四、顺序存储结构的插入与删除

1、获取元素

2、插入操作

3、删除操作

4、线性表顺序存储结构的优缺点

五、线性表的链式存储结构

1、线性表的链式存储结构定义

2、头指针和头结点的异同

3、线性表链式存储结构代码描述

六、单链表

1、单链表的遍历

2、单链表的插入

3、单链表的删除

4、单链表的整表创建

5、单链表的整表删除

6、单链表结构与顺序存储结构的优缺点

七、静态链表

八、循环链表

九、双向链表


一、线性表的定义

        线性表:是零个或多个数据元素的有限序列。

        线性表中的元素有且只有一个前驱和后继(其中头元素只有后继没有前驱,尾元素只有前驱没有后继)。当线性表的长度为0时,称为空表。如果a是线性表中的第k个元素,则称k为数据元素a在线性表中的位序。在比较复杂的线性表中,一个数据元素可以由若干个数据项组成(如学生点名册中每一位学生的基本信息)。

二、线性表的抽象数据类型

ADT 线性表(List)

Data

    线性表的数据对象集合为{a1,a2,......,an},每个元素的类型为DataType。其中,除第一个元素a1外,每
一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个后继元素。数据元素之间
的关系是一对一的关系。

Operation

    InitList(*L):    初始化操作,建立一个空的线性表L
    ListEmpty(L):    若线性表为空,返回true,否则返回false
    ClearList(*L):   将线性表清空
    GetElem(L,i,*e): 将线性表L中第i个位置元素返回给e
    LocateElem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,则返回该元素在表中序号;否
则返回0表示失败
    ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e
    ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值
    ListLength(L):    返回线性表L的元素个数
    
endADT

三、线性表的顺序存储结构

1、顺序存储定义

        线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

2、顺序存储方式

        简单来说,就是在内存空间中找一块地儿,通过占位的形式,把一定的内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中。C语言中可以用一维数组来实现顺序存储结构,即把第一个元素放到数组下标为0的位置,后面的元素依次存放。

        描述线性表的存储结构需要三个属性:

        ① 存储空间的起始位置;

        ② 线性表的最大容量;

        ③ 线性表的当前长度。

        下面是代码示例:

//线性表的顺序存储结构
#define MAXSIZE 20 //存储空间初始容量分配
typedef int ElemType; //存储元素类型ElemType根据实际情况而定,这里假设为int
typedef struct{
	ElemType data[MAXSIZE]; //定义数组存储数据元素个数最大为MAXSIZE
	int length; //线性表的当前长度
}SqList;

3、数据长度与线性表长度区别

        数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。

        线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行这个量是动态变化的。

4、地址计算方法

        C语言中的数组是从0开始第一个下标的,于是线性表的第i个元素是要存储在数组下标为i-1的位置。用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于或等于当前线性表的长度。

        假设一个数据元素占用c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置计算公式如下:

fun(Ai+1) = fun(Ai) + c

四、顺序存储结构的插入与删除

1、获取元素

//用e返回L中第i个数据元素的值
#define OK 1
#define ERROR 0
#define TURE 1
#define FALSE 0
typedef int Status; //函数结果状态码,如OK等
Status GetElem(SqList L, int i, ElemType *e){
	if(L.length == 0 || i<1 || i>L.length){
		return ERROR;
	}
	*e = L.data[i - 1];
	return OK;
}

2、插入操作

插入算法的思路:

① 如果插入位置不合理,抛出异常;

② 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;

③ 从最后一个位置开始向前遍历到第i个位置,分别将它们都向后移动一个位置;

④ 表长加1。

实现代码如下:

//在线性表L中的第i个位置插入新元素e
Status ListInert(SqList *L, int i, ElemType e){
	if(L->length == MAXSIZE){//线性表已经满了
		return ERROR;
	}
	if(i < 1 || i > L->length+1){//i不在数组的范围之内
		return ERROR;
	}

	if(i <= L->length){//如果插入的数据不是表尾,则需要将插入位置后的元素向后移动一位
		for(int t = L->length-1;t >= i - 1; t--){
			L->data[t+1] = L->data[t];
		}
	}
	L->data[i-1] = e;
	L->length++;
	return OK;
}

3、删除操作

删除算法的思路:

① 如果删除位置不合理,则抛出异常;

② 取出删除元素;

③ 从删除元素位置开始遍历到最后一个元素的位置,分别将它们都向前移动一个位置;

④ 表长减1;

实现代码如下:

//删除线性表L的第i个元素,并用e返回其值,L的长度减1
Status ListDelete(SqList *L, int i, ElemType *e){
	if(L->length == 0){//如果线性表为空
		return ERROR;
	}
	if(i<1 || i>L->length){//如果删除的位置不正确
		return ERROR;
	}
	*e = L->data[i-1];
	if(ilength){//如果删除的不是最后位置
		for(int j=i;jlength;j--){//将元素的位置向前移
			L->data[j-1] = L->data[j];
		}
	}
	L->length--;
	return OK;
}

4、线性表顺序存储结构的优缺点

线性表的存、取数据时间复杂度都是O(1);而在插入或删除时,时间复杂度都是O(n)。

优点 缺点
无须为表中元素之间的逻辑关系增加额外的存储空间 插入和删除操作需要移动大量元素
可以快速的存取表中的任一元素 当线性表长度变化较大时,难以确定存储空间的容量
  造成存储空间的“碎片”

五、线性表的链式存储结构

1、线性表的链式存储结构定义

        线性表的链式存储结构的特点是用一组任意的存储单元来存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。之前在顺序结构中,每个数据元素只需要存数据元素信息就行了,而在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。

        我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息称做指针或链。这两部分信息组成数据元素的存储映像,称为结点。

        n个结点链结成一个链表,即为线性表的链式存储结构,因为此链表中的每个结点中只包含一个指针域,所以叫做单链表。而链表中第一个结点的存储位置叫做头指针,链表的最后一个结点指针为“空”(通常用NULL或^来表示)。有时,为了更加方便地的对链表进行操作,会在单链表的第一个结点前附加一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。

2、头指针和头结点的异同

头指针 头结点
它是链表指向第一个节点的指针,若链表有头结点,则是指向头结点的指针 头结点放在第一元素节点之前,其数据域一般无意义(也可存储链表的长度)
头指针具有标识作用,所以常用头指针冠以链表的名字 有了头结点,在第一元素结点前插入节点或删除第一结点,其操作与其它结点的操作就统一了
无论链表是否为空,头指针均不为空,头指针是链表的必要元素 头结点不一定是链表的必要元素

3、线性表链式存储结构代码描述

//线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList; //定义单链表集合
//结点由存储数据元素的数据域和存放后继结点地址的指针域组成
//假设p是指向线性表第i个元素的指针,那该结点a的数据域可以用p->data来表示,指针域可以用p->next来表示
//p->next是一个指针,指向i+1结点,即i+1结点的数据域为p->next->data

六、单链表

1、单链表的遍历

获取链表第i个数据元素的算法思路:

① 声明一个指针p指向链表第一个结点,初始化j从1开始;

② 当j

③ 若到链表尾p为空,则说明第i个结点不存在;

④ 否则查找成功,返回结点p的数据。

实现代码如下:

//从链表中查找第i个数据,用e返回其值(最坏的情况下时间复杂度为O(n))
Status GetElem(LinkList L, int i,ElemType *e){
	int j = 1; //计数器
	LinkList p; //声明指针
	p = L->next; //让p指向链表L中的第一个结点
	while(p && jnext; //p指向下一个结点
		++j;
	}
	if(!p || j>i){
		return ERROR; //节点不存在
	}
	*e = p->data; //取第i个节点的数据
	return OK;
}

2、单链表的插入

单链表第i个数据插入结点的算法思路:

① 声明一指针p指向链表头结点,初始化j从1开始;

② 当j

③ 若到链表末尾p为空,则说明第i个结点不存在;

④ 否则查找成功,在系统中生成一个空结点s;

⑤ 将数据元素e赋值给s->data;

⑥ 单链表的插入标准语句s->next=p->next,p->next=s;

⑦ 返回成功。

实现算法代码如下:

//在链表中第i个结点位置之前插入新的数据元素e,L的长度+1
Status ListInsert(LinkList *L, int i, ElemType e){
	int j = 1;
	LinkList p,s;
	p = *L;
	while(p && jnext;
		++j;
	}
	if(!p || j>i){
		return ERROR; //结点不存在
	}
	s = (LinkList) malloc(sizeof(Node)); //生成新的结点,用来存放数据e
	s->data = e;
	s->next = p->next; //插入节点的指针指向前一个节点的后继
	p->next = s; //前一个节点的指针指向插入节点
	return OK;
}

3、单链表的删除

单链表第i个数据删除结点的算法思路:

① 声明一个指针p指向链表头指针,初始化j从1开始;

② 当j

③ 若到链表末尾p为空,则说明第i个结点不存在;

④ 否则查找成功,将欲删除的结点p->next赋值给q;

⑤ 单链表的删除标准语句p->next=q->next;

⑥ 将q结点中的数据赋值给e,作为返回;

⑦ 释放q结点;

⑧ 返回成功。

算法实现代码如下:

//删除L的第i个结点,并用e返回其值,L的长度减1
Status ListDelete(LinkList *L, int i, ElemType *e){
	int j = 1;
	LinkList p,q;
	p = *L;
	while(p->next && jnext;
		++j;
	}
	if(!(p->next) && j>i){
		return ERROR;
	}
	q = p->next; //将要删除的结点赋给q
	p->next = q->next; //将要删除结点的下一个结点赋值给当前节点的下一个节点
	*e = q->data; //将要删除结点的数据赋给e
	free(q); //释放删除节点的内存
	return OK;
}

        单链表的插入和删除的时间复杂度都是O(n),如果我们不知道第i个结点的指针位置,单链表比线性表的顺序存储结构并没有太大优势。但如果我们希望从第i个位置开始,插入10个结点,对于顺序存储结构而言,每一次插入都需要移动n-i个结点,每次操作的时间复杂度都是O(n)。而单链表,我们只有在第一次找到第i个位置的指针时,时间复杂度为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。因此,插入或删除数据越频繁的操作,单链表的效率优势也就越明显。

4、单链表的整表创建

        创建单链表的过程就是一个动态生成链表的的过程,即从“空表”状态依次建立起各元素的结点,并逐个插入链表。

1) 头插法

单链表整表创建的算法思路:

① 声明一指针p和计数器变量i;

② 初始化一空链表L;

③ 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;

④ 循环:生成一新结点赋值给p,随机生成一数字赋值给p的数据域p->data,将p插入到头结点和上一个新结点之间。

算法代码实现如下:

//使用头插法,随机生成n个元素的值,建立带表头结点的单链表L
void CreateListHead(LinkList *L, int n){
	LinkList p;
	*L = (LinkList) malloc(sizeof(Node));
	(*L)->next = NULL; //建立一个带头结点的单链表
	for(int i=1;idata = rand()%100+1;
		//将新的结点插入到表头
		p->next = (*L)->next;
		(*L)->next = p;
	}
}

2) 尾插法

算法代码实现如下:

//使用尾插法,随机生成n个元素的值,建立带表头结点的单链表L
void CreateListTail(LinkList *L, int n){
	LinkList p,r;
	*L = (LinkList) malloc(sizeof(Node)); //创建一个空的单链表
	r = *L;
	for(int i=0;idata = rand()%100+1;
		//新节点插入到上一节点的后面
		r->next = p;
		//将r重新定义为尾节点
		r=p;
	}
	r->next = NULL;
}

5、单链表的整表删除

单链表删除的算法思路:

① 声明一个结点p和q;

② 将第一个结点赋值给p;

③ 循环:将下一结点赋值给q,释放p,将q赋值给p。

算法实现代码如下:

//单链表的整表删除
Status ClearList(LinkList *L){
	LinkList p,q;
	p = (*L)->next;
	while(p){
		q = p->next;
		free(p);
		p = q;
	}
	(*L)->next = NULL; //头结点指针域置为空
	return OK;
}

6、单链表结构与顺序存储结构的优缺点

存储分配方式 时间性能 空间性能
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素 查找:顺序存储结构是O(1)、单链表是O(n) 顺序存储结构需要预分配存储空间,分大了浪费,分小了容易发生上溢
单链表采用链式存储结构,用一组任意的存储单元存储线性表的元素 插入和删除:顺序存储结构是O(n)、单链表是O(1) 单链表不需要分配存储空间,只要有空间就可以分配,元素个数也不受限制

七、静态链表

        用数组描述的链表叫做静态链表,这种描述方法又叫做游标实现法。数组的元素一般由data和index组成,数据域data用来存放数据元素,而index就相当于链表中的指针。

        我们通常把未被使用的数组元素称为备用链表。在使用数组实现备用链表时,数组的第一个元素,即下标为0的元素的index就存放备用链表的第一个结点的下标;而数组的最后一个元素的index则存放第一个有数值的元素的下标,相当于单链表的头结点作用。

八、循环链表

        将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。

        在单链表中,有了头结点时,我们可以用O(1)的时间访问第一个结点,但要访问最后一个结点却需要O(n)的时间,因为需要将链表全部遍历一遍。这时如果不用头指针,而是让终端结点用尾指针rear指示,则查找终端结点的时间就是O(1),而开始结点rear->next->next,其时间复杂度也为O(1)。

如果要将两个循环链表合并成一个链表时,算法思路如下:

① 首先定义一个指针p,保存链表A的头结点地址,即p=rearA->next;

② 然后将链表A的尾指针指向链表B的第一个结点,即rearA->next = rearB->next->next;

③ 之后定义一个指针q,保存链表B的头结点地址,即q=rearB->next;

④ 再将链表B的尾指针指向链表A的头结点,即rearB->next=p;

⑤ 最后释放链表B的头结点,即free(q)。

九、双向链表

        双向链表是在每一个结点中,再设置一个指向其前驱结点的指针域。所有在双向链表中有两个指针域,一个指向直接后继,另一个指向直接前驱。单链表有循环链表,双链表也有循环链表。在双向链表中一个结点p,它的后继的前驱以及它的前驱的后继都是它自己,即:

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

如果要把一个结点p插入到结点s和s->next之间,算法思路如下:

//更改结点p的前驱和后继
p->pre = s
p->next = s->next
//更改结点s-next的前驱
s->next->pre = p
//更改结点s的后继
s->next = p

如果要删除一个结点p,其算法思路如下:

//使p结点的后继结点的前驱指针指向p结点的前驱
p->next->pre = p->pre
//使p结点的前驱结点的后继指针指向p结点的后继
p->pre->next = p->next
free(p)

 

你可能感兴趣的:(数据结构与算法)