資料結構與演算法(二)-線性表之單鏈表順序儲存和鏈式儲存
阿新 • • 發佈:2018-12-10
前言:前面已經介紹過資料結構和演算法的基本概念,下面就開始總結一下資料結構中邏輯結構下的分支——線性結構線性表
一、簡介
1、線性表定義
線性表(List):由零個或多個數據元素組成的有限序列; 這裡有需要注意的幾個關鍵地方: 1.首先他是一個序列,也就是說元素之間是有個先來後到的。 2.若元素存在多個,則第一個元素無前驅,而最後一個元素無後繼,其他元素都有且只有一個前驅和後繼。 3.線性表強調是有限的,事實上無論計算機發展到多錢大,他所處理的元素都是有限的。 使用數學語言來表達的話: a1,…,ai-1,ai,ai+1,…an 表中ai-1領先於ai,ai領先於ai+1,稱ai-1是ai的直接前驅元素,ai+1是ai的直接後繼元素。所以線性表元素的各數n(n>0)定義為線性表的長度,當n=0時,稱為空表。2、 抽象資料型別
ADT 抽象資料型別 Data 資料結構 Operation 具體資料操作(例,增刪改查)比如1+1=2這樣一個操作,在不同CPU的處理上可能不一樣,但由於其定義的數學特性相同,所以在計算機程式設計者看來,它們都是相同的。“抽象”的意義在於資料型別的數學抽象特性。 而且,抽象資料型別不僅僅指那些已經定義並實現的資料型別,還可以是計算機程式設計者在設計軟體程式時自己定義的資料型別。 例如一個3D遊戲中,要定位角色的位置,那麼總會出現x,y,z三個整型資料組合在一起的座標。我們就可以定義一個point的抽象資料型別,它擁有x,y,z三個整型變數,這樣我們就可以方便的對一個角色的位置進行操作。所以抽象資料型別就是把資料型別和相關操作捆綁在一起。
二、線性表實現及優缺點
1、 線性表的順序儲存結構
線性表的順序儲存結構,指的是用一段地址連續的儲存單元一次儲存線性表的資料元素。 線性表(a1,a2,……,an)的順序儲存如下: 上面的線性表的順序儲存結構是不是與陣列一樣樣的?!?! 事實上物理上的儲存方式事實上就是在記憶體中找個初始地址,然後通過佔位的形式,把一定的記憶體空間給佔了,然後把相同資料型別的資料元素依次放在這塊空地中。 總結一下,順序儲存結構封裝需要三個屬性:- 儲存空間的起始位置,陣列data,它的儲存位置就是線性表儲存空間的儲存位置;
- 線性表的最大儲存容量:陣列的長度MaxSize;
- 線性表的當前長度length;
public class Linear<T> { //儲存線性表資料的陣列 private Object[] objects; //線性表的長度 private Integer length; //陣列的長度 private Integer objectsLength; //初始化 Linear(Integer objectsLength) { objects = new Object[objectsLength]; } //查 public T get(Integer index) { return (T)objects[index]; } //增 public void add(T t) { //增加之前先判斷是否需要擴容 isFull(); objects[length+1]=t; } //刪 public void remove(Integer index) { //迴圈查詢index,移除該元素並將後面元素前移一位 } //判斷當新增一個元素時,是否會陣列溢位,若溢位則新建陣列並擴大陣列長度 private void isFull() { } }線上性表的順序儲存結構中,它具有隨機儲存結構的特點,即直接通過下標獲取資料或儲存,那儲它的時間複雜度為O(1)。而當該結構的資料型別做插入操作時,就不能只插入而不管後面的元素,所以插入操作,也要考慮清楚。 插入演算法的思路:
- 如果插入位置不合理,丟擲異常;
- 如果線性表長度大於等於陣列長度,則丟擲異常活動太增加陣列容量;
- 從最後一個元素開始向前遍歷到第i個位置,分別將它們都向後移動一個位置;
- 將要插入元素填入位置i處;
- 線性表長+1;
public void add(Integer index,T t) throws Exception { isFull(); if (index<0 || index>length) throw new Exception("index outof length"); for (int i = length; i >= index; i--) { objects[length+1] = objects[length]; } objects[index] = t; }同理,刪除元素,也需要將刪除元素後的元素依次前移一位; 現在分析一下,插入和刪除的時間複雜度。 最好情況:插入和刪除操作剛好要求在最後一個位置操作,因為不需要移動任何元素,所以此時的時間複雜度為O(1)。 最壞情況:如果要插入和刪除的位置是第一個元素,那就意味著要移動所有的元素向後或者向前,所以這個時間複雜度為O(n)。 平均情況,就取中間值O((n-1)/2)。 這樣來看,平均情況複雜度簡化後還是O(n)。 總結: 線性表的順序儲存結構,在存、讀資料時,不管是哪個位置,時間複雜度都是O(1)。而在插入或刪除時,時間複雜度都是O(n)。 這就說明,它比較適合元素個數比較穩定,不經常插入和刪除元素,而更多的操作是存取資料的應用。 我們接下來給大家簡單總結下線性表的順序儲存結構的優缺點: 優點:
- 無需為表中元素之間的邏輯關係而增加額外的儲存空間;
- 可以快速地存取表中任意位置的元素;
- 插入和刪除操作需要移動大量元素;
- 當線性表長度變化較大時,難以確定儲存空間的容量;
- 容易造成儲存空間的“碎片”;
2 、 線性表的鏈式儲存結構
上一小節介紹了線性表的順序結構,它最大的缺陷是插入和刪除時需要移動大量元素,這是非常耗時的。那我們怎麼才能解決這個缺陷呢?這就需要找到原因了。 原因在於順序儲存結構在記憶體中的位置是連續的、無縫隙的,相鄰的儲存位置也具有鄰居關係,所以當插入和刪除時,需要移動大量的元素來保證記憶體地址的順序; 而鏈式的儲存結構就不會受記憶體的儲存順序影響,它可以在任意位置儲存,只需要指定元素的後繼元素就可以了,也就是說除了儲存其本身的資訊外,還需儲存一個指示其後繼的儲存位置的資訊。 鏈式儲存結構的特點:用一組任意的儲存單元儲存線性表的資料元素,這組儲存單元可以存在記憶體中未被佔用的任意位置。 我們把儲存資料元素資訊的域稱為資料域,把儲存直接後繼位置的域稱為指標域。指標域中儲存的資訊稱為指標或鏈。這兩部分資訊組成資料型別稱為儲存映像,也成為結點(Node)。 Java程式碼來表示結點:class Node<T> { private Object object; private Node next; public Object getObject() { return object; } public void setObject(Object object) { this.object = object; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }單鏈表結構圖: 對於線性表來說,總得有個頭有個尾,連結串列也不例外。我們把連結串列中的第一個結點的儲存位置叫做頭指標,最後一個結點指標為空(NULL)。 頭指標:
- 頭指標是指連結串列指向第一個節點的指標,若連結串列有頭結點,則是指向頭結點的指標;
- 頭指標具有表示作用,所以常用頭指標冠以連結串列的名字(指標變數的名字);
- 無論連結串列是否為空,頭指標均不為空;
- 頭指標是連結串列的必要元素
- 頭結點是為了操作的統一和方便而設立的,放在第一個元素的結點之前,其資料域一般無意義(但也可以用來存放連結串列的長度);
- 有了頭結點,對在第一元素結點前插入結點和刪除第一結點起操作與其它結點的操作就統一了;
- 頭節點不一定是連結串列的必要元素;
public class Chain<T> { //頭結點直接引用 private Node<T> head; //初始化 Chain() { head = new Node<T>(); head.setNext(null); } class Node<T> { ... } }在順序儲存結構中,有隨機儲存結構的特點,計算任意一個元素的儲存位置是很容易的,但是在單鏈表中,想知道其中一個元素的位置,就得從第一個結點開始遍歷,因此,對於單鏈表實現獲取第i個元素的資料的操作,在演算法上較為複雜。 獲取連結串列第i個數據的演算法思路:
- 宣告一個節點p指向連結串列第一個節點,初始化j從1開始;
- 當j<i時,酒便利連結串列,讓p的指標向後移動,不斷指向下一個節點,j+1;
- 若到連結串列末尾p為空,則說明第i個元素不存在;
- 否則查詢成功,返回節點p的資料;
public T get(Integer index) throws Exception { return (T)getNode(index).getObject(); } private Node<T> getNode(Integer index) throws Exception { if (index > size || index < 0) throw new Exception("index outof length"); Node<T> p = head; for (int i = 0; i < index; i++) p = p.next; return p; }由於這個演算法的時間複雜度取決於i的位置,當i=1時,則不需要遍歷,而i=n時則遍歷n-1次才可以。因此最壞情況的時間複雜度為O(n)。 在Java中有運用到線性的鏈式儲存結構的類LinkedList。檢視原始碼,在該類中的獲取節點的方法比較巧妙:
Node<E> node(int index) { //通過比較下標在list中的位置,來決定是從前往後還是從後往前遍歷,以提高效率 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }Java中的演算法需要Node類中有前置節點(也就是雙向連結串列,後面會介紹),這樣可以從後面向前便利,提升效能,它的時間複雜度為O(size-n)或O(n),也就是空間換時間。 單鏈表的插入:
- 建立新的結點p;
- 將新節點p的next指向當前節點s的next;
- 將當前結點s的next指向新節點p;
- 完成插入;
public void add(T t,Integer index) throws Exception { //獲取該位置的上一個節點 Node<T> s = getNode(index - 1); //建立新節點 Node<T> p = new Node<>(); p.setObject(t); //將本節點的next節點放入新節點的next節點 p.setNext(s.getNext()); //將新節點放入本節點的next節點位置 s.setNext(p); }而單鏈表的刪除與插入本質是相同的,無非一個是放,一個是拿,所以直接上程式碼:
public Node<T> remove(Integer index) throws Exception { //獲取該位置的上一個節點 Node<T> s = getNode(index - 1); //獲取該位置節點的下一個節點 Node<T> next = getNode(index).getNext(); //將本節點的next節點放在本節點的前一個節點的next節點位置 s.setNext(next.getNext()); return next; }我們發現無論是單鏈表插入還是刪除演算法,他們其實都是由兩部分組成:
- 遍歷查詢第i個元素;
- 實現插入和刪除元素;
- 先讓新節點的next指向頭結點之後;
- 然後讓表頭的next指向新節點;
3 、 總結
儲存分配方式:- 順序儲存結構用一段連續的儲存單元依次儲存線性表的資料元素;
- 單鏈表採用鏈式儲存結構,用一組任意的儲存單元存放線性表的元素;
- 查詢
- 順序儲存結構O(1);
- 單鏈表O(n);
- 插入和刪除
- 順序儲存結構需要平均移動表唱一半的元素,時間為O(n);
- 單鏈表在計算出某位置的指標後,插入和刪除時間僅為O(1);
- 順序儲存結構需要預分配儲存空間,分多了,容易造成空間浪費,分少了,容易溢位;
- 單鏈表不需要分配儲存空間,只要有就可以分配,元素個數也不瘦限制;
- 若需要頻繁查詢,很少進行插入和刪除操作時,宜採用順序儲存結構;
- 若需要頻繁插入和刪除時,宜採用單鏈表結構;
本系列參考書籍:
《寫給大家看的演算法書》
《圖靈程式設計叢書 演算法 第4版》