垃圾代码评析——关于《C程序设计伴侣》9.4——链表(二)
前文链接:http://www.cnblogs.com/pmer/archive/2012/11/21/2781703.html
【重构】
首先,什么是链表?无论《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. #include <stdlib.h>
2. #include <stdio.h>
3.
4. //----------通用函数---------------------//
5. void *my_malloc( size_t );
6. //--------------------------------------//
7.
8.
9. //----------“数据”类型----------------//
10. typedef
11. struct
12. {
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 node
26. {
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(队列)。