当前位置:   article > 正文

链表OJ题目合集第一弹:移除元素,反转链表,中间结点,倒数第k个结点,合并有序链表,回文结构,相交链表判断。(C语言版,有详细解析、图示和链接)

链表OJ题目合集第一弹:移除元素,反转链表,中间结点,倒数第k个结点,合并有序链表,回文结构,相交链表判断。(C语言版,有详细解析、图示和链接)

目录

前言

1. 移除链表元素

(1)题目及示例

(2)解析

(3)代码

2. 反转链表

(1)题目及示例

(2)题目解析及思路

3.链表的中间结点

(1)题目及示例

(2)题目解析及思路

4.链表中倒数第k个结点

(1)题目及示例

(2)题目解析及思路

5.合并两个有序链表

(1)题目及示例

(2)题目解析及思路

6.链表的回文结构

(1)题目及测试样例

(2)题目解析及思路

7.相交链表

(1)题目及示例

(2)题目解析及思路

总结


前言

上篇文章是有关于两个较常用链表的结构的实现。实现完链表过后,我们还需要会熟练运用链表这种数据结构,所以这里将会有十道链表OJ题目,本篇文章将会先分析解决七道题目,剩下三道留在下一篇文章。每道题都题目后都有链接,还会有大量的图示,帮助你理解题目解法,附上解题代码


1. 移除链表元素

(1)题目及示例

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。链接--力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

  1. //题目结构题定义如下
  2. struct ListNode
  3. {
  4. int val;
  5. struct ListNode *next;
  6. };

 注意:之后题目若没有详细说明,都是以上面单链表结构体为准!!!

示例1:

输入:head = [1,2,6,3,4,5,6],val = 6

输出:[1,2,3,4,5]

示例2:

输入:head = [ ],val = 1

输出:[ ]

示例3:

输入:head = [7,7,7,7],val = 7

输出:[ ]

(2)解析

从题目和示例来看,就是去除链表中存储数据相同的结点时,跟单链表的删除操作十分相似。需要注意的是特殊情况,链表为空的时候直接返回空链表即可。在删除的过程中要分两种情况,分别是头部删除和中间删除,这是因为改题目要求返回新的头结点,中间的删除不会影响头结点的指向。

下面分析这两步操作,在执行前,我会定义prev和cur这两个指针,prev指向空指针,cur指向头结点。prev指向的是cur指针的前一个结点,但是一开始cur指向头结点,所以指向空。

  • 头部删除,假设val = 1。

  • 中间删除,假设val = 3。

  • 迭代往后走,是中间删除这一步的后续。

(3)代码

  1. struct ListNode* removeElements(struct ListNode* head, int val) {
  2. struct ListNode* prev = NULL;
  3. struct ListNode* cur = head;
  4. while(cur)
  5. {
  6. if (cur->val == val)
  7. {
  8. //1.头部珊除
  9. //2.中间删除
  10. if (cur == head)
  11. {
  12. head = cur->next;
  13. free(cur);
  14. cur = head;
  15. }
  16. else
  17. { //删除
  18. prev->next = cur->next;
  19. free(cur);
  20. cur = prev->next;
  21. }
  22. }
  23. else
  24. { //往后走
  25. prev = cur;
  26. cur = cur->next;
  27. }
  28. }
  29. return head;
  30. }

2. 反转链表

(1)题目及示例

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

示例1:

输入:head = [ 1,2,3,4,5 ]

输出:[ 5,4,3,2,1 ]

示例2:

输入:head = [ 1,2,3,4,5 ]

输出:[ 5,4,3,2,1 ]

示例3:

输入:head = [  ]

输出:[  ]

(2)题目解析及思路

这个题目意思比较明显,我们需要把链表整个指向反转过来。有以下几种思路:

1. 正常来说,最平常的思路就是自己开辟与题目示例所给相同的个数的结点,并先遍历链表找到尾结点,依次寻找前面的结点,但是十分麻烦。倘若有五个节点需要遍历4+3+2+1+0次次,时间复杂度相当于O(N^2),空间复杂度为O(N)

2. 在第一种做法的基础上,进一步优化,还是开辟示例所给相同个数的结点,但是这次我们只需要遍历一次链表即可。我们需要三个指针,是cur,newCur,newPrev,分别指向原链表,新链表

  • 一开始cur指向原链表的头结点,newCur,newPrev都指向空。
  • 当开辟第一个结点时,newCur指向它,将其next指针指向空,并让newCur的val赋值为cur的val值,cur指向下一个结点。
  • 当开辟第二个结点时,newCur指向它,newPrev指向前一个结点,即第二个结点。让newCur的next指针指向第一个结点,也就是现在的newPrev,然后赋值,cur指向下一个结点。
  • 中间不断进行上述操作……
  • 当cur指向原链表最后一个结点的时候,进行赋值,修改指针指向,cur将会继续往后走,此时cur为NULL空指针。因此写一个while循环,判断条件就是cur指针,当它为空时循环结束,返回新链表头结点newCur。

这个做法只需遍历链表一次,时间复杂度为O(N),空间复杂度还是O(N)。代码如下:

  1. struct ListNode* reverseList(struct ListNode* head) {
  2. struct ListNode* cur = head;
  3. struct ListNode* newCur= NULL;
  4. struct ListNode* newPrev= NULL;
  5. while(cur)
  6. {
  7. newCur = (struct ListNode*)malloc(sizeof(struct ListNode));
  8. if (cur == head)
  9. { //赋值
  10. newCur->val = cur->val;
  11. newCur->next = NULL;
  12. //迭代往后
  13. newPrev = newCur;
  14. cur = cur->next;
  15. }
  16. else
  17. { //赋值
  18. newCur->val = cur->val;
  19. newCur->next = newPrev;
  20. //迭代往后
  21. newPrev = newCur;
  22. cur = cur->next;
  23. }
  24. }
  25. return newCur;
  26. }

3. 不过大家可以仔细想想,既然我们创建新链表的时候可以反转,为什么不直接在原链表进行反转。这就是此题的最优解,在空间上,只需要创建几个指针变量,空间复杂度为O(1),在时间上,只需要遍历一次链表即可,时间复杂度为O(N)

在开始反转前,需要创建三个指针变量,cur,newhead和next,其中next是放在while循环中创建的指针变量。cur先指向头结点,newhead指向空指针。

  • 假设一个链表head = [ 1,2,3,4 ],一开始情况如下图,next指针指向的是cur的下一个结点。

  •  把cur的next指针指向newhead,即把第一个结点指向空,然后迭代往后走,next指针的作用就体现出来了。newhead指向cur的位置,cur指向next的位置。

  • 在下一次操作中,next指向cur下一个结点的位置,在改变第二个结点的指向。

  • 接下来重复上述操作,如图所示

  • 当cur指针到达最后一个结点的时候,再次改变指针指向。而cur指向空,newhead指向最后一个结点时,完成反转链表,所以while循环判断是cur不为空时,最后返回newhead指针就行。

代码如下:

  1. struct ListNode* reverseList(struct ListNode* head) {
  2. struct ListNode* cur = head;
  3. struct ListNode* newhead = NULL;
  4. while(cur)
  5. {
  6. struct ListNode* next = cur->next;
  7. //头插
  8. cur->next = newhead;
  9. newhead = cur;
  10. //迭代往后走
  11. cur = next;
  12. }
  13. return newhead;
  14. }

3.链表的中间结点

(1)题目及示例

给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

示例1:

输入:head = [ 1,2,3,4,5 ]

输出:[ 3,4,5 ]

解释:链表只有一个中间结点,值为3.

示例2:

输入:head = [ 1,2,3,4,5 ]

输出:[ 3,4,5 ]

解释:链表只有两个中间结点,值分别为3和4,返回第二个结点。

(2)题目解析及思路

寻找中间结点需要分奇数和偶数两种情况。

  • 如果是奇数,可以直接找中间结点
  • 如果是偶数,中间结点有两个,根据题目要求,返回第二个中间结点,这里是要注意的地方。

解法一:

正常来说,因为单链表不知道结点个数,需要利用一个指针变量遍历整个链表,记下链表长度,然后再次从头节点开始,让指针指向链表长度的一半。总的来说,在时间消耗上,如果有N个结点,需要执行N + N / 2 + 1次。

解法二:

那有没有更好的解法呢?那就得请出快慢指针了,在第一种解法上进一步优化,只需遍历一遍链表。怎么做呢?快慢指针顾名思义有两个指针,一个走的快,一个走得慢。

  • 假设是奇数个结点的链表,快指针走两步,慢指针走一步,以示例一为例,当slow指针走到中间结点时,fast指针刚好走到最后一个结点。

  • 如果是偶数结点,以示例二为例,下列图示第二张直接从第一个中间结点3开始。当走到第二个中间结点时,fast指针指向空。

综上所述,while循环结束条件是fast指针为空或者fast的next指针为空。

  1. struct ListNode* middleNode(struct ListNode* head)
  2. {
  3. struct ListNode* slow, *fast;
  4. slow = fast = head;
  5. while (fast && fast->next)
  6. {
  7. slow = slow->next;
  8. fast = fast->next->next;
  9. }
  10. return slow;
  11. }

4.链表中倒数第k个结点

(1)题目及示例

输入一个链表,输出该链表中倒数第k个结点。链接:链表中倒数第k个结点_牛客题霸_牛客网

示例1:

输入:1, [ 1,2,3,4,5 ]

输出:[ 5 ]

解释:倒数第一个结点是5,返回5字后的链表

(2)题目解析及思路

这道题跟中间结点类似,只不过是求倒数第k个结点,下面我提供两种思路

解法一:

正常来说,大家首先想到的应该是遍历知道链表节点个数,然后再从头结点开始,走n-k步。在时间消耗上,最坏的情况是要走2 * n步。

解法二:

这里会使用快慢指针,但与找中间结点不同的是,这次是fast指针先走,然后slow指针和fast指针都走一步。为什么是这样子呢?我们以链表 [ 1,2,3,4,5 ],K = 2为例。

  • 上图我们可以发现倒数第K个结点与空结点相差2步,我们可以让slow指针和fast指针相差K步,然后每次都走一步直到结束。现在我们让fast指针先行K步。

  • 然后,slow指针和fast指针同时都走一步。

所以说在时间消耗上,只需要遍历一次链表,是第一种解法的优化。我们已经分析完了过程,代码怎么写呢?首先用一个while循环让fast指针先走K步。其次,再用while循环让两个指针一起走,结束条件是fast的指针为空。不过得注意的是,如果是空链表或者k小于零的情况,直接返回空指针。

  1. struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
  2. {
  3. if (pListHead == NULL || k <= 0)
  4. return NULL;
  5. struct ListNode* slow, *fast;
  6. slow = fast = pListHead;
  7. while(k--)//先走K步
  8. {
  9. if (fast == NULL)
  10. return NULL;
  11. fast = fast->next;
  12. }
  13. while(fast != NULL)//同时走一步
  14. {
  15. slow = slow->next;
  16. fast = fast->next;
  17. }
  18. return slow;
  19. }

5.合并两个有序链表

(1)题目及示例

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

  1. struct ListNode* mergeTwoLists
  2. (struct ListNode* list1,
  3. struct ListNode* list2) {
  4. }

示例1:

输入:l1 = [ 1,2,4 ],l2 = [ 1,3,4 ]

输出:[ 1,1,2, 3,4,4 ]

示例2:

输入:l1 = [  ],l2 = [  ]

输出:[  ]

示例3:

输入:l1 = [  ],l2 = [ 0 ]

输出:[ 0 ]

(2)题目解析及思路

这道题有递归和迭代两种方式,这里只详解迭代的方式。迭代思路还是比较好想的,难的是代码实现。思路就是链表一和链表二从第一个结点开始比较,较小的结点就成为新链表的结点,较大的继续跟下一个结点比较,直到所有结点比较完。在写代码上,有两种形式,就是带不带哨兵位的结点。自己创建一个指针head表示新链表的头,还有一个tail指针,为插入后续节点服务,将这两个指针都赋值为空指针。

  • 如果不带哨兵位结点,需要注意第一次比较,不论是第一个链表还是第二个链表结点成为新链表的头,都要单独处理,head和tail都指向list1的头结点。其他的情况,让tail的next指针指向较小结点,并且每步的较小结点的链表需要指向下一个结点。

  • 此时list1已经走到tail指针的后面,就是空指针,我们写一个while循环结束的条件就是其中一个题目给的链表指针走到空指针。最后我们要加上一个判断,判断哪个链表为空,就将tail的next指针指向现在list1。

在这之前还要判断链表有没有空的情况。代码如下:

  1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
  2. if (list1 == NULL)
  3. return list2;
  4. if (list2 == NULL)
  5. return list1;
  6. struct ListNode* head = NULL, *tail = NULL;
  7. while (list1 && list2)
  8. {
  9. if (list1->val < list2->val)
  10. {
  11. if (head == NULL)
  12. {
  13. head = tail = list1;
  14. }
  15. else
  16. {
  17. tail->next = list1;
  18. tail = list1;
  19. }
  20. list1 = list1->next;
  21. }
  22. else
  23. {
  24. if (head == NULL)
  25. {
  26. head = tail = list2;
  27. }
  28. else
  29. {
  30. tail->next = list2;
  31. tail = list2;
  32. }
  33. list2 = list2->next;
  34. }
  35. }
  36. if (list1)
  37. {
  38. tail->next = list1;
  39. }
  40. if (list2)
  41. {
  42. tail->next = list2;
  43. }
  44. return head;
  45. }
  • 如果有哨兵位结点,就不用处理head指针为空的情况。并且还要释放掉我们动态开辟的结点,返回head的下一个结点。
  1. struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
  2. if (list1 == NULL)
  3. return list2;
  4. if (list2 == NULL)
  5. return list1;
  6. struct ListNode* head = NULL, *tail = NULL;
  7. head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
  8. while (list1 && list2)
  9. {
  10. if (list1->val < list2->val)
  11. {
  12. tail->next = list1;
  13. tail = list1;
  14. list1 = list1->next;
  15. }
  16. else
  17. {
  18. tail->next = list2;
  19. tail = list2;
  20. list2 = list2->next;
  21. }
  22. }
  23. if (list1)
  24. {
  25. tail->next = list1;
  26. }
  27. if (list2)
  28. {
  29. tail->next = list2;
  30. }
  31. struct ListNode* list = head->next;
  32. free(head);
  33. return list;
  34. }

6.链表的回文结构

(1)题目及测试样例

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。链接:链表的回文结构_牛客题霸_牛客网

  1. class PalindromeList {
  2. public:
  3. bool chkPalindrome(ListNode* A) {
  4. }
  5. };

这道题在牛客网中没有提供C语言版,有C++函数,可以直接在此函数内部写C语言版代码。因为C++兼容C语言。

测试样例:

输入:A = [ 1->2->2->1 ]

输出:true

(2)题目解析及思路

这道题目要判断是否为回文结构,我们可以观察回文链表,发现回文链表从中间结点断开,两边是对称的,那我们可以找到中间结点,并把中间结点之后的链表逆置,再进行比较存储的整型值。因此,之前写的两道题反转链表和找中间结点就可以派上用场了。不过得注意奇数偶数的情况。

  • 如果是奇数个结点,利用之前写的寻找中间结点刚好只有一个。

  •  如果是偶数个结点,中间结点有两个,之前的寻找中间结点函数是找到第二个中间结点。

要反转中间结点后的链表,有的人会直接断开,就像下面图例的情况,如果是偶数个结点,从头开始比较,到空指针的时候结束了。如果是奇数个结点,断开的时候会多出一个中间结点,只要其中一个链表走到空指针就结束,所以比较判断结束条件是其中一个链表为空。但是反转链表后,还需要断开链表,比较麻烦,我们可以看看不断开的情况。

如下图所示,这就是不断开的情况。如果是奇数个结点,从两边开始比较,最后都会走向空指针。如果是偶数个结点,从两边开始比较,其中一个会先走到空指针,就结束对比的过程。

你可以把寻找中间结点函数和反转链表函数再重新写一遍,也可以直接借用,再进行比较。

  1. //寻找中间结点
  2. struct ListNode* middleNode(struct ListNode* head)
  3. {
  4. struct ListNode* slow, *fast;
  5. slow = fast = head;
  6. while (fast && fast->next)
  7. {
  8. slow = slow->next;
  9. fast = fast->next->next;
  10. }
  11. return slow;
  12. }
  13. //反转链表
  14. struct ListNode* reverseList(struct ListNode* head)
  15. {
  16. struct ListNode* cur = head;
  17. struct ListNode* newhead = nullptr;
  18. while(cur)
  19. {
  20. struct ListNode* next = cur->next;
  21. //头插
  22. cur->next = newhead;
  23. newhead = cur;
  24. //迭代往后走
  25. cur = next;
  26. }
  27. return newhead;
  28. }
  29. class PalindromeList {
  30. public:
  31. bool chkPalindrome(ListNode* A)
  32. { //找到中间结点
  33. struct ListNode* mid = middleNode(A);
  34. //中间结点的头
  35. struct ListNode* rHead = reverseList(mid);
  36. //原链表的左右结点
  37. struct ListNode* curLeft = A;
  38. struct ListNode* curRight = rHead;
  39. while (curLeft && curRight)
  40. {
  41. if (curLeft->val != curRight->val)
  42. {
  43. return false;
  44. }
  45. else
  46. {
  47. curLeft = curLeft->next;
  48. curRight = curRight->next;
  49. }
  50. }
  51. return true;
  52. }
  53. };

7.相交链表

(1)题目及示例

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。注意,函数返回结果后,链表必须 保持其原始结构

示例1:

输入:intersectVal = 8, listA = [ 4,1,8,4,5 ],listB = [ 5,6,1,8,4,5 ],skipA = 2,skipB = 3

输出:IntersectVal at ‘8’

解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置

示例1:

输入:intersectVal = 0, listA = [ 2,6,4 ],listB = [ 1,5 ],skipA = 3,skipB = 2

输出:null

解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

(2)题目解析及思路

我们要判断是否为相交链表,并找出相交点,而且不能破坏原链表的结构。我们先要清楚相交链表的概念,题目给的图示就很直观,有两个链表的最后一个节点不指向空指针,但是同时指向一个结点。还要注意的是,不能形成环形链表,就是一个链表出现头尾相连的情况。

这道题我会提供两个思路。

思路一:

我们观察相交链表,会发现不管从A链表出发,还是B链表出发,在指向空指针之前的结点是相同的,我们可以根据这个判断是否为相交链表。并在遍历AB链表的时候,记录链表从A出发和从B出发的长度,算出差距多少步。定义两个指针变量,让长的链表指针变量先走差距步,然后两个链表指针同时走,当走的过程中,两个指针相同时,必然是相交的节点。

代码如下:

  1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
  2. {
  3. struct ListNode* tailA = headA;
  4. struct ListNode* tailB = headB;
  5. int lenA = 1;
  6. while(tailA->next)
  7. {
  8. lenA++;
  9. tailA = tailA->next;
  10. }
  11. int lenB = 1;
  12. while (tailB->next)
  13. {
  14. lenB++;
  15. tailB = tailB->next;
  16. }
  17. if(tailA != tailB)
  18. {
  19. return NULL;
  20. }
  21. //abs时绝对值
  22. int gap = abs(lenA - lenB);
  23. //先假设A链表是长链表,再比较,如果不是再重新赋值
  24. struct ListNode* longlist = headA;
  25. struct ListNode* shortlist = headB;
  26. if (lenA < lenB)
  27. {
  28. shortlist = headA;
  29. longlist =headB;
  30. }
  31. // 长的先走差距步,再同时走交点
  32. while(gap--)
  33. {
  34. longlist = longlist->next;
  35. }
  36. while(longlist != shortlist)
  37. {
  38. longlist = longlist->next;
  39. shortlist = shortlist->next;
  40. }
  41. return longlist;
  42. }

思路二:

思路二更加巧妙,我用图示展示更直观。A链表结点总个数用totalA表示,在相交结点之前A链表节点个数用lenA表示,相交结点之后公共部分用comlenth表示。B链表同理。

totalA = lenA + comlenth

totalB = lenB + comlenth

  • 当curA和curB这两个指针同时走到最后一个结点时,curA走了totalA步,curB走了totalB步

  • 当遇上空指针的时候,把curA指针指向链表B,把curB指针指向链表A。继续走。

  • 当都走到相交结点时,我们会发现curB走的步数是totalB + lenA,是八步,curA走的步数是totalA + lenB,也是八步。这难道是巧合吗?还记得最开始的式子吗,我们做个化简,会发现按照我刚刚的方式走到相交结点时,他们俩的步数一定是一样。

totalA = lenA + comlenth                                                                                                        

totalB = lenB + comlenth

将其带入到curAlen和curBlen中,

curAlen = totalA + lenB = (lenA + comlenth) + lenB = (len A + lenB) + comlenth

curBlen = totalB + lenA = (lenB + comlenth) + lenA = (len A + lenB) + comlenth

  • 如果不是相交链表呢?还是想按照上面的办法,当走到各自链表最后一节点,遇到空指针,从头开始,但是位置互换,放走到空指针时,步数刚好相同都是两个链表个数之和返回空指针。

那么代码怎么写呢,先要注意如果有其中一个链表为空,就无法形成相交链表,直接返回空。先创建两个指针curA和curB,写一个while循环,结束条件是curA = curB的时候,最后返回其中一个指针变量即可。

  1. struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
  2. {
  3. if (headA == NULL || headB == NULL)
  4. {
  5. return NULL;
  6. }
  7. struct ListNode *curA = headA,
  8. struct ListNode *curB = headB;
  9. while (curA != curB)
  10. {
  11. pA = pA == NULL ? headB : pA->next;
  12. pB = pB == NULL ? headA : pB->next;
  13. }
  14. return curA;
  15. }


总结

这次七道链表OJ题目的解析可以说是干货满满,建议收藏。每做一道题,可以参考上面的图示来分析理解思路,多画图有助于你思路顺畅,后期写代码不卡壳。事后可以尝试自己总结其中用到的方法,话不多说,练起来!

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/585976
推荐阅读
相关标签
  

闽ICP备14008679号