一、链表相较于顺序表的优点和缺点
1.优点
1.时间性能差
在顺序表中插入和删除一个元素时,在等概率的情况下,需要移动表中约一半的元素。当表中元素较多时,顺序表的时间性能较低。
2.容易造成空间的浪费
此外,顺序表需要使用连续的内存空间,且空间大小要按照最大的需求分配,可能导致内存空间利用率不高。
2.缺点
1.存储空间的利用率低
每一个结点除了存储要存储的数据之外,还存储了一个指针(如果是双循环链表,则存储了两个指针)。
2.需要查找某一元素的时候,链表不能像顺序表一样直接定位到某一个位置,而是需要去遍历
顺序表由于在内存中连续存储,所以可以快速定位到某一个位置。
二、单链表的存储描述(单链表结构体的定义)
1.结构体定义方法
单链表结点结构由数据域和指针域构成。下面给出代码片段
方法一:
注意:代码中的elementType表示某种数据类型,实际代码中根据需要自行选择。
struct slNode
{
elementType data;//数据域
struct slNode * next;//指针域,结构(结点)自身引用
};
typedef struct slNode node, * linkList;//或typedef slNode node, * linkList;
方法二:(将方法一两部分集成描述)
typedef struct slNode
{
elementType data;//数据域
struct slNode * next;//指针域,结构(结点)自身引用
}node, * linkList;
上面的描述中使用typedef将结点类型重新命名为node和结点指针类型linkList。以后可以使用类型node来定义结点变量或结点指针变量,也可用linkList直接定义结点指针变量。
2.申请、释放内存方法
这里的链表要求按需分配结点的存储空间,即要求程序在运行期间可以动态地向操作系统申请需要的存储空间,使用完毕后立即释放空间,这样的链表称为“动态链表”。(与之对应的,也有“静态链表”,在Java、python等没有指针的语言中,常采用数组来实现链表,这样的链表称之为静态链表)
C语言中使用malloc()库函数申请空间,使用free()库函数释放空间。
C++使用new操作符申请空间,使用delete操作符释放空间。
new申请结点:
node * p;
p=new node;//动态申请一个结点的内存空间,返回结点指针
delete释放结点:
delete p;
定义了一个结点指针p并申请了结点的内存空间之后,使用"->"符号来引用结点的分量,即p->data;p->next。
3.头指针的定义
一个链表由头指针唯一确定,头指针类型与上面定义的指针p或next指针类型相同,所以头指针可以定义为:node * head。也常用单个字符表示头指针,如node * H或node * L。
4.头指针的意义
加了头结点之后,链表的插入和删除操作只能在头结点之后进行,这使得链表所有位置的插入和删除操作步骤相同。此外,只要申请了头结点,则整个链表在存续期间头指针始终不变。
三、链表的基本运算(初始化、求长度、按序号取元素、按值查询元素、插入、删除)
1.初始化
初始化链表即建立一个不含元素结点的空链表,对于带头结点的单链表来说,就是要编写一个函数,在此函数中申请头结点,头结点的next指针置为空,并将头结点的指针(头指针)返回给主调函数。
方法一:
void initiaList(node *&L)
{
L=new node;//产生头结点,也可用如下语句产生头结点:L=(node * )malloc(sizeof(node))
L->next=NULL;//设置后继指针为空
}
注意:这里使用了C++的引用往主调函数回传头结点指针(头指针),不能不使用引用,只把初始化函数参数定义为结点指针,即定义为initiaList(node * L),这样不能正确地回传头指针。
方法二:
void initiaList(node * * pL)
{
( * pL)=new node;//pL为结点指针的指针,所以(* pL)为结点指针
//产生头结点,也可用下面语句产生头结点:( * pL)=(node *)malloc(sizeof(node));
( * pL)->next=NULL;//设置后继指针为空
}
方法三(最容易理解):
node * initiaList()
{
node *p;//声明结点指针变量
p=new node;//产生一个结点
p->next=NULL;//节点的next指针置为空
return p;//返回已申请结点的指针
}
2.求链表长度
求链表L的长度就是求出链表L中元素的个数,而链表中没有存储其长度值,因此需要逐个“数”出其结点个数。“数”结点时用一指针(如p)依次指向每个元素结点(初值置为L->next),p每指到一个结点就作一次计数(因此需要设置一个计数变量len,初值为0),直到搜索到最后,即p移出链表。算法描述如下:
int listLength(node* L)
{
int len = 0;//遍历开始之前,链表长度当然需要置为0
node* p = L->next;//p初始指向首元素结点
while (p != NULL)
{
len++;//p指向元素结点,计数+1
p = p->next;//p移到下一个结点,继续后继结点的计数
}
return len;//返回链表长度
}
时间复杂度为O(n)。
3.按序号取元素结点
从首元素结点依次“数”过去即可。另外需要考虑所指结点不存在时所返回的值。
node* getElement(node* L, int i)//i时目标元素结点的序号
{
node* p = L->next;
int j = 1;
while (j != i && p != NULL)//当前结点不是目标结点,并且不为空时就继续搜索
{
j++;
p = p->next;
}
return p;//返回结果
}
时间复杂度为O(n)。
4.按值查询元素
设置一个指针,依次指示各元素结点,每指向一个结点就判读一次是否指向了目标元素,若是,返回该结点的指针,若不是,继续搜索直到表尾,若没有找到目标节点,返回空指针(一般这样要求)
node* listLocate(node* L, elementType x)
{
node* p = L->next;//p初始指向首元素结点
while (p != NULL && p->data != x)//p指元素结点,又不是目标节点,继续搜索下一目标
{
p = p->next;
}
return p;
}
时间复杂度为O(n)。
5.插入
bool listInsert(node* L, int i, elementType x)
{
node* p = L;//p指针指向头节点
node* S;
int k = 0;
while (k != i - 1 && p != NULL)//搜索ei-1结点,并取得指向ei-1的指针p
{
p = p->next;//p指向下一结点
k++;
}
if (p == NULL)
{
return false;//p为空指针,说明插入位置i无效,返回false
}
else
{
//此时,k=i-1,p为ei-1结点的指针
S = new node;//动态申请内存,创建一个新节点,即要插入的结点
S->data = x;
S->next = p->next;
p->next = S;
return true;//正确插入返回true
}
}
时间复杂度为O(n)。
6.删除
注意C或C++中用户申请的内存,不用时必须用free()或delete显示地释放,系统不能自动回收这部分内存空间。
bool listDelete(node* L, int i)
{
node* u;
node* p = L;//指向头结点
int k = 0;
while (k != i - 1 && p != NULL)//搜索ei-1结点
{
p = p->next;
k++;
}
if (p == NULL || p->next == NULL)
{
return false;//删除位置i超出范围,删除失败,返回false
}
else
{
//此时p指向ei-1
u = p->next;//u指向待删除结点ei
p->next = u->next;//ei-1的next指向ei+1结点,或为空(ei-1为最后结点)
delete u;//释放ei结点
return true;//成功删除,返回true
}
}
7.构造
如果先初始化一个链表,然后反复调用插入结点运算,也可以实现目的,但是每次都要搜索插入位置i,时间性能不理想。因此,要在构造算法中,想办法记住上一次的插入位置。
1.尾插法创建链表
1.结束符控制创建结束
void createListR(node*& L)
{
elementType x;//保存键盘输入的数据元素值
node* u, * R;//L为头结点指针(头指针),R为尾结点的指针
L = new node;//产生头结点,头指针为L
L->next = NULL;//头结点的指针域为空
R = L;//设置尾指针,对空链表,头尾指针显然相同
cout << "请输入元素数据(整数,9999退出):" << endl;//以9999为例
cin >> x;
while (x != 9999)
{
u = new node;//动态申请内存,产生新结点
u->data = x;
u->next = NULL;//新结点的next指针置空
R->next = u;//新结点链接到表尾
R = u;//修改尾指针,使指向新的尾结点
cin >> x;//读入下一个键盘输入数据
}
}
时间复杂度为O(n)。
2.结点个数控制创建结束
void createList(node*& L)
{
int i, n;//n为结点个数,不含头结点
int x;//x为数据元素值
node* u, * R;//R为尾结点指针
L = new node;//产生头结点
L->next = NULL;//头结点的指针域为空
R = L;//设置尾指针,对空链表,头尾指针显然相同
cout << "请输入元素结点个数(整数)" << endl;
cin >> n;
cout << "请输入元素数据(整数)" << endl;
for (i = n; i > 0; i--)
{
u = new node;//动态申请内存,产生新结点
cin >> x;
u->data = x;//元素数据装入新结点
u->next = NULL;
R->next = u;//新结点链接到表尾
R = u;
}
}
时间复杂度为O(n)。
2.头插法
1.结束符控制创建结束
void createListH(node*& L)
{
node* u;
elementType x;//存放元素数值
L = new node;
L->next = NULL;
cout << "请输入元素数据(整数,9999退出):" << endl;//以9999为例
cin >> x;
while (x != 9999)
{
u = new node;//动态申请内存,产生新结点
u->data = x;
u->next = L->next;//新结点链接到表头,使成为首元素结点
L->next = u;
cin >> x;//读入下一个键盘输入数据
}
}
时间复杂度为O(n)。
2.结点个数控制创建结束
类比即可,此处从略
四、其他结构形式的链表
1.单循环链表
将单链表的表尾结点中的后继指针(next)改为指向表头结点,即构成单循环链表。单循环链表要用P==L或P->next==L来判断是否搜索到表尾。
使用单链表要注意不要造成死循环。
2.带尾指针的单循环链表
能够方便地搜索到链表的表头和表尾结点。
3.双链表结构
每个结点同时有后继指针和前驱指针,使得可以方便地访问前驱、后继。
1.双循环链表的初始化
void initialList(dnode*& L)
{
L=new dnode;
L->piror=L;
L->next=L;
}
2.在双循环链表中插入结点
此处省略代码,仅展示步骤:
第一步:搜索插入位置,获取ei结点的指针p
第二步:申请新结点
第三步:新结点向前链接到ei-1
第四步:新结点向后链接到ei
第五步:ei结点前向链接到新结点
第六步:ei-1结点后向链接到新结点
3.在双循环链表中删除结点
此处省略代码,仅展示步骤:
第一步:搜索ei结点,以指针p指向
第二步:ei-1结点的next指向ei+1结点
第三步:ei+1结点的prior指向ei-1结点
第四步:释放结点p(delete操作符或free()函数)
五、没有指针的语言(例如Java、python)中如何实现链表
以数组模拟,以元素下标表示位置,这里不详细展开介绍。