代码改变世界

Power up C++ with the Standard Template Library:Part II: Advanced Uses [翻译]

2007-02-01 16:53  老博客哈  阅读(1867)  评论(2编辑  收藏  举报

  Discuss this articlePower up C++ with the Standard Template Library: Part II: Advanced Uses
   【原文见: http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=standardTemplateLibrary2
                                                                                                          作者    By DmitryKorolev
                                                                                                                      TopCoder Member
                                                                                                          翻译    农夫三拳@seu

在这篇教程中我们将使用教程第一部分的一些宏(macros)和类型定义(typedefs)

 Creating Vector from Map
 Copying Data Between Containers
 Merging Lists
 Calculating Algorithms
 Nontrivial Sorting
 Using Your Own Objects in Maps and Sets
 Memory Management in Vectors
 Implementing Real Algorithms with STL
     Depth-first Search (DFS)
     A word on other container types and their usage
     Queue
     Breadth-first Search (BFS)
     Priority_Queue
     Dijkstra
     Dijkstra priority_queue
     Dijkstra by set
 What Is Not Included in STL


Creating Vector from Map

正如你所知道的,map事实上包含着pair元素。所以你可以像下面这样写:

 map<stringint> M; 
 
//  
 vector< pair<stringint> > V(all(M)); // remember all(c) stands for 
 (c).begin(),(c).end() 


现在vector将含有和map一样的元素了。当然,vector将和map一样被排序。当你不想改变map中的元素
却想使用在map中无法使用的元素索引方式时,这个特性将会有用。

Copying data between containers
让我们看看copy(...)算法。原型如下:

copy(from_begin, from_end, to_begin); 

这个算法拷贝第一个区间的到第二段中。第二段区间应有有足够大的空间进行容纳。看下面的代码: 

vector<int> v1;
 vector
<int> v2; 
  
 
//  
  
 
// Now copy v2 to the end of v1
 v1.resize(v1.size() + v2.size()); 
 
// Ensure v1 have enough space 
 copy(all(v2), v1.end() - v2.size()); 
 
// Copy v2 elements right after v1 ones 

另外一个和copy在一起的特性是inserters, 由于篇幅有限我就不详述了, 看下面的代码:

vector<int> v; 
 
//  
 set<int> s; 
 
// add some elements to set
 copy(all(v), inserter(s)); 

最后一行的意思是:

 tr(v, it) 
// remember traversing macros from Part I
      s.insert(*it); 
 }
 

但是当有一个标准函数的时候为什么用我们自己的宏呢(在gcc下可用)?使用像copy一样的标准算法在STL中是一个好的习惯,因为这便于其他人读懂你的代码。

使用 push_back向vector中插入元素,而使用back_inserters或者front_inserter向deque容器中插入元素。在一些情况下,记住copy的前两个元素不仅仅可以是 begin/end, 还可以是 rbegin/rend是很有用的,其中后者是以逆序进行拷贝。

Merging lists
另外一个常见的任务是操作排好序的元素。假设你有两个已经排好序的列表元素--A和B,你想从它们中得到一个新的列表。
这里有4个常见的操作:
合并列表, R = A+B
求列表交集, R = A*B
求差集, R = A*(~B) 或者 R = A-B
求异或, R = A XOR B
STL 提供了相对应的4个算法:set_union(...), set_intersection(...), set_difference(...) 和 set_symmetric_difference(...).
他们的调用方式一样,所以让我们看看set_intersection, 一个轻便一点的原型和下面的差不多:
end_result = set_intersection(begin1, end1, begin2, end2, begin_result);
这里[begin1,end1)和[begin2,end2)都是输入列表。 'begin_result'是结果开始写的迭代器,但是结果集的大小是未知的。所以这个函数返回的是输出的end迭代器(这样就可以决定在结果集中有多少元素了).看下面具体的用法:

int data1[] = 12568910 }
int data2[] = 02347810 }
 
vector
<int> v1(data1, data1+sizeof(data1)/sizeof(data1[0]));
vector
<int> v2(data2, data2+sizeof(data2)/sizeof(data2[0])); 
 
vector
<int> tmp(max(v1.size(), v2.size()); 
 
vector
<int> res = vector<int> (tmp.begin(), set_intersection(all(v1), all(v2), tmp.begin());

看最后一行,我们构建了一个新的名为'res'的vector,它是通过区间构造函数构造的,并且区间的开始是tmp的开始。区间的结束是set_intersection算法的返回值。这个算法求得v1和v2的交集然后将结果写到输出迭代器,从'tmp.begin()'开始,他的返回值实际上是组成结果集的区间结尾。

举一个例子可能更加容易帮助你理解:如果你想获得交集的元素个数,那么可以使用 int cnt = set_intersection(all(v1),all(v2),tmp.begin()) - tmp.begin();

事实上,我从来不使用一个像 'vector<int> tmp'的构造。我认为不必为每一个set_***的算法调用进行内存分配。
相反的,我定义一个对应类型的足够大的全局或者静态的变量。如下:

set<int> s1, s2; 
for(int i = 0; i < 500; i++
        s1.insert(i
*(i+1% 1000); 
        s2.insert(i
*i*% 1000); 
}
 
 
static int temp[5000]; // greater than we need 
 
vector
<int> res = vi(temp, set_symmetric_difference(all(s1), all(s2), temp)); 
int cnt = set_symmetric_difference(all(s1), all(s2), temp) – temp;

这里'res'将包含输入集的不同元素。

注意,输入集在使用这些算法时要求已经排序。并且另外一个需要注意的是,由于set总是已经排好序的,所以我们可以使用set(甚至map,如果你没有被pair吓坏的话)作为这些算法的参数。

Calculating Algorithms
另外一个有趣的算法是accumulate(...), 如果调用一个元素是int的vector并且第3个参数是0, accumulate(...)将会返回vector中元素的总和:

vector<int> v; 
//  
int sum = accumulate(all(v), 0); 

accumulate始终返回第三个参数的类型。所以,如果你不确信和在int的范围内,显式的指定第3个参数的类型:

vector<int> v; 
//  
long long sum = accumulate(all(v), (long long)0); 

accumulate甚至可以计算值的积,第4个参数包含了在计算时使用的谓词。所以,如果你想使用积的话:

vector<int> v; 
//  
double product = accumulate(all(v), double(1), multiplies<double>()); 
// don’t forget to start with 1 !

另外一个有趣的算法是inner_product(...)。它计算两个区间的数量积。例如:

vector<int> v1; 
vector
<int> v2; 
for(int i = 0; i < 3; i++
      v1.push_back(
10-i); 
      v2.push_back(i
+1); 
}
 
int r = inner_product(all(v1), v2.begin(), 0); 

'r'将等于(v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2]), 或者 (10*1+9*2+8*3),结果是52

由于返回值的类型是由最后一个参数指定的,而最后一个参数是结果的初始值。所以,你可以使用
inner_product来计算多维空间内内超平面对象的数量积:只要写inner_product(all(normal), point.begin(), -shift).

你现在应该清楚inner_product需要迭代器的增加操作,所以queue,set都可以被当作参数。
中值计算中的卷积可以像下面这样计算:

set<int> values_ordered_data(all(data));
int n = sz(data); // int n = int(data.size());
vector<int> convolution_kernel(n);
for(int i = 0; i < n; i++{
     convolution_kernel[i] 
= (i+1)*(n-i);
}

double result = double(inner_product(all(ordered_data), convolution_kernel.begin(), 0)) / accumulate(all

(convolution_kernel), 
0);

当然,这段代码仅仅是一个例子--实际点来说,这样比从一个vector中拷贝值然后排序要快。

写成下面的构造形式也是可以的:

vector<int> v; 
//  
int r = inner_product(all(v), v.rbegin(), 0); 

这样将会求得V[0]*V[N-1] + V[1]+V[N-2] + ... + V[N-1]*V[0], 这里N是V中元素的个数

Nontrivial Sorting

实际上,sort(...)使用了和所有STL一样的技巧:

所有的比较是基于'operator <'

这就意味着你只需要重写 'operator <',样例代码如下:

struct fraction 
      
int n, d; // (n/d) 
      
//  
      bool operator < (const fraction& f) const 
           
if(false
                
return (double(n)/d) < (double(f.n)/f.d); 
                
// Try to avoid this, you're the TopCoder! 
           }
 
           
else 
                
return n*f.d < f.n*d; 
           }
 
      }
 
 }

  
 
//  
  
 vector
<fraction> v; 
  
 
//  
  
 sort(all(v));  

在普通的情况下,你的对象将包含缺省构造函数和拷贝构造函数(也许还有赋值操作符--这个不是对TopCoder的参与者而言的)
记住 'operator <'的原型: 返回值类型bool,const修饰, 参数为const引用。

另外一种可能就是去创建用于比较的函数对象。特定的比较谓词可以作为sort(...)的第三个参数。例子:
将点(pair<double, double>)按照极坐标进行排序.

typedef pair<doubledouble> dd; 

const double epsilon = 1e-6;
 
struct sort_by_polar_angle 
       dd center; 
       
// Constuctor of any type 
       
// Just find and store the center 
       template<typename T> sort_by_polar_angle(T b, T e) 
            
int count = 0;
            center 
= dd(0,0); 
            
while(b != e) 
                        center.first 
+= b->first;
                        center.second 
+= b->second;
                   b
++
                count
++;
            }
 
                   
double k = count ? (1.0/count) : 0;
            center.first 
*= k;
                   center.second 
*= k;
       }
 
 
// Compare two points, return true if the first one is earlier 
 
// than the second one looking by polar angle 
 
// Remember, that when writing comparator, you should 
 
// override not ‘operator <’ but ‘operator ()’ 
       bool operator () (const dd& a, const dd& b) const 
            
double p1 = atan2(a.second-center.second, a.first-center.first); 
            
double p2 = atan2(b.second-center.second, b.first-center.first); 
            
return p1 + epsilon < p2; 
       }
 
 }

  
 
//  
  
 vector
<dd> points; 
  
 
//  
 
       sort(all(points), sort_by_polar_angle(all(points))); 

这段代码足够复杂了,但是它足以显示出了STL的能力。我需要指出的是,在这个例子中,所有的代码将会在
编译时进行内联, 所以事实上它非常快。

Using your own objects in Maps and Sets
set和map中的元素都是有序的。这个是一个基本准则。所以,如果你想在set或者map中使用你自己的对象,那么
你应该保证他们是可以比较的。你已经知道STL中的比较准则了:
所有的比较都是基于 'operator <'

另外,你应该像这样理解它: "我要做的仅仅是为将要保存在set/map中的对象实现operator <".

假设你想使用'struct point'(或者'class point')。我们想求一些线段的交集然后得到一系列的点(听起来熟悉吗?)
由于计算机的精度,有些坐标仅仅相差一点点的点将会认为一样。我们应该这样写:

const double epsilon = 1e-7
 
 
struct point 
       
double x, y; 
  
       
//  
  
  
       
// Declare operator < taking precision into account 
       bool operator < (const point& p) const 
            
if(x < p.x - epsilon) return true
            
if(x > p.x + epsilon) return false
            
if(y < p.y - epsilon) return true
            
if(y > p.y + epsilon) return false
            
return false
       }
 
 }
;  

现在你可以使用set<point>或者map<point, string>了,例如, 查看在交集中是否存在某些点。
一个更加高级的用途:使用 map<point,vector<int> >来得到相交在这个点的线段的索引列表。

一个有趣的概念是STL中的'equal'不意味着'the same', 这里我们不深究。

Memory management in Vectors
正如前面所说,vector并不为每一次的push_back重新分配内存,事实上,当push_back被调用后,vector将会分配
多于一个元素所需要的内存。许多STL实现都是在push_back()被调用后将分配的内存扩大一倍。
这个在实际使用中并不是很好,因为你的程序将会占用2倍你所需要的内存。这里有两个简单的方法和一个复杂的
方法来解决这个问题。

第一个方法是使用vector的reserve()成员函数。这个函数命令vector分配附加的内存。vector将不会扩大内存直到
大小达到reserve()指定的大小。

考虑下面的例子。如果你有一个1000个元素的vector并且它分配的大小是1024.如果你想加入50个元素。你可以调用
push_back() 50次, 而这个操作后vector分配的内存将会达到2048, 但是如果你像这样写:
v.reserve(1050);
在使用一系列的push_back()之前, vector将会分配正好1050个元素的空间。

如果你喜欢用很多的push_back(),那么reserve()将会是你的好朋友。

顺便说一下, 对于vector在v.reserve()后加上copy(..., back_inserter(v))是一个好的模式。

另外一种情形: 在vector上进行一些操作以后你发现没有更多的元素需要加入到其中,那么你怎么样除去潜在的额外分配的
内存呢?
解决方案如下:

vector<int> v; 
//  
vector<int>(all(v)).swap(v);  

这个构造的意思是:创建一个与v一样内容的临时vector,然后将其与v进行交换。在交换之后,原始的大小过大的v将会消失。
但是,我们在SRMs的时候不需要这么做。

正确并且复杂的做法是为vector创建自己的分配器,而这个无疑不是TopCoder STL教程的一个话题。

Implementing real algorithms with STL
有了STL这个武器,让我们一起进入这个教程最有趣的部分:怎样高效的去实现真正的算法。

Depth-first search (DFS)
我将不在这解释DFS的理论了--取而代之的,可以读gladius的Introduction to Graphs and Data Structures 教程。
这里我将向你展示STL可以做什么。

首先,假设我们有一个无向图。在STL中最简单的存储一个图的方法是使用链表存储与每个顶点相连的顶点。
这个将会导致vector<vector<int> >W的结构, 这里W[i]表示与定点i相邻的顶点列表。让我们通过DFS来检查我们
的图是否连通:

/*
Reminder from Part 1:
typedef vector<int> vi;
typedef vector<vi> vvi;
*/


 
int N; // number of vertices 
 vvi W; // graph 
 vi V; // V is a visited flag 
 
 
void dfs(int i) 
       
if(!V[i]) 
            V[i] 
= true
            for_each(all(W[i]), dfs); 
       }
 
 }
 
  
 
bool check_graph_connected_dfs() 
       
int start_vertex = 0
       V 
= vi(N, false); 
       dfs(start_vertex); 
       
return (find(all(V), 0== V.end()); 
 }
 

就这么多。STL中算法'for_each'为每一个在指定范围的元素调用了指定函数'dfs', 在check_graph_connected()
函数中我们首先初始化了Visited数组(大小正确并且以0填充)。在DFS之后我们要么访问了所有的顶点,要么没有--
这个可以使用find()通过在V中查找至少有一个0来确定。

for_each的注意点:这个算法的最后一个参数可以为任何“像函数一样”的任何东西。它可以是一个全局的函数,也可以是
适配器,标准算法甚至成员函数。在最后一个例子中,你将需要使用mem_fun或者mem_fun_ref适配器,现在我们先不考虑。

这段代码的一个注意点: 我不建议使用vector<bool>。尽管在特殊情况下它是安全的,我建议你不要使用它。
使用预先定义好的vi(vector<int>),为vi分配true和false是可以的。当然,它需要 8*sizeof(int) = 8*4=32倍的内存,
但是在大多数情况下它可以很好的工作并且在TopCoder中它是相当快的。

A word on other container types and their usage
由于vector是最简单的数组容器,因此它是相当受欢迎的。在很多情况下你只需要vector的数组功能-但是,有的时候,你需要一个
更加高级的容器。

在SRM中去查阅一些STL容器的所有功能不是一个好的习惯。如果你对你将要使用的容器不熟悉,你最好使用vector或者map/set。
例如,stack是通过vector来实现的。如果你不记得stack容器的语法,使用vector可以更快的完成。

STL提供了如下的容器:list,stack, queue, deque, priority_queue.我发现在SRM中list和deque基本没用(除了,在一些基于
这些容器的特殊问题上)。但是queue和priority_queue还是值得说一两句的。

Queue
queue是一个仅有3个操作的数据类型,所有的都是在均摊复杂度O(1)下:在开头添加一个元素(to "head"), 从结尾移除一个元素
(from "tail"),获得第一个没有取得的元素("tail")。换句话说,queue是一个先进先出的缓冲区。

Breadth-first search (BFS)
同样的,如果你不熟悉BFS算法的话,请先参考TopCoder教程。queue在BFS中相当的方便,正如下面这样:

/*
Graph is considered to be stored as adjacent vertices list.
Also we considered graph undirected.
 
vvi is vector< vector<int> >
W[v] is the list of vertices adjacent to v
*/

 
 
int N; // number of vertices
 vvi W; // lists of adjacent vertices
   
   
 
bool check_graph_connected_bfs() 
      
int start_vertex = 0
      vi V(N, 
false); 
      queue
<int> Q; 
      Q.push(start_vertex); 
      V[start_vertex] 
= true
      
while(!Q.empty()) 
           
int i = Q.front(); 
           
// get the tail element from queue
           Q.pop(); 
           tr(W[i], it) 

                
if(!V[*it]) 
                     V[
*it] = true
                     Q.push(
*it); 
                }
 
           }
 
      }
 
      
return (find(all(V), 0== V.end()); 
 }
 

更加精确的说,queue支持front(),back(),push()(==push_back()),pop(==pop_front()).如果你需要push_front()和pop_back(),使用deque.deque提供了均摊O(1)复杂度下的链表操作。

这是一个使用queue和map通过BFS对一个复杂的图实现最短路搜索的有趣的应用。试想我们有个图,但是顶点由一些非常复杂的对象组成,像:
pair< pair<int,int>, pair< string, vector< pair<int, int> > > >

 (this case is quite usual: complex data structure may define the position in
 some game, Rubik’s cube situation, etc…)
考虑我们直到我们将要寻找的路径非常短,而且位置的数目也很少。如果这个图上所有的边的长度都为1,我们可以使用BFS
在这个图中找到一条路径。伪代码如下:

// Some very hard data structure 
 
typedef pair
< pair<int,int>, pair< string, vector< pair<intint> > > > POS; 
 
//  
 
int find_shortest_path_length(POS start, POS finish) 
    
     map
<POS, int> D; 
     
// shortest path length to this position 
     queue<POS> Q; 
    
     D[start] 
= 0// start from here 
     Q.push(start); 
    
     
while(!Q.empty()) 
          POS current 
= Q.front(); 
          
// Peek the front element 
          Q.pop(); // remove it from queue 
    
          
int current_length = D[current];
    
          
if(current == finish) 
               
return D[current]; 
               
// shortest path is found, return its length 
          }
 

          tr(all possible paths from 
'current', it) 
               
if(!D.count(*it)) 
               
// same as if(D.find(*it) == D.end), see Part I
                    
// This location was not visited yet 
                    D[*it] = current_length + 1
               }
 
          }
 
     }
 
  
     
// Path was not found 
     return -1
}
 

//  

如果边长的长度不一样,BFS将不再适用。我们应该使用Dijkstra。通过priority_queue来实现Dijkstra是可能的,如下:

Priority_Queue
priority_queue是二叉堆。它是一个可以进行如下3个操作的数据结构:
添加一个元素(push)
查看顶端元素(top)
弹出顶端元素(pop)

对于STL priority_queue的应用,可以看SRM307中的TrainRobber问题。

Dijkstra
在这个教程的上一个部分,我描述了怎样使用STL容器高效的对稀疏图实现Dijkstra算法。请参考这个教程来获得关于Dijkstra
算法的相关内容。

假设我们有一个加权有向图,保存为vector<vector<pair<int,int> > > G,这里

G.size()是这个图中的顶点数目
G[i].size()是索引为i的定点直接相邻的定点的数目
G[i][j].first是顶点i能够到达的索引为j的定点
G[i][j].second是从顶点i到顶点G[i][j].second的边的长度

架设我们定义下面两个代码段:

typedef pair<int,int> ii;
typedef vector
<ii> vii;
typedef vector
<vii> vvii;

Dijstra via priority_queue
感谢misof花时间给我解释为什么这个算法在不从queue移除无用条目时复杂度很好。

 vi D(N, 987654321); 
      
// distance from start vertex to each vertex

      priority_queue
<ii,vector<ii>, greater<ii> > Q; 
      
// priority_queue with reverse comparison operator, 
      
// so top() will return the least distance
      
// initialize the start vertex, suppose it’s zero
      D[0= 0;
      Q.push(ii(
0,0));

      
// iterate while queue is not empty
      while(!Q.empty()) {

            
// fetch the nearest element
            ii top = Q.top();
            Q.pop();
                        
            
// v is vertex index, d is the distance
            int v = top.second, d = top.first;

            
// this check is very important
            
// we analyze each vertex only once
            
// the other occurrences of it on queue (added earlier) 
            
// will have greater distance
            if(d <= D[v]) {
                  
// iterate through all outcoming edges from v
                  tr(G[v], it) {
                        
int v2 = it->first, cost = it->second;
                        
if(D[v2] > D[v] + cost) {
                              
// update distance if possible
                              D[v2] = D[v] + cost;
                              
// add the vertex to queue
                              Q.push(ii(D[v2], v2));

                        }

                  }

            }

      }

我不在这个教程中谈论算法本身,你需要注意的是priority_queue的对象定义。一般情况下,priority_queue<ii>就可以
工作了,但是top()成员函数会返回最大值,不是最小值。是的,我经常使用的一个简单的解决方法是在pair
第一个元素中存储-distance而不是distance。但是如果你想“正确的”实现它,我们需要调换priority_queue的
比较函数。比较函数是作为priority_queue的第三个参数的而第二个参数是容器的存储类型。所以你应该写作
priority_queue<ii,vector<ii>,greater<ii> >.

Dijkstra via set
当我问Petr怎样在C#中高效的实现Dijkstra实现时, 他告诉我了一个主意。在实现Dijkstra算法时我们使用priority_queue
来添加"正在处理的顶点"到队列中。添加和取得的时间复杂度都是O(logN)。但是除了priority_queue以外,还有另外一个
可以提供这种功能的容器--它就是'set'!我做了很多实验发现基于priority_queue和set的性能是一样的。

这里是代码:

     vi D(N, 987654321);

      
// start vertex
      set<ii> Q;
      D[
0= 0;
      Q.insert(ii(
0,0));
 
      
while(!Q.empty()) {

           
// again, fetch the closest to start element 
           
// from “queue” organized via set
           ii top = *Q.begin();
           Q.erase(Q.begin());
           
int v = top.second, d = top.first;
 
           
// here we do not need to check whether the distance 
           
// is perfect, because new vertices will always
           
// add up in proper way in this implementation

           tr(G[v], it) 
{
                
int v2 = it->first, cost = it->second;
                
if(D[v2] > D[v] + cost) {
                     
// this operation can not be done with priority_queue, 
                     
// because it does not support DECREASE_KEY
                     if(D[v2] != 987654321{
                           Q.erase(Q.find(ii(D[v2],v2)));
                     }

                     D[v2] 
= D[v] + cost;
                     Q.insert(ii(D[v2], v2));
                }

           }

      }

另外一个重要的事情是:STL的priority_queue不支持DECREASE_KEY 操作。如果你需要这种操作,
'set'将会是最好的赌注。

我花了很长时间了解为什么从queue移除元素的代码和第一个一样快。

这两个实现有着相同的复杂度并且在相同的时间内完成。此外,我设置了一些实际的实验而结果近乎一样(差别大约在~%0.1时间).

对我来说,我更喜欢用'set'来实现Dijkstra因为'set'的逻辑简单便于理解并且我们不需要记住重写'greater<int>'谓词。

What is not included in STL
如果你读教程到现在,我希望你能看到STL是一个强大的工具,尤其对于TopCoder SRMs。但是在你准备全心全意使用
STL之前,记住它里面没有的东西。

首先,STL不支持大数。如果SRM里需要计算大数运算,尤其是乘法或者除法,你有3个选择:

使用一个先前写好的模版
使用java,如果你很熟悉的话
说"很好,这个不是我的SRM!"

我建议第一个选择。

同样的问题还有几何运算库,STL中没有几何运算支持,所以你又有了3个选项可以选择了。

最后一件事情--有的时候是一件很苦恼的事情--STL没有一个内建的分割字符串的函数。尤其令人恼火的是,
这个函数被包含在了C++ ExampleBuilder插件里面作为缺省模版了!但是事实上,我发现在普通情况下使用istringstream(s)和在复杂情况下使用sscanf(s.c_str(),...)已经足够了。

闪人之前,我希望你觉得这篇教程对你有用并且我希望你发现STL是你使用C++的一个有用的附加工具。祝你在角斗场好运!

作者小结: 在这篇教程的两个部分我建议使用一些模版来简化实现一些东西的时间。我必须说这个建议对程序员很好。摒除模版对于SRM是一个好的或是坏的战术不谈,单就在每天的生活中你就会被那些试图想要读懂你代码的人烦扰。我的确依赖过模版一段时间,但最终我决定不那样做了。我鼓励你权衡模版的正面和负面性并且做出你自己的选择。