線性結構:
一、概念
- 線性結構作為最常用的數據結構,其特點是數據元素之間存在一對一的線性關系。
- 線性結構擁有兩種不同的存儲結構,即順序存儲結構和鏈式存儲結構。順序存儲的線性表稱為順序表,順序表中的存儲元素是連續的,鏈式存儲的線性表稱為鏈表,鏈表中的存儲元素不一定是連續的,元素節點中存放數據元素以及相鄰元素的地址信息。
- 線性結構中存在兩種操作受限的使用場景,即隊列和棧。棧的操作只能在線性表的一端進行,就是我們常說的先進后出(FILO),隊列的插入操作在線性表的一端進行而其他操作在線性表的另一端進行,先進先出(FIFO),由於線性結構存在兩種存儲結構,因 此隊列和棧各存在兩個實現方式。
二、部分實現
- 順序表(順序存儲)
按照我們的習慣,存放東西時,一般是找一塊空間,然后將需要存放的東西依次擺放,這就是順序存儲。計算機中的順序存儲是指在內存中用一塊地址連續的空間依次存放數據元素,用這種方式存儲的線性表叫順序表其特點是表中相鄰的數據元素在內存中存儲位置也相鄰,如下圖:1 // 倒置線性表 2 public void Reverse() 3 { 4 T tmp = default(T); 5 6 int len = GetLength() - 1; 7 for (int i = 0; i <= len / 2; i++) 8 { 9 if (i.Equals(len - i)) 10 { 11 break; 12 } 13 14 tmp = data[i]; 15 data[i] = data[len - i]; 16 data[len - i] = tmp; 17 } 18 }
- 鏈表(鏈式存儲)
假如我們現在要存放一些物品,但是沒有足夠大的空間將所有的物品一次性放下(電腦中使用鏈式存儲不是因為內存不夠先事先說明一下...,具體原因后續會說到),同時設定我們因為腦容量很小,為了節省空間,只能記住一件物品位置。此時我們很機智的找到了解決方案:存放物品時每放置一件物品就在物品上貼一個小紙條,標明下一件物品放在那里,只記住第一件物品的位置,尋找的時候從第一件物品開始尋找,通過小紙條我們可以找到所有的物品,這就是鏈式存儲。鏈表實現的時候不再像線性表一樣只存儲數據即可,還有下一個數據元素的地址,因此先定義一個節點類(Node),記錄物品信息和下一件物品的位置,我們把物品本身叫做數據域,存儲下一件物品地址信息的小紙條稱為引用域。鏈表結構示意圖如下:尋找物品的時候發現了一個問題,我們從一件物品找下一件物品的時候很容易,但是如果要找上一件物品就得從頭開始找,真的很麻煩。為了解決這個問題我們又機智了一把,模仿之前的做法,在存放物品的時候多放置一個小紙條記錄上一件物品的位置,這樣就可以很快的找到上一件物品了。我們把這種方式我們稱為雙向鏈表,前面只放置一張小紙條的方式稱為單向鏈表。
1 // 倒置單鏈表 2 public void Reverse() 3 { 4 Node<T> oldHead = Head; 5 Node<T> tmp ; 6 Head = null; //清空鏈表,解除Head跟oldHead之間的相同引用 7 8 while (oldHead != null) 9 { 10 tmp = Head; 11 Head = oldHead; 12 //解除Head跟oldHead之間的相同引用 13 oldHead = oldHead.Next; 14 Head.Next = tmp; 15 } 16 }
由於數據存儲結構不同導致使用場景上的巨大差異,順序表由於元素連續具有隨機存儲的特點,所以查找數據很方便效率很高,但是插入、刪除操作為了確保數據元素連續,需要移動大量的數據導致效率很低。而鏈表由於存儲空間不要求連續,插入、刪除只需修改相鄰元素的引用域地址即可,所以效率很高,但查詢需要從頭引用開始遍歷鏈表,效率很低。因此,如果只是進行查找操作而不經常插入、刪除線性表中的數據元素,則使用順序存儲結構,反之,使用鏈式存儲結構。
- 棧
其實成功完成順序表和鏈表之后,棧已經沒太多可說的了,主要是邏輯上的不同,畢竟棧也是一種特殊的線性結構。棧是一種操作限定在表尾部進行的線性表,表尾稱為棧頂(Top),另一端固定不動,稱為棧底(Bottom)。進棧、出棧示意圖如下:
1 //鏈棧入駐 2 public void Push(T item) 3 { 4 Node<T> tmp = new Node<T>(item); 5 if (Top == null) 6 { 7 Top = tmp; 8 } 9 else 10 { 11 tmp.Next = Top; 12 Top = tmp; 13 } 14 Num++; 15 } 16 17 //順序棧入棧 18 public void Push(T item) 19 { 20 if (IsFull()) 21 { 22 throw new Exception("Stack is full"); 23 } 24 25 data[++Top] = item; 26 }
- 隊列
隊列與棧類似,僅僅是邏輯有一丟丟不同。隊列是一種插入操作限定在表尾其他操作限定在表頭的線性表。把進行插入操作的表尾稱為隊尾(Rear),把進行其它操作的頭部稱為隊首(Front)。入隊、出隊示意圖如下:1 //鏈隊入隊 2 public void In(T item) 3 { 4 Node<T> node = new Node<T>(item); 5 if (Rear == null) 6 { 7 Rear = node; 8 Front = Rear; 9 } 10 else 11 { 12 Rear.Next = node; 13 Rear = Rear.Next; 14 } 15 ++num; 16 } 17 18 //循環隊列入隊 19 public void In(T item) 20 { 21 if (IsFull()) 22 { 23 throw new Exception("Queue is full"); 24 } 25 data[++Rear] = item; 26 }
非線性結構:
一、相關概念
樹作為一種應用廣泛的一對多非線性數據結構,不僅有數據間的指向關系,還有層級關系,示例見圖一。因樹的結構比較復雜,為了簡化操作及存儲,我們一般將樹轉換為二叉樹處理,因此本文主要討論二叉樹。
- 二叉樹
二叉樹是每個節點最多擁有兩個子節點的樹結構,若移除根節點則其余節點會被分成兩個互不相交的子樹,分別稱為左子樹和右子樹。二叉樹是有序樹,左右子樹有嚴格的次序,若顛倒則成為一棵不一樣的二叉樹。 - 滿二叉樹
滿二叉樹,顧名思義除葉子節點外所有節點都擁有兩個孩子,且葉子節點在同一層的二叉樹,示例見圖二。 - 完全二叉樹
完全二叉樹,移除最后一層節點后是滿二叉樹,且最后一層的節點都連續集中在最左面,示例見圖三。
二、二叉樹存儲結構
- 順序存儲
根據完全二叉樹的特性,可以計算出任意節點n的雙親節點及左右孩子節點的序號,因此完全二叉樹的節點可以按照從上到下從左到右的順序依次存儲到一維數組中。非完全二叉樹存儲時應先將其改造為完全二叉樹,以空替代不存在的節點,比較浪費存儲空間,存儲示意圖見圖四。 - 鏈式存儲
樹結構鏈式存儲類似線性結構鏈式存儲,先定義包含數據域和引用域的節點(Node),然后通過引用域存儲節點之間的關系。根據二叉樹的結構來看,節點Node至少包含數據域(Data),引用域(左孩子LChild、右孩子RChild),為了方便通過孩子節點查找父節點,引用域中可以考慮添加父節點引用(Parent),存儲示意圖見圖五。
三、樹與二叉樹的轉換
- 樹轉二叉樹
加線,所有兄弟結點之間加一條連線。
抹線,對樹中的每個結點,只保留他與第一個孩子結點之間的連線,刪除它與其它孩子結點之間的連線。
整理,整理前兩步得到的樹,使之結構層次分明。 - 二叉樹轉樹
加線,若某結點的左孩子結點存在,將左孩子結點的右孩子結點、右孩子結點的右孩子結點……都作為該結點的孩子結點,將該結點與這些右孩子結點用線連接起來。
抹線,刪除原二叉樹中所有結點與其右孩子結點的連線。
整理,整理前兩步得到的樹,使之結構層次分明。
四、樹遍歷實現
1 /// <summary> 2 /// 先序遍歷(DLR) 3 /// </summary> 4 /// <![CDATA[首先訪問跟節點,然后遍歷左子樹,最后右子樹]]> 5 static void PreOrder(Node<char> root) 6 { 7 if (root == null) 8 { 9 return; 10 } 11 12 Print(root); 13 PreOrder(root.LChild); 14 PreOrder(root.RChild); 15 } 16 17 /// <summary> 18 /// 中序遍歷(LDR) 19 /// </summary> 20 /// <![CDATA[先遍歷左子樹,然后根節點,最后遍歷右子樹]]> 21 static void InOrder(Node<char> root) 22 { 23 if (root == null) 24 { 25 return; 26 } 27 28 InOrder(root.LChild); 29 Print(root); 30 InOrder(root.RChild); 31 } 32 33 /// <summary> 34 /// 后序遍歷(LRD) 35 /// </summary> 36 /// <![CDATA[先遍歷左子樹,然后遍歷右子樹,最后遍歷根節點]]> 37 static void PostOrder(Node<char> root) 38 { 39 if (root == null) 40 { 41 return; 42 } 43 44 PostOrder(root.LChild); 45 PostOrder(root.RChild); 46 Print(root); 47 } 48 49 /// <summary> 50 /// 層序遍歷 51 /// </summary> 52 /// <![CDATA[從上向下從左到右]]> 53 static void LevelOrder(Node<char> root) 54 { 55 if (root == null) 56 { 57 return; 58 } 59 CSeqQueue<Node<char>> sq = new CSeqQueue<Node<char>>(50); 60 sq.In(root); 61 while (!sq.IsEmpty()) 62 { 63 Node<char> tmp = sq.Out(); 64 Print(tmp); 65 66 if (tmp.LChild != null) 67 { 68 sq.In(tmp.LChild); 69 } 70 71 if (tmp.RChild != null) 72 { 73 sq.In(tmp.RChild); 74 } 75 } 76 }