抽象概念一定要被具象化才能被理解吗?

转载自 https://www.yueyao1982.com/phil_faq/faq_abstraction.html 若侵犯了内容创作者的权益,请联系删除 正文: 恰恰相反,具体的问题一定要抽象化之后才能理解,虽然这乍听起来比较奇怪。我们不妨想一想中学物理中最简单的问题,比如斜面上物体的运动。我们是先把物体和斜面都作了抽象化——物体和斜面都有很多细节,即“象”,而这些“象”被我们忽略了,或者说是抽离了,亦即“抽象”。所以,我们通过牛顿力学来理解机械运动,只是在理解抽象物体的相互关系,比如它们之间的作用,即力,比如它们之间的相对运动:简而言之,我们是在研究一个抽象模型,并试图理解这个模型。我们能做到最好的,就是我们的抽象化很合理,亦即被抽掉的“象”都是次要的;相应的,我们能得到最好的结果就是:模型的预测结果和实际的观测结果之间误差很小,一般来说不能没有误差,除非被观测值的可能性是离散甚至是有限的——比如电子的自旋只有两种可能。即使我们真的把主要因素抽掉,我们仍然可以理解那个被抽象出来的模型,只是那个模型和经验世界已经不能很好的对应了。 稍微深入一些来说,并非所有的抽象概念都能具象化,或者说概念的世界至少从规则和可能性这些方面来讲,并不受制于经验的世界,虽然没有经验概念就无从产生。比如我们观察过很多“经验中的马”,忽略了彼此不同的“象”,比如颜色、大小、公母等因素,就产生了“马的概念”;同样,我们观察过很多“经验世界中的鸟”,忽略了彼此不同的“象”,就产生了“鸟的概念”。“马的概念”和“鸟的概念”都可以被具象化到自然界中真实的动物。然而,我们的思维还可以对概念进行加工,比如我们对“鸟”概念进行切分,得到了“翅膀的概念”(当然它还是可以被具象化的)。然后我们把“翅膀的概念”和“马的概念”进行组合,得到了“天马”的概念——这个概念虽然可以具象化为具体的玩具或模型,但不再能具象化为自然界中的动物了。与此相似,我们还可以进行更复杂的概念建构,比如用更多的翅膀构建出“六翼天使”,比如用多种动物的不同部分构建出了“龙”和“麒麟”等神兽。当然,到此为止,这些概念至少还是可以被在一定程度上具象化,比如画成图画,比如做成雕刻。但是,更进一步的概念建构使这点也不可能了。如果我们把自然数的计数泛化到空间的维度上,我们就构建出了N维空间——1维空间、2维空间和3维空间是可以被具象化的,4维...

STL的“增删查改”

 增加元素

    为了增加元素,两种容器都提供了insert方法,该方法支持在特定的位置加入新的元素。在STL中位置一般用迭代器指定:
    
       
       这里添加代码
int ary1[5] = {1, 2, 3, 4, 5}; // 整型数组
vector<int> vec(ary1, ary1+5); // 用数组初始化向量
vector<int>::iterator iter = vec.begin() + 2; 
 // 定义迭代器,指向容器 vec 的第三个元素
vec.insert( iter, 6 ); // 在 vec 的第三个元素处插入一个新元素 6 
set<int> intSet( vec.begin(), vec.end() ); // 构造一个整型数集合
set<int>::iterator iter = intSet.begin() + 2; // 集合的迭代器,指向第三个元素
intSet.insert( iter, 7 ); // 在集合的第三个元素处插入一个新元素 7

    由于在序列式容器中,相邻两个元素的存储单元存在先后关系,插入元素时要维护这段关系。所以,对于连续存储的容器,要移动插入位置后面的元素;对于链式存储的容器,要建立新的连接关系。连续存储的容器,如果其容量不能满足数据插入的需求,则应当重新分配足够大的内存复制原来的数据,并插入新值。。在上述例子中,向量中插入数据的效果如图所示。

    


    ?关联式容器插入数据的效果

        关联式容器插入数据的效果和序列式容器插入数据的效果是不同的。由于关联式容器是自动排序的,插入的元素会按照其键值的大小放置到合适的地方,所以关联式的容器并不会再期望的地方插入元素,其传入的迭代器只能作为参考。
    

    ?其他增加元素的方法

        除了insert方法,序列式容器还提供了压入(push)方法,根据容器的不同,分别使用push_back, push_front, push方法。压入元素没有insert操作那么灵活,而只能在容器头或者容器尾操作,例如:
    

       
       这里添加代码
vector<int> vec(); // 声明一个向量
vec.push_back( 1 ); // 压入数据
vec.push_back( 2 ); 
list<int> ls(); // 声明一个链表
ls.push_back( 1 ); // 压入数据
ls.push_front( 2 ); 
stack<int> st(); // 声明一个栈
st.push( 1 ); // 压入数据
st.push( 2 );

流程图:

    


删除元素

    为了删除元素,两种容器都提供了erase方法,改方法支持删除指定位置、指定区间的元素。另外,也可以用clear方法,删除容器中的所有元素。对于关联式容器,还可以删除指定键值的元素。当然,删除数据之后,应当对容器的存储结构进行重新组织,以保证结构的完整性。列如:
    

       
       这里添加代码
int ary1[6] = {1, 2, 3, 4, 5, 6}; // 整型数组
vector<int> vec(ary1, ary1+6); // 用数组初始化向量
vector<int>::iterator iter = vec.begin() + 2; 
 // 定义迭代器,指向容器 vec 的第三个元素
vec.erase( iter ); // 删除第三个元素
ter = vec.begin() + 2; // 定义迭代器,指向容器 vec 的第三个元素
vec.erase( iter, vec.end() ); // 删除第二个元素之后的所有元素
set<int> intSet( ary1, ary1+5 ); // 构造一个集合
set.erase( 4 ); // 删除键值为 4 的元素(集合的键值与实值是一致的)

流程图:

查找元素

    并不是所有容器都提供了专门的查找方法。序列式容器由于其存储结构的限制(未排序),查找效率较低,所以没有提供查找方法。而关联式容器在创建时就排好了序,插入元素时也会自动排序,所以非常适合查找。因此,关联式容器提供了专门的查找方法find,其参数是元素的键值,返回值是指向目标元素的迭代器。
    
    对于序列式容器,可以使用某些特殊的方法来查找特定的元素。例如有的元素重载了下标运算符“[]”,给出索引就可以找到对应的元素,这一点类似数组。也可以通过成员函数at实现相同的功能。另外,序列式容器普遍都有front和back方法,用来返回第一个和最后一个元素,栈是个例外,只有函数top用来返回栈顶元素。例如:

       
       这里添加代码
int ary1[6] = {1, 2, 3, 4, 5, 6}; // 整型数组
vector<int> vec(ary1, ary1+6); // 用数组初始化向量
int x = vec[ 3 ]; // 查找向量中的第三个元素
list<int> intList( vec.begin(), vec.end() ); // 构造链表
x = intList.front(); // 查找链表中的第一个元素
x = intList.back(); // 查找链表中的最后一个元素
stack<int,vector<int> > st; // 构造栈
……
= st.top(); // 查找栈顶元素

修改元素

    若要修改元素,则必须先查早到元素。如果查找到的结果是容器中元素的引用,则可以直接对该引用赋值。如果查找到的结果是指向容器中元素的迭代器,则可以通过迭代器的方法修改元素的值。例如:
    
       
       这里添加代码
int ary1[6] = {1, 2, 3, 4, 5, 6}; // 整型数组
vector<int> vec(ary1, ary1+6); // 用数组初始化向量
vec[3] = 7; // []返回的是向量元素的引用
int &x = vec[3]; 
x = 7; // 效果与第 3 行相同
stack<int,vector<int> > st; // 构造栈
……
st.top() = x; // 修改栈顶元素
int &y = st.top(); // 查找栈顶元素
y = x; // 效果等同于第 8 行
map<int, int> intMap(); // 构造映射
……
map<int, int>::iterator iter = intMap.find( 5 ); // 查找键值为 5 的元素
iter->second = 3; // 将该元素的实值修改为 3 
map[5] = 3; // 效果与第 12 行和第 13 行相同

此博客中的热门博文

算法的八大常用思想