基礎概念
數據結構:是相互之間存在一種或多種關系的數據元素的集合。
邏輯結構和物理結構
關於數據結構,我們可以從邏輯結構和物理結構這兩個維度去描述
邏輯結構是數據對象中數據元素之間的關系,是從邏輯意義上去描述的數據之間的組織形式。
邏輯結構有4種:
- 集合結構(數據元素之間僅以集合的方式體現,元素之間沒有別的關系)
- 線性結構(數據元素之間存在一對一的關系)
- 樹(數據元素之間為一對多或多對一的關系)
- 圖(數據元素之間為多對多的關系)
物理結構則是邏輯結構在計算機中內存中的存儲形式,分為兩種:
- 順序存儲結構
- 鏈式存儲結構
線性表(list)
線性表是零個或多個數據元素的的有限序列
線性表是線性結構,元素之間存在一對一的關系,線性表可通過順序和鏈式兩種方式來實現。
順序存儲結構,是用一段地址連續的存儲單元依次存儲線性表的數據元素
鏈式存儲結構,用一組任意的存儲單元來存儲數據元素,不要求物理存儲單元的連續性,由一系列結點組成,每個結點除了要存儲數據外,還需存儲指向后繼結點或前驅結點的存儲地址。
順序存儲和鏈式存儲對比
- 順序存儲結構
- 優點
- 實現比較簡單
- 查找指定位置的元素效率很快,時間復雜度為常數階O(1)
- 無需額外存儲元素之間的邏輯關系(鏈式存儲由於存儲空間隨機分配,需要存儲元素之間的邏輯關系)
- 缺點
- 需要預先分配存儲空間,如果數據元素數量變化較大,很難確定存儲容量,並導致空間浪費
- 若頻繁進行插入刪除操作,則可能需要頻繁移動大量數據元素
- 優點
- 鏈式存儲結構
- 優點
- 不需要提前分配存儲空間,元素個數不受限制
- 對於插入刪除操作,在已找到目標位置前提下,效率很高,僅需處理元素之間的引用關系,時間復雜度為O(1)
- 缺點
- 實現相對復雜
- 查找效率較低,最壞情況下需要遍歷整張表
- 由於物理存儲位置不固定,需要額外存儲數據元素之間的邏輯關系
- 優點
鏈式存儲代碼實現
單鏈表
package listdemo; /** * Created by chengxiao on 2016/10/18. */ public class MyLinkedList { /** * 指向頭結點的引用 */ private Node first ; /** * 線性表大小 */ private int size; /** * 結點類 */ private static class Node{ //數據域 private int data; //指向后繼結點的引用 private Node next; Node(int data){ this.data = data; } } /** * 從頭部進行插入 * 步驟:1.新結點的next鏈指向當前頭結點;2.將first指向新節點 * 時間復雜度:O(1) * @param data */ public void insertFirst(int data){ Node newNode = new Node(data); newNode.next = first; first = newNode; size++; } /** * 從頭部進行刪除操作 * 步驟:1.將頭結點的next鏈置空 2.將first引用指向第二個結點 * 時間復雜度為:O(1) * @return */ public boolean deleteFirst(){ if(isEmpty()){ return false; } Node secondNode = first.next; first.next = null; first = secondNode; size--; return true; } /** * 取出第i個結點 * 步驟:從頭結點進行遍歷,取第i個結點 * 時間復雜度:O(n),此操作對於利用數組實現的順序存儲結構,僅需常數階O(1)即可完成。 * @param index * @return */ public int get(int index) throws Exception { if(!checkIndex(index)){ throw new Exception("index不合法!"); } Node curr = first; for(int i=0;i<index;i++){ curr = curr.next; } return curr.data; } /** * 遍歷線性表 * 時間復雜度:O(n) */ public void displayList(){ Node currNode = first; while (currNode!=null){ System.out.print(currNode.data+" "); currNode = currNode.next; } System.out.println(); } /** * 鏈表是否為空 * @return */ public boolean isEmpty(){ return first == null; } /** * index是否合法 * @param index * @return */ private boolean checkIndex(int index){ return index >= 0 && index < size; } /** * 鏈表大小 * @return */ public int size() { return size; } public static void main(String []args) throws Exception { MyLinkedList myLinkedList = new MyLinkedList(); //從頭部插入 myLinkedList.insertFirst(1); myLinkedList.insertFirst(2); myLinkedList.insertFirst(3); myLinkedList.insertFirst(4); //遍歷線性表中元素 myLinkedList.displayList(); //獲取第二個元素 System.out.println(myLinkedList.get(2)); //刪除結點 myLinkedList.deleteFirst(); myLinkedList.displayList(); } }
輸出結果
4 3 2 1 2 3 2 1
雙端鏈表
上面羅列了線性表中的幾種基本操作,考慮下,如果要提供一個在鏈表尾部進行插入的操作insertLast,那么由於單鏈表只保留了指向頭結點的應用first,需要從頭結點不斷通過其next鏈找后繼結點來遍歷,時間復雜度為O(n)。其實,我們可以在保留頭結點引用的時候,也保留一個尾結點的引用。這樣,在從尾部進行插入時就方便多了
雙端鏈表同時保存對頭結點和對尾結點的引用
/** * 指向頭結點的引用 */ private Node first ; /** * 指向尾結點的引用 */ private Node rear;
從尾部進行插入
/** * 雙端鏈表,從尾部進行插入 * 步驟:將當前尾結點的next鏈指向新節點即可 * 時間復雜度:O(1) * @param data */ public void insertLast(int data){ Node newNode = new Node(data); if(isEmpty()){ first = newNode; rear = newNode; size++; return; } rear.next = newNode; rear = newNode; size++; }
做其他操作的時候也需注意保持對尾結點的引用,此處不再贅述。
雙向鏈表
再考慮下,如果我們要提供一個刪除尾結點的操作,步驟很簡單:在刪除尾結點的過程中需要將其前驅結點(即倒數第二個結點)的next鏈引用置為空,但由於我們的鏈表是單鏈表,一條道走到黑,要找倒數第二個結點得從頭開始遍歷,這種情況下,我們就可以考慮使用雙向鏈表。
雙向鏈表的的每一個結點,包含兩個指針域,一個指向它的前驅結點,一個指向它的后繼結點。
/** * 刪除尾結點 * 主要步驟:1.將rear指向倒數第二個結點 2.處理相關結點的引用鏈 * 時間復雜度:O(1) * @return */ public void deleteLast() throws Exception { if(isEmpty()){ throw new Exception("鏈表為空"); } Node secondLast = rear.prev; rear.prev = null; rear = secondLast; if(rear == null){ first = null; }else{ rear.next = null; } size--; }
其他操作同理,在過程中需要同時保持對結點的前驅結點和后繼結點的引用,刪除操作時,需要注意解除廢棄結點的各種引用,便於GC。
總結
本文對數據結構的一些基本概念,邏輯結構和物理結構,線性表等概念進行了基本的闡述。同時,介紹了線性表的順序存儲結構和鏈式存儲結構,對線性表的鏈式存儲結構(單鏈表,雙端鏈表,雙向鏈表),使用Java語言做了基本實現。數據結構的重要性毋庸置疑,它是軟件設計的基石,由於自己非科班出身,雖曾自學過一段時間,也不夠系統,最近希望能重新系統地梳理下,本篇就當自己數據結構再學習的開篇吧,共勉。