0.前言🐑
🌵🌵
大家好啊,2天不见,甚是想念,呜呜网课要结束了,今天就要开始线下上课了,ε=(´ο`*)))唉,美好生活不复返了。话不多说,今天开始回顾链表中的无头单向非循环链表。

讯享网
🌵🌵

本节重点:
- 链表&顺序表对比
- 单链表各个接口的实现
1.链表 🐱
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
关于顺序表的不足:
- 扩容有性能消耗且有可能存在空间浪费。
扩容时,如果扩小了,大量插入数据时,频繁扩容,性能消耗较大;如果扩大了,又会存在空间浪费。
例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。- 头部或中间插入数据时,需要挪动数据,降低效率
链表优点
- 按需申请内存,不存在空间浪费
- 任意位置O(1)时间插入删除数据
链表缺点
- 不支持下标的随机访问
总结:顺序表和链表相辅相成,使用要看具体应用场景
2.单链表🐶

struct SListNode {
int data; struct SListNode* next; }; void Test() {
struct SListNode* node1 = (struct SListNode*)malloc(sizeof(struct SListNode)); struct SListNode* node2 = (struct SListNode*)malloc(sizeof(struct SListNode)); struct SListNode* node3 = (struct SListNode*)malloc(sizeof(struct SListNode)); struct SListNode* node4 = (struct SListNode*)malloc(sizeof(struct SListNode)); printf("%p\n", node1); printf("%p\n", node2); printf("%p\n", node3); printf("%p\n", node4); } //node1 等其实只是结点地址
讯享网

地址均不连续,堆上使用的空间地址由高到低
逻辑结构&物理结构
链表逻辑结构线性,但物理结构是非线性的。
pList(头结点)是指针变量,存的是第一个节点的地址
当我们看到链表的实现都有箭头指向下一个节点,但实际上是没有箭头的,只不过是把下一个节点的地址存到了当前节点的next的值

链表组合
- 单向双向
- 带头不带头
![]()
- 循环非循环
![]()
最多有8种组合
虽然有这么的组合,但实际中最常用的只有2种:

- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
3.单链表实现🐦
定义
讯享网typedef int SListDataType; typedef struct SListNode // Single Link List {
SListDataType data; struct SListNode *next; //存储下一个节点的地址 } SListNode;
创建节点
SListNode *CreateNewNode(SListDataType x) {
SListNode *newNode = (SListNode *)malloc(sizeof(SListNode)); if (newNode == NULL) {
printf("malloc newNode fail\n"); exit(-1); } else {
newNode->data = x; newNode->next = NULL; } return newNode; }
打印
讯享网void SListPrint(SListNode* pList) {
//不需要assert(plist) SListNode* cur = pList; while (cur != NULL) {
printf("%d->", cur->data); cur = cur->next;//cur->next里面存的就是下一个结点的地址 } printf("NULL\n"); }
提问:这里需要用断言吗?为什么不需要?
记住:一定不能为空的才需要用断言,此处既是链表为空,那我们就什么也不打印就好了啊,断言太暴力了。
提问:这里需要传二级指针吗?为什么?
可以传,但没必要,Print只是打印数据,不会修改数据,因此不需要传二级指针。
查找
查找不需要修改,也就不用传址调用,也就不需要传二级指针。
但如果传了也没问题。
//单链表查找 SListNode *SListFind(SListNode *plist, SListDataType x) {
SListNode *cur = plist; // while(cur != NULL) while (cur) {
if (cur->data == x) {
return cur; //查找兼具修改的作用 } cur = cur->next; } return NULL; }
效果展示:
尾删
- 没有节点,无法删除,直接return
- 一个节点,直接free掉并置空即可,但注意需要对实参进行操作,所以需要传实参地址,也就是传二级指针
注意:free 之后要置空,因为free掉的是指针指向的内容,但指针还是指向那块空间的,因此要把指针置空- 多个节点
多个节点时,删除尾节点需要把prev->next置为NULL 再把tail free掉。
如果用多个节点的代码去针对单个节点的情况,会产生解引用空指针的情况。
讯享网void SListPopBack(SListNode** ppList) {
//1.没有节点,无法删除,直接return if(*ppList == NULL) {
return; } //2.单个节点 else if((*ppList)->next == NULL) {
free(*ppList); *ppList = NULL; } //3.多个节点 else {
SListNode* prev = NULL; SListNode* tail = *ppList; while (tail->next != NULL) {
prev = tail; tail = tail->next; } free(tail); tail = NULL; prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行 } }
效果展示:
传引用:
void SListPopBack(SListNode*& pList) {
//1.没有节点,无法删除,直接return if(pList == NULL) {
return; } //2.单个节点 else if((pList)->next == NULL) {
free(pList); pList = NULL; } //3.多个节点 else {
SListNode* prev = NULL; SListNode* tail = pList; while (tail->next != NULL) {
prev = tail; tail = tail->next; } free(tail); tail = NULL; prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行 } }
效果展示:
头删
- 没有节点:
直接return- 单个节点:
- 多个节点:
![]()
讯享网void SListPopFront(SListNode **ppList) {
// 1.没有节点 if (*ppList == NULL) {
return; } // 2.单个节点 // 3.多个节点 //先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。 //保存plist->next,如果直接free plist就找不到后面的空间了 SListNode *next = (*ppList)->next; free(*ppList);//这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。 *ppList = next; }
效果展示:

传引用:
void SListPopFront(SListNode *&pList) {
// 1.没有节点 if (pList == NULL) {
return; } // 2.单个节点 // 3.多个节点 //先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。 //保存plist->next,如果直接free plist就找不到后面的空间了 SListNode *next = pList->next; free(pList); //这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。 pList = next; }
效果展示:
头插

讯享网void SListPushFront(SListNode *&pList, SListDataType x) {
//即使传进来的是NULL也能解决 SListNode *newNode = CreateNewNode(x); newNode->next = pList; //pList指向的就是第一个节点,其实存的也就是第一个节点的地址 pList = newNode; }

传引用的写法:
void SListPushFront(SListNode *&plist, SListDataType x) {
//即使传进来的是NULL也能解决 SListNode *newNode = CreateNewNode(x); newNode->next = plist; //*pplist 其实就是 plist plist = newNode; }

尾插
- 链表本身为空,直接插入就好。
- 链表不为空,遍历找到尾。
- 注意需要用二级指针,因为在判断空链表的情况时,需要对实参sList进行操作,才需要传址调用。
*ppList 其实就是 pList
讯享网void SListPushBack(SListNode **ppList, SListDataType x) {
//同样不需要断言空,因为本来就有可能传空链表 SListNode* newNode = CreateNewNode(x); //1.空链表 if(*ppList == NULL) {
*ppList = newNode;//也就是把newNode的地址覆盖掉sList原来的NULL地址 //要修改sList必须传址调用 } //2.正常链表,去找尾 else {
SListNode* tail = *ppList;//不能直接修改plist,plist一改就找不到链表了 while (tail->next != NULL) {
tail = tail->next; } //出来时tail->next 指向的是NULL tail->next = newNode; } }
带上头节点可以不用传址调用,因为不需要修改plist
演示效果:
如果不想用二级指针,也可以传引用。
void SListPushBack(SListNode *&pList, SListDataType x) {
//同样不需要断言空,因为本来就有可能传空链表 SListNode* newNode = CreateNewNode(x); //1.空链表 if(pList == NULL) {
pList = newNode;//也就是把newNode的地址覆盖掉pList原来的NULL地址 //传进来空链表,要修改plist必须传址调用 } //2.正常链表,去找尾 else {
SListNode* tail = pList;//不能直接修改plist,plist一改就找不到链表了 while (tail->next != NULL) {
tail = tail->next; } //出来时tail->next 指向的是NULL tail->next = newNode; } }
效果展示:
pos后插入
2种插入方式:
注意要操作顺序,如果先让pos-> next 指向了newNode 之后就找不到pos的下一个了。
第二种:
将pos的下一个临时保存起来就行,这样顺序就没关了。
讯享网//在pos后面插入 void SListInserAfter(SListNode *pos, SListDataType x) {
assert(pos); SListNode *newNode = CreateNewNode(x); //注意顺序不要反了 newNode->next = pos->next; pos->next = newNode; }
//或者临时保存 pos->next //在pos后面插入 void SListInserAfter(SListNode *pos, SListDataType x) {
assert(pos); SListNode *newNode = CreateNewNode(x); SListNode* next = pos->next;//这样就无需关心顺序问题了 pos->next = newNode; newNode->next = next; }
效果展示:
pos前插入
除了要让 newNode 指向pos,还需pos之前的节点指向 newNode ,因此需要找到pos的前一个位置,只能从头开始遍历,非常麻烦且不实用。
- 多个节点的情况:
- 如果pos是第一个节点呢?
如果还让prev指向newNode,那就发生了空指针解引用的问题。可以直接用 if 过滤掉这种情况。
pos是第一个节点的,其实就相当于是头插,但是头插的话,需要改变实参的sList,要让传进来的pList指向newNode,因此还需要传二级指针或者传引用。
讯享网void SListInserBefore(SListNode** ppList, SListNode* pos, SListDataType x) {
assert(pos); SListNode* newNode = CreateNewNode(x); if(*ppList == pos)//相当于头插 {
newNode->next = pos; *ppList = newNode; } else {
SListNode* prev = NULL; SListNode* cur = *ppList; while (cur != pos) {
prev = cur; cur = cur->next; } prev->next = newNode; newNode->next = cur; } }
效果展示:
传引用的写法:
void SListInserBefore(SListNode *&pList, SListNode *pos, SListDataType x) {
assert(pos); SListNode *newNode = CreateNewNode(x); if (pList == pos) //相当于头插 {
newNode->next = pos; pList = newNode; } else {
SListNode *prev = NULL; SListNode *cur = pList; while (cur != pos) {
prev = cur; cur = cur->next; } prev->next = newNode; newNode->next = cur; } }
效果展示:
提问:
在一个无头(不告诉头指针)单链表的某一个节点前面插入一个值x,怎么插?

pos后擦除
- 只有一个节点:
没有可删除的,直接return- 多个节点
![]()
先记录pos的下一个节点,如何让pos指向它的下一个节点的下一个节点,再free 之前记录的pos的下一个节点并置空。
- 后一个为空时,同样适用。
讯享网void SListEraseAfter(SListNode *pos) {
assert(pos); //只有一个节点的情况 if (pos->next == NULL) {
return; } else {
SListNode *next = pos->next; pos->next = next->next; free(next); next = NULL; } }
pos擦除
- 多个节点,需要找pos位置的前一个节点prev,然后free pos,让prev指向pos后面的那个
![]()
- pos指向的是第一个节点,其实就相当于头删,需要改变实参,因此传二级指针或传引用。需要先保存pList的下一个节点,然后free pos,再让pList指向next
void SListEraseCur(SListNode** ppList, SListNode* pos) {
//pos指向第一个节点,相当于头删 if(pos == *ppList) {
SListNode* next = (*ppList)->next; free(*ppList); *ppList = next; } else {
SListNode* prev = NULL; SListNode* cur = *ppList; while (cur != pos) {
prev = cur; cur = cur->next; } //出来时cur指向的pos,prev指向pos前一个 prev->next = cur->next; free(cur); cur = NULL; } }
效果展示:
讯享网void SListEraseCur(SListNode *&pList, SListNode *pos) {
// pos指向第一个节点,相当于头删 if (pos == pList) {
SListNode *next = pList->next; free(pList); pList = next; } else {
SListNode *prev = NULL; SListNode *cur = pList; while (cur != pos) {
prev = cur; cur = cur->next; } //出来时cur指向的pos,prev指向pos前一个 prev->next = cur->next; free(cur); cur = NULL; } }
效果展示:
4.源代码:🐘
SLinkList.h
#pragma once #include <stdio.h> #include <assert.h> #include <string.h> #include <stdlib.h> typedef int SListDataType; typedef struct SListNode // Single Link List {
SListDataType data; struct SListNode *next; //存储下一个节点的地址 } SListNode; void SListPrint(SListNode *pList); SListNode *CreateNewNode(SListDataType x); // void SListPushBack(SListNode ppList, SListDataType x); void SListPushBack(SListNode *&pList, SListDataType x); void SListPushFront(SListNode *&pList, SListDataType x); // void SListPushFront(SListNode pplist, SListDataType x); // void SListPopBack(SListNode ppList); void SListPopBack(SListNode *&pList); // void SListPopFront(SListNode ppList); void SListPopFront(SListNode *&pList); SListNode *SListFind(SListNode *plist, SListDataType x); void SListInserAfter(SListNode *pos, SListDataType x); // void SListInserBefore(SListNode ppList, SListNode* pos, SListDataType x); void SListInserBefore(SListNode *&pList, SListNode *pos, SListDataType x); void SListEraseAfter(SListNode *pos); void SListEraseCur(SListNode *&pList, SListNode *pos); // void SListEraseCur(SListNode ppList, SListNode* pos);
SLinkList.cpp
讯享网#include "SLinkList.h" void SListPrint(SListNode *pList) {
//不需要assert(plist) SListNode *cur = pList; while (cur != NULL) {
printf("%d->", cur->data); cur = cur->next; // cur->next里面存的就是下一个结点的地址 } printf("NULL\n"); } SListNode *CreateNewNode(SListDataType x) {
SListNode *newNode = (SListNode *)malloc(sizeof(SListNode)); if (newNode == NULL) {
printf("malloc newNode fail\n"); exit(-1); } else {
newNode->data = x; newNode->next = NULL; } return newNode; } // void SListPushBack(SListNode ppList, SListDataType x) // {
// //同样不需要断言空,因为本来就有可能传空链表 // SListNode* newNode = CreateNewNode(x); // //1.空链表 // if(*ppList == NULL) // {
// *ppList = newNode;//也就是把newNode的地址覆盖掉pList原来的NULL地址 // //传进来空链表,要修改plist必须传址调用 // } // //2.正常链表,去找尾 // else // {
// SListNode* tail = *ppList;//不能直接修改plist,plist一改就找不到链表了 // while (tail->next != NULL) // {
// tail = tail->next; // } // //出来时tail->next 指向的是NULL // tail->next = newNode; // } // } void SListPushBack(SListNode *&pList, SListDataType x) {
//同样不需要断言空,因为本来就有可能传空链表 SListNode *newNode = CreateNewNode(x); // 1.空链表 if (pList == NULL) {
pList = newNode; //也就是把newNode的地址覆盖掉pList原来的NULL地址 //传进来空链表,要修改plist必须传址调用 } // 2.正常链表,去找尾 else {
SListNode *tail = pList; //不能直接修改plist,plist一改就找不到链表了 while (tail->next != NULL) {
tail = tail->next; } //出来时tail->next 指向的是NULL tail->next = newNode; } } void SListPushFront(SListNode *&pList, SListDataType x) {
//即使传进来的是NULL也能解决 SListNode *newNode = CreateNewNode(x); newNode->next = pList; // pList指向的就是第一个节点,其实存的也就是第一个节点的地址 pList = newNode; } // void SListPopBack(SListNode ppList) // {
// //1.没有节点,无法删除,直接return // if(*ppList == NULL) // {
// return; // } // //2.单个节点 // else if((*ppList)->next == NULL) // {
// free(*ppList); // *ppList = NULL; // } // //3.多个节点 // else // {
// SListNode* prev = NULL; // SListNode* tail = *ppList; // while (tail->next != NULL) // {
// prev = tail; // tail = tail->next; // } // free(tail); // tail = NULL; // prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行 // } // } void SListPopBack(SListNode *&pList) {
// 1.没有节点,无法删除,直接return if (pList == NULL) {
return; } // 2.单个节点 else if ((pList)->next == NULL) {
free(pList); pList = NULL; } // 3.多个节点 else {
SListNode *prev = NULL; SListNode *tail = pList; while (tail->next != NULL) {
prev = tail; tail = tail->next; } free(tail); tail = NULL; prev->next = NULL; //尾删时要将最后一个结点的上一个结点的next置为NULL才行 } } // void SListPopFront(SListNode ppList) // {
// // 1.没有节点 // if (*ppList == NULL) // {
// return; // } // // 2.单个节点 // // 3.多个节点 // //先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。 // //保存plist->next,如果直接free plist就找不到后面的空间了 // SListNode *next = (*ppList)->next; // free(*ppList);//这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。 // *ppList = next; // } void SListPopFront(SListNode *&pList) {
// 1.没有节点 if (pList == NULL) {
return; } // 2.单个节点 // 3.多个节点 //先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。 //保存plist->next,如果直接free plist就找不到后面的空间了 SListNode *next = pList->next; free(pList); //这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。 pList = next; } //单链表查找 SListNode *SListFind(SListNode *plist, SListDataType x) {
SListNode *cur = plist; // while(cur != NULL) while (cur) {
if (cur->data == x) {
return cur; //查找兼具修改的作用 } cur = cur->next; } return NULL; } // //在pos后面插入 // void SListInserAfter(SListNode *pos, SListDataType x) // {
// assert(pos); // SListNode *newNode = CreateNewNode(x); // //注意顺序不要反了 // newNode->next = pos->next; // pos->next = newNode; // } //或者临时保存 pos->next //在pos后面插入 void SListInserAfter(SListNode *pos, SListDataType x) {
assert(pos); SListNode *newNode = CreateNewNode(x); SListNode *next = pos->next; //这样就无需关心顺序问题了 pos->next = newNode; newNode->next = next; } // void SListInserBefore(SListNode ppList, SListNode* pos, SListDataType x) // {
// assert(pos); // SListNode* newNode = CreateNewNode(x); // if(*ppList == pos)//相当于头插 // {
// newNode->next = pos; // *ppList = newNode; // } // else // {
// SListNode* prev = NULL; // SListNode* cur = *ppList; // while (cur != pos) // {
// prev = cur; // cur = cur->next; // } // prev->next = newNode; // newNode->next = cur; // } // } void SListInserBefore(SListNode *&pList, SListNode *pos, SListDataType x) {
assert(pos); SListNode *newNode = CreateNewNode(x); if (pList == pos) //相当于头插 {
newNode->next = pos; pList = newNode; } else {
SListNode *prev = NULL; SListNode *cur = pList; while (cur != pos) {
prev = cur; cur = cur->next; } prev->next = newNode; newNode->next = cur; } } void SListEraseAfter(SListNode *pos) {
assert(pos); //只有一个节点的情况 if (pos->next == NULL) {
return; } else {
SListNode *next = pos->next; pos->next = next->next; free(next); next = NULL; } } // void SListEraseCur(SListNode ppList, SListNode* pos) // {
// //pos指向第一个节点,相当于头删 // if(pos == *ppList) // {
// SListNode* next = (*ppList)->next; // free(*ppList); // *ppList = next; // } // else // {
// SListNode* prev = NULL; // SListNode* cur = *ppList; // while (cur != pos) // {
// prev = cur; // cur = cur->next; // } // //出来时cur指向的pos,prev指向pos前一个 // prev->next = cur->next; // free(cur); // cur = NULL; // } // } void SListEraseCur(SListNode *&pList, SListNode *pos) {
// pos指向第一个节点,相当于头删 if (pos == pList) {
SListNode *next = pList->next; free(pList); pList = next; } else {
SListNode *prev = NULL; SListNode *cur = pList; while (cur != pos) {
prev = cur; cur = cur->next; } //出来时cur指向的pos,prev指向pos前一个 prev->next = cur->next; free(cur); cur = NULL; } }
Test.cpp
#include "SLinkList.h" void Test1() {
SListNode *sList = NULL; SListPrint(sList); // NULL } void Test2() {
SListNode *sList = NULL; //空链表 // SListPushBack(&sList, 1); // SListPushBack(&sList, 2); // SListPushBack(&sList, 3); SListPushBack(sList, 1); SListPushBack(sList, 2); SListPushBack(sList, 3); SListPrint(sList); // SListPushFront(&sList, 2); // SListPushFront(&sList, 3); SListPushFront(sList, 1); SListPushFront(sList, 2); SListPushFront(sList, 3); SListPrint(sList); } void Test3() {
SListNode *sList = NULL; //空链表 SListPushBack(sList, 1); SListPushBack(sList, 2); SListPushBack(sList, 3); // SListPrint(sList); // SListPopBack(&sList); // SListPrint(sList); SListPrint(sList); SListPopBack(sList); SListPrint(sList); // SListPopFront(&sList); SListPopFront(sList); SListPrint(sList); } void Test4() {
SListNode *sList = NULL; //空链表 SListPushBack(sList, 1); SListPushBack(sList, 2); SListPushBack(sList, 3); SListPrint(sList); SListPopFront(sList); SListPrint(sList); SListPopFront(sList); SListPrint(sList); SListPopFront(sList); SListPrint(sList); SListPopFront(sList); SListPrint(sList); } void Test5() {
SListNode *sList = NULL; //空链表 SListPushBack(sList, 1); SListPushBack(sList, 2); SListPushBack(sList, 3); SListPrint(sList); SListNode *pos = SListFind(sList, 3); if (pos) {
pos->data = 30; printf("找到了并修改为30\n"); } else {
printf("找不到\n"); } SListPrint(sList); } void Test6() {
SListNode *sList = NULL; //空链表 SListPushBack(sList, 1); SListPushBack(sList, 2); SListPushBack(sList, 3); SListPrint(sList); SListNode *pos = SListFind(sList, 2); SListInserAfter(pos, 10); SListPrint(sList); // SListInserBefore(&sList, pos, 20); // SListPrint(sList); // SListNode *pos2 = SListFind(sList, 1); // SListInserBefore(&sList, pos2, 100); // SListPrint(sList); SListInserBefore(sList, pos, 20); SListPrint(sList); SListNode *pos2 = SListFind(sList, 1); SListInserBefore(sList, pos2, 1000); SListPrint(sList); SListNode *pos3 = SListFind(sList, 1000); SListEraseAfter(pos3); SListPrint(sList); } void Test7() {
SListNode *sList = NULL; //空链表 SListPushBack(sList, 1); SListPushBack(sList, 2); SListPushBack(sList, 3); SListPrint(sList); // SListNode *pos1 = SListFind(sList, 2); // SListEraseCur(&sList, pos1); // SListPrint(sList); // SListNode *pos2 = SListFind(sList, 1); // SListEraseCur(&sList, pos2); // SListPrint(sList); SListNode *pos1 = SListFind(sList, 2); SListEraseCur(sList, pos1); SListPrint(sList); SListNode *pos2 = SListFind(sList, 3); SListEraseCur(sList, pos2); SListPrint(sList); } int main(int argc, char const *argv[]) {
Test7(); system("pause"); return 0; }
5.尾声🐜
🌵🌵
今天的单链表就回顾到这里啦。
写文不易,如果有帮助烦请点个赞~ 👍👍👍
🌹🌹Thanks♪(・ω・)ノ🌹🌹
👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接





















版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/51881.html