【 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; } } } }
相關推薦
【 C# 資料結構】(一) -------------------------- 泛型帶頭節點的單鏈表,雙向連結串列實現
在程式設計領域,資料結構與演算法向來都是提升程式設計能力的重點。而一般常見的資料結構是連結串列,棧,佇列,樹等。事實上C#也已經封裝好了這些資料結構,在標頭檔案 System.Collections.Generic 中,直接建立並呼叫其成員方法就行。不過我們學習當然要知其然,亦知其所以然。 本文實現的是連結
【C++併發實戰】(一)併發基本概念
什麼是併發 併發,最簡單的理解就是,兩個或者以上的活動同時進行。舉個比較實際的例子,你可以手腳並用,兩隻手做不同的動作等等。 在計算機中的“併發”,是指一個系統可以同時執行多個獨立的活動。在以前大多數計算機都只有一個處理單元(或者核心),這種計算機在同一時刻只能執行一個任務,任務
淺析C#資料結構—集合(一)
定義:結構化的資料型別 分類:可分為非線性集合和線性集合 集合的描述: 1.直接存取集合 陣列、字串、stuct
C與資料結構——概述(一)
引言: 未進職場,真正接觸產品開發之前,基礎還是才是最重要的。大學的好處就是隨心所欲的學。怎麼理解就怎麼理解,不違規,不犯法。呵呵,從今天開始的一段時間,我開始將c語言和資料結構的理解寫寫,算大學的最後珍惜了。一千個讀者就有一千個哈姆雷特。錯與對無關緊要。也許今日膚
【顆粒歸倉】(一)jQuery easyui datagrid 的資料載入
其實easyuidatagrid載入資料只有兩種方式:一種是ajax載入目標url返回的json資料;另一種是載入js物件,也就是使用loadDate方法,這種方法用於載入本地js資料(非url呼叫)。在專案中我用到的以及研究別人程式碼中用到的普遍是第一種,下
【C++ Primer 筆記】(二) 變量
class tro ++ div bsp mail post c++ 系列 本系列文章由 Nick-Pem 原創編寫,轉載請說明出處。 作者:Nick-Pem 郵箱:[email protected] 留坑【C++ Primer 筆記
資料結構排序(一)
插入排序:直接插入排序,希爾排序 直接插入排序: 穩定性:不改變相同關鍵字序列,穩定 ASL: : 解釋說明: 序 號:0 1 2 3 4 5 6 7 8 監視哨: 34 12 49 28 31 52 51 49* 第一趟: 34 第二趟: 12 34 第三趟: 12 3
【完全分散式Hadoop】(一)從虛擬機器Centos6.5的安裝開始
一、虛擬機器安裝 不做過多介紹,自行去某度搜索 二、Centos6.5配置 1. 關閉防火牆 service iptables stop 關閉防火牆開機啟動 chkconfig iptables off 2. windows中檢視VM8的IPv4 ipconfig -
資料結構學習(一):高精度演算法
高精度演算法,屬於處理大數字的數學計算方法。在一般的科學計算中,會經常算到小數點後幾百位或者更多,當然也可能是幾千億幾百億的大數字。一般這類數字我們統稱為高精度數,高精度演算法是用計算機對於超大資料的一種模擬加,減,乘,除,乘方,階乘,開方等運算。對於非常龐大的數字無法在計算機中正常儲存
學習【JavaScript權威指南】(一)
不看不得了,一看嚇一跳。。。好多簡單的東西都不懂的,寫個 $(function(){ } ); 還需要查詢下才記起來。。。 var book = { topic: "Javascript", fat: true //true, fa
資料結構淺析(一):資料結構基本概念
轉載自https://m.meiwen.com.cn/subject/kzgvhttx.html 首先會有個疑問,什麼是資料結構呢? 資料結構(data structure),可以概括為是互相之間存在一種或多種特定關係的資料元素的集合。 開篇配圖
資料結構入門(一)棧的實現
從這一篇文章開始,筆者將會正式進入資料結構的領域,後面也將會持續更新。 本文將會講述一種特殊的線性表結構:棧(stack)。 棧,是限定僅在表尾進行插入或刪除操作的線性表。因此,對棧來說,表尾端有其特殊含義,稱為棧頂(top),相應地,表頭端稱為棧底(bottom)。不含任何元素的空表稱為空棧。
【Unity 實戰記錄】(一)攝像機區域內跟隨物體
模仿Cinemachined 效果,用Cinemachine可輕鬆實現,貼程式碼 1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEngine; 4 5 public cla
【C++併發實戰】(二)執行緒管理
前一篇沒用markdown編輯器感覺不好看,刪了重新發 本篇主要講述執行緒的管理,主要包括建立和使用執行緒 啟動執行緒 執行緒出現是為了執行任務,執行緒建立時會給一個入口函式,當這個函式返回時,該執行緒就會退出,最常見的main()函式就是主執行緒的入口函式,在main()函式返回時主執行緒就結束了。 如
【GANs學習筆記】(一)初步瞭解GANs
** 第一章 初步瞭解GANs ** ** 1. 生成模型與判別模型 ** 理解對抗網路,首先要了解生成模型和判別模型。判別模型比較好理解,就像分類一樣,有一個判別界限,通過這個判別界限去區分樣本。從概率角度分析就是獲得樣本x屬於類別y的概率,是一個條件概率P(
【PhotonEngine 學習筆記】(一)簡單使用
【PhotonEngine 學習筆記】(一)簡單使用 前言 PhotonEngine簡介 Photon SDKs SDK:SELF-HOSTED 下載安裝 伺服器端邏輯 建立自己的伺服器專案(類庫)
資料結構::迷宮(一)--棧的一個應用
【前情描述】:我們先來看一張圖片: (在這張圖片裡我們用“1”來表示牆,用“0”來表示通路。紅色方塊表示入口點,綠色方塊表示出路) 我們要從迷宮的出口開始走找出路,即紅色走到綠色,那麼怎麼解決這個問題呢?彆著急,往下看,聽我細細講解: 【解決迷宮問題】: 方法一:利
【JVM學習筆記】(一)jvm初體驗-記憶體溢位問題分析及解決方案
####1、開始 建立Main類和Demo類,在Main類的main方法中建立List,並向List中無限建立Demo物件,造成記憶體溢位, 並輸出記憶體溢位錯誤檔案在專案目錄下,為了使等待時間減小,設定執行堆記憶體大小。 ####2、建立Demo類 package com.ch
【OkHttp3原始碼分析】(一)Request的execute
簡單使用OkHttp3 閱讀本文需要對OkHttp3的使用有一定了解。 首先我們先看看如何簡單進行一個get請求的Request。 Request qqRequest = new Request.Builder()
【C++併發實戰】(三) std::future和std::promise
std::future和std::promise std::future std::future期待一個返回,從一個非同步呼叫的角度來說,future更像是執行函式的返回值,C++標準庫使用std::future為一次性事件建模,如果一個事件需要等待特定的一次性事件,那麼這執行緒可以獲取一個future物