1. 程式人生 > >【 C# 資料結構】(一) -------------------------- 泛型帶頭節點的單鏈表,雙向連結串列實現

【 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物