线性表
参考书籍:《大话数据结构》
线性表说明
线性表(List):零个或多个数据元素的有限序(元素之间有顺序,第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继)。
线性表元素的个数n(n≥0)定义为线性表的长度,当n=0时,称为空表。
线性表的抽象数据类型
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
例,使用上面的函数实现功能:
/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void union(List *La,List *Lb)
{
int La_len,Lb_len,i;
ElemType e; /*声明与La和Lb相同的数据元素e*/
La_len = ListLength(La); /*求线性表的长度*/
Lb_len = ListLength(Lb);
for(i=1; i<=Lb_len; i++)
{
GetElem(Lb,i,e); /*取Lb中第i个数据元素赋给e*/
if(!LocateElem(La,e,equal)) /*La中不存在和e相同数据元素*/
{
ListInsert(La, ++La_len, e); /*插入*/
}
}
}
线性表的顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
构建顺序存储结构需要三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置;
- 线性表的最大存储容量:数组长度MaxSize;
- 线性表的当前长度:length。
/*线性表顺序存储结构代码*/
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType; /*数据元素类型*/
typedef struct
{
ElemType data[MAXSIZE]; /*数据存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
顺序存储结构的插入与删除模式:
优先考虑异常后再进行插入和删除操作
插入步骤
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置i处;
- 表长加1。
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
/*顺序线性表已经满,线性表长等于线性表的最大存储容量*/
if(L->length==MAXSIZE)
{
return ERROR;
}
/*当i的位置不在数组下标范围内*/
if(i<1 || i>L->length+1)
{
return ERROR;
}
/*若插入数据位置不在表尾*/
if(i<=L -> length)
{
/*将要插入位置后数据元素向后移动一位*/
for(k=L->length-1;k>=i-1;k--)
{
L->data[k+1]=L->data[k]);
}
}
/*将新元素插入*/
L->data[i-1] = e;
L->length ++;
return OK;
}
删除步骤
- 如果插入位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长减1
/*初始条件:顺序线性表L已存在,l<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L,int i, ElemType *e)
{
int k;
/*线性表为空*/
if(L->length==0)
{
return ERROR;
}
/*删除位置不正确*/
if(i<1 || i>L->length)
{
return ERROR;
}
*e=L->data[i-1];
if(i<L->length)
{
/*将删除位置后继元素前移*/
for(k=i;k<L->length;k++)
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
线性表顺序存储结构的优缺点:
线性表的顺序存储结构,在存、读数据时,不管哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。说明它比较适合元素个数不太变化,而更多是存取数据的应用。
优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
- 可以快速地存取表中任一位置的元素;
缺点:
- 插入和删除操作需要移动大量元素;
- 当线性表长度变化比较大时,难以确定存储空间的容量;
- 造成存储空间的“碎片”
线性表的链式存储结构
为了表示每个数据元素ai与其后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域(存储的信息称为指针或链)。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /*定义LinkList*/
单链表
单链表的读取:
获得链表第i个数据的算法思路:
- 声明一个结点p指向链表第一个结点,初始化 j 从 1 开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,返回结点p的数据
注:读取思想就是重头开始找,直到第i个元素为止。所以其最坏情况的时间复杂度是O(n)。且由于单链表结构中没有定义表长,故不方便用for来控制循环。其主要核心思想就是“工作指针后移”。
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; /*声明一结点p*/
p = L->next; /*让p指向链表L的第一个结点*/
j = 1; /*j为计数器*/
while(p && j<i) /*p不为空或者计数器j还没有等于i时,循环继续*/
{
p = p->next; /*让p指向下一个结点*/
++j;
}
if(!p || j>i)
reruen ERROR; /*第i个元素不存在*/
*e = p->data; /*取第i个元素的数据*/
return OK;
}
插入与删除
插入:
s->next = p->next; p->next = s;
让p的后继结点改成s的后继结点,再把结点s变成p的后继结点。
单链表第i个数据插入结点的算法思路:
- 声明一结点p指向链表第一个结点,初始化 j 从1开始;
- 当 j < i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j 累加1;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数组元素e赋值给s->data;
- 单链表的插入标准语句 s->next = p->next; p->next = s;
- 返回成功。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L),*/
/*操作结果:在L中第i个位置之前输入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
/*寻找第i个结点*/
while(p && j<i)
{
p = p->next;
++j;
}
if(!p || j>i)
{
/*第i个元素不存在*/
return ERROR;
}
/*生成新结点*/
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next; /*将p的后继结点赋值给s的后继*/
p->next = s; /*将s赋值给p的后继*/
return OK;
}
删除:
q=p->next; p->next = q->next;
让p的后继的后继结点改为p的后继结点。
单链表第i个数据删除结点的算法思路:
- 声明一结点 p指向链表第一个结点,初始化 j从1开始;
- 当 j<i时,就遍历链表,让 p的指针向后移动,不断指向下一个结点, j 累加1;
- 若到链表末尾 p为空,则说明第 i 个元素不存在;
- 否则查找成功,将欲删除的结点 p->next赋值给q;
- 单链表的删除标砖语句 p->next = q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
/*遍历寻找第i个元素*/
while(p->next && j<i)
{
p = p->next;
++j;
}
if(!(p->next) || j>i)
{
/*第i个元素不存在*/
return ERROR;
}
q = p->next;
p->next = q->next; /*将q的后继赋值给p的后继*/
*e = q->data; /*将q结点中的数据给e*/
free(q); /*释放内存空间*/
return OK;
}
从单链表的算法中,发现插入或删除的时间复杂度都是O(n)。如果在不知道第 i 个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构时没有太大优势。对于插入或删除数据超频繁的操作,单链表的优势比顺序存储结构就明显很多了,后者查找和插入或删除的时间复杂度都是O(n),而前者的查找时O(n),但插入或删除的时间复杂度是O(1);
单链表的整表创建:
创建单链表的过程就是一个动态生成链表的过程。 即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单个链表创建的算法思路(头插法):
- 声明一个结点 p 和计数器变量i;
- 初始化一空链表 L;
- 让 L 的头结点的指针指向 NULL,即建立一个带头结点的单链表;
- 循环;
a.生成一新结点赋值给 p;
b.随机生成一数字赋值给p的数据域p->data;
c.将 p 插入到头结点与前一新结点之间。
/*随机产生 n 个元素的值,建立带表头结点的单链线性表 L(头插法)*/
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand( time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc (sizeof(Node));
(*L) ->next = NULL; /*先建立一个带头结点的单链表*/
for( i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p ->data = rand( ) %100+1; /*随机生成100以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
尾插法示例
尾插法:创建过程中把新结点放到最后排队插入,先来后到。每次把新结点都插在终端结点的后面。
void CreateListTail ( LinkList *L, int n)
{
/*L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表*/
LinkList p,r;
int i;
srand( time (0)); /*初始化随机数种子*/
*L = (LinkList) malloc (sizeof (Node)); /*为整个线性表*/
r = *L; /*r 为指向尾部的结点*/
for( i= 0; i<n; i++)
{
p = (Node *)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand( )%100 + 1; /*随机生成100以内的数字*/
r->next = p; /*将表尾终端结点的指针指向新结点*/
r = p; /*将当前的新结点定义为表尾终端结点*/
}
r->next = NULL; /*表示当前链表结束*/
}
单链表的整表删除:
当不打算使用单链表时,需要把它销毁,就是在内存中把它给释放掉,以便留出空间给其他程序或软件使用。
单链表整表删除的算法思路如下:
- 声明一结点p和q;
- 将第一个结点赋值给p;
- 循环:
a.将下一个结点赋值给q;
b.释放p;
c.将q赋值给p;
/*初始条件:顺序线性表 L 已存在,操作结果:将 L 重置为空表*/
Status ClearList( LinkList *L)
{
LinkList p,q;
p = (*L)->next; /* p 指向第一个结点*/
while(p) /*是否到表尾*/
{
q = p->next;
free( p );
p = q;
}
(* L) ->next = NULL; /*头结点指针域为空*/
return OK;
}
循环链表
在单链表的基础上,将单链表中的尾指针原指向NULL,现在改成指向头指针。两条循环链表合并。
P = rearA->next; /*保存A表的头结点,即①*/
rearA->next = rearB->next->next; /*将本是指向B表的第一个结点(不是头结 点 )赋值给reaA->next即②*/
rearB->next = p; /*将原A表的头结点赋值给rearB->next,即③*/
Free( p ); /*释放p*/
双向链表
双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。一个指向直接后继,另一个指向直接前驱。
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; /*直接前驱指针*/
struct DulNode *next; /*直接后继指针*/
}DulNode, *DuLinkList;
双向链表的插入
插入顺序:先搞定 s 的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。
s -> prior = p; //1
s ->next = p -> next; //2
p ->next -> prior = s; //3
p ->next = s; //4
双向链表的删除
p ->prior->next = p->next; //1
p ->next->prior = p->prior; //2
free(p);
静态链表
用数组描述的链表叫静态链表,也叫游标实现法。该数组的元素都是由两个数据域组成,data 和 cur。data为数据域用来存放数据元素;而游标cur相当于单链表中的 next 指针,存放该元素的后继在数组中的下标。