【 C# 資料結構】(一) -------------------------- 泛型帶頭節點的單鏈表,雙向連結串列實現
在程式設計領域,資料結構與演算法向來都是提升程式設計能力的重點。而一般常見的資料結構是連結串列,棧,佇列,樹等。事實上C#也已經封裝好了這些資料結構,在標頭檔案 System.Collections.Generic 中,直接建立並呼叫其成員方法就行。不過我們學習當然要知其然,亦知其所以然。
本文實現的是連結串列中的單鏈表和雙向連結串列,並且實現了一些基本方法
一. 定義一個連結串列介面MyList
接口裡聲明瞭我們要實現的方法:
interface MyList<T> { int GetLength();//獲取連結串列長度 void Clear();//清空連結串列 bool IsEmpty();//判斷連結串列是否為空 void Add(T item);//在連結串列尾部新增新節點 void AddPre(T item,int index);//在指定節點前新增新節點 void AddPost(T item,int index);//在指定節點後新增新節點 T Delete(int index);//按索引刪除節點 T Delete(T item,bool isSecond = true);//按內容刪除節點,如果有多個內容相同點,則刪除第一個 T this[int index] { get; }//實現下標訪問 T GetElem(int index);//根據索引返回元素 int GetPos(T item);//根據元素返回索引地址 void Print();//列印 }
二. 實現單鏈表
2.1 節點類
先定義一個單鏈表所用的節點類, Node。 而且我們要實現泛型
先定義一個數據域和下一節點(“Next”),並進行封裝,然後給出數個過載構造器。這一步比較簡單,這裡直接給出程式碼
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 線性表 { /// <summary> /// 單向連結串列節點 /// </summary> /// <typeparam name="T"></typeparam> class Node<T> { private T data;//內容域 private Node<T> next;//下一節點 public Node() { this.data = default(T); this.next = null; } public Node(T value) { this.data = value; this.next = null; } public Node(T value,Node<T> next) { this.data = value; this.next = next; } public T Data { get { return data; } set { data = value; } } public Node<T> Next { get { return next; } set { next = value; } } } }
2.2 連結串列類
建立一個連結串列類,命名為LinkList 並繼承MyList 。
先定義一個頭結點,尾節點和一個 count;
其中,head 表示該連結串列的頭部,不包含資料;
tail 表示尾節點,指向該連結串列最後一個節點,當連結串列中只有 head 時,tail 指向 head。定義 tail 會方便接下來的操作
count 用來表示該連結串列中除了 head 以外的節點個數
建構函式:
/// <summary> /// 構造器 /// </summary> public LinkList() { head = new Node<T>(); tail = head; count = 0; }
在我們實現成員函式之前,先實現兩個特別的方法,因為在許多的成員方法中都要做兩個操作:
- 判斷索引 index 是否合法,即是否小於0或者大於當前連結串列的節點個數
- 尋找到 index 所代表的節點
①. 判斷索引是否合法,然後可以根據其返回的數值進行判斷操作
②. 尋找節點。
定義這兩個方法主要是它們的重複使用率高,所以把它們的程式碼抽出來。
相對於陣列,連結串列的插入與刪除更方便,而查詢卻更加費時,一般都是從頭結點開始遍歷連結串列,時間複雜度為 O(n) ,而跳躍連結串列 則會對查詢進行優化,當然這會在下一篇中詳述。現在繼續來實現成員方法。
1. 獲取連結串列長度
這個方法實際上是比較簡單的,因為 count 會隨著新增,刪除等操作自動增減,所以直接返回 count 就相當於 連結串列長度。
需要注意的是,本文中的 count 是不計算空頭結點的,即 head 不會計算入內
2. 清空連結串列
這裡要注意對 tail 的操作,而 head.Next 原本所指的節點不再被引用後,會被GC自動回收
3. 判斷連結串列是否為空
因為本文實現的連結串列是帶空頭結點的,所以這裡認為,當除了頭結點外沒有別的節點時,則為空連結串列
4. 在連結串列尾部新增節點
在連結串列尾新增節點一般考慮兩種情況:
- 當前除了頭結點沒有別的節點,此時相當於建立第一個節點
- 尋找到最後一個節點
對於帶空頭結點 的連結串列來說,這兩種情況有著一樣的操作,只不過第一種情況要多做一步:讓 head 指向新建立的節點
定義了 tail 節點省去了 遍歷尋找最後節點的步驟,如果此時是空連結串列的話,tail 則指向 head
5. 在指定索引的前或後新增節點
這兩個方法的思路實際上相差無幾的
如圖,當 index 為 F 時:
- AddPost: ① 找到 F 節點 ②建立 NEW 節點;③ NEW 節點指向 G;④ F 指向 NEW 節點
- AddPre : ① 找到 E 節點 ②建立 NEW 節點;③ NEW 節點指向 F ;④ E 指向 NEW 節點
AddPre 相當於 index - 1 處的 AddPost;AddPost 相當於 index + 1 處的 AddPre(當然,這是在 index -1 與 index + 1 合法的情況下)
6. 兩種刪除節點方法
- 按索引刪除:找到索引所指節點,刪除
- 按元素刪除:找元素所在的索引;當找不到該元素時表明連結串列中不存在應該刪除的節點,不執行刪除操作;當連結串列中存在多個相同的元素時,找到並刪除第一個
兩種刪除方法操作都是相似的,只是搜尋節點的方法不同,刪除時要嚴格注意節點間指向的 ,即注意書寫程式碼時的順序
7. 實現下標訪問
這是個比較有趣的實現。前文說過對比於陣列,連結串列勝於增減,弱於訪問。對連結串列實現下標式訪問,雖然它的核心依然是遍歷連結串列,然後返回節點,但在使用上會方便許多,如同使用陣列一般。
8. 根據索引返回元素
這個和 GetNode 方法一致
9. 根據元素返回索引地址
這個方法也是比較簡單的,只是需要注意的一點是:while迴圈條件中 && 號兩端的條件不能調換位置。因為如果調換位置後,當連結串列遍歷到最後一個節點仍沒找到元素時,pstr 會被賦值下一節點(此時為NULL),然後迴圈繼續執行,執行到 !pstr.Data.Equals(item) 這一句時會報空指標,因為此時 pstr 就是空指標;還有因為這是泛型,所以判斷兩個值是否相等不能用 == 號,除非你過載 == 號。
10.列印連結串列
至此,所以的成員方法都實現了,先來測試一下。
1
.
其它功能讀者可以自行測試,完整程式碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 線性表 { class LinkList<T> : MyList<T> { private Node<T> head;//頭結點 private Node<T> tail;//尾節點 private int count;//節點個數 /// <summary> /// 構造器 /// </summary> public LinkList() { head = new Node<T>(); tail = head; count = 0; } /// <summary> /// 實現下標訪問法 /// </summary> /// <param name="index"></param> /// <returns></returns> public T this[int index] { get { int i = IsIndexVaild(index); if(i == -1) return default(T); int k = 0; Node<T> pstr = head; while (k++ < index ) { pstr = pstr.Next; } return pstr.Data; } } /// <summary> /// 在連結串列最末端新增新節點 /// </summary> /// <param name="item"></param> public void Add(T item) { Node<T> tailNode = new Node<T>(item); tail.Next = tailNode; tail = tailNode; if (count == 0) head.Next = tailNode; count++; } /// <summary> /// 在第 index 號元素後插入一個節點 /// <param name="item"></param> /// <param name="index"></param> public void AddPost(T item, int index) { int i = IsIndexVaild(index); if (i == -1) return; //找到索引元素 Node<T> pstr = GetNode(index); //連結新節點 Node<T> node = new Node<T>(item); node.Next = pstr.Next; pstr.Next = node; if (index == count) tail = node; count++; pstr = null; } /// <summary> /// 在第 index 號元素前插入一個節點 /// </summary> /// <param name="item"></param> /// <param name="index"></param> public void AddPre(T item, int index) { int i = IsIndexVaild(index); if (i == -1) return; //找到索引的前一位元素 Node<T> pstr = GetNode(index - 1); //連結新節點 Node<T> node = new Node<T>(item); node.Next = pstr.Next; pstr.Next = node; count++; pstr = null; } /// <summary> /// 清空連結串列 /// </summary> public void Clear() { head.Next = null; tail = head; } /// <summary> /// 刪除指定位置的元素 /// </summary> /// <param name="index"></param> /// <returns></returns> public T Delete(int index) { int i = IsIndexVaild(index); if (i == -1) return default(T); //找到索引的前一位元素 Node<T> pstr = GetNode(index - 1); if (pstr.Next == null) return default(T); Node<T> qstr = pstr.Next; pstr.Next = qstr.Next; T t = qstr.Data; pstr = null; qstr.Next = null; qstr = null; count--; return t; } /// <summary> /// 按內容刪除 /// </summary> /// <param name="item"></param> /// <param name="isSecond"></param> /// <returns></returns> public T Delete(T item,bool isSecond = true) { int k = GetPos(item); if (k == -1) return default(T); int i = 0; Node<T> pstr = head; while (i++ < k -1) { pstr = pstr.Next; } Node<T> qstr = pstr.Next; pstr.Next = qstr.Next; T t = qstr.Data; pstr = null; qstr.Next = null; qstr = null; count--; return t; } /// <summary> /// 返回指定索引的元素 /// </summary> /// <param name="index"></param> /// <returns></returns> public T GetElem(int index) { int i = IsIndexVaild(index); if (i == -1) return default(T); return GetNode(index).Data; } /// <summary> /// 返回連結串列長度 /// </summary> /// <returns></returns> public int GetLength() { return count; } /// <summary> /// 根據元素返回其索引值 /// </summary> /// <param name="item"></param> /// <returns></returns> public int GetPos(T item) { int k = 0; Node<T> pstr = head.Next; while (pstr != null && item != null && !pstr.Data.Equals(item)) { pstr = pstr.Next; k++; } if (pstr == null) { Console.WriteLine("所查詢元素不存在"); return -1; } return k ; } /// <summary> /// 判斷連結串列是否為空 /// </summary> /// <returns></returns> public bool IsEmpty() { if (head == null || head.Next == null) return true; return false; } /// <summary> /// 列印 /// </summary> public void Print() { Node<T> pstr = head.Next; int i = 1; while(pstr != null) { Console.WriteLine("第 " + i++ + "個元素是: " + pstr.Data); pstr = pstr.Next; } } /// <summary> /// 判斷索引是否錯誤 /// </summary> /// <param name="index"></param> /// <returns></returns> public int IsIndexVaild(int index) { //判斷索引是否越界 if (index < 0 || index > count) { Console.WriteLine("索引越界,不存在該元素"); return -1; } return 0; } /// <summary> /// 根據索引找到元素 /// </summary> /// <param name="index"></param> /// <returns></returns> public Node<T> GetNode(int index) { int k = 0; Node<T> pstr = head; while (k++ < index) { pstr = pstr.Next; } return pstr; } } }
三. 雙向連結串列
雙向連結串列在思路上和單鏈表差不多,只是多了一個指向上一個節點的 Prev,所以程式碼上要更小心地處理。具體就不多贅述了,直接給出程式碼吧
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 線性表 { class DBNode<T> { private T data; private DBNode<T> next; private DBNode<T> prev; public DBNode() { this.data = default(T); this.next = null; this.prev = null; } public DBNode(T value) { this.data = value; this.next = null; this.prev = null; } public DBNode(T value, DBNode<T> next) { this.data = value; this.next = next; this.prev = null; } public T Data { get { return data; } set { data = value; } } public DBNode<T> Next { get { return next; } set { next = value; } } public DBNode<T> Prev { get { return prev; } set { prev = value; } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 線性表 { class DBLinkList<T> : MyList<T> { private DBNode<T> head; private DBNode<T> tail; private int count; /// <summary> /// 構造器 /// </summary> public DBLinkList() { head = new DBNode<T>(); tail = head; count = 0; } /// <summary> /// 實現下標訪問法 /// </summary> /// <param name="index"></param> /// <returns></returns> public T this[int index] { get { int i = IsIndexVaild(index); if (i == -1) return default(T); int k = 0; DBNode<T> pstr = head; while (k++ < index) { pstr = pstr.Next; } return pstr.Data; } } /// <summary> /// 在連結串列最末端新增新節點 /// </summary> /// <param name="item"></param> public void Add(T item) { if (count == 0) { DBNode<T> DbNode = new DBNode<T>(item); DbNode.Prev = head; head.Next = DbNode; tail = DbNode; count++; return; } DBNode<T> tailDBNode = new DBNode<T>(item); tailDBNode.Prev = tail; tail.Next = tailDBNode; tail = tailDBNode; count++; } /// <summary> /// 在第 index 號元素後插入一個節點,index 為 1,2,3,4..... /// </summary> /// <param name="item"></param> /// <param name="index"></param> public void AddPost(T item, int index) { //判斷索引是否越界 int i = IsIndexVaild(index); if (i == -1) return; //找到索引元素 DBNode<T> pstr = GetNode(index); //連結新節點 DBNode<T> newNode = new DBNode<T>(item); newNode.Next = pstr.Next; newNode.Prev = pstr; if(pstr.Next != null) pstr.Next.Prev = newNode; pstr.Next = newNode; //如果是在最後節點新增 if (index == count) tail = newNode; count++; pstr = null; } /// <summary> /// 在第 index 號元素前插入一個節點,index 為 1,2,3,4..... /// </summary> /// <param name="item"></param> /// <param name="index"></param> public void AddPre(T item, int index) { //判斷索引是否越界 int i = IsIndexVaild(index); if (i == -1) return; //找到索引的前一位元素 DBNode<T> pstr = GetNode(index - 1); //連結新節點 DBNode<T> newNode = new DBNode<T>(item); newNode.Next = pstr.Next; newNode.Prev = pstr; pstr.Next.Prev = newNode; pstr.Next = newNode; count++; pstr = null; //在 index 處AddPre相當於在 index - 1 處 AddPost,不過並不需要判斷尾節點 } /// <summary> /// 清空連結串列 /// </summary> public void Clear() { head.Next = null; tail = head; } /// <summary> /// 刪除指定位置的元素 /// </summary> /// <param name="index"></param> /// <returns></returns> public T Delete(int index) { //判斷索引是否越界 int i = IsIndexVaild(index); if (i == -1) return default(T); //找到索引的前一位元素 DBNode<T> pstr = head; int k = 0; while (k++ < index - 1 && pstr != null) { pstr = pstr.Next; } if (pstr.Next == null) return default(T); DBNode<T> qstr = pstr.Next; T t = qstr.Data; pstr.Next = qstr.Next; qstr.Next.Prev = pstr; pstr = null; qstr.Next = null; qstr = null; count--; return t; } /// <summary> /// 按內容刪除 /// </summary> /// <param name="item"></param> /// <param name="isSecond"></param> /// <returns></returns> public T Delete(T item,bool isSecond = true) { int k = GetPos(item); if (k == -1) return default(T); int i = 0; DBNode<T> pstr = head; while (i++ < k - 1) { pstr = pstr.Next; } DBNode<T> qstr = pstr.Next; T t = qstr.Data; pstr.Next = qstr.Next; if(qstr.Next != null) qstr.Next.Prev = pstr; pstr = null; qstr.Next = null; qstr = null; count--; return t; } /// <summary> /// 返回指定索引的元素 /// </summary> /// <param name="index"></param> /// <returns></returns> public T GetElem(int index) { int i = IsIndexVaild(index); if (i == -1) return default(T); int k = 0; DBNode<T> pstr = head; while (k++ < index) { pstr = pstr.Next; } return pstr.Data; } /// <summary> /// 返回連結串列長度 /// </summary> /// <returns></returns> public int GetLength() { return count; } /// <summary> /// 根據元素返回其索引值 /// </summary> /// <param name="item"></param> /// <returns></returns> public int GetPos(T item) { int k = 0; DBNode<T> pstr = head.Next; while (pstr != null && item != null && !pstr.Data.Equals(item)) { pstr = pstr.Next; k++; } if (pstr == null) { Console.WriteLine("所查詢元素不存在"); return -1; } return k; } /// <summary> /// 判斷連結串列是否為空 /// </summary> /// <returns></returns> public bool IsEmpty() { if (head == null || head.Next == null) return true; return false; } /// <summary> /// 列印 /// </summary> public void Print() { DBNode<T> pstr = head.Next; while (pstr != null) { Console.WriteLine(pstr.Data); pstr = pstr.Next; } } /// <summary> /// 判斷索引是否錯誤 /// </summary> /// <param name="index"></param> /// <returns></returns> public int IsIndexVaild(int index) { //判斷索引是否越界 if (index < 0 || index > count) { Console.WriteLine("索引越界,不存在該元素"); return -1; } return 0; } /// <summary> /// 根據索引找到元素 /// </summary> /// <param name="index"></param> /// <returns></returns> public DBNode<T> GetNode(int index) { int k = 0; DBNode<T> pstr = head; while (k++ < index) { pstr = pstr.Next; } return pstr; } } }
總結
事實上,連結串列是一種比較簡單且常用的資料結構。實現起來並不困難,只是要小心謹慎。下一篇會說到跳躍連結串列 ,跳躍連結串列的效率更高。好了,希望本文能對大家有所幫助