數據結構,你還記得嗎(中)

2000年6月,微軟公司發佈了一種新的編程語言C#,主要由安德斯·海爾斯伯格(Anders Hejlsberg)主持開發,它是第一個面向組件的編程語言,其源碼會編譯成msil(中間語言)再運行。
  C#是一種安全的、穩定的、簡單的、優雅的,由C和C++衍生出來的面向對象的編程語言。它在繼承C和C++強大功能的同時去掉了一些它們的複雜特性(例如沒有宏以及不容許多重繼承)。C#綜合了VB簡單的可視化操做和C++的高運行效率,以其強大的操做能力、優雅的語法風格、創新的語言特性和便捷的面向組件編程的支持成爲.NET開發的首選語言。
  接下來,我會介紹C#的數據結構。

跟上一篇《數據結構,你還記得嗎(上)》目錄進行一一對應講解C#中各類數據結構,以此來提高理解。html

數組

同一類型和不一樣類型的多個對象

  • 同一類型多個對象
    • 可使用集合和數組管理。
    • C#用特殊的記號聲明、初始化和使用數組。
    • Array類在後臺發揮做用,它爲數組中元素的排序和過濾提供了幾個方法。
    • 使用枚舉器,能夠迭代數組中的全部元素。
  • 不一樣類型多個對象
    • 可使用Tuple(元組)類型管理。

數組類型

  • 一維數組
  • 多維數組
  • 鋸齒數組
多維數組,行和列是固定的:
        int[][] arrMore=new int[3][6];

        鋸齒數組只要在第一個方括號設置行數,每行的個數是可變的。
        int[][] jagged=new int[3][];
        jagged[0]=new int[2]{1,2};
        jagged[1]=new int[6]{1,2,3,4,5,6};
        jagged[2]=new int[3]{1,2,3};

數組演變

1. Array

  Array 類是 C# 中全部數組的基類,它是在 System 命名空間中定義(System.Array)。Array 類提供了各類用於數組的屬性和方法。
用方括號[] 聲明數組是C#中使用Array類的表示法。在後臺使用C#語法,會建立一個派生自抽象基類Array的新類。這樣,就可使用Array類爲每一個C#數組定義的方法和屬性了。算法

  • 數組存儲在連續的內存上。
  • 數組的內容都是相同類型。
  • 數組能夠直接經過下標訪問。數據庫

     建立一個新的數組時將在 CLR 託管堆中分配一塊連續的內存空間,來存放數量爲n,類型爲所聲明類型的數組元素。若是類型爲值類型,則將會有n個未裝箱的該類型的值被建立。若是類型爲引用類型,則將會有n個相應類型的引用被建立。編程

  • 優勢
    因爲是在連續內存上存儲的,因此它的索引速度很是快,訪問一個元素的時間是恆定的也就是說與數組的元素數量無關,並且賦值與修改元素也很簡單。
  • 缺點
    因爲是連續存儲,因此在兩個元素之間插入新的元素就變得不方便。並且就像上面的代碼所顯示的那樣,聲明一個新的數組時,必須指定其長度,這就會存在一個潛在的問題,那就是當咱們聲明的長度過長時,顯然會浪費內存,當咱們聲明長度太短的時候,則面臨這溢出的風險。有點投機,針對這種缺點,引出了ArrayList。c#

2. ArrayList

  爲了解決數組建立時必須指定長度以及只能存放相同類型的缺點而推出的數據結構。ArrayList是System.Collections命名空間下的一部分,因此若要使用則必須引入System.Collections。正如上文所說,ArrayList解決了數組的一些缺點。數組

  • 優勢
  1. 沒必要在聲明ArrayList時指定它的長度,這是因爲ArrayList對象的長度是按照其中存儲的數據來動態增加與縮減的。
  2. ArrayList能夠存儲不一樣類型的元素。這是因爲ArrayList會把它的元素都當作Object來處理。於是,加入不一樣類型的元素是容許的。
  • 缺點
  1. ArrayList不是類型安全的。由於把不一樣的類型都當作Object來作處理,頗有可能會在使用ArrayList時發生類型不匹配的狀況。
  2. 數組存儲值類型時並未發生裝箱,可是ArrayList因爲把全部類型都當作了Object,因此不可避免的當插入值類型時會發生裝箱操做,在索引取值時會發生拆箱操做。這麼多缺點,固然不能忍,這開始引出了List 泛型List 

3. List泛型

  爲了解決ArrayList不安全類型與裝箱拆箱的缺點,因此出現了泛型的概念,做爲一種新的數組類型引入。也是工做中常常用到的數組類型。和ArrayList很類似,長度均可以靈活的改變,最大的不一樣在於在聲明List集合時,咱們同時須要爲其聲明List集合內數據的對象類型,這點又和Array很類似,其實List 內部使用了Array來實現,內部的容量成本擴展。 安全

  • 優勢
  1. 即確保了類型安全。
  2. 也取消了裝箱和拆箱的操做。
  3. 它融合了Array能夠快速訪問的優勢以及ArrayList長度能夠靈活變化的優勢。
  • 缺點
  1. 因爲內部使用Array實現,因此一樣繼承了Array的缺點,在兩個元素之間插入新的元素就變得不方便。 由此引出鏈表的概念。

4. 其餘的列表

有序列表 SortedList<Tkey,TElement> 只容許每一個鍵有一個對應的值,若是須要每一個鍵對應多個值,就須要使用Lookup<Tkey,TElement>數據結構

動態建立數組

  Array類是一個抽象類,因此不能使用構造函數來建立數組。但除了可使用C#語法建立數組實例以外,還可使用靜態方法CreateInstance()建立數組。若是事先不知道元素的類型,該靜態方法就很是有用,由於類型能夠做爲Type對象傳遞給CreateInstance()方法。多線程

例如:
  Array arr=Array.CeateInstance(typeof(int),5);
  for(int i=0;i<5;i++)
   { 
     arr.SetVaule(i,i);
   }
  for(int i=0;i<5;i++)
   { 
     int  vaule=arr.getVaule(i);
   }

羽毛球筒編程語言

Stack

  堆棧(Stack)表明了一個後進先出的對象集合。當您須要對各項進行後進先出的訪問時,則使用堆棧。當您在列表中添加一項,稱爲推入元素,當您從列表中移除一項時,稱爲彈出元素。

  • 棧以及泛型棧 
public class Stack<T> : IEnumerable<T>, ICollection, IEnumerable

public class Stack : ICollection, IEnumerable, ICloneable
屬性 描述
Count 獲取 Stack 中包含的元素個數
方法 描述
Pop public virtual object Pop();移除並返回在 Stack 的頂部的對象
push public virtual void Push(object obj);向 Stack 的頂部添加一個對象
peek public virtual object Peek();返回在 Stack 的頂部的對象,但不移除它
ToArray public virtual object[] ToArray();建立數組並將堆棧元素複製到其中
Contains public virtual bool Contains(object obj);判斷一個元素是否在棧中
Clear public virtual void Clear();從 Stack 中移除全部的元素。

隊列

水管子

Queue

隊列(Queue)表明了一個先進先出的對象集合。當您須要對各項進行先進先出的訪問時,則使用隊列。當您在列表中添加一項,稱爲入隊,當您從列表中移除一項時,稱爲出隊。

  • 泛型隊列
public class Queue<T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>, ICollection
屬性 描述
Count 獲取 Queue 中包含的元素個數
方法 描述
Clear public virtual void Clear(); 從 Queue 中移除全部的元素。
Contains public virtual bool Contains( object obj ); 判斷某個元素是否在 Queue 中。
Dequeue public virtual object Dequeue();移除並返回在 Queue 的開頭的對象。
Enqueue public virtual void Enqueue( object obj ); 向 Queue 的末尾添加一個對象。
ToArray public virtual object[] ToArray();複製 Queue 到一個新的數組中。
TrimToSize public virtual void TrimToSize();設置容量爲 Queue 中元素的實際個數。

鏈表

單鏈表

  • 啥是單鏈表?
     單鏈表是一種鏈式存取的數據結構,用一組地址任意的存儲單元存放線性表中的數據元素。這組存儲單元既能夠是連續的,也能夠是不連續的。
     鏈表中的數據是以結點來表示的,每一個結點的構成:元素(數據元素的映象) + 指針(指示後繼元素存儲位置),元素就是存儲數據的存儲單元,指針就是鏈接每一個結點的地址數據。
  • 鏈表的結點結構
    ┌───┬───┐
    │data│next │
    └───┴───┘
     data域--存放結點值的數據域[元素]
     next域--存放結點的直接後繼的地址(位置)的指針域(鏈域)[指針]
實現方式
public class Node<T>
{
    public T Data { set; get; }          //數據域,當前結點數據
    public Node<T> Next { set; get; }    //位置域,下一個結點地址

    public Node(T item)
    {
        this.Data = item;
        this.Next = null;
    }

    public Node()
    {
        this.Data = default(T);
        this.Next = null;
    }
}
  • 優缺點
  1. 既然鏈表最大的特色就是存儲在內存的空間不必定連續,那麼鏈表相對於數組最大優點和劣勢就顯而易見了。
  2. 向鏈表中插入或刪除節點無需調整結構的容量。由於自己不是連續存儲而是靠各對象的指針所決定,因此添加元素和刪除元素都要比數組要有優點。
  3. 鏈表適合在須要有序的排序的情境下增長新的元素,這裏還拿數組作對比,例如要在數組中間某個位置增長新的元素,則可能須要移動移動不少元素,而對於鏈表而言可能只是若干元素的指向發生變化而已。
  4. 有優勢就有缺點,因爲其在內存空間中不必定是連續排列,因此訪問時候沒法利用下標,而是必須從頭結點開始,逐次遍歷下一個節點直到尋找到目標。因此當須要快速訪問對象時,數組無疑更有優點。

    綜上,鏈表適合元素數量不固定,須要兩端存取且常常增減節點的狀況。

請轉到《數據結構:單鏈表》查看更詳細內容!

雙向鏈表

  LinkedList C#封裝的是一個雙向鏈表,其元素會指向它前面和後面的元素。這樣,經過移動到下一個元素能夠正向遍歷鏈表,經過移動到前一個元素能夠反向遍歷鏈表。

鏈表在存儲元素時,不只要存儲元素的值,還必須存儲每一個元素的下一個元素和上一個元素的信息。這就是LinkedList 包含LinkedListNode 類型的元素的緣由。使用LinkedListNode ,能夠得到列表中的下一個和上一個元素。LinkedListNode 定義了屬性List,Next,Previous和Value。List屬性返回與節點相關的LinkedList 對象。Next和Previous屬性用於遍歷鏈表,訪問當前節點以後和以前的節點。Value屬性返回與節點相關的元素,其類型是T。
  鏈表的優勢是,若是將元素插入到列表的中間位置,使用鏈表就會很快。在插入一個元素時,只須要修改上一個元素的Next引用和下一個元素的Previous引用,使它們引用所插入的元素。在List 中,插入一個元素,須要移動該元素後面的因此元素。
  鏈表的缺點是,鏈表元素只能一個接一個的訪問,這須要較長時間來查找位於鏈表中間或尾部的元素。
LinkedList 類定義的成員能夠訪問鏈表中的第一個和最後一個元素(First和Last);
  在指定位置插入元素:AddAfter(),AddFirst()和AddLast();
  刪除指定位置的元素:Remove(),RemoveFirst(),RemoveLast();
  搜索:Find(),FindLast()。


菜單樹

C#中沒有實現樹的具體類,通常能夠經過本身實現。
結點樹包含:父結點(根結點的父結點爲null)、子結點(List集合)、數據對象。

請轉到《 數據結構:樹》查看更詳細的內容!


  圖狀結構簡稱圖,是另外一種非線性結構,它比樹形結構更復雜。樹形結構中的結點是一對多的關係,結點間具備明顯的層次和分支關係。每一層的結點能夠和下一層的多個結點相關,但只能和上一層的一個結點相關。而圖中的頂點(把圖中的數據元素稱爲頂點)是多對多的關係,即頂點間的關係是任意的,圖中任意兩個頂點之間均可能相關。也就是說,圖的頂點之間無明顯的層次關係,這種關係在現實世界中大量存在。所以,圖的應用至關普遍,在天然科學、社會科學和人文科學等許多領域都有着很是普遍的應用。

c#沒有實現圖的數據結構,可是能夠本身實現,參考以下
請轉到《數據結構:圖》查看更詳細內容!


字典樹

  字典樹,又稱爲單詞查找樹,Tire數,是一種樹形結構,它是一種哈希樹的變種。

基本性質

  • 根節點不包含字符,除根節點外的每個子節點都包含一個字符
  • 從根節點到某一節點。路徑上通過的字符鏈接起來,就是該節點對應的字符串
  • 每一個節點的全部子節點包含的字符都不相同

應用場景

典型應用是用於統計,排序和保存大量的字符串(不只限於字符串),常常被搜索引擎系統用於文本詞頻統計。

c#也沒有實現字典樹,能夠本身實現,參考以下
請轉到《數據結構:字典樹》查看更詳細內容!
請轉到《字典樹(Trie樹)實現與應用》查看更詳細內容!

利用字符串的公共前綴來減小查詢時間,最大限度的減小無謂的字符串比較,查詢效率比哈希樹高。


散列表(哈希表)

哈希表(HashTable)簡述

  Hashtable是System.Collections命名空間提供的一個容器,用於處理和表現相似keyvalue的鍵值對,其中key一般可用來快速查找,同時key是區分大小寫;value用於存儲對應於key的值。Hashtable中keyvalue鍵值對均爲object類型,因此Hashtable能夠支持任何類型的keyvalue鍵值對.

什麼狀況下使用哈希表

  • 某些數據會被高頻率查詢
  • 數據量大
  • 查詢字段包含字符串類型
  • 數據類型不惟一

使用方法

  • 哈希表須要使用的namespace
using System.Collections;
using System.Collections.Generic;
  • 哈希表的基本操做:
//添加一個keyvalue鍵值對:
HashtableObject.Add(key,value);

//移除某個keyvalue鍵值對:
HashtableObject.Remove(key);

//移除全部元素:           
HashtableObject.Clear(); 

// 判斷是否包含特定鍵key:
HashtableObject.Contains(key);
  • 遍歷哈希表
遍歷哈希表須要用到DictionaryEntry Object,代碼以下:
for(DictionaryEntry de in ht) //ht爲一個Hashtable實例
{
   Console.WriteLine(de.Key);  //de.Key對應於keyvalue鍵值對key
   Console.WriteLine(de.Value);  //de.Key對應於keyvalue鍵值對value
}

請轉到《數據結構:哈希表》查看更詳細內容!

字典 Dictionary

表示索引鍵和值的集合。

[System.Runtime.InteropServices.ComVisible(false)]
[System.Serializable]
public class Dictionary<TKey,TValue> : System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>, System.Collections.Generic.IDictionary<TKey,TValue>, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey,TValue>>, System.Collections.Generic.IReadOnlyCollection<System.Collections.Generic.KeyValuePair<TKey,TValue>>, System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>, System.Collections.IDictionary, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable

  由於字典的實現方式就是哈希表的實現方式,只不過字典是類型安全的,也就是說當建立字典時,必須聲明key和item的類型。

  • 優點
    1. 各類方便操做
    2. 由於指定了類型,因此安全
  • 缺點
    以空間換時間。經過更多的內存開銷來知足咱們對速度的追求。在建立字典時,咱們能夠傳入一個容量值,但實際使用的容量並不是該值。而是使用「不小於該值的最小質數來做爲它使用的實際容量,最小是3。」(老趙),當有了實際容量以後,並不是直接實現索引,而是經過建立額外的2個數組來實現間接的索引,即int[] buckets和Entry[] entries兩個數組(即buckets中保存的實際上是entries數組的下標),這裏就是第二條字典與哈希表的區別,還記得哈希衝突嗎?對,第二個區別就是處理哈希衝突的策略是不一樣的!字典會採用額外的數據結構來處理哈希衝突,這就是剛纔提到的數組之一buckets桶了,buckets的長度就是字典的真實長度,由於buckets就是字典每一個位置的映射,而後buckets中的每一個元素都是一個鏈表,用來存儲相同哈希的元素,而後再分配存儲空間。

    所以,咱們面臨的狀況就是,即使咱們新建了一個空的字典,那麼伴隨而來的是2個長度爲3的數組。因此當處理的數據很少時,仍是慎重使用字典爲好,不少狀況下使用數組也是能夠接受的。

結論:Dictionary<K,V>是泛型的,當K或V是值類型時,其速度遠遠超過Hashtable。

因爲 Hashtable 和 Dictionary 同時存在, 在使用場景上必然存在選擇性, 並不任什麼時候刻都能相互替代.

  1. 單線程程序中推薦使用 Dictionary, 有泛型優點, 且讀取速度較快, 容量利用更充分.
  2. 多線程程序中推薦使用 Hashtable, 默認的 Hashtable 容許單線程寫入, 多線程讀取, 對 Hashtable 進一步調用 Synchronized() 方法能夠得到徹底線程安全的類型. 而 Dictionary 非線程安全, 必須人爲使用 lock 語句進行保護, 效率大減.
  3. Dictionary 有按插入順序排列數據的特性 (注: 但當調用 Remove() 刪除過節點後順序被打亂), 所以在須要體現順序的情境中使用 Dictionary 能得到必定方便.

哈希 Hashing

關鍵字和它在表中存儲位置之間存在一種函數關係。這個函數咱們稱爲爲哈希函數。

  hash : 翻譯爲「散列」,就是把任意長度的輸入,經過散列算法,變成固定長度的輸出,該輸出就是散列值。
這種轉換是一種壓縮映射,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來惟一的肯定輸入值,由此引出hash衝突。
簡單的說就是一種將任意長度的消息壓縮到固定長度的消息的函數。

  • hash衝突
    就是鍵(key)通過hash函數獲得的結果做爲地址去存放當前的鍵值對(key-value)(這個是hashmap的存值方式),可是卻發現該地址已經有人先來了,一山不容二虎,就會產生衝突。這個衝突就是hash衝突了。若是兩個不一樣對象的hashCode相同,這種現象稱爲hash衝突。

  • 解決hash衝突的辦法
  1. 開發定址法(線性探測再散列,二次探測再散列,僞隨機探測再散列)

    這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=H(key)出現衝突時,以p爲基礎,產生另外一個哈希地址p1,若是p1仍然衝突,再以p爲基礎,產生另外一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi ,將相應元素存入其中。這種方法有一個通用的再散列函數形式:
    Hi=(H(key)+di)% m i=1,2,…,n
    其中H(key)爲哈希函數,m 爲表長,di稱爲增量序列。增量序列的取值方式不一樣,相應的再散列方式也不一樣。主要有如下三種:
    1) 線性探測再散列
    2) 二次(平方)探測再散列
    3) 僞隨機探測再散列

  2. 再哈希法

    這種方法是同時構造多個不一樣的哈希函數:
    Hi=RH1(key) i=1,2,…,k
    當哈希地址Hi=RH1(key)發生衝突時,再計算Hi=RH2(key)……,直到衝突再也不產生。這種方法不易產生彙集,但增長了計算時間。

  3. 鏈地址法

    將全部哈希地址相同的都連接在同一個鏈表中 ,於是查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於常常進行插入和刪除的狀況。
    hashmap就是用此方法解決衝突的。

  4. 創建一個公共溢出區

    將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一概填入溢出表。

開放散列(open hashing)/ 拉鍊法(針對桶鏈結構)

  • 優勢
  1. 對於記錄總數頻繁可變的狀況,處理的比較好(也就是避免了動態調整的開銷)
  2. 刪除記錄時,比較方便,直接經過指針操做便可
  • 缺點
  1. 存儲的記錄是隨機分佈在內存中的,這樣在查詢記錄時,相比結構緊湊的數據類型(好比數組),哈希表的跳轉訪問會帶來額外的時間開銷 。
  2. 若是全部的 key-value 對是能夠提早預知,並以後不會發生變化時(即不容許插入和刪除),能夠人爲建立一個不會產生衝突的完美哈希函數(perfect hash function),此時封閉散列的性能將遠高於開放散列。
  3. 因爲使用指針,記錄不容易進行序列化(serialize)操做。

封閉散列(closed hashing)/ 開放定址法

  • 優勢
  1. 記錄更容易進行序列化(serialize)操做
  2. 若是記錄總數能夠預知,能夠建立完美哈希函數,此時處理數據的效率是很是高的
  • 缺點
  1. 存儲記錄的數目不能超過桶數組的長度,若是超過就須要擴容,而擴容會致使某次操做的時間成本飆升,這在實時或者交互式應用中可能會是一個嚴重的缺陷。
  2. 使用探測序列,有可能其計算的時間成本太高,致使哈希表的處理性能下降 。
  3. 刪除記錄時,比較麻煩。好比須要刪除記錄a,記錄b是在a以後插入桶數組的,可是和記錄a有衝突,是經過探測序列再次跳轉找到的地址,因此若是直接刪除a,a的位置變爲空槽,而空槽是查詢記錄失敗的終止條件,這樣會致使記錄b在a的位置從新插入數據前不可見,因此不能直接刪除a,而是設置刪除標記。這就須要額外的空間和操做。
    引用來自《解決hash衝突的三個方法

總結

綜上所述,找了相關的文檔以後,發現C#自己沒有封裝部分數據結構,多是讓你們本身發揮,也可能跟它當初設計的緣由有關,由於它不是專們爲處理數據而誕生的。寫完以後,發現寫到這裏還不夠,因而將標題改成《數據結構,你還記得嗎(中)》,接下來還要繼續《數據結構,你還記得嗎(下)》 未完待續!

其餘系列的C#數據結構參考《C# 數據結構

補充淺薄的關係線

數組

數組存儲區間是連續的,佔用內存嚴重,故空間複雜的很大。但數組的索引查找複雜度小,爲O(1);數組的特色是:尋址容易,插入和刪除困難;

鏈表

鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。鏈表的特色是:尋址困難,插入和刪除容易。

哈希表

那麼咱們能不能綜合二者的特性,作出一種尋址容易,插入刪除也容易的數據結構?答案是確定的,這就是咱們要提起的哈希表。哈希表((Hash table)既知足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便。

哈希表綜合以上兩個優勢,但同時還有一個缺點,就是在連續查詢的時候性能很是差。那怎麼尋址容易,插入,刪除也容易,連續查詢也容易呢? 這個就引出了數據庫底層採用的存儲數據結構,B+樹。

相關文章
相關標籤/搜索