链表(Linked List)
链表是由链式存储
的方式实现的顺序表。(不支持随机存取)
0x01 单链表
顺序表(顺序存储):
- 优点:可随机存取,存储密度高
- 缺点:要求大片连续空间,改变容量不方便
单链表(链式存储):
- 优点:不要求大片连续空间,改变容量方便
- 缺点:不可随机存取,要耗费一定空间存放指针
0x0101单链表的实现
1 2 3 4 5 6
| struct LNode{ ElemType data; struct LNode *next; }; typedef struct LNode LNode; typedef struct LNode *LinkList;
|
或者,
1 2 3 4
| typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList;
|
两者等价。
要表示一个单链表时,只需声明一个头指针
L,指向单链表的第一个节点:
或者,
强调这是一个单链表 – 使用LinkList
强调这是一个结点 – 使用LNode
单链表又分为带头结点
和不带头结点
两种:
如果不带头结点,写代码更麻烦,对对一个数据结点
和后续数据结点
的处理需要用不同的代码逻辑.
对空表
和非空表
的处理需要用不同的代码逻辑
总之,用带头结点的单链表写代码更方便.
下面将会分别用代码演示一下带头结点和不带头结点的区别.
如果是考研的好哥哥,带头结点和不带头结点都有可能考,审题就完了.
0x02 带头结点的单链表
(头节点不存放数据data)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include<stdio.h> #include<stdlib.h>
typedef struct LNode{ int data; struct LNode* next; }LNode, *LinkList;
bool InitList(LinkList& L) { L = (LNode*)malloc(sizeof(LNode)); if (L == NULL) return false; L->next = NULL; return true; }
bool isEmpty(LinkList L) { if (L->next == NULL) return true; else return false; }
int main(int argc, char const *argv[]) { LinkList L; InitList(L); return 0; }
|
0x0201 按位序插入操作
算法思想: 遍历链表, 找到需要插入的地方直接插入即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
bool ListInsert(LinkList& L, int i, int e) { if (i < 1) return false; LNode* p; int j = 0; p = L; while (p != NULL && j < i - 1) { p = p->next; j++; } if (p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
|
注意⚠️: 第21行开始将结点e插入新开辟的空间s中,第22行和第23行的代码不能互换,
否则p->next = s
之后p->next
将会指向它自己,从而导致后方的链表被丢失.
0x0202 指定结点的后插操作
算法思想: 向p结点之后插入元素e, 只需要新分配一个空间, 然后将需要插入的元素e放进这个空间, 再把这个新开辟的空间给放入我们需要插入的地方就行了, 具体实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
bool InsertNextNode(LNode* p, int e) { if (p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return false; s->data = e; s->next = p->next; p->next = s; return true; }
|
0x0203 指定结点的前插操作
注意⚠️: 单链表在不知道链表头指针的情况下是无法知道某一个结点之前的链表区域的.
算法思想: 在给定的结点之前插入一个新的元素, 首先循环查找所给的都结点p的前驱结点q, 然后再对q进行后插操作.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
bool InsertPriorNode(LinkList& L, LNode* p, int e) { LNode* q; int j = 0; q = L; while (p != NULL) { q = q->next; j++; if (q->next == p) return InsertNextNode(q, e); } return false;
}
|
通过上面的代码可以看出这个算法的时间复杂度为 $O(n)$.
而且必须知道链表的头指针L
, 那有没有什么办法可以直接在所给结点p
前面直接插入呢?答案是有的:
算法思想: 首先申请一片空间s
, 先把新结点s
连接🔗到所给结点p
之后, 然后把p
中的元素复制到新结点s
中, 最后将结点p
中的元素(data)覆盖为所要插入的元素e
.
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
bool InsertPriorNode(LNode* p, int e) { if (p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return false; s->next = p->next; p->next = s; s->data = p->data; p->data = e; return true; }
|
通过上面的代码可以看出这个算法的时间复杂度为 $O(1)$, 优于第一种方法.
0x0203 按位序删除操作
算法思想: 遍历链表, 寻找位序i的上一个位序为i-1的结点p
, 结点p下一个结点q
就是我们需要删除的结点, 然后先用e存放该结点q的元素用于返回删除的元素, 然后使用p->next = q->next
直接删除该结点.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
bool ListDelete(LinkList& L, int i, int& e) { if (i < 1) return false; LNode* p; int j = 0; p = L; while (p != NULL && j < i - 1) { p = p->next; j++; } if (p == NULL) return false; if (p->next == NULL) return false; LNode* q = p->next; e = q->data; p->next = q->next; free(q); return true; }
|
0x0204 指定结点的删除操作
算法思想: 删除结点p, 只要把结点p
的后继结点
完完整整的复制
过来,再把结点p的后继结点从链表中断开,并且释放这个p后继结点的空间.
注意⚠️:
必须要判断删除的结点p是否位于链表末尾,否则有p->next = NULL
, 这时候如果再有p->next->next
会报指向空指针的报错.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
bool DeleteNode(LNode* p) { if (p == NULL) return false; if (p->next == NULL) { p = NULL; free(p); } else { LNode* q = p->next; p->data = p->next->data; p->next = q->next; free(q); } return true; }
|
0x0205 按位查找操作
算法思想: 遍历链表, 查找位序为i的结点, 并返回.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
LNode* GetElem(LinkList L, int i) { if (i < 0) return NULL; LNode* p; int j = 0; p = L; while (p != NULL && j < i) { p = p->next; j++; } return p; }
|
0x0206 按值查找操作
算法思想: 遍历链表, 一一对比每个结点的数据域与所需查找的值e是否相等,找到则返回该结点的指针,否则返回NULL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
LNode* LocateElem(LinkList L, int e) { LNode* p = L->next; while (p != NULL && p->data != e) { p = p->next; } return p; }
|
0x0207 求表的长度
算法思想: 从头到尾依次遍历, 然后用一个变量进行计数操作即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
int Length(LinkList L) { int len = 0; LNode* p = L; while (p->next != NULL) { p = p->next; len++; } return len; }
|
如果是不带头结点的单链表, 则需要在带头结点的表长的基础上加1, 因为带头结点的单链表头结点不带数据域不计入表长中.
0x03 不带头结点的单链表
不带头结点的单链表实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include<stdio.h> #include<stdlib.h> typedef struct LNode{ int data; struct LNode* next; }LNode, *LinkList;
bool InitList(LinkList& L) { L = NULL; return true; }
bool isEmpty(LinkList L) { if (L == NULL) return true; else return false; }
int main(int argc, char const *argv[]) { LinkList L; InitList(L); return 0; }
|
不带头结点的单链表,
- 第一个结点从位序i=1算起
- 而带头结点的位序从i=0这个头指针算起, 且头指针不带数据域.
只需要注意这两点即可, 下面的操作只做按位插入的演示(躺平开摆.jpg🤤), 其他的可参考带头结点的代码, 大致相同.
如果不涉及遍历操作, 两者的代码实现基本一致.
0x0301 按位序插入
不存在“第0个”结点,因此i=1
时需要特殊处理.
在代码实现的时候第i个位置
其实是找到第i-1个结点后将新的结点插入其后.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
bool ListInsert(LinkList& L, int i, int e) { if (i < 1) return false; if (i == 1) { LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode* p; int j = 1; p = L; while (p != NULL && j < i - 1) { p = p->next; j++; } if (p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
|
0x04 单链表的建立
单链表的建立分为:
如果给你很多个数据元素(ElemType), 需要把它们存到一个单链表里, 该怎么做呢?
Step 1: 初始化一个单链表
Step 2: 每次取一个数据元素, 插入到 表尾
或者 表头
.(对应尾插法
和头插法
)
0x0401 尾插法
算法思想:
初始化单链表;
设置变量length记录链表长度;
While 循环{
每次取一个数据元素e;
ListInsert(L, Length+1, e); //插到链表尾部
length++;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
LinkList List_TailInsert(LinkList& L) { int x; L = (LNode*)malloc(sizeof(LNode)); LNode* s, * r = L; scanf("%d", &x); while (x != 999) { s = (LNode*)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; scanf("%d", &x); } r->next = NULL; return L; }
|
0x0402 头插法
**算法思想: **
初始化单链表;
while 循环{
每次取一个数据元素e;
InsertNextNode(L, e); //使用结点后插法插入数据元素
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
LinkList List_HeadInsert(LinkList& L) { LNode* s; int e; L = (LNode*)malloc(sizeof(LNode)); L->next = NULL; scanf("%d", &e); while (e != 999) { s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = L->next; L->next = s; scanf("%d", &e); } return L; }
|
关于头插法的应用还有很重要的一项: 链表的逆置! 其主要思想还是在于头插法.🥳