c語言實現一個鏈表

1、基礎研究

咱們在這裏要理解和實現一種最基本的數據結構:鏈表。首先看看實現的程序代碼:java

List .h編程

 

 

 

 

 

 

 

 

 

 

 

 

事實上咱們觀察list.h發現前面一部分是數據結構的定義和函數的聲明,後面一部分是函數的實現。咱們僅僅觀察前面一部分就能夠知道這個鏈表的結構是怎麼實現的了。數組

程序將處理的對象分紅了三類:線性表、結點和元素,分別定義了它們的數據類型和操做函數,對線性表有建立、撤銷、清空操做,對元素有追加、加入、刪除、取操做,對結點有取、遍歷、建立操做,每個操做都用一個子函數來實現。它們所有被封裝進了頭文件list.h,這是對共性的封裝。安全

咱們用m.clist.h進行測試:數據結構

 

執行結果以下:函數

 

m.c首先建立了一個字符數組來裝載要存入線性表中的元素,再定義了顯示線性表的函數showlist和顯示單個元素的函數putelement。在主函數中首先調用CreateList函數建立一個線性表,若是建立失敗會提示錯誤並返回,若是成功則調用ListAppend函數將字符數組裏的內容放進線性表中,再調用showlist函數顯示字符串。以後咱們調用ListInsert函數向鏈表中插入一個元素結點並顯示,再調用ListDelete函數刪除以前插入的元素,並顯示字符串。其中CreateList函數、ListInsert函數、ListDelete函數都是在list.h中的函數,是有關鏈表自己的操做,是共性,而showlist函數和putelement函數是在c文件中實現的,它們的功能是個性,是需求。showlist函數是調用TraverseList函數遍歷鏈表,並對每一個元素用putelement函數進行處理,而putelement函數是將該元素打印出來。爲何在TraverseList函數裏要將遍歷鏈表和處理函數分開呢?這裏也是將共性和個性分離開,不少時候咱們都須要遍歷鏈表,可是不必定每一次都要用同一個函數來處理。那麼就把個性也用函數封裝起來。測試

list.h的第一個語句typedef char EleType改成typedef int EleType,再用m1.c測試:spa

 

運行結果爲:設計

 

這裏把鏈表元素由字符型改爲整形,只須要再在m.c裏進行極小的改動,就能夠實現相關功能。指針

再將list.h的第一個語句typedef char EleType改成typedef struct{char a;int b;} EleType,再用m2.c測試:

 

執行結果爲:

 

這裏要處理的鏈表元素爲結構體,因此咱們要定義一個結構體變量,並進行初始化,以後再插入鏈表中,而後作一些修改,則能夠實現相關功能。

咱們能夠發現List裏只有一個數據項「ChainNode *head」,爲何還要定義這個數據類型?一樣地,咱們用typedef char EleType定義了線性表存儲的元素類型,其實只是將char取名爲EleType而已,爲何要取這個別名而不是直接用char呢?咱們在編寫程序的過程當中,須要一些符號來幫助咱們認識、理解、記憶變量的名字,這些符號最好是有特殊含義、能讓咱們聯想起它的功能的,若是元素的類型就用char表示,那麼在定義和使用元素時很容易把它與別的變量弄混,會形成程序的可讀性下降。並且若是鏈表的元素變成了int型,咱們只須要將typedef char EleType改爲typedef int EleType就能夠了,這樣使程序易於修改和擴展。一樣地,List裏只有一個數據項「ChainNode *head」,可是咱們還要將它封裝在一個List數據類型中,也是考慮到了程序的擴展性和可讀性。並且若是咱們在這裏只定義一個頭指針的話,表達不出定義線性表的意思,是線性表裏麪包括頭結點,這個結點能夠用一個頭指針指向,因此頭指針能夠表明一個線性表,可是它們不是一個層次的東西,咱們要將線性表的屬性都封裝起來才能更好的對它進行操做,這個屬性是咱們抽象出來的,咱們一樣能夠抽象出更多的線性表的屬性添加進來以方便實現更多功能。如今咱們向線性表中添加一個tail指針,使它指向鏈表的最後一個結點,那麼首先要修改線性表的定義:

 

 

修改建立線性表的函數CreateList,由於建立線性表後只有一個頭結點,因此headtail指針都指向這個結點:

 

撤銷線性表時要將頭尾兩個指針都釋放:

 

由於咱們要提升ListAppend的速度,而加入元素是在線性表尾端加入的,因此咱們用tail指針加入會更快:

 

這樣咱們不用改動線性表的程序m.c就能夠實現了,由於這裏咱們把共性和個性分離開了,使每個函數的功能單一,獨立性高,與外部的隔絕性好。也就是咱們從外部看,不用管一個函數的功能是怎麼實現的,而只須要知道它的參數是什麼,功能是什麼,返回值是什麼,這樣就保證了咱們要改動程序只須要改動較小的部分。

爲何要使用一個頭結點呢?由於線性表有爲空的狀況,這時若是沒有頭結點,咱們加入元素就沒有地方存放結點的地址,並且咱們寫函數時還要專門對第一個元素進行處理。這樣容易出錯,也會使程序變得更加複雜。

程序中實現的鏈表裏的元素類型都是固定的,怎麼實現一個鏈表使它的元素類型爲任意類型呢?要在鏈表裏結點的數據空間存聽任意類型的數據是不可能的,由於每一個節點定義時的大小都是固定的。咱們能夠這樣實現:在結點裏的數據空間存放指針,指針指向每個元素處的空間,這個空間的大小能夠是任意的,根據咱們定義的數據類型而改變,用malloc函數動態分配內存。

可是如今的問題是咱們不知道用戶傳入的數據大小是多少,像printf函數同樣用類型說明符只能實現基本數據類型而不能實現用戶自定義類型,而用戶用結構體定義的自定義大小能夠爲任意大小,甚至理論上是無窮大的。以前我覺得要實現鏈表的每個元素的類型均可以是不同的,可是後來發現應該實現的是元素類型都是同樣的,可是這個類型是由用戶決定的而不是提早先規定好的。

由於不知道數據大小,因此咱們要在線性表中加入一個數據項int datasize以表示數據大小,並在main函數中建立線性表時用sizeof計算數據大小並傳給datasize;

 

咱們將ListAppend函數、ListInsert函數實現爲不定函數,這樣它們接受的參數類型就沒有限制了:

 

由於咱們傳入ListAppend函數的鏈表數據是一個局部變量,保存在棧段中,而且在函數返回後會被釋放,因此要另外開闢空間來存儲它。這裏&lp表示傳入的線性表lp在棧中的地址,&lp+1表示下一個參數,即咱們要添加的數據在棧中的地址。咱們用malloc函數建立一個傳入數據大小的空間並將它的地址賦給指針target。而後用memcpy函數將數據從戰中轉移到target指向的咱們動態開闢的空間中。Memcpy函數的原型爲:void *memcpy( void *dest, const void *src, size_t count);即從指針src指向的空間拷貝count個字節到指針dest指向的空間裏。

 

以後修改NewChainCode函數、GetElement函數、CreateList函數就能夠了,這也體現了各個函數的獨立性,不然咱們可能就要修改整個程序了。

這裏必定要注意的是,咱們在一個指針進行賦值以後,必定要對它進行判斷,若是是0則返回,這樣可使程序更安全、更容易調試。

如今咱們就能夠在c文件裏定義數據結構而不用更改頭文件的內容了。咱們用m1.c進行測試:

 

結果是正確的,注意在用CreateList建立線性表時必定要先用sizeof計算傳入的數據大小。

2、擴展研究

一、這個程序有什麼特點?表現了一種什麼樣的程序設計思想?

答:這個程序將共性抽象開並封裝到頭文件裏,咱們能夠很清楚地看到頭文件list.h裏封裝的都是鏈表的數據結構和方法,咱們在c文件裏只須要將數據傳入並用自定義的方法(好比輸出)來進行操做就能夠了。這個程序的結構很是清楚:共性的抽象、個性的實現,每個函數實現一個功能,函數與函數之間沒有聯繫,這樣就能夠保證一個函數出問題不會影響到其它函數。這個list.h頭文件徹底能夠當作一個模塊,調用它就能實現鏈表的相關功能,這是結構化的思想。

3、研究總結

程序設計須要綜合的能力和視野。這個程序頭文件裏的函數其實和java裏的類很像,每個需求都是由專門的函數來實現的,函數與函數之間沒有聯繫,只與調用的函數傳遞數據,這樣咱們只須要考慮單個函數的功能怎麼實現就夠了。而這樣首先要把問題細化爲一個個小需求來實現,這須要咱們在程序設計時先對問題有清楚的認識和深度的思考分析。肯定每個函數的功能、參數、返回值,而後再來實現函數,這時就是編程的細節問題了,相對程序設計要簡單得多。咱們要更多地思考怎麼來進行程序設計,而不是具體的技術細節。

相關文章
相關標籤/搜索