单链表
1. 单链表的定义
单链表是通过任一存储单元来存储线性表中的元素。这些存储单元可以是连续的也可以是不连续的。为了每个数据元素都能链接起来,每个数据元素中除了存储数据以及其它的信息外,还应该储存该元素直接后继(也就是该元素的下一个元素)的位置信息(也就是下一个元素的地址)。就像每个人手拉着手一样,每一个人的手都拉着下一个人的手。
数据元素和下一个数据元素的存储位置的信息组成一个“结点”。对于C语言来说,储存下一个节点的信息的的是指针。所以在C语言中每一个节点包含的信息为:
1. 数据域:储存数据
1. 指针域:存储下一个结点的信息。
C语言中结点的定义如下:
typedef int ElemType;
typedef struct node {
ElemType data;
struct node * next;
} *LinkList, LNode;
其中ElemType可以根据需要定义,当数据多于一个时,可以使用结构体来定义ElemType。
2. 头结点和头指针
这里可以举一个火车的例子,火车有一个车头,车头后面会有一个东西会将车头与下一节车厢连接起来。只要知道了车头就可以找到它的任意一节车厢。链表也是这样,有时会为了操作方便会在第一个节点之前增加一个节点,称为头结点,这个节点中可以存储一些与该链表相关的信息,比如:链表的长度,当然也可以不存储任何信息,指针与存储第一个节点的信息。这样做的目的是为了使第一个节点不在具有特殊性(对于尾节点来说,可以认为它的后驱为NULL),链表中的每一个有数据的节点都有前驱和后继。当然也可以不使用头结点,那么每一次只需要记住第一个节点即可的存储位置即可。
通常我们把链表中第一存储位置的指针称为头指针。有了这个头指针我们就可以得到第一个节点的信息,也可以得到下一个节点位置,从而得到下一个节点的信息。
头结点和头指针的不同点:
- 头指针
- 头指针是指链表的第一个节点的指针,若链表有头节点则是指向头结点的指针
- 头指针具有表示作用,所以常常用头指针作为链表的名字
- 头节点
- 头节点是为了操作方便而设立的,放在第一个元素之前,其数据域通常是毫无意义的,当然也可以存放链表的长度和其他信息
- 头结点使得第一个节点不具有特殊性,在插入和删除时能相对简单一些。
注意:头节点不是链表中必须要有的。
3. 单链表的建立
一个链表是有头尾的,所以单链表的建立也就分为头插法和尾插法建立链表。
所谓头插法:就是每一次都将新的节点称为第一个元素。
下面的是头插法创建链表的C语言的实现:
void CreateListHead(LinkList * L, int n) {
LinkList p;
int i;
*L = (LinkList) malloc (sizeof(LNode));
(*L)->next = NULL;
for (i = 0; i < n; i++) {
p = (LinkList) malloc(sizeof(LNode));
p->data = i;
p->next = (*L)->next;
(*L)->next = p;
}
}
尾插法:尾插法是将每一个元素插在链表的尾部,也就是说,每一次都将新的节点称为链表的最后一个节点。
尾插发创建链表是比较常用的创建链表的方法。
下面的是尾插发创建链表的实现:
void CreateListTail(LinkList * L, int n) {
LinkList p, r;
int i;
*L = (LNode *) malloc(sizeof(LNode));
r = *L;
for (i = 0; i < n; i++) {
p = (LinkList) malloc(sizeof(LNode));
p->data = i;
r->next = p;
r = p;
}
r->next = NULL;
}
4. 读取链表中的值
链表就像一个链子,元素是一个一个链接起来的,要找第i个元素,那么就得从头开始往后找。
下面我们来先看一下单链表的遍历。
LinkList p = L->next;
while (p != NULL) {
printf("%d\n", p->data);
p = p->next;
}
单链表的遍历就是从第一个元素开始往后知道遇到NULL。
读取第i个元素是,从第一个元素开始遍历到第i个元素,但是当没到第i个时,已经遇到了NULL,那么证明i大于链表中元素的个数。代码如下:
// 读取
int GetElem(LinkList L, int i, int *e) {
int j = 1;
LinkList p;
p = L->next;
while (p && j < i) {
p = p->next;
++j;
}
if (!p || j > i) {
printf("读取的节点不存在!");
return 0;
}
*e = p->data;
return 1;
}
5. 删除
在一个链表中如果想要删除第i个节点,那么应该找到它的第i个节点的前一个节点,让它的next指向第i个节点之后的哪一个节点。如下图:
代码实现如下:
// 删除
int ListDelete(LinkList *L, int i, int *e) {
int j = 1;
LinkList p, q;
p = *L;
while (p->next != NULL && j < i) {
p = p->next;
++j;
} // 找到第i个节点的前一个节点
if (!(p->next) || j < 1) {
printf("要删除的元素不存在!");
return 0;// 0代表失败
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return 1;// 1代表成功
}
6. 插入
单链表的插入也非常的简单,想要插入q节点在第二个位置,那么得找到第二个位置的前一个节点p,那么也就知道了第二个位置的节点p->next。那么首先让q->next = p->next,也就是让q指向p->next这个节点,然后再p->next = p,让p指向q。核心如下:
q->next = p->next;
p->next = q;
实现如下:
// 插入
int ListInsert(LinkList *L, int i, int e) {
int j = 1;
LinkList p, s;
p = *L;
while (p != NULL && j < i) {
p = p->next;
++j;
}
if (p == NULL || j > i) {
printf("要插入的位置不存在!!\n");
return 0;
}
s = (LinkList) malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return 1;
}
7. 还有一点:删除整个链表
当我们不在使用被创建的链表时,就要删除整个链表。这个过程也很简单。从头遍历到尾,把每个节点都free()。
int ClearList(LinkList *L) {
LinkList p, q;
p = (*L)->next;
while (p) {
q = p->next;
free(p);
p = q;
}
(*L) = NULL;
return 1;
}