590 likes | 667 Views
第 4 章 鏈結串列(Linked Lists). 4-1 動態記憶體配置 4-2 鏈結串列的基礎 4-3 單向鏈結串列 4-4 環狀鏈結串列 4-5 雙向鏈結串列 4-6 鏈結串列的應用 - 多項式表示法. 4-1 動態記憶體配置 - 說明. 動態記憶體配置不同於陣列的靜態記憶體配置是在編譯階段就配置記憶體空間, 動態記憶體配置 是等到 執行階段,才向作業系統要求配置所需的記憶體空間 ,可以讓程式設計者靈活運用程式所需的記憶體空間。
E N D
第4章 鏈結串列(Linked Lists) • 4-1 動態記憶體配置 • 4-2 鏈結串列的基礎 • 4-3 單向鏈結串列 • 4-4 環狀鏈結串列 • 4-5 雙向鏈結串列 • 4-6 鏈結串列的應用 - 多項式表示法
4-1 動態記憶體配置-說明 • 動態記憶體配置不同於陣列的靜態記憶體配置是在編譯階段就配置記憶體空間,動態記憶體配置是等到執行階段,才向作業系統要求配置所需的記憶體空間,可以讓程式設計者靈活運用程式所需的記憶體空間。 • 在C語言<stdlib.h>標頭檔的標準函式庫提供兩個函數:malloc()和free(),可以配置和釋放程式所需的記憶體空間。
4-1 動態記憶體配置-malloc() malloc()函數:配置記憶體空間 • C語言的程式碼可以呼叫malloc()函數向作業系統取得一塊可用的記憶體空間,函數的語法,如下所示: fp = (資料型態*) malloc(sizeof(資料型態)); • 上述語法因為函數傳回void通用型指標,所以需要加上型態迫換,將函數傳回的指標轉換成指定資料型態的指標,sizeof運算子可以計算指定資料型態的大小。例如:配置一個浮點數變數的記憶空間,如下所示: fp = (float *) malloc(sizeof(float)); struct test *score; score=(struct test *) malloc(num*sizeof(struct test));
4-1 動態記憶體配置-free() free()函數:釋放配置的記憶體空間 • free()函數可以釋放malloc()函數配置的記憶體空間,例如:指標fp是一個指向malloc()函數傳回的浮點數記憶體空間的指標,呼叫free()函數釋放這塊記憶體,如下所示: free(fp); • 上述程式碼的指標fp可以是float浮點數指標,也可以是malloc()函數傳回的其它資料型態指標、陣列或結構指標。
4-2 鏈結串列的基礎-說明 • 「有序串列」(Ordered List)或稱為「線性串列」(Linear List)是一種元素間擁有順序的集合,如下所示: (a0, a1, a2, …, an),ai,0 <= i <= n • 上述集合是一個線性串列,如果是空的線性串列,表示串列中沒有任何元素,是使用( )空括號表示。
4-2 鏈結串列的基礎-範例 • 一些線性串列的範例,如下所示: • 英文的月份:( Jan, Feb, March, …, Oct, Nov, Dec )。 • 英文的星期:( Mon, Tue, Wed, Thu, Fri, Sat, Sun )。 • 撲克牌的點數:( A, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K )。 • 樓層:( B2, B1, 1, 2, 3, 4, 5, 6 )。 • 生肖:( 鼠, 牛, 虎, …… , 狗, 猪 )。
4-2 鏈結串列的基礎-運算 • 線性串列的相關運算,如下所示: • length():取得線性串列的長度。 • get():存取線性串列的第i個元素。 • search():從左到右,或從右到左走訪線性串列。 • delete():在線性串列第i個元素刪除元素。 • insert():在線性串列第i個元素插入元素。
4-2 鏈結串列的基礎-使用陣列實作線性串列(問題) • 複雜的新增與刪除運算:在新增或刪除名單時,因為陣列儲存的是循序且連續資料,所以在陣列中需要搬移大量元素,才能滿足同縣巿位在同一區段。例如:在桃園巿新增江小魚,則王小美、李光明和周星星都需要依序往後搬移1個元素,才能將江小魚插入,同理,刪除王大毛時,之後的所有元素也需要往前搬移。 • 浪費記憶體空間:因為並不知道名單有多少位,所以需要宣告一個很大的結構陣列來儲存名單,如果最後只使用到幾個元素,就會造成大量記憶體空間的閒置。
4-2 鏈結串列的基礎-使用鏈結串列實作線性串列 • 「鏈結串列」(Linked Lists)是一種實作線性串列的資料結構。在現實生活中,鏈結串列如同火車掛車廂以線性方式將車廂連結起來,每個車廂是鏈結串列的節點(Nodes),儲存線性串列的資料,車廂和車廂之間的鏈結,就是節點間的鏈結(Link)。如下圖所示:
4-2 鏈結串列的基礎-使用鏈結串列實作線性串列(實作) • 在C語言建立鏈結串列是宣告一個結構作為節點,內含指標的成員變數來鏈結其它節點,使用動態記憶體配置在執行階段配置節點所需的記憶體空間,即可解決結構陣列實作上浪費記憶體的問題。 • 例如:使用鏈結串列建立的郵寄名單,筆者僅以編號(從大到小)代表名單的節點資料,郵寄名單的鏈結串列,如下圖所示:
4-3 單向鏈結串列 • 4-3-1 建立和走訪單向鏈結串列 • 4-3-2 刪除單向鏈結串列的節點 • 4-3-3 插入單向鏈結串列的節點
4-3 單向鏈結串列-說明 • 單向鏈結串列是最簡單的一種鏈結串列,因為節點指標都是指向同一個方向,依序從前一個節點指向下一個節點,然後最後1個節點指向NULL,所以稱為單向鏈結串列,如下圖所示:
4-3 單向鏈結串列-標頭檔 01: /* 程式範例: Ch4-3.h */ 02: struct Node { /* Node節點結構 */ 03: int data; /* 結構變數宣告 */ 04: struct Node *next; /* 指向下一個節點 */ 05: }; 06: typedef struct Node LNode; /* 串列節點的新型態 */ 07: typedef LNode *List; /* 串列的新型態 */ 08: List first = NULL; /* 串列的開頭指標 */ 09: /* 抽象資料型態的操作函數宣告 */ 10: extern void creatList(int len, int *array); 11: extern int isListEmpty(); 12: extern void printList(); 13: extern List searchNode(int d); 14: extern int deleteNode(List ptr); 15: extern void insertNode(List ptr, int d);
4-3-1 建立和走訪單向鏈結串列-建立(說明) 建立單向鏈結串列 • 在createList()函數是使用for迴圈將取得的陣列值建立成串列節點,每執行一次迴圈,就在串列開頭插入一個新節點,如下所示: for ( i = 0; i < len; i++ ) { newnode = (List) malloc(sizeof(LNode)); newnode->data = array[i]; newnode->next = first; first = newnode; }
4-3-1 建立和走訪單向鏈結串列-走訪(說明) 單向鏈結串列的走訪 • 單向鏈結串列的「走訪」(Traverse)和一維陣列的走訪十分相似,其差異在於陣列是遞增索引值來走訪陣列,串列是使用指標運算來處理節點的走訪,如下所示: List current = first; while ( current != NULL ) { ………….. current = current->next; }
4-3-2 刪除單向鏈結串列的節點-情況1 • 刪除串列的第1個節點:只需將串列指標first指向下一個節點,如下圖所示: first = first->next;
4-3-2 刪除單向鏈結串列的節點-情況2 • 刪除串列的最後1個節點:只需將最後1個節點ptr的前一個節點指標指向NULL,如下圖所示: while (current->next!=ptr) current = current->next; current->next = NULL;
4-3-2 刪除單向鏈結串列的節點-情況3(1) • 刪除串列的中間節點:將刪除節點前一個節點的next指標,指向刪除節點下一個節點,例如:刪除節點3,如下圖所示:
4-3-2 刪除單向鏈結串列的節點-情況3(2) • 在執行刪除節點3操作後的串列圖形,如下所示: while (current->next!=ptr) current = current->next; current->next = ptr->next;
4-3-3 插入單向鏈結串列的節點-情況1 • 將節點插入串列第1個節點之前:只需將新節點newnode的指標指向串列的第1個節點first,新節點就成為串列的第1個節點,如下圖所示: newnode->next = first; first = newnode;
4-3-3 插入單向鏈結串列的節點-情況2 • 將節點插在串列的最後1個節點之後:只需將原來串列最後1個節點的指標指向新節點newnode,新節點指向NULL,如下圖所示: ptr->next = newnode; newnode->next = NULL;
4-3-3 插入單向鏈結串列的節點-情況3(1) • 將節點插在串列的中間位置:假設節點是插在p和q兩個節點之間,p是q的前一個節點,如下圖所示:
4-3-3 插入單向鏈結串列的節點-情況3(2) • 只需將p指標指向新節點newnode,然後將新節點指標指向q,就可以插入新節點,如下圖所示: newnode->next=ptr->next; ptr->next = newnode;
4-4 環狀鏈結串列-說明 • 如果將最後一個節點的指標改為指向單向鏈結串列開始的第1個節點,這種串列稱為「環狀鏈結串列」(Circular Lists)。
4-4 環狀鏈結串列-建立與走訪 • 環狀鏈結串列的建立只需將最後1個節點的last指標指向第1個節點,即可完成環狀鏈結串列的建立,如下所示: last->next = first; • 環狀鏈結串列的走訪檢查是否到串列結束的條件是current->next == first,如下所示: CList current = first; do { ……… current = current->next; } while ( current != first );
4-4 環狀鏈結串列-插入節點(情況1-步驟) • 將節點插入第1個節點之前成為串列開始,可以分成三個步驟,如下所示: • Step 1: 將新節點newnode的next指標指向串列的第1個節點。 newnode->next = first; • Step 2: 然後找到最後1個節點previous且將其指標指向新節點。 previous = first; while ( previous->next != first ) previous = previous->next; previous->next = newnode; • Step 3: 將串列的開始指向新節點,新節點成為串列的第1個節點。 first = newnode;
4-4 環狀鏈結串列-插入節點(情況2-步驟) • 將節點插在串列中指定節點之後,例如:將節點插在節點ptr之後,分成二個步驟,如下所示: • Step 1:將新節點newnode的next指標指向節點ptr的下一個節點。 newnode->next = ptr->next; • Step 2:將節點ptr的指標指向新節點newnode。 ptr->next = newnode;
4-4 環狀鏈結串列-刪除節點(情況1-步驟) • 刪除環狀串列的第1個節點可以分成二個步驟,如下所示: • Step 1: 將串列開始的first指標移至第2個節點。 first = first->next; • Step 2: 將最後1個節點的previous指標指向第2個節點。 previous->next = ptr->next;
4-4 環狀鏈結串列-刪除節點(情況2-步驟) • 刪除環狀串列的中間節點,例如:刪除節點ptr分成二個步驟,如下所示: • Step 1:先找到節點ptr的前一個節點previous。 while ( previous->next != ptr ) previous = previous->next; • Step 2:將前節點的指標指向節點ptr的下節點。 previous->next = ptr->next;
4-5 雙向鏈結串列 • 4-5-1 雙向鏈結串列的建立與走訪 • 4-5-2 雙向鏈結串列內節點的插入 • 4-5-3 雙向鏈結串列內節點的刪除
4-5 雙向鏈結串列-說明 • 「雙向鏈結串列」(Doubly Linked Lists)是另一種常見的鏈結串列結構,這是一種允許無方向性走訪的鏈結串列。 • 所以,我們可以將兩個方向相反的單向鏈結串列結合起來,建立一種無方向性的鏈結串列,這就是雙向鏈結串列,如下圖所示:
4-5 雙向鏈結串列-標頭檔 01: /* 程式範例: Ch4-5.h */ 02: struct Node { /* Node節點結構 */ 03: int data; /* 結構變數宣告 */ 04: struct Node *next; /* 指向下一個節點 */ 05: struct Node *previous; /* 指向前一個節點 */ 06: }; 07: typedef struct Node DNode; /* 雙向串列節點的新型態 */ 08: typedef DNode *DList; /* 雙向串列的新型態 */ 09: DList first = NULL; /* 雙向串列的開頭指標 */ 10: DList now = NULL; /* 雙向串列目前節點指標 */ 11: /* 抽象資料型態的操作函數宣告 */ 12: extern void creatDList(int len, int *array); 13: extern void printDList(); 14: extern void nextNode(); 15: extern void previousNode(); 16: extern void resetNode(); 17: extern DList readNode(); 18: extern void insertDNode(DList ptr, int d); 19: extern void deleteDNode(DList ptr);
4-5-1 雙向鏈結串列的建立與走訪-建立(步驟) • 雙向鏈結串列比單向鏈結串列多一個指向前一個節點的指標,createDList()函數使用for迴圈建立雙向串列的其它節點,其作法是將每一個新節點都插入在雙向鏈結串列的最後,一共有四個步驟,如下所示:又必須在別地方輸入再拷貝過來 • Step 1:將新節點的next指標指向NULL。 • Step 2:將新節點的previous指標指向before指標。 • Step 3:將before指向節點的next指標指向新節點。 • Step 4:before指標指向串列的最後1個節點(新節點)。
4-5-1 雙向鏈結串列的建立與走訪-建立(圖例) for ( i = 1; i < len; i++ ) { newnode = (DList) malloc(sizeof(DNode)); newnode->data = array[i]; newnode->next = NULL; newnode->previous=before; before->next=newnode; before = newnode; }
4-5-1 雙向鏈結串列的建立與走訪-走訪 • 雙向鏈結串列走訪比單向鏈結串列靈活,因為可以分別使用next和previous指標從兩個方向進行走訪,模組函數nextNode()、previousNode()和resetNode()可以移動now指標來走訪雙向串列下一個或前一個節點。 • 如果讀者學過C++語言的STL(Standard Template Library)或Java語言的Collections物件,雙向串列的相關走訪函數就是容器物件(Container)的「重複指標」(Iterators)。
4-5-2 雙向鏈結串列內節點的插入-情況1(步驟) • 將節點插在串列中第1個節點之前:新節點將成為串列的第1個節點,其步驟如下所示: • Step 1:將新節點newnode的next指標指向雙向串列的第1個節點。 newnode->next = first; • Step 2:將原串列第1個節點的previous指標指向新節點。 first->previous = newnode; • Step 3:將原串列的first指標指向新節點,新節點成為環狀(???)串列的開始。 first = newnode;
4-5-2 雙向鏈結串列內節點的插入-情況2(步驟) • 將節點插在串列的最後:新節點是插入成為串列的最後1個節點,其步驟如下所示: • Step 1:將最後1個節點ptr的next指標指向新節點newnode。 ptr->next = newnode; • Step 2:將新節點的previous指標指向原串列的最後1個節點。 newnode->previous=ptr; • Step 3:將新節點的next指標指向NULL。 newnode->next = NULL;
4-5-2 雙向鏈結串列內節點的插入-情況3(步驟) • 將節點插入成為串列的中間節點:如果節點是插在ptr節點之後,插入步驟如下所示: • Step 1:將ptr節點next指向下一個節點的previous指標指向新節點。 ptr->next->previous = newnode; • Step 2:新節點的next指標指向ptr節點next指標的下一個節點。 newnode->next = ptr->next; • Step 3:新節點的previous指標指向ptr節點。 newnode->previous = ptr; • Step 4:將ptr節點的next指標指向新節點。 ptr->next = newnode;
4-5-3 雙向鏈結串列內節點的刪除-情況1 • 刪除串列的第1個節點:原串列的第2個節點就成為第1個節點,其步驟如下所示: • Step 1:將first指標指向第2個節點。 first = first->next; • Step 2:將原串列第2個節點的previous指標指定成NULL。 first->previous = NULL;
4-5-3 雙向鏈結串列內節點的刪除-情況2 • 刪除最後1個節點:原串列最後第2個節點就成為最後1個節點,其步驟如下所示: • Step 1:將原串列的最後1個節點的前一個節點的next指標指定成NULL。 ptr->previous->next = NULL;