计算机基础数据结构讲解第七篇-链表操作
本篇文章我们学习线性表的链式表示,也就是链表。我们知道,顺序表可以随机存取,查找方便,但是插入和删除需要移动大量元素。链式存储线性表的时候,不需要使用地址连续的存储单元,而是通过"链"建立起数据元素之间的逻辑关系,不要求物理位置连续,插入和删除只需要修改指针,很方便。但是这样的话由于不要求物理位置连续,就会失去随机存取的优点。下面就来介绍各种链表的。
由于链表在网上的介绍很多,所以具体结构就不用图片展示了,直接介绍如何用代码实现。
一:单链表定义
线性表最简单的链式储存是单链表,是通过一组任意的存储单元存储线性表中的数据元素。和顺序表不同的是,任意代表存储单元位置不相邻,而单链表的结点也和顺序表不同。
单链表除存放元素自身的信息之外,还需要存放一个指向其后继结点的指针。也就是结点包括数据域和指针域,指针存放继结点的地址,因为附加了一个指针域,所以存在浪费空间,空间利用率不高的情况。因为不是随机存储的,所以就造成了它的缺点是如果需要查找某个特定的结点时,需要从表头开始遍历,依次查找。
单链表的结点类型描述如下:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
通常用头指针来标识一个单链表,头指针为NULL表示一个空表。有时候为了操作上的方便,在单链表头指针和第一个结点之间附加一个结点,称为头结点。头结点的数据域可以不包含任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
引入头结点优点:
在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
无论链表是否为空,头指针都是指向头结点的非空指针(空表中头结点的指针域为空),空表和非空表的处理得到了统一。
二:单链表的操作
1.头插法建立单链表
该方法将新结点插入到链表的表头,即头结点之后。
该算法相当于改变了头结点和其后继结点之间的对应关系。代码如下:
LinkList List_HeadInsert(LinkList &L){
LNode *s,int x;
L = (LinkList)malloc(sizeof(LNode)); //创建头结点
L->next = NULL; //初始为空链表
scanf("%d", &x);
while(x!=99999){
//创建新结点,区别头结点用 LNode*指向
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}
由于头插法是在表头插,只改变头结点的指向,则插入后的时间复杂度为O(1),则对于一个表长为n的单链表,头插法建立完单链表的总的时间复杂度为O(n)。
2.尾插法建立单链表
还有一种算法是在链表的表尾插入,因此必须增加一个尾指针r,使其指向当前链表的尾结点,然后把尾结点的指针指向要插入的元素。代码如下:
LinkList List_TailInsert(LinkList &L){
int x;
L = (LinkList)malloc(sizeof(LNode)); //创建头结点
LNode *s,*r = L; //r为尾指针
scanf("%d", &x);
while(x!=99999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r=s; //r指向新的尾结点
scanf("%d", &x);
}
s->next = NULL; //尾指针置为空
return L;
}
因为附设了一个指向尾结点的指针,所以时间复杂度和头插法相同。
3.按序号查找结点值
这个就是最简单查找结点的方法,从第一个结点出发,直到找到第i个结点为止,否则返回最后一个节点的指针域NULL。代码如下:
LNode *GetElem(LinkList L,int i){
int j = 1; //计数,初始值为1
LNode *p = L->next; //头结点赋值给p
if(i == 0)
return L; //若i等于0,则返回头结点
if(i < 1)
return NULL; //若i无效,则返回NULL
//从第1个结点开始找,知道找到第i个结点
while(p && j < i){
p = p->next;
j++;
}
//返回第i个结点的指针,若i大于表长则返回NULL
return p;
}
4.按值查找结点值
这个也是最简单查找结点的方法,从第一个结点出发,直到找到第1个结点等于e的值为止,否则返回最后一个节点的指针域NULL。代码如下:
LNode *LocateElem(LinkList L,int e){
LNode *p = L->next;
//从第一个结点查找data域为e的结点
while(p != NULL && p->data != e)
p = p->next;
//找到后返回该结点指针,否则返回NULL
return p;
}
因为最快情况要遍历整个链表,所以按值查找和按位查找元素的平均时间复杂度都是O(n)。
5.随机插入结点操作
该操作把值为x的新结点插入到单链表的第i个位置上。首先调用查找算法GetElem(L,i-1),查找到第i-1个结点,然后改变指针即可。算法的位置不能颠倒,否则会造成等号两边不相等的错误,代码如下:
p = GetElem(L,i-1)
s->next = p->next;
p->next = s;
本算法的时间主要花在查找第i-1个元素,时间复杂度是O(n),如果给定结点插入新结点,时间复杂度仅为O(1)。该算法是前插操作。
也可以对线性表进行前插操作,但是也可以转化为后插操作,重要的是找到前插位置元素的前驱结点。时间复杂度为O(n)。也可以用如下的算法转化为后插操作实现,代码如下:
//和之前的后插操作一样
s->next = p->next;
p->next = s;
temp = s->data;
s->data = p->data;
p->data = temp;
//交换数据域
6.删除结点操作
删除结点操作将单链表的第i个结点删除先找到要删除元素的前驱结点,然后修改指针,就可以将其删除。代码如下:
p = GetElem(L,i-1)
q = p->next;
p->next = q->next;
free(q);
由于删除操作也是先要找到其前驱结点,因此算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。
也可以用其他方法删除结点。
7.求表长操作
求表长的操作就是计算单链表中数据结点的个数,需要设置一个计数器变量,最后求得表长,算法的时间复杂度为O(n)。
8.注意事项
由于单链表的长度是不包括头结点的,因此不带头结点和带头结点的单链表在求表长操作上会有不同。
三:双链表
单链表只有一个指针,因此单链表只能依次从后往前遍历,访问后继结点的时间复杂度是O(1),访问前驱结点的时间复杂度是o(n)。
为了克服这种缺点,引入了双链表,有两个指针prior和next。结构代码如下:
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior,*next; //前驱和后继指针
}DNode,*DLinkList;
双链表增加了一个指向前驱的prior指针,因此按值查找和安危查找的操作与单链表相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同。是由于也要对prior指针修改,保证在修改过程中不断链。双链表很方便地找到其前驱结点,因此,插入和删除操作的时间复杂度仅为O(n)。
1.双链表的插入操作
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
2.双链表的删除操作
p->next = q->next;
q->next->prior = p;
free(q);
四.其他链表
还有其他链表,这里进行介绍.
1.循环单链表
循环单链表相当于把最后一个结点的指针从NULL改为指向头结点,从而使整个链表形成一个环。因此循环单链表的判空条件是指针是否等于头指针。
循环单链表插入,删除算法和单链表一样,不用的是如果操作在表尾进行,让单链表保持循环的性质。正是因为循环单链表是一个环,所以在任何位置上的插入和删除操作都是等价的,无需判断是否是表尾。
循环单链表可以从表中的任意一个结点开始遍历单链表。有时操作在表头和表尾进行,仅设尾指针,使操作效率更高。是由于r->next即为头指针,对于表头和表尾的操作都只需要O(1)的时间复杂度。
2.循环双链表
循环双链表就是循环单链表然后加上头结点的prior指针还要指向表尾结点。在循环双链表中,某结点为尾结点时,它的指针域是头指针;当循环双链表为空表时,头结点的prior和next域都等于头指针。
3.静态链表
静态链表借助数组表示,也有数据域和指针域,但这里的指针是结点的相对地址,也就是数组下标,又称游标,需要预先分配一块连续的内存空间。可以理解为顺序表数据元素加一个指针域。
静态链表以next==-1作为结束的标志。静态链表的插入,删除操作与动态链表相同,但没有单链表使用方便。是为了不支持指针的高级语言设计的,比如python,Basic。