前文連結: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. 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(隊列)。