データ構造系列学習(2) - 系列テーブル(Contiguous_List)

目次

導入:

勉強:

シーケンス テーブルについて簡単に説明します。

ファイル間の関係:

ヘッダー ファイル内の記号 "" と <> の違い:

シーケンステーブル関数の説明:

操作機能(ファンクション):

挿入機能(インサート):

削除機能(削除):

判定関数(Is):

ヘッダー ファイル (Contiguous.h) 内の関数宣言:

ソース ファイル (Contiguous.cpp) 内の関数の具体的な実装:

初期化関数 (Init_Sqlist):

シーケンス テーブルが空の関数 (IsEmpty) かどうかを判断します。

シーケンス テーブルがいっぱいかどうかを判断する関数 (IsFull): 

検索機能(検索):

拡張機能(Inc): 

 ヘッド挿入関数(Insert_head):

末尾挿入関数 (Insert_tail):

位置による関数の挿入 (Insert_pos): 

ヘッド削除関数(Delete_head): 

末尾削除関数 (Delete_tail): 

位置による関数の削除 (Delete_pos): 

値による関数の削除 (Delete_val):

シーケンステーブルクリア機能(Clear): 

シーケンステーブル関数の破棄 (Destroy):

印刷順序テーブル機能 (表示):

テスト ファイル (Test.cpp) で実装された関数をテストします。

テスト ファイルでヘッド シーケンス テーブルを定義し、初期化します。

初期化後にシーケンス テーブルに 100 個の要素を入力します。

テストヘッダー挿入関数と拡張関数 (Insert_gead):

テスト末尾挿入関数 (Insert_tail):

位置による補間関数 (Insert_pos) をテストします。

テストヘッド削除関数 (Delete_Head):

末尾削除関数 (Delete_tail) をテストします。 

位置による削除関数 (Delete_pos) をテストします。

値 (Delete_val) によって削除関数をテストします。

検索機能 (検索) をテストします。 

クリア関数 (Clear) をテストします。 

 破壊関数 (Destroy) をテストします。

要約:

参考文献:


導入:

前回のデータ構造の最初の記事では、データ構造の基本概念とそれに関連する時間計算量の概念を紹介しました。データ構造の基本は理解しました。数日前、順序テーブルを体系的に学びました。テーブルは最初から手動で入力し、シーケンス テーブル内のほぼすべての関数とステートメントのコメントもコード内に注意深く書きました。この記事では、シーケンス テーブルの知識を学び、まとめます。

データ構造学習ディレクトリ:

データ構造シリーズ学習 (1) - データ構造入門

勉強:

シーケンス テーブルについて簡単に説明します。

線形テーブル (線形構造を持つ数値テーブル) をコンピュータ内に格納する最も便利で簡単な方法は、連続したアドレス メモリ ユニットのグループを使用して線形テーブル全体を格納することです。この記憶構造をシーケンシャル記憶構造と呼びます。この記憶構造の下にある線形テーブルを順序テーブルと呼びます。

シーケンス テーブルには次の特性が含まれます。

シーケンシャル テーブルを識別するための一意のテーブル名があります。

メモリ ユニットは連続的に格納されます。つまり、シーケンシャル テーブルは連続したメモリ スペースを占有する必要があります。

データは順番に保存され、要素間には順序関係があります。

シーケンステーブルを定義するということは、メモリ上に連続した記憶空間を開くことであり、同時に、追加、削除、確認、変更という4つの機能も実現する必要があります。

ファイル間の関係:

プログラミングの際、ファイルのコンパイル順序は上から下の順で行うのが一般的ですが、関数を main 関数の前に記述した場合は main 関数内で直接呼び出すことができますが、関数を main 関数の後に記述した場合は、 main 関数内で関数を呼び続けると、プログラムはエラーを報告します。ここでは簡単な例を示します。

たとえば、ここでは Add 関数を定義しますが、main 関数の後に記述します。

プログラムエラーでは、未定義のキーワードAddが使用されていることが示されていますが、現時点でこのエラーを解消したい場合は、main関数の前に関数を宣言し、次の文を追加する方法です。

int Add(int a,int b);

その後にセミコロンが必要であることに注意してください。

ファイル内には関数宣言も必要であり、関数宣言と関数関数の実現はそれぞれ別のファイルで完了します。ここでは 2 つのファイル形式、つまり .h および .cpp 接尾辞を持つファイルを紹介します。

.h ファイルの形式はヘッダー ファイルと呼ばれ、より複雑な関数を含むプログラムを実装する場合は、すべての関数宣言を .h ファイルに記述し、そのファイル内で構造体、定数変数、マクロを設計できます。意味;

次に、cpp ファイルに特定の関数を実装し、先頭で h ファイルをヘッダー ファイルの形で参照することで、その組み合わせをコンパイラ内でエンジニアリング プロジェクトと呼びます。次に実現します。

ヘッダー ファイル内の記号 "" と <> の違い:

ここで、ヘッダー ファイルを参照する場合は、「」と <> の違いを区別する必要があることに注意してください。

ただし、 #include<Contiguous_List.h> のように直接記述すると、エラーが報告されます。その理由は、<> 記号を使用すると、コンパイラが次のようなシステム変数パス内のヘッダー ファイルを直接検索するためです。通常作成するプログラムの先頭に #include<stdio.h> という標準入出力ヘッダー ファイルがあり、Contiguous_List.h ファイルはシステム変数に含まれないカスタム ヘッダー ファイルです。ファイル名を含めるには、「」記号を使用する必要があります。

#include"Contiguous.h"

このように、作成プログラムはエラーを報告しませんが、C 自体のヘッダー ファイルを引用するときに実際に "" 記号を使用できますが、これには順序の問題が含まれます。

<> 記号は、システム変数パス内でヘッダー ファイルを直接検索する場合にのみ使用されます。

「」記号を使用すると、ユーザー定義ヘッダー ファイルとシステム変数パス内のヘッダー ファイルの両方を検索できますが、検索順序としては、ユーザー プロジェクト パスが最初に検索され、次にシステム変数パスが検索されます。

そのため、上記のユーザー定義ヘッダー ファイルを参照した後に #include "stdio.h" を追加しても、エラーは報告されません。

シーケンステーブル関数の説明:

まず、完全なシーケンス テーブルに必要な機能について明確にする必要があります。

操作機能(ファンクション):

初期化関数(Init_Sqlist)

クリア機能(クリア)

デストロイ機能(Destroy)

拡張機能(Inc)

印刷機能(表示)

挿入機能(インサート):

ヘッド挿入関数(Insert_head)

末尾挿入関数(Insert_tail)

指定した位置に関数を挿入 (Insert_pos)

削除機能(削除):

ヘッド削除関数(Delete_head)

末尾削除関数(Delete_tail)

指定位置の削除関数(Delete_pos)

値指定削除関数(Delete_val)

判定関数(Is):

シーケンステーブルが満杯かどうかを判定する関数(Is_Empty)

シーケンステーブルが空かどうかを判定する関数(Is_Full)

ヘッダー ファイル (Contiguous.h) 内の関数宣言:

構造を設計する前に、まずシーケンス テーブルの初期長を定義する必要があります。

#define LIST_INIT_SIZE 100//顺序表的初始长度

まず、シーケンステーブルの記憶領域のベースアドレスを格納するために、シーケンステーブルの要素の型に応じたポインタ変数を定義する必要があります。

シーケンステーブルの現在の長さ。

シーケンステーブルの現在の総容量。

Elemtype を使用して int 型のパラメータを再定義し、length を使用してシーケンス テーブルの現在の長さを表し、listsize を使用してシーケンス テーブルの現在の総容量を表します。構造は次のように表すことができます。

typedef int Elem_type;//对int类型起一个别名
typedef struct Sqlist
{
    Elem_type * elem;//存储空间基址(用来接收malloc返回在堆上申请的连续空间块开辟)
    int length;//当前有效长度
    int listsize;//当前总空间大小
}Sqlist,*PSqlist;//结构体末尾的重命名相当于后面这两个语句:
//typedef struct Sqlist Sqlist;
//typedef struct Sqlist* PSqlist;

ヘッダー ファイル内のすべての関数宣言:

void Init_Sqlist(struct Sqlist* sq);//初始化
bool Insert_head(struct Sqlist* sq,Elem_type val);//头插(插入需要判满,自然,删除也需要判空)
bool Insert_tail(struct Sqlist* sq,Elem_type val);//尾插
bool Insert_pos(struct Sqlist* sq,int pos,Elem_type val);//按位置插
bool Delete_head(struct Sqlist* sq);//头删
bool Delete_tail(struct Sqlist* sq);//尾删
bool Delete_pos(struct Sqlist* sq,int pos);//按位置删
bool Delete_val(struct Sqlist* sq,Elem_type val);//按值删(找这个值在顺序表中第一次出现的位置,然后删除它)
bool Is_Empty(struct Sqlist* sq);//判空
bool Is_Full(struct Sqlist* sq);//判满
void Inc(struct Sqlist* sq);//扩容函数
int Search(struct Sqlist* sq,Elem_type val);//查找(找这个值在顺序表中第一次出现的位置)
void Clear(struct Sqlist* sq);//清空(将数据晴空,认为没有有效值)
void Destroy(struct Sqlist* sq);//销毁(将数据存储空间都释放掉)
void Show(struct Sqlist* sq);//打印

ソース ファイル (Contiguous.cpp) 内の関数の具体的な実装:

初期化関数 (Init_Sqlist):

(1) 必要に応じて、線形テーブルの最大容量であるあらかじめ定義されたサイズの記憶領域 LIST_INIT_SIZE を割り当てます。記憶領域の割り当てに失敗した場合は、エラー メッセージが表示されます。
(2) 線形テーブルの初期長を 0 に設定します。
(3) 線形テーブルの現在の記憶容量を順序テーブルの最大容量に設定します。

コード:

void Init_Sqlist(struct Sqlist* sq)//初始化
{
    sq->elem = (Elem_type*)malloc(LIST_INIT_SIZE * sizeof(int));
    assert(sq->elem != NULL);//安全性判断
    sq->length = 0;//将顺序表初始长度设为0
    sq->listsize = LIST_INIT_SIZE;//将顺序表初始总容量设置为前面的宏定义
}

シーケンス テーブルが空の関数 (IsEmpty) かどうかを判断します。

この機能は分かりやすく、順序表の長さが0の場合、当然、順序表は空の順序表となる。

コード:

bool Is_Empty(struct Sqlist* sq)//判空
{
    //1 安全性处理
    assert(sq != NULL);
    //2 顺序表的长度为0时,判断顺序表为空
    return sq->length == 0;
}

シーケンス テーブルがいっぱいかどうかを判断する関数 (IsFull): 

同様に、シーケンステーブルの長さがシーケンステーブルの総容量と等しい場合、シーケンステーブルはフル状態となる。

コード:

bool Is_Full(struct Sqlist* sq)//判满
{
    //1 安全性处理
    assert(sq != NULL);
    //2 当顺序表的长度等于顺序表的总容量时判为满
    return sq->length == sq->listsize;
}

検索機能(検索):

この関数は実際には広義の逐次検索であり、順序テーブルの最初の要素から逆方向に検索し、要素が見つかった場合はその要素の添え字を返し、要素が見つからなかった場合は直接 -1 を返します。

コード:

int Search(struct Sqlist* sq,Elem_type val)//查找(找这个值在顺序表中第一次出现的位置)
{
    //1 安全性处理
    assert(sq != NULL);
    //2 顺序查找,找到了返回下标,找不到了直接返回-1
    for(int i = 0;i < sq->length;i++){
        if(val == sq->elem[i]){
            return i;
        }
    }
    return -1;
}

拡張機能(Inc): 

拡張関数は <malloc.h> ヘッダー ファイルの realloc 関数を使用する必要があります。シーケンス テーブルがいっぱいでシーケンス テーブルに要素を追加する必要がある場合は、拡張操作を実行して sq->elem に名前を付ける必要があります。格納する realloc 関数の拡張後の空間の新しいメモリ ベース アドレス。デフォルトの拡張は 2 倍で、シーケンス テーブルの記憶容量 * 2 です。

コード:

void Inc(struct Sqlist* sq)//扩容函数
{
    //1 安全性处理
    assert(sq != NULL);
    //2 使用sq->elem保存realloc函数扩容后的新内存基址
    sq->elem = (Elem_type*)realloc(sq->elem,(sq->listsize*sizeof(int))* 2);
    //3 安全性处理
    assert(sq->elem != NULL);
    //4 sq->length并不需要改变,需要改变的是顺序表总的长度
    sq->listsize *= 2;
}

 ヘッド挿入関数(Insert_head):

ヘッド挿入関数を実行するときに、最初に明確にすることは、2 つの状況が存在するということです (Opt は 2 つの状況を表します)。

まず、完全な操作を実行する必要があります。シーケンス テーブルがいっぱいの場合は、拡張操作を実行する必要があります。拡張操作が完了すると、すべての要素が 1 単位ずつ移行されます (最後の要素から開始されることに注意してください) 、最初から 1 つの要素で開始する場合、格納順序とアドレスは破壊されます)、その後、先頭挿入操作を実行します。シーケンス テーブルがいっぱいでない場合は、すべての要素を 1 単位ずつ逆方向に直接移動し、その後、挿入操作を実行します。

コード:

bool Insert_head(struct Sqlist* sq,Elem_type val)//头插
{
    //1安全性处理
    assert(sq != NULL);
    //2判满操作
    if(Is_Full(sq)){
        Inc(sq);
    }
    //3向后挪动元素(从尾部向前进行移动)
    for(int i = sq->length - 1;i >= 0;i--){
        sq->elem[i + 1] = sq->elem[i];
    }
    //4插入值
    sq->elem[0] = val;
    //5length+1
    sq->length++;
    return true;
}

末尾挿入関数 (Insert_tail):

先頭挿入関数とは異なり、末尾挿入関数の実行には要素の移行は必要なく、シーケンス テーブルの直後に挿入するだけでよいため、次のフローチャートに示すように、この関数の時間計算量は O(1) です。 :

先頭挿入関数と同様にフルと判定する必要があり、判定がフルの場合は拡張関数を実行し、拡張関数後に末尾挿入操作を実行し、判定が偽の場合は直接挿入操作を実行します。 

コード:

bool Insert_tail(struct Sqlist* sq,Elem_type val)//尾插
{
    //尾插并不需要挪动元素,如果满了就扩容,再在尾部进行添加,如果没满直接在后面插就行
    //1安全性处理
    assert(sq != NULL);
    //判满操作
    if(Is_Full(sq)){
        Inc(sq);
    }
    sq->elem[sq->length] = val;
    //length+1
    sq->length++;
    return true;
}

位置による関数の挿入 (Insert_pos): 

位置による挿入機能を実行すると、ヘッダー挿入機能に対応して、シーケンス テーブルがいっぱいの場合は容量拡張操作が実行され、容量拡張完了後は要素が一律に後方に移動されます。指定された位置 (添字) から 1 単位ずつ移動し、指定された位置 (添字) に要素を挿入します。順序テーブルがいっぱいでない場合は、直接移行操作を実行して、指定された位置に要素を挿入します。フローチャート:

コード: 

bool Insert_pos(struct Sqlist* sq,int pos,Elem_type val)//按位置插
{
    //默认当pos == 0时为头插,所以当pos == length的时候也就是尾插了
    //1安全性处理
    assert(sq != NULL);
    //2判满
    if(Is_Full(sq)){
        Inc(sq);
    }
    //3移动元素位置
    for(int i = sq->length;i >= pos;i--){
        sq->elem[i + 1] = sq->elem[i];
    }
    //4将值放进去
    sq->elem[pos] = val;
    //5length+1
    sq->length++;
    return true;
}

ヘッド削除関数(Delete_head): 

挿入は満杯と判断する必要があり、削除は空と判断する必要があり、順序表自体が空の場合は当然削除操作は行われません。したがって、先頭削除機能でも、配列表が空の場合と空でない場合の2つの場合が発生し、以下のようなフローチャートになります。

コード:

bool Delete_head(struct Sqlist* sq)//头删
{
    //1安全性处理
    assert(sq != NULL);
    //2判空
    if(Is_Empty(sq)){
        return false;
    }
    //3移动元素位置
    for(int i = 0;i < sq->length - 1;i++){
        sq->elem[i - 1] = sq->elem[i];
    }
    //4length-1
    sq->length--;
    return true;
}

末尾削除関数 (Delete_tail): 

先頭削除とは異なり、末尾削除関数は移動する必要がないため、時間計算量はO(1)ですが、空であると判断する必要もあります。

コード:

bool Delete_tail(struct Sqlist* sq)//尾删
{
    //1 安全性判断
    assert(sq != NULL);
    //2 判空
    if(Is_Empty(sq)){
        return false;
    }
    //3 length - 1
    sq->length--;
    return true;
}

位置による関数の削除 (Delete_pos): 

まずnull判定を行い、空の場合はそのままfalseを返し、空でない場合は削除する要素の後ろの値を1単位前に進め、長さを1減らします。フローチャート:

ここで注意すべき点は、posの値が0の場合に実際に先頭削除操作が行われるため、位置による挿入操作と同様にpos >= 0という判定条件を追加する必要があり、posのときに実行される。 == 0 これもプラグイン操作です。

コード:

bool Delete_pos(struct Sqlist* sq,int pos)//按位置删
{
    //1 安全性处理
    assert(sq != NULL);
    //2 将待删除节点后面的节点统一向前移一位,假设pos = 0时为头删判断pos的合法性
    assert(pos >= 0 && pos < sq->length);
    //3 判空
    if(Is_Empty(sq)){
        return false;
    }
    //4 假设让i指向被覆盖者的一方
    for(int i = pos;i < sq->length - 1;i++){
        sq->elem[i] = sq->elem[i + 1];
    }
    //假设让i指向覆盖者的一方
//    for(int i = pos + 1;i < sq->length;i++){
//        sq->elem[i - 1] = sq->elem[i];
//    }
    //5 length - 1
    sq->length--;
    return true;
}

値による関数の削除 (Delete_val):

値による削除機能は順序表が空ではないことを前提としているため、NULL判定を行う必要がありません。まず一時変数 temp を定義してシーケンス テーブルで見つかった要素の添え字を保存し、次にここで位置による削除関数 (Delete_pos) を直接呼び出します。フローチャートは次のとおりです。

コード:

bool Delete_val(struct Sqlist* sq,Elem_type val)//按值删(找这个值在顺序表中第一次出现的位置,然后删除它)
{
    //1 安全性处理
    assert(sq != NULL);
    //2 定义临时变量保存要删除元素的下标(先查找)
    int temp = Search(sq,val);
    //如果找到了,temp保存这个值的下标,如果没找到temp返回-1
    if(temp == -1){
        return false;
    }
    //3 如果代码执行到了这一行
    return Delete_pos(sq,temp);
}

シーケンステーブルクリア機能(Clear): 

シーケンス テーブルをクリアするとは、シーケンス テーブルの長さを直接ゼロに割り当てることを意味します。

コード:

void Clear(struct Sqlist* sq)//清空(将数据晴空,认为没有有效值)
{
    //1 安全性处理
    assert(sq != NULL);
    //2 将顺序表的长度直接赋值为0
    sq->length = 0;
}

シーケンステーブル関数の破棄 (Destroy):

順序表を破棄するという意味は、初期化関数のmallocで申請したメモリ空間(拡張されているかどうかは問わない)を直接解放することであり、当然順序表は存在しません。

コード:

void Destroy(struct Sqlist* sq)//销毁(将数据存储空间都释放掉)
{
    //1 安全性处理
    assert(sq != NULL);
    //2 释放掉申请开辟的空间
    free(sq->elem);
    //3 将顺序表的长度和总容量全部置为0
    sq->length = 0;
    sq->listsize = 0;
}

印刷順序テーブル機能 (表示):

この関数は比較的単純で、実際にはトラバースしてから出力します。

コード:

void Show(struct Sqlist* sq)//打印
{
    //1 安全性处理
    assert(sq != NULL);
    //2 打印操作
    for(int i = 0;i < sq->length;i++){
        printf("%3d",sq->elem[i]);
    }
    printf("\n");
}

テスト ファイル (Test.cpp) で実装された関数をテストします。

テスト ファイルでヘッド シーケンス テーブルを定義し、初期化します。

struct Sqlist head;
Init_Sqlist(&head);

初期化後にシーケンス テーブルに 100 個の要素を入力します。

for(int i = 0;i < 100;i++){
        Insert_pos(&head,i,i + 1);
    }

テストヘッダー挿入関数と拡張関数 (Insert_gead):

順序テーブルの先頭に要素 100 を挿入し、順序テーブルの長さと記憶領域の長さを出力します。順序テーブルがいっぱいの場合は、まず容量を拡張してから、次を挿入します。

    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);
    printf("\n");

    Insert_head(&head,100);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

図に示すように、初期の合計容量は 100 ユニットに設定されています。値シーケンス テーブルの先頭に要素 100 を追加する必要がある場合、最初に完全な操作を実行し、次にシーケンス テーブルの容量を 2 倍にし、次に要素を移行し、シーケンス テーブルのヘッダーに 100 を追加すると、実行結果は正しくなります。

テスト末尾挿入関数 (Insert_tail):

要素 100 を順序テーブルの最後に挿入します。このとき、順序テーブルがいっぱいであるため、最初に容量を拡張してから挿入する必要があります。listsize 変数は元の値の 2 倍 (200) になります。

    Insert_tail(&head,100);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

位置による補間関数 (Insert_pos) をテストします。

5 つの要素を挿入する 2 番目の添字位置を指定します。

    Insert_pos(&head,2,5);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

テストヘッド削除関数 (Delete_Head):

シーケンステーブルの先頭要素を削除します。

    Delete_head(&head);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果: 

末尾削除関数 (Delete_tail) をテストします。 

シーケンス リストの末尾要素を削除します。

    Delete_tail(&head);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

位置による削除関数 (Delete_pos) をテストします。

図に示すように、配列表の添字番号 2 の要素を削除したいと考えています。

    Delete_pos(&head,2);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

値 (Delete_val) によって削除関数をテストします。

削除したい値は 2 です。まず、シーケンス テーブルに 2 が存在するかどうかを確認します。存在する場合は、2 要素以降の値を上書きします。

    Delete_val(&head,2);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

検索機能 (検索) をテストします。 

検索したい値は 10 で、temp を使用して find 関数の戻り値を保存します。要素 10 がシーケンス テーブルで見つかった場合は添字を返し、そうでない場合は -1 を返します。

    int temp = Search(&head,10);
    printf("result = %d\n",temp);

操作結果:

クリア関数 (Clear) をテストします。 

    Clear(&head);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

 破壊関数 (Destroy) をテストします。

    Destroy(&head);
    Show(&head);
    printf("\nlength = %d",head.length);
    printf("\nlistsize = %d",head.listsize);

操作結果:

要約:

順次テーブルの利点は、直接アクセスできることと、その削除および挿入操作が主に最後に集中することです。しかし、メリットがあるということは、相応のデメリットもあり、順序表の先頭や途中での挿入や削除などの演算量が多く、時間とスペースのオーバーヘッドが大きく、頻繁な拡張演算が発生し、コストも比較的高くなります。高い。

ここまでで、数列テーブルの実装はほぼ完了しました。この記事は、授業で先生が話した内容をまとめるために使用しました。他にも機能があれば、追加しても構いません。数列テーブルは、私が初めて学んだデータ構造であり、動的メモリ管理、構造体、ポインタ、シーケンステーブルを記述するコードなどの言語の知識も、これらの知識の復習に相当します。ヤン・ウェイミン「データ構造」でフォローアップします。 (C言語編)』 線形テーブルのその他の関数は後日追加予定です。シーケンス テーブルの完全なコードをリソースにアップロードします。

参考文献:

ヤン・ウェイミン - 「データ構造(C言語編)」

おすすめ

転載: blog.csdn.net/weixin_45571585/article/details/127478056