单链表
定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。每个链表的结点,除存放元素自身的信息之外,还需要存放一个指向其后继结点的指针。即单链表的结构分为两部分,其中data为数据域,用来存放元素;next为指针域,用来存放其后继结点的地址。
单链表中结点类型的描述如下:
typedef struct LNode{ //定义单链表结点类型
ElemeType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
由于单链表是离散地分布在存储空间中,所以单链表是非随机存储的存储结构。
通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示为一个空表;另外,为了操作方便,会在单链表第一个结点前加一个结点,成为头结点,头结点的指针域指向线性表的第一个元素结点。
单链表的逻辑结构图如下:
单链表上基本操作
单链表是用户不断申请存储单元和改变链接关系而得到的一种特殊数据结构,将链表的左边称为链头,右边称为链尾。
1、头插法建立单链表
该方法从一个空表开始,生成新的结点,并将读取到的数据结构存放到新结点的数据域中,然后将新的结点插入到当前链表的表头,即将链表右端看成固定的,链表不断向左延伸而得到的。头插法最先得到的是尾结点。
头插法建立单链表的算法如下:
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //创建头结点
L->next = NULL; //初始化链表为空
scanf("%d",&x); //输入结点的值
while(x! = 9999 ){ //输入9999表示结束
s = (LNode*)malloc(sizeof(LNode)); //创建新结点
s->data = x;
s->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
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 != 9999 "){ //输入9999表示结束
s = (LinkList *)malloc(sizeof(LNode)); //创建头结点
s->data = x;
r->next = s;
r = s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
r->next = NULL; //尾结点指针置空
return L;
}
因为附设一个指向表尾结点的指针,故时间复杂度和头插法相同;
3、按序号查找结点的值
在单链表中从第一个结点出发,顺时针 next 域逐个往下搜索,直到找到第 i 个结点为止,否则返回最后一个结点指针域为 NULL。
按序号查找结点值的算法如下:
LNode *GetElem(LinkList L, int i){
int j = 1; //计数,初始为1;
LNode *p = L->next; //头结点指针赋给p;
if(i == 0) //若 i=0,则返回头结点
return 1;
if(i < 1) //若 i 无效,则返回NULL;
return NULL;
while(p && j < i){ //从第 1 个结点开始找,查找第 i 个结点
p = p->next;
j++;
}
return p; //返回第 i 个结点的指针,若 i 大于表长则返回 NULL;
}
按序号查找操作的时间复杂度为O(n);
4、按值查找表结点
从单链表的第一个结点开始,从后往前依次比较表中各结点数据域的值,若结点数据域的值等于给定 e ,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL。
按值查找表结点的算法如下:
LNode *LocateElem(Linklist L, ElemType e){
LNode *p = L->next;
while(p != NULL && p-> data != e) //从第1个结点开始查找data域为e的结点
p = p->next;
return p; //找到后返回该结点指针,否则返回NULL
}
按值查找操作的时间复杂度为O(n)。
5、插入结点操作
插入结点操作是将值为 x 的结点新插入到单链表的第 i 个位置上。先检查插入位置的合法性,然后找到插入位置的前驱结点,即第 i -1 个结点,然后在其后插入新结点。其操作过程如图所示。
实现插入结点的代码片段如下:
p = GetElem(L, i-1); //查找插入位置的前驱结点
s->next = p->next;
p->next = x;
插入结点操作的时间复杂度为O(n)。
另外:前插是指在某结点的前面插入一个新的结点,后插定义刚好和它相反。在单链表的插入算法中,通常使用后插操作。
以上面的算法为例,首先调用GetElem()找到第 i - 1 个结点,即插入结点的前驱结点后,在对其执行后插操作。由此可知,对结点的前插操作均可转化为后插操作,前提是从单链表的头结点开始顺序查找到其前驱结点,时间复杂度为O(n).
此外,可采用另外一种方法将其转化为后插操作来实现,设待插入结点为 * s,将 * s插入到 * p的前面,我们仍然将 * s插入到 * p的后面,然后将 p->data 与 s->data 交换,这样既满足了逻辑关系,又使得时间复杂度为O(1),算法代码片段如下:
s->next = p->next; //修改指针域,不能颠倒
p->next = s;
temp = p->data; //交换数据域部分
p->data = s->data;
s->data = temp;
6、删除结点操作
删除结点操作是将单链表的第 i 的结点删除。先检查要删除位置的合法性, 后查找表中第 i-1 个结点,即被删除结点的前驱结点,然后再将其删除。其操作过程如图所示。
实现删除结点的代码片段如下:
p = GetMlem(L, i-1); //查找删除结点的前驱结点
q = p->next;
p->next = q->next;
free(q); //释放结点的存储空间
删除结点操作的时间复杂度为O(n)。
另外:删除结点 * p的操作可通过删除 * p的后继结点来实现,实质就是将其后继结点的值赋予自身,然后删除后继结点,也使得时间复杂度为O(1)。
实现算法部分代码如下:
q = q->next; //令 q 指向*p的后继结点
p->data = p->next->data; //和后继结点交换数据域
p->next = q->next; //将*p结点从链中断开
free(q); //释放后继结点的存储空间
7、求表长的操作
求表长的操作就是计算单链表中数据结点(不含头结点)的个数,需要从第一个结点开始顺序依次访问表中每个结点,为此需要设置一个计数器变量,每访问一个结点,计数器加 1,直到访问空结点为止。算法时间复杂度为O(n)。
【注】单链表的长度是不包括头结点的,因此不带头结点和带头结点的单链表在求表长操作上会略有不同。对不带头结点的单链表,当表为空时,要单独处理。