此文章仅作为自己学习过程中的记录和总结,同时会有意地去用英文来做笔记,一些术语的英译不太准确,内容如有错漏也请多指教,谢谢!
一、线性表(List)的定义:
- 线性表(List):零个或多个数据元素的有限序列。
(通常将线性表记作(a1,a2,…,an),n指表长。当n=0时,称该表为空表。同时注意此处下标是从1开始的。)
线性表包括顺序表和链表。二者的主要区别在于:
-在顺序表中,数据元素是用一段地址连续的存储单元依次存储的。
-而在链表中,结点之间地址不是连续的,是用指针“连接”起来的。
此处需要特别注意几点:
- 线性表是一种逻辑结构而不是物理结构,表示元素之间一对一相邻的关系。
- 线性表是有限的。
- 线性表中元素都是数据元素,每个元素都是单个元素。
- 线性表中元素都是相同类型的。
二、线性表的ADT:
线性表的数据对象集合为{a1,a2,…,an},每个元素的类型均为DataType。
其中,除了第一个元素a1外,每一个元素有且只有一个直接前驱元素;除最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
[ADT of lists.]
Operations:
InitList(*L): Set up an empty list. //初始化操作,建立一个空的线性表。
ListEmpty(L):(Bool type) If the list is empty, return true, else return false. //若线性表为空,返回true,否则返回false。
ClearList(*L): Empty the list. //线性表清空。
DestroyList(*L): Destroy the list and free the space it occupies. //销毁线性表,并释放其占用的存储空间。
GetElem(L,i,*e): (按位查找)Get the No.i element's value, and give it to "e". //将线性表L中第i个位置元素返回给e。
LocateElem(L,e): (按值查找)Search for an element that has the same value as "e", and return the element's location if finding it, else return 0. //在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序列号;否则,返回0表示失败。
ListInsert(*L,i,e): Insert a new element "e" into the list right before No.i element. //在线性表的第i个位置插入元素e。
ListDelete(*L,i,*e): Delete the No.i element in the list, give its value to "e", and return "e". //删除线性表L中的第i个元素,并用e返回其值。
ListLength(L): Return the amount of the elements in the list. //返回线性表L的元素个数。
PrintList(L): Print the list. //打印线性表
end ADT
三、线性表的顺序存储结构(Sequential List)定义:
- 定义: 用一段地址连续的存储单元依次存储线性表的数据元素。
(Example: C’s one-dimensional array)
SqList的三个基本属性:
- The max size of a list.(1.线性表的最大存储容量。)
- An initial position of a list.(2.存储空间的起始位置。)
- The length of a list.(3.线性表的长度。)
接下来是线性表SqList的结构代码:
[The structure code of a Sequential List.]//线性表顺序存储的结构代码
//Here are three basic attributes of a SqList:
#define MAXSIZE 100 //No.1: The max size of a list.(1.线性表的最大存储容量。)
typedef struct
{ //ElemType为typedef的int类型数据。
ElemType data[MAXSIZE]; //No.2: An initial position of a list.(2.存储空间的起始位置。)
int length; //No.3: The length of a list.(3.线性表的长度。)
}SqList;
四、顺序表的部分操作具体实现方法:
首先,为了示意清楚以及方便,先声明定义:
#define OK 1
#define ERROR 0
typedef int ElemType; //ElemType的类型根据实际需求而定,此处假设为int。
typedef int Status;/*"Status" is defined as a type of a function, with its returned value representing the result of the function.
(Status是函数的类型,它的返回值势函数结果状态代码,0或1.)*/
- "GetElem"(获取元素): T(n) = O(1)
[P50: Achieve the operation of "GetElem" in a SqList.]//获取顺序存储结构链表中的某一元素
//INITIAL CONDITIONS : The list exists, and 1<=i<= ListLength(L).
//RESULT: Return the value of No.i element in the list, and give it to "e". 用e返回L中第i个元素的值。
Status GetElem ( SqList L, int i, ElemType *e )
{
if( L.length==0 || i<1 || i>L.length ) //Empty list or Index out of range is invalid. 空表/下标不在合法范围内。
{
return ERROR;
}
*e = L.data[i-1];
return OK;
}
T(n) = O(1)
- "ListInsert"(插入): T(n) = O(n)
[Achieve the operation of "ListInsert" in a SqList.]//向顺序存储结构链表中插入元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=Listlength(L).
//RESULT: Insert "e" into the place right before No.i, and lenghthen the list for 1.(操作结果:在L中第i个位置前插入数据元素e,并且L的长度加1.)
Status ListInsert ( SqList *L, int i, ElemType e )
{
int k;
if( L->length==MAXSIZE ) //If the length is already the MAXSIZE, we cannot insert more elements into it.
{
return ERROR;
}
if( i<1 || i>L->length+1 ) //If "i" is not in the range, we cannot insert it into the list.
{
return ERROR;
}
if( i<=L->length ) //When the place we are to insert into is not at the end of the list...
{
for( k=L->length-1 ; k>=i-1 ; k-- )
{
L->data[k] = L->data[k-1];
}
}
L->data[i-1] = e;
L->length++;
return OK;
}
T(n) = O(n)
- "ListDelete"(删除) : T(n) = O(n)
[Achieve the operation of "ListDelete" in a SqList.]//删除顺序存储结构链表中的某一元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=ListLength(L).
//RESULT: Delete the No.i element in the list, return its value to "e", and shorten the list for 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 ; i<L->length ; k++ )
{
L->data[k-1] = L->data[k];
}
}
L->length--;
return OK;
}
T(n) = O(n)
五、顺序存储结构的优缺点:
-优点:
- 可以快速地存取表中任一位置的元素,时间复杂度为O(1)。
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间。
-缺点:
- 插入和删除操作需要移动大量的元素,平均时间复杂度为O(n)。
- 当线性表长度变化较大时,难以确定存储空间的容量。
- 造成存储空间的“碎片”。
六、线性表的链式存储结构(Linked List)定义:
为了解决线性表的顺序存储结构在进行插入和删除操作时需要移动大量元素,耗费大量时间的问题,链式存储结构应运而生。以前在顺序结构中,每个数据元素只需要存储数据元素的信息。而现在在链式结构中,除了数据元素信息,我们还需要存储它后继元素的存储地址。
线性表的链式存储结构的特点是:单线索,无分支。 用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些元素可以存在内存未被占用的任意位置。
对于数据元素来说,我们把它存储数据元素信息的域称为“数据域”(Data field),把存储直接后继位置的域称为“指针域”(Pointer field)。指针域中存储的信息称作“指针”或“链”。这两部分信息组成该数据元素的存储映像,称为“结点”(Node)。
n个结点链接成一个链表,即为线性表(a1,a2,…,an)的链式存储结构。
因为该链表的每个结点中只包含一个指针域(之后会学到不止一个指针域的),所以叫做单链表。
为了更加方便地对链表进行操作(如使得在第一结点前插入结点、删除第一结点的处理与对其他结点的处理相同),我们会在单链表的第一个结点前附设一个结点,称为头结点(Head node) (注意头结点不是第一个结点,而是第一个结点前附设的一个结点)。头结点的数据域可以不存储任何信息,也可以存储如ListLength等附加信息。头结点的指针域存储指向第一个结点的指针。
我们把链表中第一个结点的存储位置叫做头指针,即意味着头结点的指针域内存储的是头指针。
同时我们规定线性链表的最后一个结点指针需为“NULL”(“空”),通常也用 “^” 符号来表示。
[ TIPS:头结点与头指针的异同:]
– 头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用,常用头指针冠以链表的名字(指针变量的名字)。
- 无论链表是否为空,头指针均不为空。
- 头指针是链表的必要元素。
– 头结点:
- 头结点是为了使空链表与非空链表处理一致而设置的,附设在第一个结点之前,其数据域一般没有意义(也可用来存放链表的长度)。
- 有了头结点,对在第一元素结点前插入结点和删除第一结点的操作就与对其他结点的操作统一了。
- 头结点不是链表的必要元素。
接下来是线性表LinkList的结构代码:
[The structure code of a singly linked list.] //线性表的单链表存储结构
typedef struct Node
{ //ElemType为typedef的int类型数据。
ElemType data[MAXSIZE]; //Data field
struct Node *next; //Pointer field
}Node;
typedef struct Node *LinkList;
七、单链表的部分操作具体实现方法:
首先,为了示意清楚以及方便,先声明定义:
#define OK 1
#define ERROR 0
typedef int ElemType; //ElemType的类型根据实际需求而定,此处假设为int。
typedef int Status; /*"Status" is defined as a type of a function, with its returned value representing the result of the function.
(Status是函数的类型,它的返回值势函数结果状态代码,0或1.)*/
- "GetElem"(获取元素): T(n) = O(n)
-算法思路:
- 声明一个指针 p 指向链表第一个结点,定义一个counter如 “j“ 并从1开始;
- 当 j<i 时,遍历链表,让p向后移动不断指向下一结点,同时counter++;
- 若到链表末尾 p 为NULL,则说明第 i 个结点不存在 [ if( !p || j>i ) ];
- 否则代表获取成功,同时返回结点 p 的数据;
- Return OK。
实现代码如下:
方法①:(while循环)
[Achieve the operation of "GetElem" in a singly linked list.] //获取单链表中的某一元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=ListLength(L).
//RESULT: Return the value of the No.i element in list "L", and give it to "e".
Status GetElem ( LinkList L, int i, ElemType *e )
{
int j=1; //Act as a counter.
LinkList p=*L; //Let "p" points to the first node of the list.
while( p && j<i )
{
p = p->next;
j++;
}
if( !p || j>i ) //If "p" is NULL, then it represents that No.i node doesn't exist. 若到链表末尾p为空,代表第i个结点不存在。
{
return ERROR;
}
*e = p->data;
return OK;
}
T(n) = O(n)
方法②:(for循环)
[Achieve the operation of "GetElem" in a singly linked list.] //获取单链表中的某一元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=ListLength(L).
//RESULT: Return the value of the No.i element in list "L", and give it to "e".
Status GetElem ( LinkList L, int i, ElemType *e )
{
LinkList p;
for( p=*L ; p ; p=p->next )
{
;
}
if( !p )
{
return ERROR;
}
*e = p->data;
return OK;
}
T(n) = O(n)
- "ListInsert"(插入结点): T(n) = O(n)
-算法思路:
- 声明一指针 p 指向链表头结点,定义一counter如 “j” 并从1开始;
- 当 j<i 时,遍历链表,让p向后移动不断指向下一结点,同时counter++;
- 若到链表末尾 p 为NULL,则说明第 i 个结点不存在 [ if( !p || j>i ) ];
- 否则代表查找成功,在系统中生成一个新的空结点 s (malloc);
- 将数据元素 e 赋值给 s->data;
- 单链表的插入标准语句: s->next=p->next; p->next=s;
- Return OK。
实现代码如下:
方法①:(while循环)
[Achieve the operation of "ListInsert" in a singly linked list.] //向单链表中插入元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=Listlength(L).
//RESULT: Insert a new element "e" into list "L" right before No.i node, and lengthen the list for 1.
Status ListInsert ( LinkList *L, int i, ElemType e )
{
int j=1;
LinkList p=*L, s;
while( p && j<i )
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc( sizeof(Node) ); //Create a new node.
s->data = e;
s->next = p->next; //Let "s" points to the next element.
p->next = s; //Let "p" points to "s".
return OK;
}
T(n) = O(n)
方法②:(for循环)
[Achieve the operation of "ListInsert" in a singly linked list.] //向单链表中插入元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=Listlength(L).
//RESULT: Insert a new element "e" into list "L" right before No.i node, and lengthen the list for 1.
Status ListInsert ( LinkList *L, int i, ElemType e )
{
LinkList p, s;
for( p=*L ; p ; p=p->next )
{
;
}
if( !p )
{
return ERROR;
}
s = (LinkList)malloc( sizeof(Node) ); //Create a new node.
s->data = e;
s->next = p->next; //Let "s" points to the next element.
p->next = s; //Let "p" points to "s".
return OK;
}
T(n) = O(n)
- "ListDelete"(删除结点) : T(n) = O(n)
-算法思路:
- 声明一指针 p 指向链表头结点,定义一counter如 “j” 并从1开始;
- 当 j<i 时,遍历链表,让p向后移动不断指向下一结点,同时counter++;
- 若到链表末尾 p 为NULL,则说明第 i 个结点不存在 [ if( !p || j>i ) ];
- 否则代表查找成功,将要删除的结点 p->next 赋值给 q: q=p->next;
- 单链表的删除标准语句: p->next=q->next;
- 将 q 结点中的数据赋值给 e,作为返回值;
- free(q);
- Return OK;
实现代码如下:
方法①:(while循环)
[Achieve the operation of "ListDelete" in a singly linked list.] //删除单链表中的某一元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=ListLength(L)
//RESULT: Delete the No.i node in list "l", return its value and give it to "e", and shorten the list for 1.
Status ListDelete ( LinkList *L, int i, ElemType *e )
{
int j=1;
LinkList p=*L, q;
while( p && j<i )
{
p = p->next;
j++;
}
if (!(p->next) || j>i )
{
return ERROR;
}
q = p->next;
p->next = q->next; /*Any pointer at the left of "->" must be checked if it is NULL.*/
*e = q->data;
free(q);
return OK;
}
T(n) = O(n)
方法②:(for循环)
[Achieve the operation of "ListDelete" in a singly linked list.] //删除单链表中的某一元素
//INITIAL CONDITIONS: The list exists, and 1<=i<=ListLength(L)
//RESULT: Delete the No.i node in list "l", return its value and give it to "e", and shorten the list for 1.
Status ListDelete ( LinkList *L, int i, ElemType *e )
{
LinkList p, q;
for( p=*L ; p ; p=p->next )
{
;
}
if( !p )
{
return ERROR;
}
q = p->next;
p->next = q->next; /*Any pointer at the left of "->" must be checked if it is NULL.*/
*e = q->data;
free(q);
return OK;
}
T(n) = O(n)
-结论:【对于插入或删除数据越频繁的操作,单链表相较于SqList的效率优势就更加明显。】
*【创建整表的核心思路:】
创建单链表的过程其实就是动态生成链表的过程。
即从“空表”的初始状态起,依次建立起各元素结点,并逐个插入链表中。
- “CreateListHead”(整表创建–头插法):
-算法思路:(核心:始终让新结点在第一的位置,类似于插队的方法。)
-
声明一指针 p ,定义一counter如 “i”;
-
初始化一空链表 L ;
-
让 L 的头结点的指针指向NULL,即建立一个带头结点的单链表;
-
循环以下三个步骤:
-生成一新结点赋值给 p ;
-随机生成一个数字赋值给 p->data;
-将 p 插入到头结点与前一新结点之间。
实现代码如下:
[Create a singly linked list(CreateListHead).] //单链表的整表创建-头插法
//随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)
void CreateListHead ( LinkList *L, int n )
{
int i;
LinkList p;
srand( time(0) ); //初始化随机数种子
*L = (LinkList)malloc( sizeof(Node) );
(*L)->next = NULL; //Firstly, create a singly linked list with a head node.(先建立一个带头结点的单链表 )
for( i=0 ; i<n ; i++ )
{
p = (LinkList)malloc( sizeof(Node) ); //Create a new node.
p->data = rand()%100+1; //Randomly create a number lower than 100, and give it to "p->data".
p->next = (*L)->next;
(*L)->next = p; //Insert the new node into the head.
}
}
- “CreateListTail”(整表创建–尾插法):
-算法思路:(核心:每次新结点都插在终端结点的后面,即先来后到。)
-
声明一指针 p ,定义一counter如 “i”;
-
初始化一空链表 L ;
-
让 L 的头结点的指针指向NULL,即建立一个带头结点的单链表;
-
循环以下四个步骤:
-生成一新结点赋值给 p ;
-随机生成一个数字赋值给 p->data;
-让 r->next 指向新结点 p;
-更新终端结点为新建立的结点p。
实现代码如下:
[Create a singly linked list(CreateListTail).] //单链表的整表创建-尾插法
//随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)
void CreateListTail ( LinkList *L, int n )
{
int i;
LinkList p, r;
srand( time(0) ); //初始化随机种子
*L = (LinkList)malloc( sizeof(Node) );
r = *L; //"r" points to the node at the tail.
for( i=0 ; i<n ; i++ )
{
p = (Node *)malloc( sizeof(Node) ); //Create a new node.
p->data = rand()% 100+1;
r->next = p; //Let the tail-node's pointer points to the new node.
r = p; //Define the current new node as the new tail-node.
}
r->next = NULL; //Representing that the current list ends.
}
( 注意:L 是指整个单链表,而 r 是指向尾结点的变量。L 会随着循环增长为一个多结点链表,而 r 会随着循环不断地变化。)
- "ClearList"(整表删除):
-算法思路:(核心:每次新结点都插在终端结点的后面,即先来后到。)
-
声明一指针 p 和 q;
-
将第一个结点赋值给 p ;
-
循环以下三个步骤:
-将下一结点赋值给 q ;
-free( p ) ;
-p = q 。
实现代码如下:
方法①:(while循环)
[Delete a singly linked list(ClearList).] //单链表的整表删除
//INITIAL: The list exists.
//RESULT: Clear the whole list.
Status ClearList ( LinkList *L )
{
LinkList p, q;
p = (*L)->next; //Let "p" points to the first node.
while( p )
{
q = p->next; //We can see "q" as a temporary stand for "p"'s pointer field.
free(p);
p = q;
}
return OK;
}
方法②:(for循环)
[Delete a singly linked list(ClearList).] //单链表的整表删除
//INITIAL: The list exists.
//RESULT: Clear the whole list.
Status ClearList ( LinkList *L )
{
LinkList p, q;
for( p=(*L)->next, q=0 ; p ; p=q ) //Let "p" points to the first node.
{
q = p->next;
free(p);
}
return OK;
}
八、单链表结构与顺序存储结构的比较
【一】存储分配方式:
- 顺序表: 一段连续的存储单元依次存储线性表的数据元素;
- 单链表: 采用链式存储结构,用一组任意的存储单元存放线性表的元素。
【二】时间性能:
- 顺序表: 查找-O(1); 插入和删除-O(n);
- 单链表: 查找-O(n);需要平均移动表长一半的元素;插入和删除-O(1)。
【三】空间性能:
- 顺序表: 需要预分配存储空间,分大了会浪费,分小了易发生上溢;
- 单链表: 不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
九、线性表两种结构选择的结论:
- 若需要频繁查找,少插入删除,则选择顺序表。如游戏开发中用户注册个人信息;
若需要频繁插入删除,少查找,则选择单链表。如游戏开发中的武器装备列表。 - List中元素大致个数/长度已知,则选择顺序表;
List中元素个数变化大/不确定,则选择单链表。