一、概念
对于顺序存储的结构,如数组,最大的缺点就是:插入 和 删除 的时候需要移动大量的元素。所以,基于前人的智慧,他们发明了链表。
1.链表的定义
链就说明有一条链子,表就是一个结点,把结点用链子串起来,不就是链表了嘛
每个结点都有一个后继结点和前驱结点,当然,第一个结点无前驱结点,最后一个结点无后继结点
A无前驱结点,A的后继结点是B;B的前驱结点是A,后继结点是C,C的前驱结点是B,C无后继结点
链表分为单向链表、双向链表、循环链表等等,我们本文介绍的是单向链表
2.结点结构体定义
typedef int DataType;
struct ListNode {
DataType data; // (1)数据域,可以是任意类型
ListNode *next; // (2)指针域,指向后继结点
};
3.结点的创建
ListNode *ListCreateNode(DataType data) {
ListNode *node = (ListNode *) malloc ( sizeof(ListNode) ); // (1)malloc函数在堆区开辟空间
node->data = data; // (2)数据域赋值
node->next = NULL; // (3)指针域置空(孤立结点)
return node; // (4)返回这个结点
}
二、链表的创建-尾插法
1.算法描述
首先介绍 尾插法 ,顾名思义,即 从链表尾部插入 的意思,就是记录一个 链表尾结点,然后遍历给定数组,将数组元素一个一个插到链表的尾部,每插入一个结点,则将它更新为新的 链表尾结点。注意初始情况下,链表尾结点 为空。
head代表链表头结点,vtx代表要加入的结点,tail代表尾结点
2.代码示例
ListNode *ListCreateListByTail(int n, int a[]) {
ListNode *head, *tail, *vtx; // (1)定义头结点,尾结点,插入的节点
int idx;
if(n <= 0)
return NULL; // (2)创建元素为0的时候,返回NULL
idx = 0;
vtx = ListCreateNode(a[0]); // (3)vtx指向创建好的结点
head = tail = vtx; // (4)初始情况下都指向vtx创建好的结点
while(++idx < n) { // (5)开始循环
vtx = ListCreateNode(a[idx]); // (6) vtx指向新创建的结点
tail->next = vtx; // (7) 尾结点的指针域指向新创建的结点
tail = vtx; // (8) 尾结点tail后移,指向新创建的结点,
//让tail始终指向最后一个节点
}
return head; // (9) 返回头结点
}
三、链表的创建-头插法
1.算法描述
头插法就是从头结点前面进行插入,所以插入的数据元素是逆序的
特点是代码量短,时间复杂度低
2.代码示例
ListNode *ListCreateListByHead(int n, int *a) {
ListNode *head = NULL, *vtx; // (1)heda存储头结点地址,vtx存储当前要插入的元素
while(n--) { // (2) n个结点
vtx = ListCreateNode(a[n]); // (3) vtx指向创建已好的结点
vtx->next = head; // (4) 将当前创建的结点的 后继结点 置为 链表的头结点head
head = vtx; // (5) 将head置为vtx
}
return head; // (6) 返回头结点
}
四、链表的打印
1.打印的作用
可视化 能够帮助我们更好的理解数据结构。所以,对于一种数据结构,如何通过 输出函数 将它 打印到控制台 上,就成了我们接下来要做的事情。
2.代码示例
void ListPrint(ListNode *head) {
ListNode *vtx = head;
while(vtx) { // (1)从头结点开始遍历
printf("%d -> ", vtx->data); // (2) 输出数据域
vtx = vtx->next; // (3) 指向下一个结点
}
printf("NULL\n"); // (4) 代表结束
}
五、链表元素的查找
1.算法描述
给定一个链表头head
,并且给定一个值 ,查找出这个链表上 数据域 等于
查找的过程,就是对链表的遍历
2.代码示例
ListNode *ListFindNodeByValue(ListNode *head, DataType v) {
ListNode *temp = head; // (1) temp指向头结点
while(temp) { // (2) 从头结点开始查找
if(temp->data == v) {
return temp; // (3) 找到了,返回该结点对应的指针
}
temp = temp->next; // (4) 没找到,指向下一个结点
}
return NULL; // (5) 链表里没有该元素,返回NULL
}
时间复杂度,最坏的情况就是找不到,需要遍历整个链表,时间为O(n)
六、链表结点的插入
1.算法描述
我们需要在第i个结点后插入一个值为V的结点
首先我们需要先找到第i个位置
然后执行插入操作:
插入操作分为两步:第一步就是 创建结点 的过程;第二步,是断开之前第 个结点 和 第
个结点之间的链,并将新创建的结点放到这两个结点之间链起来
2.代码示例
ListNode *ListInsertNode(ListNode *head, int i, DataType v) {
ListNode *pre, *vtx, *aft; // (1)定义三个指针
int j = 0; // (2)计数器,j==i说明找到了我们需要的那个位置
pre = head; // (3)pre指向头结点
while(pre && j < i) { // (4) pre不为空,j还没到i的位置继续遍历
pre = pre->next; // (5) pre指向下一个结点
++j; // (6) j++
}
if(!pre) {
return NULL; // (7)如果没有找到,返回NULL
}
vtx = ListCreateNode(v); // (8)找到了,创建结点
aft = pre->next; //插入这段我用一张图来说明
vtx->next = aft;
pre->next = vtx;
return vtx;
}
插入的操作时间复杂度为O(1),但是寻找位置的时候,最坏情况下就是找不到该位置,时间为O(n)
七、链表结点的删除
1.算法描述
给定位置i,将i这个位置的结点删去,返回头结点
链表结点删除分为三种情况:
空链表
非空链表:删除链表头结点,删除链表非头结点
(1)空链表,直接返回NULL
(2)非空链表删除头结点,将头结点的下一个结点位置保存作为新头结点,释放头结点
(3)非空链表删除非头结点,遍历链表,找到要删除的结点,标记删除结点的前驱结点和后继结点,释放要删除的结点,将它的前驱结点和后继节点链起来
2.代码演示
ListNode *ListDeleteNode(ListNode *head, int i) {
ListNode *pre, *del, *aft;
int j = 0;
if(head == NULL) {
return NULL; // (1)空链表,返回NULL
}
if(i == 0) { // (2)删除第0个结点(头结点)
del = head; // (3)标记头结点
head = head->next; // (4)让头结点指向它的下一个结点
free(del); // (5)释放第0个结点
return head; // (6)返回新的头结点位置
}
pre = head; // (7)从头结点开始遍历链表
while(pre && j < i - 1) { // (8)找到要删除结点的前驱结点
pre = pre->next;
++ j;
}
del = pre->next; // (9)标记删除结点del
aft = del->next; // (11)标记删除结点的后继结点
pre->next = aft; // (12)将删除结点的前驱结点和后继结点链上
free(del); // (13) 释放删除结点
return head; // (14) 返回头结点
}
八、链表的销毁
因为我们链表是在堆区里申请的,所以需要销毁
void ListDestroyList(ListNode **pHead) { // (1)这里必须用二级指针,
// 因为删除后需要将链表头置空
// 普通的指针传参无法影响外部指针变量;
ListNode *head = *pHead;
while(head) {
head = ListDeleteNode(head, 0);
}
*pHead = NULL;
}