线性表的定义
线性表(List):由零个或多个数据元素组成的有限序列。
(1)它是一个序列,也就是元素之间有个先来后到的;
(2)若元素有多个,则第一个元素无前驱,最后一个无后继;
(3)线性表是有限的;
按照取值的不同,数据类型可以分为两类:
原子类型:不可以再分解的基本类型,例如整形、浮点型、字符型等
结构类型:由若干个类型组合而成,是可以再分解的,例如整形数组是由若干个整形数据组成的。
抽象数据类型
抽象数据类型是指一个数学模型及定义在该模型上的一组操作;
抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
描述抽象数据类型的标准格式:
ADT 抽象数据类型名
Data
数据元素之间的逻辑关系的定义
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
线性表的顺序存储结构
存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置;
线性表的最大存储容量:数组的长度MaxSize;
线性表的当前长度:length。
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。而在插入或删除时,时间复杂度都是O(n)。这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用。
优点:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间;
- 可以快速地存取表中任意位置的元素;
缺点:
- 插入和删除操作需要移动大量的元素;
- 当线性表长度变化较大时,难以确定存储空间的容量;
- 容易造成存储空间的"碎片"。
线性表的链式存储结构
结点
我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息称为指针或链。这两部分信息组成数据元素称为存储映像,称为结点。
单链表
此链表的每个结点中只包含一个指针域;我们把第一个结点的存储位置叫做头指针,最后一个结点指针为空。
头指针:是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针;头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字);无论链表是否为空,头指针不为空;头指针是链表的必要元素。
头结点:头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度);有了头结点,对在第一个元素结点前插入结点和删除第一结点的操作与其它结点的操作就统一了;头结点不一定是链表的必须元素。
单链表:
头指针->头结点->第一结点->第二结点(头结点数据域一般无意义)
空链表:
头指针->头结点->NULL
在C语言中可以用结构指针来描述单链表:
cpp
typedef struct Node
{
ElemType data; //数据域
struct Node *Next; //指针域
}Node;
typedef struct Node* LinkList;
我们看到结点由存放数据元素的数据域和存放后继结点的指针域组成。
假设p是指向线性表第i个元素的指针,则该结点ai的数据域我们可以用p->data的值是一个数据元素,结点ai的指针域可以用p->next来表示,p->next的值是一个指针。
单链表的读取
-声明一个结点p指向链表第一个结点,初始化j从1开始;
-当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j+1;
-若到链表末尾p为空,则说明第i个元素不存在;
-否则查找成功,返回结点p的数据。
C语言实现GetElem
cpp
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p;
p = L->Next; //p存放第一结点的地址
j = 1;
while(p && j<i)
{
p = p->Next; //不断遍历链表
++j;
}
if(!p || j>i)
return ERROR;
*e = p->data;
return OK;
}
由于这个算法的时间复杂度取决于i的位置,当i = 1时,则不需要遍历,而i = n时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n)。
单链表的插入
要将结点s插入到p和p->Next之间,可以用下面语句实现:
s->Next = p->Next;
p->Next = s;
单链表第i个数据插入结点的算法思路:
-声明一结点p指向链表头结点,初始化j从1开始;
-当j<1时就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
-若到链表末尾p为空,则说明第i个元素不存在;
-否则查找成功,在系统中生成一个空结点s;
-将数据元素e赋值给s->data;
-单链表的插入两个标准语句,返回成功。
cpp
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j<i)
{
p = p->Next;
++j;
}
if(!p || j>i)
return ERROR;
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->Next = p->Next;
p->Next = s;
return OK;
}
单链表的删除
cpp
Status ListInsert(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->Next && j<i)
{
p = p->Next;
++j;
}
if(!p->Next || j>i)
return ERROR;
q = p->Next;
p->Next = q->Next;
*e = q->data;
free(q);
return OK;
}
单链表的整表创建
创建单链表的过程是一个动态生成链表的过程,从空表的初始状态起,依次建立各元素结点并逐个插入链表。
-声明一结点p和计数器变量i;
初始化一空链表L;
-让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
-循环实现后继结点的赋值和插入。
头插法建立单链表:
尾插法建立单链表:
cpp
void CreateListTail(LinkList *L,int i)
{
LinkList p,r;
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for(i = 0;I<n;i++)
{
p = (Node *)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
}
单链表的整表删除
算法思路如下:
-声明结点p和q;
-将第一个结点赋值给p,下一结点赋值给q;
-循环执行释放p和将q赋值给p的操作;
cpp
Status ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
单链表结构和顺序存储结构的优缺点
存储分配方式:
-顺序存储结构用一般段连续的存储单元依次存储线性的数据元素。
-单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能
-查找
·顺序存储结构O(1)
·单链表O(n)
-插入和删除
·顺序存储结构需要平均移动表长一半的元素,时间为O(n)
·单链表在计算出某位置的指针后,插入和删除时间为O(1)
空间性能
-顺序存储结构需要预分配存储空间,分大了造成浪费,分小了容易造成溢出;
-单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。