1. 引言html
這一篇博文主要介紹鏈表(linked list),指針和對象的實現,以及有根樹的表示。前端
2. 鏈表(linked list)node
咱們在上一篇中提過,棧與隊列在存儲(物理)結構上均可以用數組和鏈表來實現。數組和鏈表都是線性存儲結構,其中的各元素邏輯上都是按順序排列的。它們的不一樣點在於:數組的線性順序由數組的下標決定;而鏈表的順序是由各元素裏的指針決定的。鏈表爲動態集合提供了一種簡單而靈活的表示方法。算法
以下圖所示,雙向鏈表(doubly linked list)L的每一個元素都是一個對象,每一個對象有一個關鍵字(key)和兩個指針:next和prev。對象中還能夠包含其餘輔助數據(或稱爲衛星數據)。設x爲鏈表中的某個元素,x.next指向它在鏈表中的後繼元素(也可能沒有,此時x.next = NIL,x爲鏈表的最後一個元素,即x的頭(head));x.prev則指向它的前驅元素(也可能沒有,此時x.prev = NIL,x爲鏈表的第一個元素,即x的尾(tail))。屬性L.head指向鏈表的頭,若是爲NIL,則鏈表爲空。數組
鏈表還有多種形式:已排序的(sorted):鏈表的線性順序與鏈表元素中關鍵字的線性順序一致;單連接的(singly linked):去掉雙鏈表每一個元素的prev指針);循環鏈表(circular list):鏈表頭元素的prev指針指向鏈表尾元素,尾元素的next指針指向頭元素。數據結構
咱們能夠採用簡單的線性搜索方法來搜索鏈表L中第一個關鍵字(key)爲k的元素,並返回指向該元素的指針。若是沒有找到,則返回NIL。下面是僞代碼:ide
顯然,對於一個長度爲n的鏈表,在最壞狀況下,搜索的時間爲θ(n)。學習
給定一個已設置好關鍵字key的元素x,過程INSERT將x插入到鏈表的最前端。下面是僞代碼:this
顯然,插入的時間與鏈表的長度無關,爲常量θ(1)。spa
過程DELETE將一個元素x從L中移除。若該過程給出的是一個指向元素x的指針,則可經過修改指針將元素x刪除;若是該過程給出的是一個關鍵字key,則須要先搜索出該元素,而後將其移除。下面是僞代碼:
現最壞的狀況下,刪除操做的時間是θ(n),由於要先搜索出x。
下面給出一種雙鏈表的Java實現:
public class DoublyLinkedList<T> { private Node<T> first; private int size; /** * 將元素插入到鏈表的最前端 */ public void insert(T t) { Node<T> newNode = new Node<>(null, t, first); if (first != null) { first.prev = newNode; } first = newNode; size++; } /** * 搜索第一次出現待搜索元素的節點 * * @param t * 待搜索元素 * @return 保存該元素的節點(若沒找到,返回null) */ public Node<T> search(T t) { Node<T> node = first; while (node != null && node.key != t) { node = node.next; } return node; } /** * 刪除鏈表中第一次出現的待搜索元素的節點 * * @param t */ public void delete(T t) { Node<T> node = search(t); if (node == null) { return; } if (node.prev == null) { // node是first if (node.next != null) { node.next.prev = null; } first = node.next; return; } if (node.prev != null) { node.prev.next = node.next; if (node.next != null) { node.next.prev = node.prev; } } size--; } /** * 根據index獲取元素 * * @param index * @return */ public T get(int index) { if (index < 0 || index > size - 1) { throw new IndexOutOfBoundsException(index + ""); } Node<T> node = first; int i = 0; while (node != null && i != index) { node = node.next; i++; } return node == null ? null : node.key; } @Override public String toString() { Node<T> node = first; String result = ""; while (node != null) { String key = node.key == null ? "" : node.key.toString(); result += key + ","; node = node.next; } if (result.endsWith(",")) { result = result.substring(0, result.length() - 1); } return "[" + result + "]"; } public static class Node<T> { T key; Node<T> prev; Node<T> next; public Node(Node<T> prev, T key, Node<T> next) { super(); this.prev = prev; this.key = key; this.next = next; } } public static void main(String[] args) { DoublyLinkedList<Integer> list = new DoublyLinkedList<>(); // 插入 list.insert(1); list.insert(2); list.insert(3); list.insert(4); System.out.println(list); // 搜索 Node<Integer> node = list.search(3); System.out.println(node == null ? "null" : node.key); // 刪除 list.delete(1); System.out.println(list); //獲取 System.out.println(list.get(2)); System.out.println(list.get(1)); } }
3. 指針和對象的實現
當某種語言不支持指針和對象數據類型時,上面的實現方式是不可行的。這時咱們可考慮用數組和其下標來實現對象和指針。
咱們考慮對對象的每個屬性都用一個數組來存放,這樣就能夠表示一組具備相同屬性的的對象。咱們能夠用以下圖所示的方式來表示上面代碼中出現的Node對象。其中數組key,數組prev,數組next分別存放Node的key,prev,next屬性。
從上圖咱們能夠看出,第一個節點在數組下標爲7的位置,以後的節點在數組中的下標依次是:5,2,3。
像這樣用數組存儲的方式與通常使用數組的方式不一樣的是,被存儲的元素在物理上不是連續的。(暫時還沒想到這麼作能帶來什麼好處。但學習算法更重要的是對思惟的擴充。)。
計算機內存的字每每是從整數0到M-1進行編址的,其中M是一個足夠大的整數。在許多程序設計語言中,一個對象在計算機內存中佔據一組連續的儲存單位,指針僅僅是該對象所在的第一個存儲單位的地址(就像C中的結構體)。要訪問對象內其餘儲存單元能夠在指針上加上一個偏移量。(正如在學習C++時,老師說的,數據類型的本質是固定內存大小的別名)。
一樣,咱們能夠採用上面的這種策略來實現對象。以下圖所示,屬性key,next,prev的偏移量分別是0,1,2。
當咱們向一個雙向鏈表表示的動態數組中插入一個元素時,就必須分配一個指向該鏈表中還沒有利用的對象的指針。所以,有必要對鏈表中還沒有利用的對象空間進行管理,使其可以被分配。在某些系統中,有垃圾回收器(garbage collector,GC)負責肯定,回收哪些對象是未使用的。然而許多應用沒有GC或者該應用自己很簡單,咱們徹底能夠本身負責將未使用的對象的存儲空間返回給存儲管理器。咱們以多數組表示的雙向鏈表爲例,探討同構對象(即有相同屬性的對象)的分配與釋放的問題。
咱們假設多數組表示法中的各數組的長度爲m,且在某一時刻,該動態集合中含有n≤m個元素。這n個對象表示現存於該動態集合中的元素,而餘下的m-n個對象是自由的(free),這些自由對象表示的是將要插入該動態集合的元素。
咱們把只有對象保存在一個單鏈表中,稱爲自由表(free list)。自由表只使用next數組,該數組只存放鏈表中的next指針。自由表的頭保存在全局變量free中。當有鏈表L表示的動態集合非空時,自由表可能會和鏈表L交錯,以下圖。
自由表相似一個棧:下一個被分配的對象就是最後被釋放的那個。咱們能夠利用棧的push和pop操做來實現分配和釋放過程。僞代碼以下:
4. 有根樹的表示
上一節介紹的表示鏈表的方法能夠推廣到任意同構的數據結構上。在本節中,咱們專門討論用鏈式數據結構表示有根樹的問題。咱們從最簡單的二叉樹開始討論,而後給出針對節點的孩子樹任意的有根樹的表示方法。
咱們用對象來表示樹的節點。與鏈表相似,假設每一個節點都含有一個關鍵字key,其他咱們感興趣的屬性包括指向其餘節點的指針,它們隨樹的種類不一樣會有所變化。
以下圖所示,它展現了在二叉樹T中如何利用屬性p,left,right存放指向父節點,左孩子,右孩子的指針。屬性T.root指向整棵樹T的根節點。
二叉樹的表示方法能夠推廣到每一個節點的孩子數至多爲常數k的任意類型的樹:只須要將left和right屬性用child1,child2,…,childk代替。可是當孩子的節點樹無限制時,這種方法就失效了,由於不知道預先分配多少個屬性(在多數組表示發中就是多少個數組)。此外,即便孩子數k限制在一個大的常數之內,但當多數節點只有少許孩子時,這樣作會浪費大量儲存空間。
這時咱們能夠用一種叫作左孩子右兄弟表示法(left-child,right-sibling representation),以下圖所示。對任意n個節點的有根樹,它只須要O(n)的存儲空間。與前面相似,每一個節點都包含一個父節點指針p,且T.root指向樹T的根節點。然而,每一個節點中不是包含指向每一個孩子的指針,而是隻有兩個指針:x.left-child指向節點x最左邊的孩子節點。x.right-sibling指向節點x右側相鄰的兄弟節點。
事實上,咱們還能夠用許多其餘的方法來表示有根樹,例如在前面介紹的堆排序與優先隊列——算法導論(7)中,咱們用堆來表示一顆徹底的二叉樹,這裏就不一一介紹了。至於哪一種方法最優,須要具體狀況具體分析。