数据结构之链表
数据结构之链表
什么是链表
链表是一种常见的线性数据结构,由一系列节点组成。每个节点包含两部分:数据域和指针域。数据域用于存储具体的数据,而指针域则用于指向下一个节点。
链表的特点如下:
- 非连续存储: 链表中的节点在内存中可以随机分布,不要求连续存储。
- 指针连接: 每个节点通过指针将其与下一个节点连接,形成链式结构。
- 访问入口: 链表的头节点可以作为访问整个链表的入口。
根据指针的类型和指向的节点数目,链表可以分为多种类型,包括单向链表、双向链表和循环链表等。
链表相比于顺序表有以下优势:
- 高效插入和删除: 在链表中,插入和删除操作只需修改指针的指向,不需要移动其他节点,因此效率较高。
- 动态内存分配: 链表可以动态地分配内存空间,无需事先定义容量。
- 适用场景: 特别适用于频繁插入和删除的场景。
然而,链表的缺点包括:
- 低效访问: 访问元素的效率较低,需要从头节点开始遍历整个链表才能找到目标节点。
- 额外空间开销: 链表的存储空间通常稍高于顺序表,因为每个节点都需要额外的指针来连接其他节点。
链表的分类
链表可以根据指针的类型和指向的节点数目进行分类。以下是几种常见的链表类型:
- 有头链表: 包含固定的表头节点。
- 无头链表: 表头节点可变,可能不存在。
- 单向链表: 每个节点有一个指针,指向下一个节点。
- 双向链表: 每个节点有两个指针,分别指向前一个节点和下一个节点。
- 循环链表: 最后一个节点的指针指向头节点,形成环形结构。
有头单链表
#ifndef SINGLE_LIST_H
#define SINGLE_LIST_H
#include <iostream>
typedef int DataType;
typedef struct SingleList
{
DataType data;
struct SingleList* next;
} List;
// 创建表头
List* create_list();
// 创建链表节点
List* create_node(DataType data);
// 插入
void push_back(List* list, DataType data);
void push_front(List* list, DataType data);
// 指定位置插入
void insert_list(List* list, DataType data, DataType posData);
// 删除
void pop_back(List* list);
void pop_front(List* list);
void erase_list(List* list, DataType posData);
// 遍历
void traverse_list(List* list);
// 销毁链表
void destroy_list(List* list);
#endif // SINGLE_LIST_H
#include "SingleList.h"
// 创建链表,即创建一个头节点
List* create_list()
{
return new List(); // 创建头节点
}
// 创建链表节点
List* create_node(DataType data)
{
List* newNode = new List();
newNode->data = data;
newNode->next = nullptr;
return newNode;
}
// 在链表末尾插入节点
void push_back(List* list, DataType data)
{
List* current = list;
// 表尾节点的特点就是指针域是空的
while (current->next != nullptr)
{
current = current->next;
}
//找到了,创建节点,把表尾的next指向新节点即可
current->next = create_node(data);
}
// 在链表前面插入节点
void push_front(List* list, DataType data)
{
List* newNode = create_node(data);
newNode->next = list->next; // 将新节点的 next 指向当前第一个有效节点
list->next = newNode; // 更新头节点的 next 指针
}
// 指定位置插入节点
void insert_list(List* list, DataType data, DataType posData)
{
List* current = list->next; // 从第一个有效节点开始
while (current != nullptr && current->data != posData)
{
current = current->next;
}
if (current != nullptr)
{ // 找到位置
List* newNode = create_node(data);
newNode->next = current->next; // 新节点指向当前节点的下一个节点
current->next = newNode; // 将当前节点的 next 指向新节点
}
}
// 删除链表末尾的节点
void pop_back(List* list)
{
if (list->next == nullptr) return; // 链表为空
List* current = list;
while (current->next->next != nullptr)
{
current = current->next;
}
delete current->next; // 删除最后一个节点
current->next = nullptr; // 更新当前节点的 next 指针
}
// 删除链表头部的节点
void pop_front(List* list)
{
if (list->next == nullptr)
{
return; // 链表为空
}
List* toDelete = list->next; // 要删除的节点
list->next = toDelete->next; // 更新头节点的 next 指针
delete toDelete; // 释放内存
}
// 删除指定值的节点
void erase_list(List* list, DataType posData)
{
List* current = list;
while (current->next != nullptr)
{
if (current->next->data == posData)
{
List* toDelete = current->next;
current->next = current->next->next; // 跳过要删除的节点
delete toDelete; // 释放内存
return;
}
current = current->next;
}
}
// 遍历链表并打印节点数据
void traverse_list(List* list)
{
List* current = list->next; // 从第一个有效节点开始
while (current != nullptr)
{
std::cout << current->data << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl; // 结束标志
}
// 销毁链表
void destroy_list(List* list)
{
List* current = list->next; // 从第一个有效节点开始
while (current != nullptr)
{
List* toDelete = current;
current = current->next;
delete toDelete; // 释放内存
}
list->next = nullptr; // 重置头节点的指针
}
#include "SingleList.h"
int main() {
List* list = create_list(); // 创建链表
push_back(list, 10);
push_back(list, 20);
push_back(list, 30);
std::cout << "链表内容: ";
traverse_list(list);
push_front(list, 5);
std::cout << "在头部插入5后的链表内容: ";
traverse_list(list);
insert_list(list, 15, 10);
std::cout << "插入15后链表内容: ";
traverse_list(list);
pop_back(list);
std::cout << "删除末尾节点后的链表内容: ";
traverse_list(list);
pop_front(list);
std::cout << "删除头部节点后的链表内容: ";
traverse_list(list);
erase_list(list, 15);
std::cout << "删除值为15后的链表内容: ";
traverse_list(list);
destroy_list(list); // 销毁链表
return 0;
}
无头单链表
#ifndef SINGLE_LIST_H
#define SINGLE_LIST_H
#include <iostream>
typedef int DataType;
typedef struct SingleList
{
DataType data;
struct SingleList* next;
} List;
// 创建链表节点
List* create_node(DataType data);
// 插入
void push_back(List** head, DataType data);
void push_front(List** head, DataType data);
// 指定位置插入
void insert_list(List** head, DataType data, DataType posData);
// 删除
void pop_back(List** head);
void pop_front(List** head);
void erase_list(List** head, DataType posData);
// 遍历
void traverse_list(List* head);
// 销毁链表
void destroy_list(List** head);
#endif // SINGLE_LIST_H
#include "SingleList.h"
// 创建链表节点
List* create_node(DataType data)
{
List* newNode = new List();
newNode->data = data;
newNode->next = nullptr;
return newNode;
}
// 在链表末尾插入节点
void push_back(List** head, DataType data)
{
List* newNode = create_node(data);
if (*head == nullptr)
{ // 如果链表为空
*head = newNode; // 更新头指针
}
else
{
List* current = *head;
while (current->next != nullptr)
{
current = current->next;
}
current->next = newNode;
}
}
// 在链表前面插入节点
void push_front(List** head, DataType data)
{
List* newNode = create_node(data);
newNode->next = *head; // 新节点指向当前头节点
*head = newNode; // 更新头指针
}
// 指定位置插入节点
void insert_list(List** head, DataType data, DataType posData)
{
if (*head == nullptr) return; // 链表为空
List* current = *head;
while (current != nullptr && current->data != posData)
{
current = current->next;
}
if (current != nullptr)
{ // 找到位置
List* newNode = create_node(data);
newNode->next = current->next; // 新节点指向当前节点的下一个节点
current->next = newNode; // 将当前节点的 next 指向新节点
}
}
// 删除链表末尾的节点
void pop_back(List** head)
{
if (*head == nullptr) return; // 链表为空
if ((*head)->next == nullptr)
{ // 只有一个节点
delete *head;
*head = nullptr;
return;
}
List* current = *head;
while (current->next->next != nullptr)
{
current = current->next;
}
delete current->next; // 删除最后一个节点
current->next = nullptr; // 更新当前节点的 next 指针
}
// 删除链表头部的节点
void pop_front(List** head)
{
if (*head == nullptr) return; // 链表为空
List* toDelete = *head; // 要删除的节点
*head = (*head)->next; // 更新头指针
delete toDelete; // 释放内存
}
// 删除指定值的节点
void erase_list(List** head, DataType posData)
{
if (*head == nullptr) return; // 链表为空
if ((*head)->data == posData)
{ // 删除头节点
List* toDelete = *head;
*head = (*head)->next;
delete toDelete;
return;
}
List* current = *head;
while (current->next != nullptr)
{
if (current->next->data == posData)
{
List* toDelete = current->next;
current->next = current->next->next; // 跳过要删除的节点
delete toDelete; // 释放内存
return;
}
current = current->next;
}
}
// 遍历链表并打印节点数据
void traverse_list(List* head)
{
List* current = head; // 从头节点开始
while (current != nullptr)
{
std::cout << current->data << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl; // 结束标志
}
// 销毁链表
void destroy_list(List** head)
{
List* current = *head; // 从头节点开始
while (current != nullptr)
{
List* toDelete = current;
current = current->next; // 移动到下一个节点
delete toDelete; // 释放内存
}
*head = nullptr; // 重置头指针
}
#include "SingleList.h"
int main()
{
List* list = nullptr; // 空链表
push_back(&list, 10);
push_back(&list, 20);
push_back(&list, 30);
std::cout << "链表内容: ";
traverse_list(list);
push_front(&list, 5);
std::cout << "在头部插入5后的链表内容: ";
traverse_list(list);
insert_list(&list, 15, 10);
std::cout << "插入15后链表内容: ";
traverse_list(list);
pop_back(&list);
std::cout << "删除末尾节点后的链表内容: ";
traverse_list(list);
pop_front(&list);
std::cout << "删除头部节点后的链表内容: ";
traverse_list(list);
erase_list(&list, 15);
std::cout << "删除值为15后的链表内容: ";
traverse_list(list);
destroy_list(&list); // 销毁链表
return 0;
}
链表其他操作
- 有序链表的构建:有序链表的构建是指将新元素插入到链表中的适当位置,以保持链表的有序性。
- 链表归并:链表归并是将两个已排序的链表合并成一个新的有序链表。
- 链表的反转问题:链表的反转是将链表的节点顺序反转,使得原来的头节点变为尾节点,原来的尾节点变为头节点。
- 链表的冒泡排序:链表的冒泡排序是一种简单的排序算法,通过重复遍历链表,比较相邻节点的值并在必要时交换它们,从而将大的元素“冒泡”到链表的末尾。
#include <iostream>
#include <memory>
#include <cassert>
class Node
{
public:
int data;
Node* next;
Node(int value) : data(value), next(nullptr) {}
};
class LinkedList
{
private:
Node* head;
public:
LinkedList() : head(nullptr) {}
// 创建新节点
Node* create_data(int data)
{
return new Node(data);
}
// 遍历链表
void traverse_list()
{
Node* current = head;
while (current != nullptr)
{
std::cout << current->data << "\t";
current = current->next;
}
std::cout << std::endl;
}
// 有序链表的构建
//找第一次大于插入元素的节点
//没找到说明插入元素是最大的,直接插入到链表末尾
void push_sort(int data)
{
Node* newNode = create_data(data);
if (head == nullptr || head->data > data)
{
newNode->next = head;
head = newNode;
}
else
{
Node* current = head;
while (current->next != nullptr && current->next->data <= data)
{
current = current->next;
}
newNode->next = current->next;
current->next = newNode;
}
}
// 链表的反转
void reverse()
{
Node* prev = nullptr; // 前一个节点
Node* current = head; // 当前节点从头节点开始
Node* next = nullptr; // 下一个节点初始化为 nullptr
while (current != nullptr)
{
next = current->next; // 保存下一个节点
current->next = prev; // 当前节点的下一个指针指向前一个节点,实现反转
prev = current; // 更新前一个节点为当前节点
current = next; // 移动到下一个节点
}
head = prev; // 最后将头指针指向新的头节点(原链表的尾节点)
}
// 链表的归并
void merge_list(LinkedList& list1, LinkedList& list2)
{
Node* first = list1.head;
Node* second = list2.head;
Node* tailNode = nullptr;
// 遍历两个链表,比较节点数据并按顺序合并
while (first != nullptr && second != nullptr)
{
Node* selectedNode = nullptr; // 选择合并的节点
if (first->data < second->data) // 如果第一个链表的节点数据小于第二个链表的节点数据
{
selectedNode = first; // 选择第一个链表的节点
first = first->next; // 移动到下一个节点
}
else
{
selectedNode = second; // 选择第二个链表的节点
second = second->next; // 移动到下一个节点
}
// 如果尾节点为空,说明是第一次插入
if (tailNode == nullptr)
{
head = selectedNode; // 头指针指向第一个合并的节点
tailNode = head; // 尾节点也指向头节点
}
else
{
tailNode->next = selectedNode; // 将尾节点的下一个指针指向当前选择的节点
tailNode = selectedNode; // 更新尾节点为当前选择的节点
}
}
// 如果还有剩余的节点,直接连接到尾节点
if (first != nullptr)
{
if (tailNode == nullptr)
{
head = first;
}
else
{
tailNode->next = first;
}
}
if (second != nullptr)
{
if (tailNode == nullptr)
{
head = second;
}
else
{
tailNode->next = second;
}
}
}
// 链表的冒泡排序
void bubble_sort()
{
if (head == nullptr) return;
for (Node* i = head; i != nullptr; i = i->next)
{
for (Node* j = head; j != nullptr && j->next != nullptr; j = j->next)
{
if (j->data > j->next->data)
{
std::swap(j->data, j->next->data);
}
}
}
}
// 获取头指针
Node* get_head()
{
return head;
}
};
int main()
{
LinkedList list;
list.push_sort(1);
list.push_sort(8);
list.push_sort(2);
list.push_sort(7);
std::cout << "原始有序链表: ";
list.traverse_list();
list.reverse();
std::cout << "反转后的链表: ";
list.traverse_list();
list.bubble_sort();
std::cout << "冒泡排序后的链表: ";
list.traverse_list();
LinkedList result;
LinkedList one;
one.push_sort(1);
one.push_sort(3);
one.push_sort(5);
LinkedList two;
two.push_sort(2);
two.push_sort(4);
two.push_sort(6);
two.push_sort(8);
result.merge_list(one, two);
std::cout << "归并后的链表: ";
result.traverse_list();
return 0;
}
双向循环链表
双向循环链表(Doubly Circular Linked List)是一种数据结构,其中每个节点都包含两个指针,分别指向前一个节点和后一个节点。这种结构的特点在于它形成了一个闭环,即最后一个节点的下一个指针指向头节点,而头节点的前一个指针指向最后一个节点。这种设计使得链表可以从任意节点开始进行遍历,方便了链表的操作。
特点
- 双向性:每个节点都有两个指针,使得可以双向遍历链表,既可以往前移动,也可以往后移动。
- 循环性:链表的最后一个节点指向头节点,头节点的前一个指针指向最后一个节点,形成一个循环结构。这意味着从链表的任何一个节点出发,都能遍历到其他所有节点。
常用操作
双向循环链表常用的操作包括:
-
初始化链表:创建一个空的双向循环链表。通常会用一个特殊的哨兵节点(头节点)来简化插入和删除操作。
-
插入节点:在链表的指定位置插入一个新的节点。可以选择在链表的前端、末尾或指定值之后插入节点。
-
删除节点:从链表中删除指定位置的节点。需要考虑删除的节点是否为头节点、尾节点或中间节点。
-
遍历链表:
- 前向遍历:从头节点开始,依次访问每个节点,直到回到头节点。
- 反向遍历:从尾节点开始,依次访问每个节点,直到再次回到尾节点。
-
查找节点:在链表中查找具有指定值的节点,并返回该节点的指针。如果节点不存在,则返回空。
应用场景
双向循环链表在许多应用中都非常有用,例如:
- 音乐播放器:可以使用双向循环链表来实现播放列表,以便用户可以在前后歌曲之间轻松切换。
- 浏览器历史:可以利用双向循环链表来记录用户的浏览历史,并允许用户在历史记录中前后导航。
实现代码
#include <iostream>
#include <cassert>
class Node
{
public:
int data; // 节点存储的数据
Node* front; // 指向前一个节点的指针
Node* tail; // 指向下一个节点的指针
Node(int value) : data(value), front(nullptr), tail(nullptr) {} // 构造函数
};
class DoublyCircularLinkedList
{
private:
Node* head; // 头节点指针
public:
DoublyCircularLinkedList()
{
head = new Node(0); // 创建一个哨兵节点作为头节点
head->front = head; // 头节点的前指针指向自己
head->tail = head; // 头节点的后指针指向自己
}
~DoublyCircularLinkedList()
{
// 析构函数,释放所有节点
Node* current = head->tail;
while (current != head)
{
Node* nextNode = current->tail;
delete current;
current = nextNode;
}
delete head; // 删除头节点
}
// 插入到链表前端
void push_front(int data)
{
Node* newNode = new Node(data);
newNode->tail = head->tail; // 新节点的后指针指向当前最后一个节点
newNode->front = head; // 新节点的前指针指向头节点
head->tail->front = newNode; // 当前最后一个节点的前指针指向新节点
head->tail = newNode; // 更新头节点的后指针指向新节点
}
// 插入到链表末尾
void push_back(int data)
{
Node* newNode = new Node(data);
newNode->front = head->front; // 新节点的前指针指向当前最后一个节点
newNode->tail = head; // 新节点的后指针指向头节点
head->front->tail = newNode; // 当前最后一个节点的后指针指向新节点
head->front = newNode; // 更新头节点的前指针指向新节点
}
// 在指定值后插入新节点
void insert_after(int posData, int data)
{
Node* current = head->tail;
while (current != head && current->data != posData)
{
current = current->tail; // 遍历链表
}
if (current == head)
{
std::cout << "未找到指定位置,无法插入!" << std::endl;
return;
}
Node* newNode = new Node(data);
newNode->front = current; // 新节点的前指针指向当前节点
newNode->tail = current->tail; // 新节点的后指针指向当前节点的后继节点
current->tail->front = newNode; // 当前节点的后继节点的前指针指向新节点
current->tail = newNode; // 当前节点的后指针指向新节点
}
// 从前端删除节点
void pop_front()
{
if (head->tail == head)
{
std::cout << "链表为空无法删除!" << std::endl;
return;
}
Node* nodeToDelete = head->tail; // 获取当前第一个节点
head->tail = nodeToDelete->tail; // 更新头节点的后指针
nodeToDelete->tail->front = head; // 更新新的第一个节点的前指针
delete nodeToDelete; // 释放内存
}
// 从后端删除节点
void pop_back()
{
if (head->front == head)
{
std::cout << "链表为空无法删除!" << std::endl;
return;
}
Node* nodeToDelete = head->front; // 获取当前最后一个节点
head->front = nodeToDelete->front; // 更新头节点的前指针
nodeToDelete->front->tail = head; // 更新新的最后一个节点的后指针
delete nodeToDelete; // 释放内存
}
// 从后向前遍历链表
void print_reverse() const
{
Node* current = head->front; // 从最后一个节点开始
while (current != head)
{
std::cout << current->data << "\t"; // 输出节点的数据
current = current->front; // 移动到前一个节点
}
std::cout << std::endl;
}
// 从前向后遍历链表
void print_forward() const
{
Node* current = head->tail; // 从第一个节点开始
while (current != head)
{
std::cout << current->data << "\t"; // 输出节点的数据
current = current->tail; // 移动到下一个节点
}
std::cout << std::endl;
}
};
int main()
{
DoublyCircularLinkedList list;
list.push_front(1);
list.push_front(2);
std::cout << "前向遍历: ";
list.print_forward(); // 输出: 2 1
std::cout << "反向遍历: ";
list.print_reverse(); // 输出: 1 2
list.push_back(888);
std::cout << "前向遍历: ";
list.print_forward(); // 输出: 2 1 888
std::cout << "反向遍历: ";
list.print_reverse(); // 输出: 888 1 2
list.insert_after(888, 666);
std::cout << "前向遍历: ";
list.print_forward(); // 输出: 2 1 888 666
std::cout << "反向遍历: ";
list.print_reverse(); // 输出: 666 888 1 2
list.insert_after(2, 999);
std::cout << "前向遍历: ";
list.print_forward(); // 输出: 2 999 1 888 666
std::cout << "反向遍历: ";
list.print_reverse(); // 输出: 666 888 1 999 2
std::cout << "pop_front....." << std::endl;
list.pop_front();
std::cout << "前向遍历: ";
list.print_forward(); // 输出: 999 1 888 666
std::cout << "pop_back....." << std::endl;
list.pop_back();
std::cout << "前向遍历: ";
list.print_forward(); // 输出: 999 1 888
return 0;
}
约瑟夫环问题
约瑟夫环问题(Josephus Problem)是一个经典的数学问题,描述了以下情境:有n个人围成一圈,从第一个人开始报数,报到某个数字m的人将被淘汰出局,然后从下一个人重新开始报数,直到最后只剩下一个人。问题是找出最后留下的那个人在初始序列中的位置。解决约瑟夫环问题的一种常用方法是使用循环链表。可以按照以下步骤进行求解:
-
创建一个含有n个节点的循环链表,节点的值分别为1到n,按顺序排列。
-
从第一个节点开始,依次数m个节点,将第m个节点删除(从链表中断开)。
-
从被删除节点的下一个节点重新开始,回到步骤2,直到只剩下一个节点为止。
最后留下的节点即为最后的胜者。
约瑟夫环问题(Josephus Problem)是一个经典的数学问题,描述了以下情境:有 n 个人围成一圈,从第一个人开始报数,报到某个数字 m 的人将被淘汰出局,然后从下一个人重新开始报数,直到最后只剩下一个人。问题是找出最后留下的那个人在初始序列中的位置。
问题描述
- 初始化:有 n 个人,编号从 1 到 n ,围成一圈。
- 报数:从第一个人开始,依次报数,每报到 m 就淘汰当前报数的人。
- 重启:被淘汰之后,从被淘汰者的下一个人重新开始报数。
- 终止条件:重复以上步骤,直到只剩下一个人。
求解步骤
为了解决约瑟夫环问题,可以使用循环链表。具体步骤如下:
- 创建循环链表:含有 n 个节点,节点的值分别为 1 到 n ,按顺序排列。
- 删除操作:从第一个节点开始,依次数 m 个节点,将第 m 个节点删除(从链表中断开)。
- 继续循环:从被删除节点的下一个节点重新开始,回到步骤 2,直到只剩下一个节点为止。
- 输出结果:最后留下的节点即为最后的胜者。
#include <iostream>
#include <cassert>
class Node
{
public:
int data; // 节点数据
Node* next; // 指向下一个节点的指针
Node* prev; // 指向前一个节点的指针
Node(int value) : data(value), next(this), prev(this) {} // 构造函数
};
class CircularLinkedList
{
public:
Node* head; // 链表头指针
CircularLinkedList() : head(nullptr) {} // 构造函数
// 添加节点
void addNode(int data)
{
Node* newNode = new Node(data);
if (head == nullptr)
{
head = newNode; // 第一个节点
}
else
{
Node* lastNode = head->prev; // 获取最后一个节点
lastNode->next = newNode; // 将新节点链接到最后一个节点
newNode->prev = lastNode;
newNode->next = head; // 新节点指向头节点
head->prev = newNode; // 头节点的前指针指向新节点
}
}
// 删除节点
void removeNode(Node* node)
{
if (node->next == node)
{ // 只有一个节点的情况
head = nullptr;
}
else
{
node->prev->next = node->next; // 更新前驱节点的后指针
node->next->prev = node->prev; // 更新后继节点的前指针
if (head == node)
{
head = node->next; // 如果删除的是头节点,更新头节点
}
}
delete node; // 释放内存
}
// 遍历链表
void traverse()
{
if (head == nullptr)
{
std::cout << "The list is empty." << std::endl;
return;
}
Node* current = head;
do
{
std::cout << current->data << " ";
current = current->next;
}
while (current != head);
std::cout << std::endl;
}
};
// 实现约瑟夫环逻辑
void joseph_circle(int n, int m)
{
if (n <= 0 || m <= 0)
{
std::cout << "Invalid input" << std::endl;
return;
}
CircularLinkedList circle;
// 创建循环链表
for (int i = 1; i <= n; i++)
{
circle.addNode(i);
}
Node* current = circle.head; // 从头节点开始
while (n > 1)
{
for (int i = 0; i < m - 1; i++)
{
current = current->next; // 移动到第 m 个节点
}
Node* nextNode = current->next; // 保存下一个节点
circle.removeNode(current); // 删除当前节点
current = nextNode; // 继续从下一个节点开始
n--;
}
std::cout << "The winning position is: " << circle.head->data << std::endl; // 输出胜者位置
}
int main()
{
int n = 10; // 一共10个人
int m = 4; // 报数为4的人出列
joseph_circle(n, m);
return 0;
}
原文地址:https://blog.csdn.net/qq_68194402/article/details/143858319
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!