STL 总结

 发现stl还是很好用的。。。。学过以后,po一发博客便于以后复习和加深记忆和理解

首先对迭代器加以说明

C++ primer (中文版第四版)第273页

9.3.2 begin和end成员

        begin和end操作产生指向容器内第一个元素和最后一个元素的下一个位置的迭代器,如下所示。这两个迭代器通常用于标记包含容器中所有元素的迭代范围。

      c.begin() 返回一个迭代器,它指向容器c的第一个元素

      c.end() 返回一个迭代器,它指向容器c的最后一个元素的下一个位置

      c.rbegin() 返回一个反向迭代器,它指向容器c的最后一个元素

      c.rend() 返回一个反向迭代器,它指向容器c的第一个元素前面的位置

        上述每个操作都有两个不同的版本:一个是const成员,另一个是非const成员。这些操作返回什么类型取决于容器是否为const。如果容器不是const,则这些操作返回iterator或reverse_iterator类型。如果容器是const,则其返回类型要加上const_前缀,也就是const_iterator和const_reverse_iterator类型。

        

其次,说明一下,定义在algorithm中的固有stl函数,find 和 count;

<1>find 函数,返回值类型为找到元素的迭代器,三个参数分别为,起始位置,结束位置的下一个位置,待查找元素

  例如:  iterator::it=vec.find(vec.begin(),vec.end(),10);

  if(it!=vec.end()) //找到

<2>count函数,返回值类型为int型;三个参数为,起始位置,结束位置的下一个位置,待count元素(如果是map的话,是key值)

  int num=m.count(m.begin(),m.end(),10);

 

1.不定数组vector

#include<vector>

总之一句话,vector就相当于一个数组,只是多了很多自身常用的函数

1.定义 vector<int>vec;

2.插入元素 

  vec.push_back(1);  //在vec尾部插入元素1;

  

  insert() 插入元素到Vector中

      iterator insert( iterator loc, const TYPE &val );                      //在指定位置loc前插入值为val的元素,返回指向这个元素的迭代器
      void insert( iterator loc, size_type num, const TYPE &val );  //在指定位置loc前插入num个值为val的元素 
      void insert( iterator loc, input_iterator start, input_iterator end ); //在指定位置loc前插入区间[start, end)的所有元素

3.删除元素

  vec.clear(); //清空vector;

  vec.pop_back(); //清空最后一个元素  

 

  erase() 删除指定元素 (可以用指针来代替迭代器)

      iterator erase( iterator loc );                            //要删除元素的迭代器
      iterator erase( iterator start, iterator end );    //删除指定区间内的所有元素【start,end),前闭后开

 4.其他固有基本函数

  vec.size(); //返回vector的大小

  vec.begin();//返回vector的第一个元素的迭代器

  vec.end(); //返回vector的最后一个元素的迭代器

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cctype>
 5 #include<map>
 6 #include<algorithm>
 7 #include<vector>
 8 #define maxn 55
 9 using namespace std;
10 
11 
12 
13 int cmp(int i,int j)
14 {
15     return i>j;
16 }
17 int main()
18 {
19     //vector
20     vector<int>vec;
21     for(int i=0; i<10; i++) vec.push_back(i);
22 
23     cout<<"size->"<<vec.size()<<endl;
24 
25     //sort(vec.begin(),vec.end(),cmp);
26     vec.erase(vec.begin()+2);
27     for(vector<int>::iterator it=vec.begin(); it!=vec.end(); it++)
28         cout<<*it<<endl;
29 
30     vec.erase(vec.begin(),vec.end());
31     cout<<"binary_search->"<<binary_search(vec.begin(),vec.end(),5);//二分查找
32     vector<int>::iterator itr=lower_bound(vec.begin(),vec.end(),5);
33     cout<<"lower_bound->"<<(itr-vec.begin())<<endl;//二分查找
34 
35 
36 
37 
38 }

2.映射map

map就是从key到value的映射。因为重载了[]运算符,map像是数组的高级版,事实上,map也被称为关联数组。

map是STL的一个容器,和set一样,map也是一种关联式容器。它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,有助于我们处理一对一数据。这里说下map内部数据的组织,map内部是自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的。学习map我们一定要理解什么是一对一的数据映射?比如:一个班级中,每个学生的学号跟他的姓名就存在着一一映射的关系,这个模型用map可能轻易描述,很明显学号用int 描述,姓名用字符串描述采用的string,于是我们使用的map形式如下:map<int , string> student;

关于map和set底层实现以及效率问题,在map和set底层实现都是采用了平衡树来实现的。这里说一下map和set容器的区别。

对于map中的每个节点存储的是一对信息,包括一个键和一个值,各个节点之间的键值不能重复。

对于set中的每个节点存储的是一个信息,只有一个键,但是每个键值也是唯一的。set表示的是集合的概念。

  首先,说明,map中每个节点存储的是两个值,一个key,一个value,所以在遍历的时候,必须要分开,具体见下面。

特别注意:map的insert操作,返回值为一个pair类型 pair<map<T,T>::iterator,bool>,返回一个pair,其first是指向插入元素的iterator,second是bool型反映是否插入成功,

  如果key在map中间已经存在,那么insert操作是不会进行的;而使用数组[]运算符进行插入,例如   m[1]="abc",操作,不管在map中是否存在key值1,首先对m[1]进行初始化,然后再进行赋值操作。但是对于multimap来说,设计者们重载了insert函数,multimap的返回值变成了iterator,因为在multimap中可以是一个key对应多个value的情况,同时去掉了对于[]运算符的使用。

1.map的定义,map<string,int>m;

2.常用函数

  m.begin();

  m.end;

  m.rbegin();

  m.rend();

  m.clear();

  m.empty();

  m.size();

  m.max_size();//反映最大容量

  m.insert();//用法见下面遍历部分

3.map的遍历

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<cctype>
 5 #include<map>
 6 #include<algorithm>
 7 #include<vector>
 8 #define maxn 55
 9 using namespace std;
10 
11 int main()
12 {
13     map<int,int>m;
14     for(int i=0;i<10;i++)
15         m[i]++;
16 
17     m.insert(pair<int,int>(10,1));//insert操作
18     m.insert(pair<int,int>(11,1));
19 
20 
21     map<int,int>::iterator it;
22     for(it=m.begin();it!=m.end();it++)
23         cout<<it->first<<"->"<<it->second<<endl;
24 }

4.map中元素的查找

  map中可以利用find进行查找

5.map中数据的插入和删除

  

  先说一说map中数据的插入,数据的插入大概有三种方式,第一种:insert(pair<T1,T2,>(key1,value1))。第二种:insert(map<T1,T2>::value_type(key1,value1)),这种插入方式和第一种基本相似。第三种:利用数组进行插入m[1]=2;。

  关于数据的删除,大概有三种方式进行删除:第一种:erase(map<T1,T2>::iterator iter),删除迭代器所指的节点。第二种:erase(key k),根据键值进行删除,删除键值k所指的节点 。第三种:erase(map<T1,T2>::iterator iter1,map<T1,T2>::iterator iter2),删除iter1和iter2之间的数据。

3.栈stack

  

同样在stack中提供了find、count等函数的接口,用法相同。

4.队列queue

 

与stack类似,其他的不再赘述

5.优先级队列(hdu1242/1053/2263/2066)->启发式搜索的基础数据结构

优先级队列priority_queue 是一种抽象数据类型(Abstract Data Type),行为有些像队列,但不是FIFO队列,而是队列中优先级高的元素先出列,STL的优先级队列也包含在#include<queue>里面,只是声明的时候使用priority_queue来声明,同时,出队方法变成了top().同样,自定义数据结构也可以组成优先级队列,但必须指定一个优先级。可以定义一个结构体cmp,重载()运算符,使其看上去就像一个函数(仿函数),然后用priority_queue<int,vector<int>,cmp>q;方式来定义,例如:

  

1 struct cmp
2 {
3     bool operator () (const int a,const int b) const
4     {
5          return a%10>b%10;
6     }
7 };//个位数越小,优先级越高,相当于是排序后,从最右端开始取

 

对于一些常见的优先队列,stl提供了简单的定义方法,

例如:

1 //注意一下最右边的>不要连起来写
2 //在定义中,有三个参数,其中第二个表示容器,第三个为比较参数
3 priority_queue<int,vector<int>,greater<int> >q;//越小的整数优先级越大
4 priority_queue<int,vector<int>,less<int> >q;//越大的整数优先级越大

下面是几种自定义优先级的方式,对固定数据类型,使用结构体重载()即可;

对于自定义结构体、数据结构,需要重载小于号<

 

 

 1 #include<bits/stdc++.h>
 2 #include<queue>
 3 using namespace std;
 4 struct cmp1
 5 {
 6     bool operator ()(int i,int j) {return i<j;}
 7 };
 8 struct cmp2
 9 {
10     bool operator()(int i,int j){return i>j;}
11 };
12 struct node
13 {
14     int x,y;
15     bool operator < ( node b) const
16     {
17         return x < b.x;
18     }
19 };
20 int main()
21 {
22     priority_queue<int>q;//默认
23     for(int i=0;i<10;i++) q.push(i);
24     for(int i=0;i<10;i++) {cout<<q.top()<<"->";q.pop();}
25     cout<<endl;
26     priority_queue<int,vector<int>,cmp1>p;//cmp1
27     for(int i=0;i<10;i++) p.push(i);
28     for(int i=0;i<10;i++) {cout<<p.top()<<"->";p.pop();}
29     cout<<endl;
30     priority_queue<int,vector<int>,cmp2>p2;//cmp2
31     for(int i=0;i<10;i++) p2.push(i);
32     for(int i=0;i<10;i++) {cout<<p2.top()<<"->";p2.pop();}
33     cout<<endl;
34     priority_queue<node>p3;//自定义数据结构
35     node a,b,c,d,e;
36     a.x=1;a.y=9;
37     b.x=2;b.y=7;
38     c.x=3;c.y=10;
39     d.x=4;d.y=12;
40     e.x=5;e.y=3;
41     p3.push(a);p3.push(b);p3.push(c);p3.push(d);p3.push(e);
42     for(int i=0;i<5;i++)
43     {
44         cout<<p3.top().x<<" "<<p3.top().y<<"->";
45         p3.pop();
46     }
47 
48 
49 }

 

6.多重映射multimap

多重映射关系,multimap是包含在#include<map>中的,与map唯一的不同就是,multimap支持一个键值key对应于多个值value,同时,multimap的设计者们去掉了对于[]运算符的重载,multimap不允许使用[]进行操作,另外insert()操作的参数必须是一个pair类型,返回值是指向插入元素的迭代器,其他的公共接口与map是相同的。

插入的时候可以使用以下方式

 

1 multimap<int,int>m;
2 m.insert(make_pair(1,2));//insert插入一个pair类型
3 m.insert(pair<int,int>(1,2));//insert插入一个pair类型的另一种方式,定义一个pair类型并进行初始化操作

 

同样,对于multimap容器的便历操作也是使用迭代器进行

1 multimap<int,int>::iterator it=m.begin();
2 for(it;it!=m.end();it++)
3 {
4     cout<<it->first<<" "<<it->second<<endl;
5 }

对于查找操作find以及count,均遇到第一个key值就返回,当做map处理,其中count会进行计数。

 8.pair键值对

 

Pair类型概述

 

pair是一种模板类型,其中包含两个数据值,两个数据的类型可以不同,基本的定义如下:

 

 

 

pair<int, string> a;

 

表示a中有两个类型,第一个元素是int型的,第二个元素是string类型的,如果创建pair的时候没有对其进行初始化,则调用默认构造函数对其初始化。

 

pair<string, string> a("James", "Joy");

 

也可以像上面一样在定义的时候直接对其初始化。

 

Pair对象的操作

 

 

 

  • 对于pair类,由于它只有两个元素,分别名为first和second,因此直接使用普通的点操作符即可访问其成员

 

pair<string, string> a("Lily", "Poly"); 

 

string name;

 

name = pair.second;

 

  • 生成新的pair对象

 

可以使用make_pair对已存在的两个数据构造一个新的pair类型:

 

int a = 8;

 

string m = "James";

 

pair<int, string> newone;

 

newone = make_pair(a, m);

 

也可以新建一种pair<int,int>a[maxn];或者vector<pair<int> >;

9.heap堆

  stl中的堆默认是最大堆,要想用最小堆的话,必须要在push_heap,pop_heap,make_heap等每一个函数后面加第三个参数greater<int>(),括号不能省略。

heap并不作为stl的容器组件,stl在algorithm中定义了几个常见的函数来对堆heap进行操作,

  1.  make_heap();

     vector<int>vec;

     make_heap(vec.begin(),vec.end());//默认在这个序列中构造小顶堆,

     make_heap(vec.begin(),vec.end(),greater<int>()> //给定序列中构造大顶堆

  2.  push_heap();

     push_heap(first_iterator,end_iterator);//此函数假设已经是有效堆,然后在尾部push

     例如:vec.push_back(1);

        push_heap(vec.begin(),vec.end());

  3.pop_heap();

     pop_heap(first_iterator,end_iterator);

  4.sort_heap();

     sort(first_iterator,end_iterator);

//(转载)

  

2.1 push_heap算法:上溯

 

 

percolate_up(上溯)程序:将新节点拿来与其父节点比较,如果其键值(key)比父节点大,就父子对换位置。如此一直上溯,直到不需对换或直到根节点为止。

 

push_heap算法的实现细节参见相关源码。

 

注意:

 

(1)为了满足complete binary tree的条件,新加入的元素一定要放在最下一层作为叶节点,便填补在由左至右的第一个空格,也就是把新元素插入在底层vector的end()处。

 

(2)当push_heap函数被调用时,新元素应已置于底部容器的最尾端。

 

另: array无法动态改变大小,因此如果heap底层采用array,便不可以对满载的array进行push_heap操作,因为那得先在array尾端增加一个元素。如果对一个满载的array执行push_heap,该函数会将最后一个元素视为新增元素,并将其余元素视为一个完整的heap结构(实际上它们的确是),因此执行后的结果等于原先的heap。

 

2.2 pop_heap算法:下溯+上溯

 

下图是 pop_heap算法的实际操演情况。既然身为max-heap,最大值必然在根节点。pop操作取走根节点(其实是设至底部容器vector的尾端节点)后,为了满足complete binary tree的条件,必须割舍最下层最右边的叶节点,并将其值重新安插至max-heap(因此有必要重新调整heap结构)。

 

 

为了满足max-heap次序特性(每个节点的键值都大于或等于其子节点键值),我们执行所谓的percolate down(下溯)程序:将空间节点和其较大子节点“对调”,并持续下放,直至叶节点为止。然后将前述被割舍之元素值设给这个“已到达叶层的空间节点”,再对它执行一次percolate up(上溯)程序。

 

pop_heap算法的实现细节参见相关源码。

 

注意:

 

pop_heap之后,最大元素只是被置于底部容器的最尾端,尚未被取走。

 

2.3 sort_heap算法

 

既然每次pop_heap可获得heap中键值最大的元素,如果持续对整个heap做pop_heap操作,每次将操作范围从后向前缩减一个元素(因为pop_heap会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕时,我们便有了一个递增序列。

 

 

 

2.4 make_heap算法

 

这个算法用来将一段现有的数据转化为一个heap。

 

make_heap算法实现参见相关源码

 

3. heap没有迭代器 heap的所有元素都必须遵循特别的(complete binary tree)排列规则,所以heap不提供遍历功能,也不提供迭代器。

10.set关联式容器

转载自http://blog.csdn.net/chaoyueziji123/article/details/38422211

 

1.关于set

 

关于set,必须说明的是set关联式容器。set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。

 

关于set有下面几个问题:

 

(1)为何map和set的插入删除效率比用其他序列容器高?

 

大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:

 

 

 

A

 

  / \

 

B C

 

/ \ / \

 

  D E F G

 

 

 

因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。

 

(2)为何每次insert之后,以前保存的iterator不会失效?

 

iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。

 

(3)当数据元素增多时,set的插入和搜索速度变化如何?

 

如果你知道log2的关系你应该就彻底了解这个答案。在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了 。

 

set使用方法:

 

begin()     ,返回set容器的第一个元素

 

end() ,返回set容器的最后一个元素

 

clear()        ,删除set容器中的所有的元素

 

empty() ,判断set容器是否为空

 

max_size() ,返回set容器可能包含的元素最大个数

 

size() ,返回当前set容器中的元素个数

 

rbegin ,返回的值和end()相同

 

rend() ,返回的值和rbegin()相同

 

简单操作实例:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	set<int> s;
	s.insert(1);
	s.insert(2);
	s.insert(3);
	s.insert(1);
	cout<<"set 的 size 值为 :"<<s.size()<<endl;
	cout<<"set 的 maxsize的值为 :"<<s.max_size()<<endl;
	cout<<"set 中的第一个元素是 :"<<*s.begin()<<endl;
	cout<<"set 中的最后一个元素是:"<<*s.end()<<endl;
	s.clear();
	if(s.empty())
	{
		cout<<"set 为空 !!!"<<endl;
	}
	cout<<"set 的 size 值为 :"<<s.size()<<endl;
	cout<<"set 的 maxsize的值为 :"<<s.max_size()<<endl;
	return 0;
}

 

运行结果:

 

 

小结: 插入3之后虽然插入了一个1,但是我们发现set中最后一个值仍然是3哈,这就是set 。还要注意begin() 和 end()函数是不检查set是否为空的,使用前最好使用empty()检验一下set是否为空.

 


 

count()  用来查找set中某个某个键值出现的次数。这个函数在set并不是很实用,因为一个键值在set只可能出现0或1次,这样就变成了判断某一键值是否在set出现过了。

 

示例代码:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	set<int> s;
	s.insert(1);
	s.insert(2);
	s.insert(3);
	s.insert(1);
	cout<<"set 中 1 出现的次数是 :"<<s.count(1)<<endl;
	cout<<"set 中 4 出现的次数是 :"<<s.count(4)<<endl;
	return 0;
}

 

按 Ctrl+C 复制代码

 

运行结果:

 

 

equal_range()  ,返回一对定位器,分别表示第一个大于或等于给定关键值的元素和 第一个大于给定关键值的元素,这个返回值是一个pair类型,如果这一对定位器中哪个返回失败,就会等于end()的值。具体这个有什么用途我还没遇到过~~~

 

示例代码:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	set<int> s;
	set<int>::iterator iter;
	for(int i = 1 ; i <= 5; ++i)
	{
		s.insert(i);
	}
	for(iter = s.begin() ; iter != s.end() ; ++iter)
	{
		cout<<*iter<<" ";
	}
	cout<<endl;
	pair<set<int>::const_iterator,set<int>::const_iterator> pr;
	pr = s.equal_range(3);
	cout<<"第一个大于等于 3 的数是 :"<<*pr.first<<endl;
	cout<<"第一个大于 3的数是 : "<<*pr.second<<endl;
	return 0;
}

 

运行结果:

 

 

erase(iterator)  ,删除定位器iterator指向的值

 

erase(first,second),删除定位器first和second之间的值

 

erase(key_value),删除键值key_value的值

 

看看程序吧:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	set<int> s;
	set<int>::const_iterator iter;
	set<int>::iterator first;
	set<int>::iterator second;
	for(int i = 1 ; i <= 10 ; ++i)
	{
		s.insert(i);
	}
	//第一种删除
	s.erase(s.begin());
	//第二种删除
	first = s.begin();
	second = s.begin();
	second++;
	second++;
	s.erase(first,second);
	//第三种删除
	s.erase(8);
	cout<<"删除后 set 中元素是 :";
	for(iter = s.begin() ; iter != s.end() ; ++iter)
	{
		cout<<*iter<<" ";
	}
	cout<<endl;
	return 0;
}

 

运行结果:

 

 

小结: set中的删除操作是不进行任何的错误检查的,比如定位器的是否合法等等,所以用的时候自己一定要注意。

 

find()   ,返回给定值值得定位器,如果没找到则返回end()。

 

示例代码:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	int a[] = {1,2,3};
	set<int> s(a,a+3);
	set<int>::iterator iter;
	if((iter = s.find(2)) != s.end())
	{
		cout<<*iter<<endl;
	}
	return 0;
}

 

insert(key_value);  将key_value插入到set中 ,返回值是pair<set<int>::iterator,bool>,bool标志着插入是否成功,而iterator代表插入的位置,若key_value已经在set中,则iterator表示的key_value在set中的位置。

 

inset(first,second); 将定位器first到second之间的元素插入到set中,返回值是void.

 

示例代码:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	int a[] = {1,2,3};
	set<int> s;
	set<int>::iterator iter;
	s.insert(a,a+3);
	for(iter = s.begin() ; iter != s.end() ; ++iter)
	{
		cout<<*iter<<" ";
	}
	cout<<endl;
	pair<set<int>::iterator,bool> pr;
	pr = s.insert(5);
	if(pr.second)
	{
		cout<<*pr.first<<endl;
	}
	return 0;
}

 

运行结果:

 

 

lower_bound(key_value)  ,返回第一个大于等于key_value的定位器

 

upper_bound(key_value), 返回最后一个大于等于key_value的定位器

 

示例代码:

 

#include <iostream>
#include <set>

using namespace std;

int main()
{
	set<int> s;
	s.insert(1);
	s.insert(3);
	s.insert(4);
	cout<<*s.lower_bound(2)<<endl;
	cout<<*s.lower_bound(3)<<endl;
	cout<<*s.upper_bound(3)<<endl;
	return 0;
}

 

运行结果:

 

     

 11.multiset多重集合

multiset与set的区别就是,multiset是允许重复的集合,multiset的遍历方式也是中序遍历;

不同的是,count()函数返回键值的个数,

find()函数搜索到值以后,就立即返回指向该值的迭代器;

同时erase()函数删除元素时,有两种:

1.erase(key_value)会删除掉所有的key_value值;

2.可以通过iterator=s.find(key_value);  s.erase(iterator);

 

 

 

posted @ 2016-12-08 01:07  TomJarry  阅读(683)  评论(1编辑  收藏  举报