• 玩转C线性表和单向链表之Linux双向链表优化


      前言:

      这次介绍基本数据结构的线性表和链表,并用C语言进行编写;建议最开始学数据结构时,用C语言;像栈和队列都可以用这两种数据结构来实现。

      一、线性表基本介绍  

      1 概念:

                     线性表也就是关系户中最简单的一种关系,一对一。

                      如:学生学号的集合就是一个线性表。

          2 特征:

                     ① 有且只有一个“首元素“。

                     ② 有且只有一个“末元素”。

                     ③ 除“末元素”外,其余元素均有唯一的后继元素。

                     ④ 除“首元素”外,其余元素均有唯一的前驱元素。

         3 存储划分:

                      ① 如果把线性表用“顺序存储”,那么就是“顺序表”。

                      ② 如果把线性表用“链式存储”,那么就是“链表”。

         4  常用操作:

      添加,删除,插入,查找,遍历,统计。

      二、线性表实现  

      主要实现线性表的创建、销毁和插入等操作,将接口封装在.h文件中,如下:

    #ifndef  __MY_SEQLIST_H__ 
    #define __MY_SEQLIST_H__
    
    typedef void SeqList;
    typedef void SeqListNode;
    
    SeqList* SeqList_Create(int capacity);    //创建线性表
    
    void SeqList_Destroy(SeqList* list);    //销毁
        
    void SeqList_Clear(SeqList* list);        //清空
    
    int SeqList_Length(SeqList* list);        //求线性表的长度
    
    int SeqList_Capacity(SeqList* list);    //求线性表的容量
    
    int SeqList_Insert(SeqList* list, SeqListNode* node, int pos);    //向线性表中插入元素
    
    SeqListNode* SeqList_Get(SeqList* list, int pos);    //获取指定位置元素
    
    SeqListNode* SeqList_Delete(SeqList* list, int pos);    //删除指定位置元素
    
    
    #endif  //__MY_SEQLIST_H__

      注意:定义头文件时,要包含“#ifndef”,防止头文件重复包含!

      1、线性表创建

       主要就是对节点分配内存,代码如下:

    //在结构体中套1级指针
    //
    typedef struct _tag_SeqList
    {
        int length;
        int capacity;
        unsigned int *node;   //int* node[]
    }TSeqList;
    
    SeqList* SeqList_Create(int capacity)
    {
        int ret = 0;
        TSeqList *tmp = NULL;
    
        tmp = (TSeqList *)malloc(sizeof(TSeqList));
        if (tmp == NULL)
        {
            ret = -1;
            printf("func SeqList_Create() err:%d 
    ", ret);
            return NULL;
        }
        memset(tmp, 0, sizeof(TSeqList));
    
        //根据capacity 的大小分配节点的空间
        tmp->node = (unsigned int *)malloc(sizeof(unsigned int *) * capacity);
        if (tmp->node  == NULL)
        {
            ret = -2;
            printf("func SeqList_Create() err: malloc err %d 
    ", ret);
            return NULL;
        }
        tmp->capacity = capacity;
        tmp->length = 0;
        return tmp;
    }
    
    void SeqList_Destroy(SeqList* list)
    {
        TSeqList *tlist = NULL;
        if (list == NULL)
        {
            return ;
        }
        tlist = (TSeqList *)list;
        if (tlist->node != NULL)
        {
            free(tlist->node);
        }
        
        free(tlist);
    
        return ;
    }

      2、线性表插入

      插入在业界有默认的规则吧,下面将要讲到的链表也是用这个规则。用图来讲解一下吧,如下:

       

      讲解:例如,已经有线性表34、33、32、31,在1位置想插入35,想把从1开始的先后移,再插入,插入后的线性表如右图所示。

      线性表插入代码如下:

    int SeqList_Insert(SeqList* list, SeqListNode* node, int pos)
    {
        int i =0, ret = 0;
        TSeqList *tlist = NULL;
    
        if (list == NULL || node==NULL ||  pos<0)
        {
            ret = -1;
            printf("fun SeqList_Insert() err:%d 
    ", ret);
            return ret;
        }
        tlist = (TSeqList*)list;
    
        //判断是不是满了
        if (tlist->length >= tlist->capacity)
        {
            ret = -2;
            printf("fun SeqList_Insert() (tlist->length >= tlist->capacity) err:%d 
    ", ret);
            return ret;
        }
    
        //容错修正  6个长度 容量20;用户pos10位置插入..
        if (pos>=tlist->length)
        {
            pos = tlist->length; //
        }
    
        //1 元素后移
        for(i=tlist->length; i>pos; i--)
        {
            tlist->node[i] = tlist->node[i-1];
            //a[7] = a[6]
        }
        // i = 3
        // 2插入元素
        tlist->node[i] = (unsigned int )node;
        tlist->length ++;
        return 0;
    }

      3、获取指定位置元素

      根据pos位置获取元素,代码如下:

    SeqListNode* SeqList_Get(SeqList* list, int pos)
    {
        int i =0;
        SeqListNode *ret = 0;
        TSeqList *tlist = NULL;
    
        if (list == NULL ||  pos<0)
        {
            printf("fun SeqList_Get() err:%d 
    ", ret);
            return NULL;
        }
        tlist = (TSeqList*)list;
    
        ret = (void *)tlist->node[pos];
        return ret;
    }

      4、删除指定位置元素

      将指定位置的元素进行删除,代码如下:

    SeqListNode* SeqList_Delete(SeqList* list, int pos)
    {
        int i = 0;
        SeqListNode *ret = 0;
        TSeqList *tlist = NULL;
    
        if (list == NULL ||  pos<0) //检查
        {
            printf("fun SeqList_Delete() err:%d 
    ", ret);
            return NULL;
        }
        tlist = (TSeqList*)list;
    
        ret = (SeqListNode *)tlist->node[pos]; //缓存pos的位置
         
        for (i=pos+1; i<tlist->length; i++)  //pos位置后面的元素前移
        {
            tlist->node[i-1] = tlist->node[i];
        }
        tlist->length --;
        return ret;
    }

      5、总体代码

      整个seqlist.c代码如下,方便使用与调试:

    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
    #include "seqlist.h"
    
    
    //在结构体中套1级指针
    //
    typedef struct _tag_SeqList
    {
        int length;
        int capacity;
        unsigned int *node;   //int* node[]
    }TSeqList;
    
    SeqList* SeqList_Create(int capacity)
    {
        int ret = 0;
        TSeqList *tmp = NULL;
    
        tmp = (TSeqList *)malloc(sizeof(TSeqList));
        if (tmp == NULL)
        {
            ret = -1;
            printf("func SeqList_Create() err:%d 
    ", ret);
            return NULL;
        }
        memset(tmp, 0, sizeof(TSeqList));
    
        //根据capacity 的大小分配节点的空间
        tmp->node = (unsigned int *)malloc(sizeof(unsigned int *) * capacity);
        if (tmp->node  == NULL)
        {
            ret = -2;
            printf("func SeqList_Create() err: malloc err %d 
    ", ret);
            return NULL;
        }
        tmp->capacity = capacity;
        tmp->length = 0;
        return tmp;
    }
    
    void SeqList_Destroy(SeqList* list)
    {
        TSeqList *tlist = NULL;
        if (list == NULL)
        {
            return ;
        }
        tlist = (TSeqList *)list;
        if (tlist->node != NULL)
        {
            free(tlist->node);
        }
        
        free(tlist);
    
        return ;
    }
    
    //清空链表 //回到初始化状态
    void SeqList_Clear(SeqList* list)
    {
        TSeqList *tlist = NULL;
        if (list == NULL)
        {
            return ;
        }
        tlist = (TSeqList *)list;
        tlist->length = 0; 
        return ;
    }
    
    int SeqList_Length(SeqList* list)
    {
        TSeqList *tlist = NULL;
        if (list == NULL)
        {
            return -1;
        }
        tlist = (TSeqList *)list;
        return tlist->length;
    }
    
    int SeqList_Capacity(SeqList* list)
    {
    
        TSeqList *tlist = NULL;
        if (list == NULL)
        {
            return -1;
        }
        tlist = (TSeqList *)list;
        return tlist->capacity;
    }
    
    int SeqList_Insert(SeqList* list, SeqListNode* node, int pos)
    {
        int i =0, ret = 0;
        TSeqList *tlist = NULL;
    
        if (list == NULL || node==NULL ||  pos<0)
        {
            ret = -1;
            printf("fun SeqList_Insert() err:%d 
    ", ret);
            return ret;
        }
        tlist = (TSeqList*)list;
    
        //判断是不是满了
        if (tlist->length >= tlist->capacity)
        {
            ret = -2;
            printf("fun SeqList_Insert() (tlist->length >= tlist->capacity) err:%d 
    ", ret);
            return ret;
        }
    
        //容错修正  6个长度 容量20;用户pos10位置插入..
        if (pos>=tlist->length)
        {
            pos = tlist->length; //
        }
    
        //1 元素后移
        for(i=tlist->length; i>pos; i--)
        {
            tlist->node[i] = tlist->node[i-1];
            //a[7] = a[6]
        }
        // i = 3
        // 2插入元素
        tlist->node[i] = (unsigned int )node;
        tlist->length ++;
        return 0;
    }
    
    SeqListNode* SeqList_Get(SeqList* list, int pos)
    {
        int i =0;
        SeqListNode *ret = 0;
        TSeqList *tlist = NULL;
    
        if (list == NULL ||  pos<0)
        {
            printf("fun SeqList_Get() err:%d 
    ", ret);
            return NULL;
        }
        tlist = (TSeqList*)list;
    
        ret = (void *)tlist->node[pos];
        return ret;
    }
    
    SeqListNode* SeqList_Delete(SeqList* list, int pos)
    {
        int i = 0;
        SeqListNode *ret = 0;
        TSeqList *tlist = NULL;
    
        if (list == NULL ||  pos<0) //检查
        {
            printf("fun SeqList_Delete() err:%d 
    ", ret);
            return NULL;
        }
        tlist = (TSeqList*)list;
    
        ret = (SeqListNode *)tlist->node[pos]; //缓存pos的位置
         
        for (i=pos+1; i<tlist->length; i++)  //pos位置后面的元素前移
        {
            tlist->node[i-1] = tlist->node[i];
        }
        tlist->length --;
        return ret;
    }
    View Code

      6、调试

      写一个main函数,进行上面的接口调用与调试,代码如下:

    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
    #include "seqlist.h"
    typedef struct _Teacher
    {
        int age;
        char name[64];
    }Teacher;
    
    void main()
    {
        int        ret = 0, i = 0;
        SeqList* list = NULL;
    
        Teacher t1, t2, t3, t4,t5;
        t1.age = 31;
        t2.age = 32;
        t3.age = 33;
        t4.age = 34;
        t5.age = 35;
    
        list = SeqList_Create(10);
        if (list == NULL)
        {
            printf("func SeqList_Create() ret :%d 
    ", ret);
            return ;
        }
    
        ret = SeqList_Insert(list, (SeqListNode*) &t1, 0); //头插法
        ret = SeqList_Insert(list, (SeqListNode*) &t2, 0); //头插法
        ret = SeqList_Insert(list, (SeqListNode*) &t3, 0); //头插法
        ret = SeqList_Insert(list, (SeqListNode*) &t4, 0); //头插法
        ret = SeqList_Insert(list, (SeqListNode*) &t5, 1); //头插法
    
        //遍历
        for (i=0; i<SeqList_Length(list); i++)
        {
            Teacher*  tmp = (Teacher *) SeqList_Get(list, i);
            if (tmp == NULL)
            {
                return ;
            }
            printf("tmp->age:%d ", tmp->age);
        }
    
        //删除链表中的节点
        while( SeqList_Length(list) > 0 )
        {
            SeqList_Delete(list, 0);
        }
    
        system("pause");
    
        return ;
    }

     三、链表

      1、介绍

      链表的大体有三种实现方式,用图进行讲解吧,如下图:

      

       说明:最上面就是最传统链表,每个节点存一个next指针,保存下一个节点的地址

          中间是linux内核实现的链表,一般是双向链表,有一个缺点要记录偏移量

          最后是改进之后的链表,不需要记录偏移量,这是前人的智慧,我将采用最后一种进行实现。

      2、定义头文件

      头文件主要包含结构体定义,和接口声明,主要实现链表初始化、插入、遍历等操作,头文件如下: 

    #ifndef LINKLIST_H
    #define LINKLIST_H
    
    #include<stdlib.h>
    #include<stdio.h>
    
    //链表结点
    typedef struct LINKNODE{
        void* data;  //指向任何类型的数据
        struct LINKNODE* next;
    }LinkNode;
    
    //链表结构体
    typedef struct LINKLIST{
        LinkNode* head;
        int size;
    }LinkList;
    
    //定义函数指针
    typedef void(*PRINTLINKNODE)(void*);
    
    //初始化链表
    LinkList* Init_LinkList();
    //指定位置插入
    void Insert_LinkList(LinkList* list,int pos,void* data);
    //删除指定位置的值
    void RemoveByPos_LinkList(LinkList* list, int pos);
    //获得链表的长度
    int Size_LinkList(LinkList* list);
    //查找
    int Find_LinkList(LinkList* list,void* data);
    //返回第一个结点
    void* Front_LinkList(LinkList* list);
    //打印链表结点
    void Print_LinkList(LinkList* list, PRINTLINKNODE print);
    //释放链表内存
    void FreeSpace_LinkList(LinkList* list);
    
    
    #endif

      3、插入节点

      初始化链表跟线性表类似,就不在过多说明了,主要讲解插入和删除节点,对插入节点进行讲解,如下图:

      

      说明:需要两个指针current、node,假如在3号位置进行插入,要先处理node->next,node指向3号位置,就把3号位置指针付给它,node->next=current->next;

    2号位置指向node,把node指针赋值给它,current->next=node;

      注意:图片左下角的红字,是理解链表插入、删除等的关键!

      代码如下:

    //指定位置插入
    void Insert_LinkList(LinkList* list, int pos, void* data){
    
        if (list == NULL){
            return;
        }
        if (data == NULL){
            return;
        }
    
        //友好的处理,pos越界 
        if (pos < 0 || pos > list->size) {
            pos = list->size;
        }
    
        //创建新的结点
        LinkNode* newnode = (LinkNode*)malloc(sizeof(LinkNode));
        newnode->data = data;
        newnode->next = NULL;
    
        //找结点
        //辅助指针变量
        LinkNode* pCurrent = list->head;
        for (int i = 0; i < pos;i++){
            pCurrent = pCurrent->next;
        }
    
        //新结点入链表
        newnode->next = pCurrent->next;
        pCurrent->next = newnode;
    
        list->size++;
    }

      4、删除节点

      

      说明:

      删除3号位置,先保存被删除节点的位置,再将2直接指向4;

      5、整体代码

      整个的代码,方便大家使用和调试,代码如下:

    #include"LinkList.h"
    
    //初始化链表
    LinkList* Init_LinkList(){
    
        LinkList* list = (LinkList*)malloc(sizeof(LinkList));
        list->size = 0;
    
        //头结点 是不保存数据信息
        list->head = (LinkNode*)malloc(sizeof(LinkNode));
        list->head->data = NULL;
        list->head->next = NULL;
    
        return list;
    }
    //指定位置插入
    void Insert_LinkList(LinkList* list, int pos, void* data){
    
        if (list == NULL){
            return;
        }
        if (data == NULL){
            return;
        }
    
        //友好的处理,pos越界 
        if (pos < 0 || pos > list->size) {
            pos = list->size;
        }
    
        //创建新的结点
        LinkNode* newnode = (LinkNode*)malloc(sizeof(LinkNode));
        newnode->data = data;
        newnode->next = NULL;
    
        //找结点
        //辅助指针变量
        LinkNode* pCurrent = list->head;
        for (int i = 0; i < pos;i++){
            pCurrent = pCurrent->next;
        }
    
        //新结点入链表
        newnode->next = pCurrent->next;
        pCurrent->next = newnode;
    
        list->size++;
    
    }
    //删除指定位置的值
    void RemoveByPos_LinkList(LinkList* list, int pos){
        if (list == NULL){
            return;
        }
    
        if (pos < 0 || pos >= list->size){
            return;
        }
    
        //查找删除结点的前一个结点
        LinkNode* pCurrent = list->head;
        for (int i = 0; i < pos;i ++){
            pCurrent = pCurrent->next;
        }
    
        //缓存删除的结点
        LinkNode* pDel = pCurrent->next;
        pCurrent->next = pDel->next;
        //释放删除结点的内存
        free(pDel);
    
        list->size--;
    }
    //获得链表的长度
    int Size_LinkList(LinkList* list){
        return list->size;
    }
    //查找
    int Find_LinkList(LinkList* list, void* data){
        if (list == NULL){
            return -1;
        }
    
        if (data == NULL){
            return -1;
        }
    
        //遍历查找
        LinkNode* pCurrent = list->head->next;
        int i = 0;
        while (pCurrent != NULL){
            if (pCurrent->data == data){
                break;
            }
            i++;
            pCurrent = pCurrent->next;
        }
    
        return i;
    }
    //返回第一个结点
    void* Front_LinkList(LinkList* list){
        return list->head->next->data;
    }
    //打印链表结点
    void Print_LinkList(LinkList* list, PRINTLINKNODE print){
        if (list == NULL){
            return;
        }
        //辅助指针变量
        LinkNode* pCurrent = list->head->next;
        while (pCurrent != NULL){
            print(pCurrent->data);
            pCurrent = pCurrent->next;
        }
    
    }
    //释放链表内存
    void FreeSpace_LinkList(LinkList* list){
    
        if (list == NULL){
            return;
        }
    
        //辅助指针变量
        LinkNode* pCurrent = list->head;
        while (pCurrent != NULL){
            //缓存下一个结点
            LinkNode* pNext = pCurrent->next;
            free(pCurrent);
            pCurrent = pNext;
        }
    
        //释放链表内存
        list->size = 0;
        free(list);
    
    }
    View Code

      6、调试代码

      写一个main函数进行调试,如下:

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "LinkList.h"
    
    
    //自定义数据类型
    typedef struct PERSON{
        char name[64];
        int age;
        int score;
    }Person;
    
    
    //打印函数
    void MyPrint(void* data){
        Person* p = (Person*)data;
        printf("Name:%s Age:%d Score:%d
    ",p->name,p->age,p->score);
    }
    
    int main(void){
        
    
        //创建链表
        LinkList* list = Init_LinkList();
    
        //创建数据
        Person p1 = { "aaa", 16, 100};
        Person p2 = { "bbb", 17, 99 };
        Person p3 = { "ccc", 18, 101 };
        Person p4 = { "ddd", 19, 97 };
        Person p5 = { "eee", 20, 59 };
    
        //数据插入链表
        Insert_LinkList(list, 0, &p1);
        Insert_LinkList(list, 0, &p2);
        Insert_LinkList(list, 0, &p3);
        Insert_LinkList(list, 0, &p4);
        Insert_LinkList(list, 2, &p5);
    
        //打印
        Print_LinkList(list, MyPrint);
    
        //删除3
        RemoveByPos_LinkList(list, 3);
    
        //打印
        printf("---------------
    ");
        Print_LinkList(list, MyPrint);
    
        //返回第一个结点
        printf("-----查找结果------------
    ");
         Person* ret = (Person*)Front_LinkList(list);
         printf("Name:%s Age:%d Score:%d
    ", ret->name, ret->age, ret->score);
    
        //销毁链表
        FreeSpace_LinkList(list);
    
        system("pause");
        return 0;
    }
    View Code

      运行结果如下,供参考:

      

      四、补充C++实现链表

      1、头文件

      头文件LinkedList.h,代码如下:

      

    #include <iostream>
    
    template<class T>
    class Node {    //节点类
    public:
        T e;    //数据域
        Node *next;    //指针域
    
        Node(T e, Node *next) : e(e), next(next) {    //构造函数的列表初始化
        }
    
        Node(T e) : e(e), next(nullptr) {
        }
        
        Node() : next(nullptr) {    //无参构造函数
        }
    };
    
    template<class T>
    class LinkedList {
    private:
        Node<T> *head;    //头结点
        int size;
    public:
        class Range {
        };
    
        class Empty {
        };
    
        LinkedList() {    //构造函数
            head = new Node<T>();
        size = 0;
        }
    
        int getSize() {    //获取大小
            return size;
        }
    
        bool isEmpty() {    //判断是否为空
            return size == 0;
        }
    
        void add(int index, T e) {    //指定位置插入元素
            if (index < 0 || index > size) {
                throw Range();
            }
            Node<T> *prev = head;
            for (int i = 0; i < index; ++i) {
                prev = prev->next;
            }
            prev->next = new Node<T>(e, prev->next);
            size++;
        }
    
        void addFirst(T e) {    //插入元素 头插法
            add(0, e);
        }
    
        void addLast(T e) {        //插入元素 尾插法
            add(size, e);
        }
    
        T get(int index) {    //获取元素
            if (size == 0) {
                throw Empty();
            }
            if (index < 0 || index >= size) {
                throw Range();
            }
            Node<T> *cur = head->next;
            for (int i = 0; i < index; ++i) {
                cur = cur->next;
            }
            return cur->e;
        }
    
        T getFirst() {
            return get(0);
        }
    
        T getLast() {
            return get(size - 1);
        }
    
        void set(int index, T e) {
            if (size == 0) {
                throw Empty();
            }
            if (index < 0 || index >= size) {
                throw Range();
            }
            Node<T> *cur = head->next;
            for (int i = 0; i < index; ++i) {
                cur = cur->next;
            }
            cur->e = e;
        }
    
        void setFirst(T e) {
            set(0, e);
        }
    
        void setLast(T e) {
            set(size - 1, e);
        }
    
        T remove(int index) {    //删除指定位置元素
            if (index < 0 || index >= size) {
                throw Range();
            }
            if (size == 0) {
                throw Empty();
            }
            Node<T> *prev = head;
            for (int i = 0; i < index; ++i) {
                prev = prev->next;
            }
            Node<T> *retNode = prev->next;
            prev->next = retNode->next;
            retNode->next = nullptr;
            size--;
            return retNode->e;
        }
    
        T removeFirst() {
            return remove(0);
        }
    
        T removeLast() {
            return remove(size - 1);
        }
    
        void removeElement(T e) {
            Node<T> *prev = head;
            while (prev->next != nullptr) {
                if (prev->next->e == e) {
                    break;
                }
                prev = prev->next;
            }
    
            if (prev->next != nullptr) {
                Node<T> *delNode = prev->next;
                prev->next = delNode->next;
                delNode->next = nullptr;
                size--;
            }
        }
    
        bool contains(T e) {    //是否包含
            Node<T> *cur = head;
            for (int i = 0; i < size; ++i) {
                cur = cur->next;
                if (cur->e == e) {
                    return true;
                }
            }
            return false;
        }
    
        void print() {    //打印输出
            Node<T> *prev = head;
            std::cout << "LinkedList: size = " << size << std::endl;
            std::cout << "[";
            for (int i = 0; i < size; ++i) {
                prev = prev->next;
                std::cout << prev->e;
                if (i < size - 1) {
                    std::cout << ", ";
                }
            }
            std::cout << "]" << std::endl;
        }
    };
    View Code

      2、main函数

      main.cpp如下:

      

    #include <iostream>
    #include "LinkedList.h"
    
    int main() {
        LinkedList<int> *linkedList = new LinkedList<int>();
        for (int i = 0; i < 5; ++i) {
            linkedList->addFirst(i);    //头插法
        }
        linkedList->add(2, 30);    //指定位置插入元素
        linkedList->print();     //打印输出
        linkedList->remove(2);    //删除指定位置元素
        linkedList->print();
        linkedList->removeFirst();
        linkedList->print();
        linkedList->removeLast();
        linkedList->print();   
        return 0;
    }

      3、测试

      运行结果如下:

      

      总结

      虽然线性表和单向链表是数据结构的基础,但完全掌握之后,再学习栈、队列等数据结构将会得心应手;

      喜欢的希望帮忙点赞,不懂的欢迎随时留言!

      

      

      

       

      

  • 相关阅读:
    老外的前端面试题
    java+tomcat 在 linux下的部署
    纯css实现slide效果
    使用 ADD-ON SDK 开发 基于 Html JQuery 和 CSS 的 firefox 插件入门教程1: 创建一个简单的 Add-on
    IOS ——OC——    @autoreleasepool的作用
    IOS ——OC——   代码块的理解
    IOS ——OC——   NSData的使用
    IOS ——OC——   NSDate的用法以及时差的消除
    IOS ——OC——  NSMutableCopy、NScopy 深拷贝与浅拷贝
    IOS ——OC——  协议的使用总结
  • 原文地址:https://www.cnblogs.com/liudw-0215/p/9843008.html
Copyright © 2020-2023  润新知