穷究链表(二)


我们需要的是细节,细节决定不了成功,但是能保证我们不失败。

      在实现链表的过程中,我们不可避免的会遇到一些bug。Bug是正常的,思维的缜密性当然是重要的,但是不太可能一开始就面面俱到,即使你一开始能够面面俱到,但是当开发进行了一个月两个月一年两年,你总会有一定的倦怠期,总会有某些地方考虑不周到的,我们只能尽我们的能力,去将实现考虑得周到一些,不然微软以及其他系统的各种升级版、补丁包,新的版本出来是做什么的呢?大家都会犯错误。当然,良好的测试也能够帮助我们减少bug的产生,毕竟发布出去的产品才是真正算作一个产品,而内部错误再多,只要发布之前都改正了,还是一款好的产品。

      接着是命名问题,链表我们可以称之为linkedlist, list,还有很多其他的称呼,尽管怎么称呼对于最后的程序没有关系,但是在实现过程中,一个好的名字,还是能够很直观的反映问题的。而且对于后期的维护和其他人阅读理解代码有着很大的好处。 
      
      下面,来定义一个链表。
      链表是由链表节点组成的一个线性结构,要了解链表,就必须了解链表节点

    链表节点其中肯定包含了数据,另外有一个或多个指向其他链表节点的指针,由于这里先写的是单向链表,那就是有一个指向其他链表节点的指针。这个指针可以指向下一个链表节点,如果这个链表节点本身就是最后一个节点,那它包含的指针,其值被设置为null
    除了链表节点外,还必须有一个指向这个链表的第一个节点的指针,用来标识这个链表,就如同数组的首地址一样,如果没有这个辅助的指针,我们就无法标识这个链表了。
    除了指向链表第一个节点的指针外,我们也还可以添加其他辅助的数据成员,用来帮助我们更好的操作这个链表,同时也能够在一定程度上节省时间。比如,我们可以增加一个指向链表尾巴节点的指针,那下次我需要得到最后的这个节点时,或者利用这个尾巴节点来实现某些功能时,我就不需要去遍历整个链表来得到最后的这个节点,而可以直接利用该指针来得到。
    因为我们此时实现的是没有头节点的链表,所以对于空链表来说,只有指向链表的指针,而且该指针的值为NULL。

    链表作为一种线性结构,一直被用来和另外一种线性结构来进行比较。正如兄弟、朋友、同学常常被拿来比较一样,就像命运的双生子,总要分出一个胜负来才行。其实,作为其本身,肯定是不喜欢这种比较的。不过,为了效率,咱们还是要来做一次。

      同样是线性结构,链表和数组分别有什么样的区别和优势呢?  
      我们可以从内存结构上面来看,这样比较简单易懂。


      数组是一块连续的内存空间,所以只需要知道数组的首地址,以及存储的数据类型(这两个在定义数组的时候都定义好了,所以肯定是知道的),就可以在

O(1)的时间内定位到数组的任意一个元素。
      但是由于数组的这个连续的特性,所以在在任意位置添加、删除数组元素的时候就比较麻烦,在删除了该元素之后,需要将其后面的元素依次向前移动。同样,在添加一个元素之前,需要将要添加的位置之后的元素依次向后移动,然后再添加该元素。复杂度为O(n)n为需要移动的元素的个数。

      而链表是不连续的内存空间,其所知道的就是链表节点的结构,以及指向链表第一个节点的指针。这样,在需要定位到某个特定的链表节点时,需要从第一个节点开始,依次遍历,直到找到需要定位的链表节点。需要
O(n)的复杂度。
      
但是链表的插入和删除操作比较简单,当然,首先还是要遍历到需要插入和删除的节点才行,如果没有遍历到,则先需要进行遍历,然后再进行插入和删除工作。插入和删除本身只需要内存的处理(申请或删除),和指针的变换。需要O(1)的复杂度。

      上面基本上是链表与数组的主要区别,这也是某些
hash使用数组来作为存储单元的原因(这里说某些,是因为对于hash的具体还不是十分了解,只是清楚,输入hash函数,然后得到输出,最基本的就是对于数进行取模操作)。

      另外的区别,也是由于其基本性质引起的。数组分配了多少内存空间就是多少,其数据成员的数目是固定死的,如果超过了其内存空间,那就是访问违例了。也就是编程中很容易发生的数组越界的错误。而耳熟能详的“溢出”攻击,也是基于这个原理来进行的。这也是在
VS2005中,很多C的函数使用时会报warning的原因,这些函数一般使用了数组来作为参数,比如sprintfprintf等函数。
      
      而链表由于其本身就是不连续的,所以其数据成员的数目是可变的。可以随意的添加、删除,这样在某些可变长度的数据的管理中,链表就是首选了。
      
      而数组又有一维数组和多维数组(这里所有的是针对
CC++,其他语言不考虑)。在内存分布上,有位于栈上的数组,也有在堆上分配的数组,其在访问速度上还是有一点区别的,但是区别也不是太大。在算法中,需要保存一些状态值的情况下,使用数组比较多。

数组的进化形式:
      由于数组在一开始分配的内存固定,造成了很多不方便,所以之后在如果要存储更多数据的时候,需要在其他地方重新分配空间,将现有的内存空间内的数组元素拷贝过去,然后再进行相应的添加等工作。
      而分配多少空间?何时进行拷贝?就产生了很多的策略。于是在C++中,在其标准库中,就对数组作了一个封装,也就是STL中的vector容器。将具体的这些细节隐藏了起来。
      至于这样消耗的时间和空间,在MIT6.014课程录像中,介绍得已经比较详细了。其实拷贝的消耗还是比较小的,一共平均下来还是常数级别的(具体如何记不太清楚了,还需要重新复习一遍)。


总的来说,数组和链表都是最常用,而且也很难用好的两个数据结构。

      在将这些概念都铺垫清楚后,下面我们就是
编写美的代码

写代码和写书一样,各人有各人的风格,而
CC++JAVA.NET也是各有不同,如果C++的程序员一开始写JAVA代码,感觉肯定是比较别扭。至少80列的约定就很难遵守了。而在code review的时候,基本上逛过几眼,就大概能够了解这是一个什么样的程序员编写的代码。

当然,大家在编写代码的过程中,都是希望自己的代码写的很美。而不是像一堆狗屎。不过,越是要求高,越是写了几句之后思考到很多问题,然后或者就不敢写了,或者就懒得想了,或者就改啊改,改到自己都不认得了。

那下面我们就来编写代码吧,一边写,一边整理思路,希望最后能够写出美的代码出来。

看来又食言了,不知道自己会不会越来越肥,不过现在还是很瘦呀,嘿嘿。看来这一篇要开始编码工作又不太可能了,那从下一篇开始吧,大家明天见。

posted on 2009-10-03 13:02  cnyao  阅读(514)  评论(0编辑  收藏  举报