1. 结点类型定义 listNode
struct listNode
{
int data; // 数据域
struct listNode *next; // 指针域
};
- data: 存储结点的数据(这里是整数)。
- next: 指向下一个结点的指针。如果这是链表的最后一个结点,next 的值为 NULL。
文本图示 (一个结点):
+------+------+
| data | next |
+------+------+
| 数据 | -----> (指向下一个结点或 NULL)
+------+------+
//定义结点类型
struct listNode
{
int data; //数据域
struct listNode *next; //指针域
};
int main()
{
int ar[] = {1, 2, 3, 4, 5};
int n = sizeof(ar) / sizeof(ar[0]);
return 0;
}
2. 尾部插入创建链表 createList(int ar[], int n)
-
功能: 根据数组
ar的内容,创建一个新的单链表。采用尾插法,新结点总是添加到链表的末尾。 -
参数:
ar[]: 包含要插入链表的数据的数组。n: 数组ar中元素的个数。
-
图示 (以
ar = {1, 2, 3},n = 3为例):
初始状态 (创建头结点 phead):
循环过程 (尾部插入):
i = 1:
- 创建一个新结点
s,s->data =ar[1] (2),s->next = NULL。
2.
p->next = s; (将 phead 的 next 指针指向 s)
3.
p = s; (移动 p 指针,使其指向新插入的结点 s)
p = s;在下一次循环中,p就指向了当前链表的最后一个节点,这样才能继续在末尾添加新节点如果没有这一步,
p会一直停留在头节点上,那么每次循环都会将新节点添加到头节点之后,并覆盖之前添加的节点,最终形成的链表只会包含头节点和最后一个添加的节点,而不是所有节点按顺序连接简而言之,就是
p把前面的节点和后面新建的节点链接起来,起到链接的作用
i = 2:
- 创建新结点
s,s->data = ar[2] (3),s->next = NULL。
p->next = s;
循环结束, 返回 phead:
//尾部插入创建链表
listNode *createList(int ar[], int n)
{
//(申请第一个结点)创建头结点
listNode* phead = (listNode*)malloc(sizeof(listNode));
phead->data = ar[0];
phead->next = NULL;
listNode* p = phead;
for(int i = 1; i < n; i++)
{
listNode* s = (listNode*)malloc(sizeof(listNode));
s->data = ar[i];
s->next = NULL;
p->next = s;
p = s; //p = p->next;
}
return phead;
}
int main()
{
listNode* head = NULL; //初始化一个空链表
head = createList(ar, n);
}
3. 打印链表 printList(listNode* phead)
-
功能: 从头到尾遍历链表,打印每个结点的数据域。
-
参数:
- phead: 指向链表头结点的指针。
-
图示 (假设链表为 1 -> 2 -> 3 -> NULL):
//打印链表
void printList(listNode* phead)
{
listNode* p = phead;
while(p != NULL)
{
printf("%d--> ", p->data);
p = p->next; //让 p 指向链表的下一个节点。
}
printf("NULL");
printf("\n");
}
Q&A
Q:同样是p往后移,这里的p = p->next;为什么不能换成p++?
A:因为p 是一个指向 listNode 结构的指针,而 p++ 仅适用于数组指针或指向连续内存的指针,而链表的节点在内存中并不一定是连续存储的。
链表的节点在内存中是动态分配的,节点的位置是随机的,并不保证连续存储。
p++ 只是让 p 增加 sizeof(listNode) 字节,而不是指向链表的下一个节点。
正确的前进方式是按照 p->next 指向的地址跳转,而 p++ 无法做到这一点。
由于 p 是 listNode*,p++ 实际上会将 p 移动到下一个 listNode 位置,假设 listNode 结构体大小为 sizeof(listNode),则 p++ 相当于 p = p + 1;,即增加 sizeof(listNode) 个字节,而不是沿着链表的 next 指针前进。
循环过程:
- p 指向 1, 打印 "1--> "
- p 移动到下一个结点 (2)
- p 指向 2, 打印 "2--> "
- p 移动到下一个结点 (3)
- p 指向 3, 打印 "3--> "
- p 移动到下一个结点 (NULL)
- p 为 NULL, 循环结束, 打印 "NULL"
输出: 1--> 2--> 3--> NULL
4. 查找结点 findNode(listNode* phead, int key)
-
功能: 在链表中查找数据域值为
key的结点。 -
参数:
phead: 指向链表头结点的指针。key: 要查找的数据值。
-
返回值:
- 如果找到值为
key的结点,返回指向该结点的指针。 - 如果未找到,返回
NULL。
- 如果找到值为
-
图示 (假设链表为
1 -> 2 -> 3 -> NULL, 查找key = 2):
//查找结点
listNode* findNode(listNode* phead, int key) //第一种方法
{
listNode* p = phead;
while(p != NULL && p->data != key)
{
p = p->next;
}
return p;
}
循环过程:
- p 指向 1, p->data (1) != key (2)
- p 移动到下一个结点 (2)
- p 指向 2, p->data (2) == key (2)
- 循环结束, 返回 p (指向结点 2 的指针)
5. 在 key 结点后面插入结点 insertNodeBack(listNode* phead, int key, int x)
-
功能: 在链表中找到数据域值为
key的结点,并在其后面插入一个数据域值为x的新结点。 -
参数:
phead: 指向链表头结点的指针。key: 要查找的结点的数据值。x: 要插入的新结点的数据值。
-
返回值: 指向链表头结点的指针 (可能因为在头部插入而改变)。
-
图示 (假设链表为
1 -> 2 -> 3 -> NULL,key = 2,x = 5):
//在key结点后面插入一个结点x
listNode* insertNodeBack(listNode* phead, int key, int x)
{
//查找key结点
listNode *pos = findNode(phead, key); //见上面第四点查找结点findNode函数
if(pos == NULL)
return phead;
//创建新结点
listNode *s = (listNode*)malloc(sizeof(listNode));
s->data = x;
//插入新结点
s->next = pos->next; //保证链接的链表不会断裂
pos->next = s;
return phead;
}
1. 查找 key = 2:
2. 创建新结点
s:
3. 插入新结点:
4. 插入完成
(return phead):
6. 在 key 结点前面插入结点 insertNodeFront(listNode* phead, int key, int x)
- 功能: 在链表中找到数据域值为
key的结点,并在其前面插入一个数据域值为x的新结点。 - 参数: 与
insertNodeBack相同。 - 图示 (假设链表
1 -> 2 -> 3 -> NULL,key = 2,x = 5):
//在key结点前面插入一个结点x
listNode* insertNodeFront(listNode* phead, int key, int x)
{
listNode *p = phead, *pre = NULL; //pre用来记录p的前一个结点
while(p!= NULL && p->data!= key)
{
pre = p;
p = p->next;
}
if(p == NULL)
return phead;
//创建新结点
listNode *s = (listNode*)malloc(sizeof(listNode));
s->data = x;
//插入新结点
s->next = p;
if(pre == NULL) //如果key是第一个结点(在第一个结点前插入)
phead = s; //在第一个结点的前面插入,需要修改表头指针
else
pre->next = s;
return phead;
}
1. 插入前:
2.创建结点
3.插入过程:
//插入
s->next = p;
if(pre == NULL) //如果key是第一个结点(在第一个结点前插入)
phead = s; //在第一个结点的前面插入,需要修改表头指针
else
pre->next = s;
假设key=1, 那么pre==NULL, phead直接指向新结点, 链表就变成了5->1->2->3->NULL
假设
key=2, 那么pre指向1, s插入到1和2之间,链表变成了1->5->2->3->NULL
4.插入结束(return phead,key=2的情况):
7. 删除节点 deleteNode(listNode* phead, int key)
-
功能: 删除链表中第一个值为
key的节点。 -
参数:
phead: 链表头指针。key: 要删除的节点的值。
-
图示 (假设链表为
1 -> 5 -> 2 -> 3 -> NULL,key = 5):
//删除结点
listNode* deleteNode(listNode* phead, int key)
{
//查找key结点
listNode *p = phead, *pre = NULL; //pre用来记录p的前一个结点,前驱结点
while(p!= NULL && p->data!= key)
{
pre = p;
p = p->next;
}
if(p == NULL)
return phead;
//删除结点
if(pre == NULL) //情况 1:如果前驱结点是空的(删除第一个结点)
phead = p->next; //删除第一个结点,需要修改表头指针
else //情况 2:要删除的不是第一个节点
pre->next = p->next; //前一个节点跳过 p,指向 p 的下一个节点
}
free(p);
return phead;
}
1.删除前:
2.删除过程:
情况1 (删除第一个节点, key = 1):
pre == NULL, phead 直接指向原链表的第二个节点, 原第一个节点被跳过
情况2(删除的不是第一个节点,
key=5):
pre 指向 1, p 指向 5, pre->next 直接指向 p->next (2), 5 被跳过
3.删除后 (key = 5 的情况):
8. 反转链表 reverseList(listNode* phead)
-
功能: 将链表中的节点顺序反转(就地反转)。
-
参数:
phead: 链表头指针。
-
图示 (假设链表为 1 -> 2 -> 3 -> 4 -> NULL):
//反转链表
listNode* reverseList(listNode* phead)
{
//断开链表
listNode *p = phead->next;
phead->next = NULL;
//反转链表
while(p!= NULL) //将剩余结点摘除头插
{
listNode* q = p->next;
p->next = phead;
phead = p;
p = q;
}
return phead;
}
1.反转前
2.反转过程 (头插法):
加入
q点的作用是保存后面的链表,因为p点移到plead前面,如果没有q点保存后面的链表,后面的链表就会丢失
修改头指针指向最新的头结点(p点)
循环反复,如此类推,知道p点为空,链表全部反转完成
3.反转完成
9. 排序链表 sortList(listNode* phead) - 使用直接插入排序
-
功能: 将链表中的节点按数据域的值升序排列 (使用插入排序)。
-
参数:
phead: 链表头指针。
-
图示 (假设链表为
4 -> 2 -> 1 -> 3 -> NULL):
//排序链表
listNode* sortList(listNode* phead)
{
//断开链表
listNode *p = phead->next;
phead->next = NULL;
//摘除剩余链表结点按位置插入
while(p!= NULL) //将剩余结点摘除头插
{
listNode* q = p->next;
listNode* cur = phead, *pre = NULL; //cur用来记录p的后一个结点,前驱结点, pre用来记录cur的前一个结点
while(cur!= NULL && cur->data < p->data) //查找插入位置
{
pre = cur;
cur = cur->next;
}
//插入p
if(pre == NULL) //是否在第一个结点前插入
phead = p;
else
pre->next = p;
p->next = cur;
p = q; //更新结点
}
return phead;
}
1.排序前:
2.排序过程(步骤分解):
定义cur指向当前头指针,pre指向cur的前驱,一开始为NULL,这两个指针拿来两两比较
第一趟循环:
while(cur!= NULL && cur->data < p->data)p点与cur对比,cur = 4 > p = 2,循环不执行跳过
if(pre == NULL)成立,执行phead = p;
第二趟循环:
while(cur!= NULL && cur->data < p->data)p点与cur对比,cur = 2 > p = 1,循环不执行跳过
if(pre == NULL)成立,执行phead = p;
第三趟循环:
while(cur!= NULL && cur->data < p->data)p点与cur对比,cur = 1 > p = 3,循环执行
走到下面这一步,循环触发了终止条件:
cur->data < p->data,cur = 4 > p = 3
if(pre == NULL)不成立,执行else下面的语句pre->next = p;
第四趟循环:
p为NULL,循环不执行,至此排序结束