前文链接:
【重构】
首先,什么是链表?无论《C程序设计》还是《C程序设计伴侣》都没有给出清晰准确的定义。
不知道什么叫链表,当然不可能正确地写出关于链表的代码,《C程序设计伴侣》中出现为链表安装了一条不伦不类的“义尾”(tail)的怪现象也就不足为怪了。 在这里我给出我对链表的定义:链表就是一个指针,这个指针,要么值为NULL,要么指向其中含有另一个链表的数据。 当然,链表有很多种,这里的定义只是最简单的一种——单向链表。 这个定义明显模仿了n!的定义方法,是一种递归式的定义。这种递归式定义的东西用递归的方法很容易实现。 其次,链表结点的描述。typedef struct { char name[20]; float score; }data_t;typedef struct node { data_t item ; struct node *next ; }node_t;
这样就建立了一个抽象的结点类型。这样处理的好处是成功地把数据从结点中在形式上剥离开了,这对代码的结构及可维护性都非常很重要。因为我们知道,在一个糟糕的数据结构上是绝对无法建立良好的代码结构的,而代码的结构糟糕则一定会导致可维护性的下降。
选择了链表就是选择了链表的优点而不是选择了链表的劣势,这就和你买辆汽车是用来开的而不是用来推的道理一样。和同样属于线性结构的数组相比,链表的优势是很容易在前面加入结点结点,但在尾部加入结点则要麻烦得多。数组则相反,即使在有足够的预留空间的情况下,不经过一番折腾也很难在数组的开始加入一个数据,但是很容易在数组的尾部加入一个数据(只要事先预留了位置)。 既然链表很容易在头部加入结点,通常情况下建立链表也应该选择这种方式——不断地向其中添加数据就行了。代码如下:1. #include2. #include 3. 4. //----------通用函数---------------------//5. void *my_malloc( size_t );6. //--------------------------------------//7. 8. 9. //----------“数据”类型----------------//10. typedef 11. struct12. {13. char name[20];14. float score;15. }16. data_t;17. //-----------关于“数据”的函数-----------//18. int input ( data_t * ) ; 19. void output ( data_t * ) ; 20. //---------------------------------------//21. 22. 23. //---------“结点”类型------------------//24. typedef 25. struct node26. {27. data_t item ;28. struct node *next ;29. }30. node_t;31. //--------“链表”操作函数---------------//32. void create( node_t ** );33. void insert( node_t ** , node_t * );34. void print ( node_t * );35. //--------------------------------------//36. 37. 38. int main( void )39. {40. node_t *head = NULL ; //空链表41. 42. puts("建立链表");43. create( &head ); 44. //测试 45. puts("输出链表");46. print( head );47. 48. return 0;49. }50. 51. //-------通用函数定义-------------------//52. void *my_malloc( size_t size )53. {54. void *p = malloc( size );55. if( p == NULL )56. {57. puts("内存不足");58. exit(1); 59. } 60. return p;61. }62. //--------------------------------------//63. 64. //---------“数据”操作函数定义---------//65. int input ( data_t *p_data )66. {67. puts("请输入姓名、成绩:");68. if( scanf( "%20s%f" , p_data->name , &p_data->score ) != 2 ) //20很重要 69. {70. while( (getchar()) != '\n' ) //清空输入缓存 71. ;72. return 0 ; 73. }74. return !0 ;75. }76. 77. void output ( data_t *p_data )78. {79. printf ("姓名:%s 成绩:%.2f\n" , p_data->name , p_data->score ) ;80. }81. //--------------------------------------//82. 83. 84. //----------“链表”操作函数定义--------//85. 86. void print( node_t *p_node )87. {88. if( p_node != NULL )89. {90. output ( &p_node->item ); 91. print( p_node->next );92. }93. }94. 95. void insert( node_t ** p_next , node_t * p_node)96. {97. p_node -> next = * p_next ;98. * p_next = p_node ;99. }100. 101. void create( node_t **p_next )102. {103. data_t data;104. 105. if( input ( & data ) != 0 )106. {107. node_t *p_node = my_malloc( sizeof (*p_node) );108. p_node->item = data ;109. insert( p_next , p_node );//插入结点110. create( p_next );//继续 111. }112. }113. //--------------------------------------//
这种写法,由于把“数据”视为结点的一个抽象成员,成功地把“数据”部分的操作与关于链表的操作像油和水一样地分离开来。在开发过程中可以把它们作为两个相对独立的模块开发(即所谓“高内聚低耦合”)。而且即使以后对“数据”部分有所修改,只要接口没有发生改变,链表部分完全不受任何影响。
或许有人仍然希望新加入的结点都接到链表的尾部,由于这里的create()函数是一次性从零开始建立链表,所以实现这点也并不困难甚至可以说是非常简单,只需要把create()函数定义中的
create( p_next );//继续
改成
create( &(*p_next)->next ); //create( p_next );//继续
就可以了。
也就是说,对于一次性建立的链表,压根不需要有tail这种东东。但在实际开发中,并不是总能碰到问题要求一次性建立链表这种“好事”,如果问题要求动态地(非一次性地)从链表尾部加入结点、并且要求从前面删除结点呢?这时才需要在head的基础上再加一个记录尾部结点的tail。但即使这样也不要“首尾两端”,而应该“首尾共济”,大大方方地
typedef struct { node_t *head; node_t *tail; } queue_t ;
好了。
只不过这种东东并不叫链表(Linked list),而叫Queue(队列)。