C++ 표준 템플릿 라이브러리(STL) 및 일반 일반 알고리즘

목차:

        1. C++ 표준 템플릿 라이브러리(STL)

                1. 헤더 파일 템플릿

                2, 벡터

                3, 설정

                4, 문자열

                5、지도

                6, 대기열

                7. 우선순위_대기열

                8, 스택

                9, 쌍

                    10. 알고리즘 헤더 파일에서 일반적으로 사용되는 함수

                    11. 연결된 목록

        둘째, 일반적으로 사용되는 일반 알고리즘

                1. 찾기

                2, find_if

                3、카운트

                4、축적하다

                5, 찾기_첫번째_of

                6, 채우기

                7, 채우기_n

                8, 정렬

                9, 독특한

                10、count_if

                11. 찾기_if

1. C++ 표준 템플릿 라이브러리(STL)

1. 헤더 파일 템플릿

/*   头文件   */
#include <vector>
#include <set>
#include <string>
#include <map>
#include <queue>
#include <stack>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <iterator>
#include <list>
#include <bitset>
#include <iostream>
using namespace std;

 2, 벡터

2.1, 벡터: 원래 벡터를 의미했지만 여기서는 " 가변 길이 배열 ", 즉 "필요에 따라 길이가 자동으로 변경되는 배열" 로 사용됩니다 .

2.2 전제 조건: 벡터 헤더 파일 추가

#include <vector>
using namespace std;

2.3 정의

vector<typename> name;
vector<typename> Arrayname[arraySize];
//举例
vector<int> name;
vector<node> name;
vector<vector<int> > name;
vector<int> Arrayname[100];

2.4 액세스(두 가지 액세스 방법: 첨자 및 반복자=포인터)

        2.4.1 첨자로 접근: 일반 배열 접근과 동일

vi[3]

        2.4.2 반복자를 통한 액세스: 반복자는 포인터와 유사한 것으로 이해할 수 있습니다.

vector<typename>::iterator it = vi.begin();//定义迭代器it
通过*it来访问vector里的元素

vi.begin()+3//迭代器=指针
*(vi.begin()+3)//和v[3]是等价的

        참고: 일반적으로 사용되는 STL 컨테이너에서는 벡터 및 문자열에서만 vi.begin()+3 반복자 및 정수를 사용할 수 있습니다.

        2.4.3, 순회

#include<cstdio>
#include<vector>
using namespace std;
int main()
{
	vector<int> vi;
	for(int i = 0; i <= 5; i++)
    {
		vi.push_back(i);
	}
	vi.insert(vi.begin()+2,-1);//将-1插在vi[2]的位置
	for(int i = 0; i < vi.size(); i++)
    {
        //下标方式
		printf("%d ",vi[i]);
	}
	for(vector<int>::iterator it = vi.begin(); it != vi.end(); it++)
    {
        //迭代器方式
		printf("%d ",*it);
	}
	return 0;
}

 2.5 자주 사용하는 함수

기능 기능
시작하다() 첫 번째 요소 주소
끝() 꼬리 요소 주소의 다음 주소(미국인들은 왼쪽 닫힘과 오른쪽 열림에 더 익숙함)
크기() 요소 수
분명한() 빈 요소
푸시백(x) 끝에 요소 추가
팝백() 후행 요소 제거
삽입(it,x) 지정된 위치에 요소 삽입
지우다 지정된 위치의 요소 삭제
지우기(처음,마지막) [ first , last )의 모든 요소를 ​​삭제합니다. 모든 요소를 ​​삭제하려면 작성 방법은 vi.erase(vi.begin(),vi.end())이며 이는 clear()와 동일합니다.

3, 설정

3.1 set: 컬렉션으로 번역하면 내부에서 자동으로 정렬되는 컨테이너로 반복되는 요소를 포함하지 않습니다.

          기능: 자동 중복 제거 및 오름차순 정렬

3.2 전제 조건: 세트 헤더 파일 추가

#include<set>
using namespace std;

3.3 정의

set<typename> name;
set<typename> Arrayname[arraySize];

 3.4, 액세스(반복자를 통해서만 = 포인터 액세스)

set<typename>::iterator it;//定义迭代器it

벡터 및 문자열 이외의 STL 컨테이너는 *(it+i) 액세스 방법을 지원하지 않으므로 다음과 같이 열거할 수만 있습니다.

#include<cstdio>
#include<set>
using namespace std;
int main()
{
	set<int> st;
	st.insert(3);
	st.insert(5);
	st.insert(2);
	st.insert(3);
	for(set<int>::iterator it = st.begin(); it != st.emd(); it++)
    {
	    printf("%d",*it);//输出结果:2 3 5
	}
	return 0;
}

 3.5 자주 사용하는 함수

기능 기능
시작하다() 첫 번째 요소 주소
끝() 꼬리 요소 주소의 다음 주소(미국인들은 왼쪽 닫힘과 오른쪽 열림에 더 익숙함)
크기() 요소 수
분명한() 빈 요소
삽입(x) 요소 삽입
찾기(엑스) 요소를 찾아 해당 요소에 대한 반복자(포인터)를 반환합니다.
지우다 찾기 기능과 함께 사용할 수 있는 지정된 위치의 요소를 삭제합니다. st.eraser(st.find(x));
지우기(엑스) st.erase(st.find(x))와 동일한 요소 x를 삭제합니다.
지우기(처음,마지막) [ first , last ) 내의 모든 요소 삭제

4, 문자열

4.1 문자열: 문자열 유형은 일반적으로 사용되는 문자열의 필수 기능을 캡슐화하여 작업을 더 편리하고 오류 발생 가능성을 줄입니다.

4.2 전제 조건: 문자열 헤더 파일 추가

#include<string>
using namespace std;

4.3 정의

string str = "abcd";

4.4 입출력

        4.4.1 전체 문자열을 읽고 출력하려면 cin과 cout만 사용할 수 있습니다.

#include<iostream>//cin和cout在iostream头文件中
#include<string>
using namespace std;
int main()
{
	string str;
	cin>>str;
	cout<<str;
	return 0;
}

        4.4.2 출력을 위해 c_str()을 사용하여 문자열 유형을 문자 배열로 변환

#include<iostream>//cin和cout在iostream头文件中
#include<string>
using namespace std;
int main()
{
	string str = "abcd";
	printf("%s\n",str.c_str());//将string型str使用c_str()变成字符数组
	return 0;
}

4.5 액세스(두 가지 액세스 방법: 첨자 및 반복자=포인터)

        4.5.1 첨자로 접근: 문자배열 접근과 동일

str[3]

        4.5.2 반복자를 통한 액세스: 반복자는 포인터와 유사한 것으로 이해할 수 있습니다.

string::iterator it;//定义迭代器it
通过*it来访问string里的元素

str.begin()+3//迭代器=指针
*(str.begin()+3)//和str[3]是等价的

        참고: 일반적으로 사용되는 STL 컨테이너에서는 벡터 및 문자열에서만 vi.begin()+3 반복자 및 정수를 사용할 수 있습니다.

4.6 일반적으로 사용되는 함수

기능 기능
시작하다() 첫 번째 요소 주소
끝() 꼬리 요소 주소의 다음 주소(미국인들은 왼쪽 닫힘과 오른쪽 열림에 더 익숙함)
분명한() 빈 요소
크기() 요소 수
길이() size()와 동일한 요소 수
+ 문자열을 연결합니다. 예: str3 = str1 + str2 또는 str1 += str1;
비교 연산자 두 문자열의 크기를 사전식으로 비교
삽입(위치,문자열) 문자열 문자열을 pos 번호 위치에 삽입하십시오.

삽입(그것,그것2,그3)

문자열 [it2,it3)이 해당 위치에 삽입됩니다.
지우다 지정된 위치의 요소 삭제
지우기(처음,마지막) [ first , last ) 내의 모든 요소 삭제
지우기(포지션,길이) POS 위치에서 길이 비트 제거
substr(포지션,길이) 위치 pos에서 시작하여 length 길이의 하위 문자열을 가로챕니다.
str.find(str2) str2가 str의 하위 문자열이면 str에서 처음 나타나는 위치를 반환합니다. str2가 str의 하위 문자열이 아니면 string::npos를 반환합니다.
str.find(str2,pos) str의 pos 번호에서 str2 일치
문자열::npos 불일치가 있을 때 찾기 함수의 반환 값으로 사용되는 상수

str.replace(pos,len,str2)

pos에서 시작하고 길이가 len인 str의 하위 문자열을 str2로 바꿉니다.
str.replace(it1,it2,str2) str의 반복자 [it1,it2) 범위의 하위 문자열을 str2로 바꿉니다.

5、map

5.1、map:翻译为映射。map可以将任何基本类型(包括STL容器)映射到任何基本类型(包括STL容器)。map会以键从小到大的顺序自动排序,这是由于map内部是使用红黑树实现的(set也是),在建立映射的过程中会自动实现从小到大的排序功能。

5.2、使用前提:添加map头文件

#include<map>
using namespace std;

5.3、定义

map<typename1,typename2> mp;
举例:
map<string,int> mp;//如果字符串作为key,必须使用string而不能char数组
map<set<int>,string> mp;

5.4、访问(两种访问方式:下标和迭代器=指针)

        5.4.1、通过下标访问:和访问普通数组一样

mp[key]

        5.4.2、通过迭代器访问:迭代器可以理解为一种类似指针的东西

map<typename1,typename2>::iterator it;//定义迭代器it
通过it->first来访问键,通过it->second来访问值
#include<cstdio>
#include<map>
using namespace std;
int main()
{
	map<char,int> mp;
	mp['m'] = 20;
	mp['r'] = 30;
	mp['a'] = 40;
	for(map<char,int>::iterator it = mp.begin(); it != mp.end(); it++)
    {
		printf("%c %d\n",it->first,it->second);
	}
	return 0;
}

5.5、常用函数

函数 功能
begin() 首元素地址
end() 尾元素地址的下一个地址(美国人思维比较习惯左闭右开)
size() 元素个数
clear() 清空元素

find(key)

查找key,返回迭代器
erase(it) 删除指定位置元素
erase(key) 删除key
erase(first,last) 删除 [ first , last ) 内的所有元素

6、queue

6.1、queue:翻译为队列,先进先出的容器

6.2、使用前提:添加queue头文件

#include<queue>
using namespace std;

6.3、定义

queue<typename> name;

6.4、访问

        只能通过front()来访问队首元素,通过back()访问队尾元素

6.5、常用函数

函数 功能
size() 元素个数
empty() 检测是否为空,返回bool
push(x) 入队列
pop() 出队列
front() 获得队首元素
back() 获得队尾元素

        Note:使用front()和pop()函数时,必须用empty()判断队列是否为空,否则可能因为队空而出现错误。

7、priority_queue

7.1、priority_queue:翻译为优先队列,其底层是用堆来进行实现的,队首元素一定是当前队列中优先级最高的那一个。

7.2、使用前提:添加queue头文件

#include<queue>
using namespace std;

7.3、定义(设置优先级)

        如何定义优先队列内元素的优先级是运用好优先队列的关键

priority_queue<int> name;//默认大的优先级越大,等价于priority_queue<int,vector<int>,less<int> > q;
//基本数据类型的优先级设置
//第二个参数是来承载底层数据结构堆的容器,第三个参数是对第一个参数的比较类
priority_queue<int,vector<int>,less<int> > q;//less表示大的优先级越大
priority_queue<int,vector<int>,greater<int> > q;//greater表示小的优先级越大
//结构体的优先级设置
struct fruit{
	string name;
	int price;
};
struct cmp{
	//优先队列的这个函数与sort中的cmp函数的效果是相反的
	bool operator () (fruit f1,fruit f2)
    {
		return f1.price > f2.price;
	}
};
priority_queue<fruit,vector<fruit>,cmp> q;

7.4、访问

        只能通过top()来访问队首元素(也可以称为堆顶元素),也就是优先级最高的元素。

7.5、常用函数

函数 功能
size() 元素个数
empty() 检测是否为空,返回bool
push(x) 入队列
pop() 出队列
top() 获得队首元素

        Note:使用top()函数时,必须用empty()判断优先队列是否为空,否则可能因为队空而出现错误。

8、stack

8.1、stack:翻译为栈,后进先出的容器。

         用来模拟实现一些递归,防止程序对栈内存的限制而导致程序运行出错。

8.2、使用前提:添加stack头文件

#include<stack>
using namespace std;

8.3、定义

stack<typename> name;

8.4、访问

        只能通过top()来访问栈顶元素

8.5、常用函数

函数 功能
size() 元素个数
empty() 检测是否为空,返回bool
push(x) 入栈
top() 获得栈顶元素
pop() 弹出栈顶元素

9、pair

9.1、pair:实际上可以看作一个内部有两个元素的结构体

常见用途一:代替二元结构体,节省编码时间
常见用途二:作为map的键值对来进行插入

#include<string>
#include<map>
int main()
{
	map<string,int> mp;
	mp.insert(make_pair("hehe",5));
	mp.insert(pair<string,int>("hehe",5));
	for(map<string,int>::iterator it = mp.begin(); it != mp.end(); it++)
    {
		printf("%c %d\n",it->first,it->second);
	}
}

9.2、使用前提:添加utility头文件或map头文件

        map内部实现涉及pair,因此添加map头文件时会自动添加utility头文件

#include<map>
//或 #include<utility> 
using namespace std;

9.3、定义

//定义
pair<typename1,typename2> name;
//定义并初始化
pair<string,int> p("haha",5);
//临时变量(只用一次的)
pair<string,int>("haha",5)
make_pair("haha",5)

9.4、访问

        pair中只有两个元素,分别是first和second,只需要按正常结构体的方式去访问即可。

9.5、常用函数

函数 功能
比较运算符 比较规则是先以first的大小作为标准,相等时才去判别second的大小

10、algorithm头文件下常用函数

函数 功能
max(x,y) 最大值 ( int和double均可)
max(x,max(y,z)) 三个数的最大值
min(x,y) 最小值
abs(int x) 绝对值( Note:浮点型的绝对值请用math里的fabs)
swap(x,y) 交换
reverse(it,it2) 将数组指针或容器迭代器在[it,it2)范围内的元素反转

next_permutation(it,it2)

给出一个序列在全排列中的下一个序列
fill(it,it2,value) 把数组或容器某区间赋为某个相同的值
sort(it,it2) 排序
sort(it,it2,cmp) 排序

lower_bound(it,it2,val)

寻找数组或容器的[it,it2)范围内第一个值大于等于val的元素的位置,返回指针或迭代器,如果不存在,则返回可插入该元素的指针或迭代器
upper_bound(it,it2,val) 寻找数组或容器的[it,it2)范围内第一个值大于val的元素的位置,返回指针或迭代器,如果不存在,则返回可插入该元素的指针或迭代器

11、链表

11.1、链表结点

struct node{
	typename data;//数据域
	node* next;//指针域
}

11.2、为链表结点分配内存空间和释放内存空间

node* p = new node;//分配内存空间
delete(p);//释放内存空间

        Note:new运算符和delete运算符必须成对出现,否则会容易产生内存泄露,从编程习惯上,应养成即时释放空间的习惯。不过一般考试中,分配的空间在程序结束时即被释放,不会有太大影响,以下代码没有释放空间。

11.3、链表基本操作

        11.3.1、创建链表

#include<stdio.h>
#include<stdlib.h>
struct node{
	int data;
	node* next;
}
node* create(int Array[],int len)
{
	node *p,*pre,*head;
	head = new node;
	head->next = NULL;
	pre = head;
	for(int i = 0; i < len; i++)
    {
		p = new node;
		p->data = Array[i];
		p->next = NULL;
		pre->next = p;
		pre = p;
	}
	return head;
}
int main()
{
	int Array[5] = {5,3,6,1,2};
	node* L = create(Array,5);
	L = L->next;
	while(L != NULL)
    {
		printf("%d",L->data);
		L = L->next;
	}
}

        11.3.2、查找元素

int search(node* head,int x)
{
	int count= 0;
	node* p = head->next;
	while(p != NULL)
    {
		if(p->data == x)
        {
			count++;
		}
		p = p->next;
	}
	return count;
}

        11.3.3、插入元素

void insert(node* head,int pos,int x)
{
	node* p = head;
	for(int i = 0; i < pos -1; i++)
    {
		p = p->next;
	}
	node* q = new node;
	q->data = x;
	q->next = p->next;
	p->next = q;
}

        11.3.4、删除元素

//删除以head为头结点的链表中所有数据域为x的结点
void del(node* head,int x)
{
	node* p = head->next;
	node* pre = head;
	while(p != NULL)
    {
		if(p->data == x)
        {
			pre->next = p->next;
			delete(p);
			p = pre->next;
		}
        else
        {
			pre = p;
			p = p->next;
		}
	}
}

11.4、静态链表

struct Node{
	typename data;
	int next;//令数组的下标直接表示结点的地址,实现原理是hash
}node[size];

二、常用泛型算法

1、find

vector<int> a;
int val = 10
//find接受两个迭代器和要查找的值,如果找到,则返回该值对应的迭代器
//否则返回a.end()
auto it = find(a.begin(), a.end(), val);

2、find_if

vector<int> a;
int val = 10
//find_if 返回第一个满足条件的迭代器
//否则返回a.end()
auto it = find(a.begin(), a.end(), [](int a)->bool { return a == 1; });

3、count

vector<int> a;
int val = 10
//count 与find 的参数类型相同,统计两个迭代器之间的值val出现的次数
int s = count(a.begin(), a.end(), val);

4、accumulate

accumulate 带有三个形参。
头两个形参指定要累加的元素范围。第三个形参则是累加的初值。accumulate 函
数将它的一个内部变量设置为指定的初值,然后在此初值上累加输入范围 accumulate
用于指定累加起始值的第三个实参是必要的,因
为 accumulate 对将要累加的元素类型一无所知,因此,除此
之外,没有别的办法创建合适的起始值或者关联的类型。
accumulate 对要累加的元素类型一无所知,这个事实有两层含义。首先,调用
该函数时必须传递一个起始值,否则,accumulate 将不知道使用什么起始值。
其次,容器内的元素类型必须与第三个实参的类型匹配,或者可转换为第三个实
参的类型。在 accumulate 内部,第三个实参用作累加的起点;容器内的元素按

顺序连续累加到总和之中。因此,必须能够将元素类型加到总和类型上。

 

int main()
{
	vector<int> vec;

	for(int i = 0; i < 10; i++)
	{
		vec.push_back(i);	
	}
	
	//在100000的基础上进行累加
	int sum = accumulate(vec.begin(),vec.end(),100000);

	cout << sum << endl;
	return 0;
}

还可以进行字符串的累加

int main()
{
	string vec = "qweqwe";
	//可以进行字符串的拼接
	string sum = accumulate(vec.begin(),vec.end(),string("pppppp"));

	cout << sum << endl;
	return 0;
}

5、find_first_of

除了 find 之外,标准库还定义了其他一些更复杂的查找算法。当中的一部
分类似 string 类的 find 操作,其中一个是 find_first_of 函数。这个算法带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器。

int main()
{
	srand(time(0));
	vector<int> vec1;
	vector<int> vec2;

	for(int i = 0; i < 1000; i++)
	{
		vec1.push_back(rand() % 100);
		vec2.push_back(rand() % 100);
	}

	auto it = find_first_of(vec1.begin(),vec1.end(),vec2.begin(),vec2.end());

	if(it != vec1.end())
	{
		cout << *it << endl;
	}
}

6、fill

fill 带有一对迭代器形参,用于指定要写入的范围,而所写的值是它的第三个
形参的副本。执行时,将该范围内的每个元素都设为给定的值。如果输入范围有
效,则可安全写入。这个算法只会对输入范围内已存在的元素进行写入操作

int main()
{
	srand(time(0));
	vector<int> vec1;

	for(int i = 0; i < 10; i++)
	{
		vec1.push_back(rand() % 100);
	}

	for(auto val:vec1)
	{
		cout << val << " ";
	}
	cout << endl;

	fill(vec1.begin(),vec1.end(),0);

	for(auto val:vec1)
	{
		cout << val << " ";
	}
	cout << endl;
}

7、fill_n

fill_n 函数带有的参数包括:一个迭代器、一个计数器以及一个值。该函
数从迭代器指向的元素开始,将指定数量的元素设置为给定的值。fill_n 函数
假定对指定数量的元素做写操作是安全的。初学者常犯的错误的是:在没有元素
的空容器上调用 fill_n 函数;

这个 fill_n 函数的调用将带来灾难性的后果。我们指定要写入 10 个元
素,但这些元素却不存在——vec 是空的。其结果未定义,很可能导致严重的运行时错误。 对指定数目的元素做写入运算,或者写到目标迭代器的算法,都不检查目标的大小是否足以存储要写入的元素。

int main()
{
	srand(time(0));
	vector<int> vec1;

	for(int i = 0; i < 10; i++)
	{
		vec1.push_back(rand() % 100);
	}

	for(auto val:vec1)
	{
		cout << val << " ";
	}
	cout << endl;
	
	//fill使用
	fill_n(vec1.begin(),5,0);

	for(auto val:vec1)
	{
		cout << val << " ";
	}
	cout << endl;
}

8、sort

sort 内部采用快排算法。

int main()
{
	srand(time(0));
	vector<int> vec1;

	for(int i = 0; i < 100; i++)
	{
		vec1.push_back(rand() % 100);
	}

	sort(vec1.begin(),vec1.end());

	for(auto val:vec1)
	{
		cout << val << " ";
	}
	cout << endl;
}

9、unique

去除重复数据

중복 항목을 삭제하려면 컨테이너 작업을 사용해야 하며 이 예에서는 이 기능을 수행하기 위해 erase를 호출합니다
. 이 함수 호출은 단어의 마지막
요소도 삭제될 때까지 자신이 가리키는 요소에서 삭제합니다. 호출 후 단어는 입력의 8개 개별 요소를 저장합니다.
알고리즘은 컨테이너의 크기를 직접 수정하지 않습니다. 요소를 추가하거나 제거해야 하는 경우
컨테이너 작업을 사용해야 합니다.
중복 요소가 없는 벡터 개체에서 지우기를 호출하는 것도 안전하다는 점은 주목할 가치가 있습니다.
중복된 요소가 없으면 unique는 words.end()를 반환하는데, 이때
erase를 호출하는 두 개의 실제 매개변수는 동일한 값을 가지며 둘 다 words.end()입니다. 두 이터레이터가 같다는 것은
지우기 함수로 삭제할 범위가 비어 있다는 뜻이다.
빈 범위를 제거해도 효과가 없으므로 입력에 중복 요소가 없더라도 프로그램은 여전히 ​​정확합니다.

int main()
{
	srand(time(0));
	vector<int> vec1;

	for(int i = 0; i < 100; i++)
	{
		vec1.push_back(rand() % 100);
	}
	sort(vec1.begin(),vec1.end());

	//去除重复
	auto it = unique(vec1.begin(),vec1.end());

	vec1.erase(it,vec1.end());
}

10、count_if

조건별로 조건을 만족하는 요소의 수를 센다.

int main()
{
	srand(time(0));
	vector<int> vec1;
	for(int i = 0; i < 100; i++)
	{
		vec1.push_back(i);
	}
	sort(vec1.begin(),vec1.end());
	//vector<int>::size_type vectmp = count_if(vec1.begin(),vec1.end(),comp());
	int a = count_if(vec1.begin(),vec1.end(),comp());

	//cout << vectmp << endl;
	cout << a << endl;
	return 0;
}

11. 찾기_if

표준 라이브러리는 find_if 함수를 정의합니다. find와 마찬가지로 find_if 함수는 작동하는 범위를 지정하는 한 쌍의 반복자 매개변수를 사용합니다. count_if와 마찬가지로 이 함수도 범위의 각 요소를 확인할 조건자 함수를 나타내는 세 번째 매개 변수를 사용합니다. find_if는 조건자 함수가 0이 아닌 값을 반환하는 첫 번째 요소에 대한 반복자를 반환합니다. 그러한 요소가 없으면
두 번째 반복자 인수가 반환됩니다. find_if 함수를 사용하여 위의 예에서 길이가 6보다 큰 단어의 수를 세는 프로그램 부분을 다시 작성하십시오.

class comp
{
public:
	bool operator()(int a)
	{
		return a > 6;
	}
};

int main()
{
	srand(time(0));
	vector<int> vec1;
	for(int i = 0; i < 100; i++)
	{
		vec1.push_back(rand() % 100);
	}
	sort(vec1.begin(),vec1.end());

	auto it = find_if(vec1.begin(),vec1.end(),comp());

	cout << *it << endl;
	return 0;
}

Guess you like

Origin blog.csdn.net/qq_72714790/article/details/127398505