C#集合类(数据结构)

一、选择数据结构

1)线性容器
List<T>数组/Stack/Dequeue按需求模型选择即可,LinkedList<T>是双向链表增删修改快.
需要有序数组SortList<T>线性排序容器都可以;如果既需要查找快又需要频繁修改那么可以用List<T>记录索引,用LinkedList<T>存储。

2)二叉树类型容器
SortedDictionary<TKey,TValue>可以提供二叉树类型插入删除查找都比较折中的键值对容器。
SortedSet<T>一个集合值类型的容器,比SortedDictionary<TKey,TValue>需要更少的空间。

3)哈希表类型的容器
Dictionary<TKey,TValue>类似于C++/java中的HashMap实现,需要一个哈希函数和一个相等判断函数解决冲突,能够有很高的插入和查找效率。
HashSet<T>适合单个元素的集合操作类型。
ILookup<TKey,TValue>可以获得一个键对应多个值的存储类型,很有用的方面是从指定集合中筛选某种类型的数据集。

4)其它支持容器的接口类,委托,拓展方法和为了观察,封装位操作,封装多线程操作的衍生类型容器
其它功能类型的接口及其委托,拓展方法:
ICollection<T>、IEnumerable<T>、IEquatable<T>、IComparer<T>、IComparable<T>、IEqualityComparer<T>、IFormattable接口
为了观察,封装位操作,封装多线程操作的衍生类型容器:
ObservableCollection<T>,BitArray/BitVector32、IProducerConsumerCollection<T>接口

大多数集合类都在System.Collections.Generic命名空间中,非泛型的System.Collections中已经很少用了。
特定集合类位于System.Collections.Specialized中,线程安全的集合类在System.Collections.Concurrent中。

集合类主要有:

二.ICollection<T>、IEnumerable<T>、IEquatable<T>、IComparer<T>、IComparable<T>、IEqualityComparer<T>、IFormattable接口。


三.Array、List<T>、队列、栈、SortedList<TKey, TValue>、LinkedList<T>双向链表

1.List<T>

List<T>在C#中实现也是数组,动态数组长度不够会加倍。
不确定的数组需要可变数组用List<T>, 确定长度和数量多用Array, 不推荐用ArrayList因为添加的是object类型要装箱和拆箱性能慢。
1)初始化:
初始化时候可以直接赋值,或者指定Count和Capacity来初始化。
对List<T>填充完数据以后可以用TrimExcess()方法去除不需要的容量,只有空白容量超过10%才会去除成功。
List<T>可以用AddRange添加多个元素。
2)访问:
可以通过索引器访问的集合类有:Array,List<T>,ArrayList, StringCollection, List<T>。
List<T>实现了IEnumerable<T>接口,所以也可以用foreach来访问。

List<T>提供了ForEach方法,该方法用Action<T>作为参数。
public void ForEach(Action<T> action);
// 需要当前类中定义该委托的实例赋值给Action委托对象,也可以用Lambda表达式声明该实例。
public delegate void Action<T> (T obj);

3)删除
用RemoveAt效率较快,如果用Remove回先查找值然后删除回查找引用,如果有重写IEquatable<T>或者Object.Equals就会用这些方法判断,否则
是用默认的引用比较,如果是相同的引用地址那么就可以删除成功。
删除还可以用RemoveRange(index, count)来进行删除。
如果要删除指定特性的元素就可以用RemoveAll()方法,指定特性在Predicate<T>参数中指定。
要直接删除所有的元素用ICollection<T>接口中定义的Clear()方法。

4)搜索
IndexOf(),LastIndexOf(), FindIndex(), FindLastIndex(), Find(),FindLast().
判断存在用Exists();
FindIndex()方法需要一个Predicate类型的参数。
public int FindIndex(Predicate<T> match);
需要给委托对象传递一个声明的委托实例,例如:
public bool FindCountryPredicate(Racer racer)
{
   if( racer == null) return false;
   return racer.Country  == country;
}
将FindCountryPredicate传入函数参数,即可,RemoveAll()中也需要传递入该委托实例。
Find(), FindIndex(), FindAll()都需要这样的比较委托实例,委托实例的广泛使用,如果只写一次可以用Lambd表达式来写,多次将其封装为函数。

5)排序
排序Sort方法也需要传递比较大小的委托实例。
有大概三种比较大小的委托函数:
默认的是IComparable<T>一个other参数的比较委托。
IComparer<T>两个参数的比较委托。
重载Sort方法,该方法需要一个Comparison<T>的委托实例。Comparison<T>的委托定义是public delegate int Comparison<T>(T x, T y);

可以调用Reverse()方法逆转整个集合的排序。

6)类型转换
使用List<T>类的ConvertAll<TOutput>()方法,可以把所有的集合类型转换为另一种类型。
该TOutput委托的定义如下:
public sealed delegate TOutput Convert<TInput, TOutput>(TInput from);
需要定义一个委托实例,传入该函数参数即可。

7)只读集合
一般集合都是要支持读写的,但是有些比较特殊的应用需要给客户提供一个只读集合,那么可以使用List<T>集合的AsReadOnly()方法就可以返回

一个ReadOnlyCollection<T>类型的对象。ReadOnlyCollection<T>和List<T>的差别只是不能写排序删除等,其它实现都一样。

List简单例子:

 static void Main()
        {
            var graham = new Racer(7, "Graham", "Hill", "UK", 14);
            var emerson = new Racer(13, "Emerson", "Fittipaldi", "Brazil", 14);
            var mario = new Racer(16, "Mario", "Andretti", "USA", 12);

            var racers = new List<Racer>(20) { graham, emerson, mario };

            racers.Add(new Racer(24, "Michael", "Schumacher", "Germany", 91));
            racers.Add(new Racer(27, "Mika", "Hakkinen", "Finland", 20));

            racers.AddRange(new Racer[] {
               new Racer(14, "Niki", "Lauda", "Austria", 25),
               new Racer(21, "Alain", "Prost", "France", 51)});

            var racers2 = new List<Racer>(new Racer[] {
               new Racer(12, "Jochen", "Rindt", "Austria", 6),
               new Racer(22, "Ayrton", "Senna", "Brazil", 41) });

            Console.WriteLine("-------racers------------");
            for( int i = 0; i < racers.Count; i++ )
            {
                Console.WriteLine(racers[i].ToString());
            }
            Console.WriteLine("-------racers2------------");
            for (int i = 0; i < racers2.Count; i++)
            {
                Console.WriteLine(racers2[i].ToString());
            }
        }


 static void Main()
        {
            var graham = new Racer(7, "Graham", "Hill", "UK", 14);
            var emerson = new Racer(13, "Emerson", "Fittipaldi", "Brazil", 14);
            var mario = new Racer(16, "Mario", "Andretti", "USA", 12);

            var racers = new List<Racer>(20) { graham, emerson, mario };

            racers.Add(new Racer(24, "Michael", "Schumacher", "Germany", 91));
            racers.Add(new Racer(27, "Mika", "Hakkinen", "Finland", 20));

            racers.AddRange(new Racer[] {
               new Racer(14, "Niki", "Lauda", "Austria", 25),
               new Racer(21, "Alain", "Prost", "France", 51)});

            var racers2 = new List<Racer>(new Racer[] {
               new Racer(12, "Jochen", "Rindt", "Austria", 6),
               new Racer(22, "Ayrton", "Senna", "Brazil", 41) });

            Console.WriteLine("-------racers------------");
            for( int i = 0; i < racers.Count; i++ )
            {
                Console.WriteLine(racers[i].ToString());
            }
            Console.WriteLine("-------racers2------------");
            for (int i = 0; i < racers2.Count; i++)
            {
                Console.WriteLine(racers2[i].ToString());
            }
        }

2.Queue<T>

先进先出,实现了ICollection和IEnumerable<T>接口,但没有实现ICollection<T>接口,因此这个接口定义的Add()和Remove()方法不能用于队列。
没有实现List<T>接口,所以也不支持索引器访问。

队列中的常用方法,Count返回个数,Dequeue()进队列,Enqueue出队列并删除队列头元素,Peek从队列头部读取队列但不删除元素。
TrimExcess()可以清除Capacity中的大于10%时候的元素。

队列Queue<T>的构造默认会4,8,16,32递增的增加容量,.net 1.0版本的Queue却是一开始就给了个32项的空数组。
队列例子:


public class DocumentManager
    {
        // readonly只是说这个队列对象不写(比如另一个对象拷贝给它),但是内部的元素是可以写的
        private readonly Queue<Document> documentQueue = new Queue<Document>();

        public void AddDocument(Document doc)
        {
            lock (this)
            {
                documentQueue.Enqueue(doc);
            }
        }

        public Document GetDocument()
        {
            Document doc = null;
            lock (this)
            {
                doc = documentQueue.Dequeue();
            }
            return doc;
        }

        public bool IsDocumentAvailable
        {
            get
            {
                return documentQueue.Count > 0;
            }
        }
    }


using System;
using System.Threading;
namespace Wrox.ProCSharp.Collections
{
    public class ProcessDocuments
    {
        private DocumentManager documentManager;
        protected ProcessDocuments(DocumentManager dm)
        {
            documentManager = dm;
        }

        public static void Start(DocumentManager dm)
        {
            // ParameterizedThreadStart;
            // public delegate void ParameterizedThreadStart(object obj);
            // 直接这样启动一个线程了。
            new Thread( new ProcessDocuments(dm).Run ).Start();
        }

        protected void Run(object obj) // 这里用不用object obj参数都可以,IL会做转换为友object的。
        {
            while (true)
            {
                if (documentManager.IsDocumentAvailable)
                {
                    Document doc = documentManager.GetDocument();
                    Console.WriteLine("Processing document {0}", doc.Title);
                }
                Thread.Sleep(new Random().Next(20));
            }
        }
    }
}

using System;
using System.Threading;

namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main()
        {
            var dm = new DocumentManager();
            ProcessDocuments.Start(dm);
            // Create documents and add them to the DocumentManager
            for (int i = 0; i < 1000; i++)
            {
                Document doc = new Document("Doc " + i.ToString(), "content");
                dm.AddDocument(doc);
                Console.WriteLine("Added document {0}", doc.Title);
                Thread.Sleep(new Random().Next(20));
            }
        }
    }
}


3.Stack<T>

后进先出,Count属性,Push(),Pop()方法会删除最顶元素,Peek()不会删除,Contains()确定某个元素是否在栈中是则返回true.

using System;
using System.Collections.Generic;
namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main()
        {
            var alphabet = new Stack<char>();
            alphabet.Push('A');
            alphabet.Push('B');
            alphabet.Push('C');

            Console.Write("First iteration: ");
            // 迭代遍历用了迭代模式,不会删除
            foreach (char item in alphabet)
            {
                Console.Write(item);
            }

            Console.Write("Second iteration: ");
            while (alphabet.Count > 0)
            {
                // Pop会删除
                Console.Write(alphabet.Pop());
            }
            Console.WriteLine();
        }
    }
}


using System;
using System.Collections.Generic;
namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main()
        {
            var alphabet = new Stack<char>();
            alphabet.Push('A');
            alphabet.Push('B');
            alphabet.Push('C');

            Console.Write("First iteration: ");
            // 迭代遍历用了迭代模式,不会删除
            foreach (char item in alphabet)
            {
                Console.Write(item);
            }

            Console.Write("Second iteration: ");
            while (alphabet.Count > 0)
            {
                // Pop会删除
                Console.Write(alphabet.Pop());
            }
            Console.WriteLine();
        }
    }
}

4.SortedList<TKey, TValue>

实现是基于数组的列表,定义了单一任意类型的键和单一任意类型的值的数据结构,可以直接创建一个空的排序列表;或者重载构造函数可以定义列表容量和传递一个IComparer<TKey>接口的对象,该接口用于给列表中的元素排序。

为容器添加元素可以用Add()方法,也可以用索引下标赋值,相同键添加时候Add方法会抛出异常不能覆盖旧键,索引下标相同键时候会覆盖旧键不抛异常。

访问时候可以用集合迭代器元素的Key,Value属性访问键和值;也可以用集合的Values和Keys属性来返回所有的键值属性,类似C++中的STL map一样。
通过索引器键值访问元素时候,如果键不存在,那么会抛出异常,为了避免异常发生,可以用ContiansKey方法来判断是否存在集合中,再用索引器访问。
还可以直接用TryGetValue方法,尝试获得该键的值,如果不存在也不会抛出异常。

实例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main(string[] args)
        {
            var books = new SortedList<string, string>();
            books.Add("C# 2008 Wrox Box", "978–0–470–047205–7");
            books.Add("Professional ASP.NET MVC 1.0", "978–0–470–38461–9");

            books["Beginning Visual C# 2008"] = "978–0–470-19135-4";
            books["Professional C# 2008"] = "978–0–470–19137–6";

            foreach (KeyValuePair<string, string> book in books)
            {
                Console.WriteLine("{0}, {1}", book.Key, book.Value);
            }

            foreach (string isbn in books.Values)
            {
                Console.WriteLine(isbn);
            }

            foreach (string title in books.Keys)
            {
                Console.WriteLine(title);
            }

            {
                string isbn;
                string title = "Professional C# 7.0";
                // 出现异常
                try
                {
                    isbn = books[title];
                }
               catch ( KeyNotFoundException err)
                {
                    Console.WriteLine("Exception " + err.ToString());
                }
              
                if (!books.TryGetValue(title, out isbn))
                {
                    Console.WriteLine("{0} not found", title);
                }
            }
        }
    }
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main(string[] args)
        {
            var books = new SortedList<string, string>();
            books.Add("C# 2008 Wrox Box", "978–0–470–047205–7");
            books.Add("Professional ASP.NET MVC 1.0", "978–0–470–38461–9");

            books["Beginning Visual C# 2008"] = "978–0–470-19135-4";
            books["Professional C# 2008"] = "978–0–470–19137–6";

            foreach (KeyValuePair<string, string> book in books)
            {
                Console.WriteLine("{0}, {1}", book.Key, book.Value);
            }

            foreach (string isbn in books.Values)
            {
                Console.WriteLine(isbn);
            }

            foreach (string title in books.Keys)
            {
                Console.WriteLine(title);
            }

            {
                string isbn;
                string title = "Professional C# 7.0";
                // 出现异常
                try
                {
                    isbn = books[title];
                }
               catch ( KeyNotFoundException err)
                {
                    Console.WriteLine("Exception " + err.ToString());
                }
              
                if (!books.TryGetValue(title, out isbn))
                {
                    Console.WriteLine("{0} not found", title);
                }
            }
        }
    }
}

5.LinkList<T>

LinkList<T>才是链表而且是双向链表,前面的都是基于数组的。链表典型的特征就是插入删除非常方便,但是查找比较慢需要O(n)的查找效率。
LinkedList<T>包含LinkedListNode<T>类型的元素,该节点定义了List、Next、Previous、Value,List属性返回和节点相关的LinkedList<T>对象。LinkedList<T>可以访问成员的第一个和最后一个元素(First和Last);可以在指定位置AddAfter()/AddBefore()/AddFirst()/AddLast()方法;删除指定位置的元素Remove()/RemoveFirst()/RemoveLast()方法;查找Find()和FindLast()。
LinkList<T>实例:
Document.cs:

namespace Wrox.ProCSharp.Collections
{
    public class Document
    {
        public string Title { get; private set; }
        public string Content { get; private set; }
        public byte Priority { get; private set; }

        public Document(string title, string content, byte priority = 0)
        {
            this.Title = title;
            this.Content = content;
            this.Priority = priority;
        }
    }

}


namespace Wrox.ProCSharp.Collections
{
    public class Document
    {
        public string Title { get; private set; }
        public string Content { get; private set; }
        public byte Priority { get; private set; }

        public Document(string title, string content, byte priority = 0)
        {
            this.Title = title;
            this.Content = content;
            this.Priority = priority;
        }
    }

}

PriorityDocumentManager.cs:

using System;
using System.Collections.Generic;

namespace Wrox.ProCSharp.Collections
{
    public class PriorityDocumentManager
    {
        // 真正存放排序好的document的结构体
        private readonly LinkedList<Document> documentList;

        // priorities 0.9, 方便索引documentList本优先级的最后一个元素;
        // 用一个List<T>数组索引器,提高效率,找到数组和链表之间平衡点的提高性能的好方法。
        private readonly List<LinkedListNode<Document>> priorityNodes;

        public PriorityDocumentManager()
        {
            documentList = new LinkedList<Document>();

            priorityNodes = new List<LinkedListNode<Document>>(10);
            for (int i = 0; i < 10; i++)
            {
                priorityNodes.Add(new LinkedListNode<Document>(null));
            }
        }

        // 对外接口
        public void AddDocument(Document d)
        {
            if (d == null) throw new ArgumentNullException("d");

            AddDocumentToPriorityNode(d, d.Priority);
        }

        private void AddDocumentToPriorityNode(Document doc, int priority)
        {
            // 外部调用要保证priority就是doc.priority, 否则后面会导致问题
            if (priority > 9 || priority < 0)
                throw new ArgumentException("Priority must be between 0 and 9");

            // 1.开始空或者中间空,递归会导致这里不进来!=null(因为小优先级的有元素时候)
            if (priorityNodes[priority].Value == null)
            {
                --priority;
                if (priority >= 0)
                {
                    // check for the next lower priority
                    // 2)递归是为了检测小于优先级的有没有存在元素的,这时priority会小于doc.priority
                    AddDocumentToPriorityNode(doc, priority);
                }
                else // now no priority node exists with the same priority or lower
                // add the new document to the end
                {
                    // 1)第一次会进来或者当前priority以下的优先级都没有的情况也会进来
                    // 更小优先级的都没有,那么它就是最小优先级的
                    documentList.AddLast(doc);
                    // priorityNodes存放的时链表最后的那个元素,doc.Priority和documentList.Last上的优先级一样的
                    priorityNodes[doc.Priority] = documentList.Last;
                }
                return;
            }
            // 直接进来,或者递归进来,说明当前优先级或者递归减到的优先级有元素。
            else // a priority node exists
            {
                // 从priorityNodes获取的是当前优先级,最后一个节点的元素
                LinkedListNode<Document> prioNode = priorityNodes[priority];
                // 1)直接进来时候,如果优先级相等,如果是递归进来的不会到这里因为priority变小了
                if (priority == doc.Priority)
                // priority node with the same priority exists
                {
                    // 是当前优先级直接添加到末尾
                    documentList.AddAfter(prioNode, doc);
                    // set the priority node to the last document with the same priority
                    // 当前优先级存的是优先级最后的节点,doc.Priority和prioNode.Next;上的优先级一样的
                    priorityNodes[doc.Priority] = prioNode.Next;
                }
                // 2)递归时候进来的,因为priority 小于了doc.Priority,且priority有值,所以要放到priority前面
                else // only priority node with a lower priority exists
                {
                    // get the first node of the lower priority
                    LinkedListNode<Document> firstPrioNode = prioNode;

                    while (firstPrioNode.Previous != null &&
                       firstPrioNode.Previous.Value.Priority == prioNode.Value.Priority)
                    {
                        firstPrioNode = prioNode.Previous;
                        prioNode = firstPrioNode;
                    }

                    //没有放到前面,为了链表按照优先级大在前面
                    documentList.AddBefore(firstPrioNode, doc);
                    // set the priority node to the new value
                    // 当前优先级存的是优先级最后的节点,doc.Priority和prioNode.Next;上的优先级一样的
                    priorityNodes[doc.Priority] = firstPrioNode.Previous;
                }
            }
        }

        // 按照从大优先级,相同优先级先来优先级高的顺序排序
        public void DisplayAllNodes()
        {
            foreach (Document doc in documentList)
            {
                Console.WriteLine("priority: {0}, title {1}", doc.Priority, doc.Title);
            }
        }

        // returns the document with the highest priority
        // (that's first in the linked list)
        // 优先级高的出链表,并且删除该document
        public Document GetDocument()
        {
            Document doc = documentList.First.Value;
            documentList.RemoveFirst();
            return doc;
        }

    }

}


using System;
using System.Collections.Generic;

namespace Wrox.ProCSharp.Collections
{
    public class PriorityDocumentManager
    {
        // 真正存放排序好的document的结构体
        private readonly LinkedList<Document> documentList;

        // priorities 0.9, 方便索引documentList本优先级的最后一个元素;
        // 用一个List<T>数组索引器,提高效率,找到数组和链表之间平衡点的提高性能的好方法。
        private readonly List<LinkedListNode<Document>> priorityNodes;

        public PriorityDocumentManager()
        {
            documentList = new LinkedList<Document>();

            priorityNodes = new List<LinkedListNode<Document>>(10);
            for (int i = 0; i < 10; i++)
            {
                priorityNodes.Add(new LinkedListNode<Document>(null));
            }
        }

        // 对外接口
        public void AddDocument(Document d)
        {
            if (d == null) throw new ArgumentNullException("d");

            AddDocumentToPriorityNode(d, d.Priority);
        }

        private void AddDocumentToPriorityNode(Document doc, int priority)
        {
            // 外部调用要保证priority就是doc.priority, 否则后面会导致问题
            if (priority > 9 || priority < 0)
                throw new ArgumentException("Priority must be between 0 and 9");

            // 1.开始空或者中间空,递归会导致这里不进来!=null(因为小优先级的有元素时候)
            if (priorityNodes[priority].Value == null)
            {
                --priority;
                if (priority >= 0)
                {
                    // check for the next lower priority
                    // 2)递归是为了检测小于优先级的有没有存在元素的,这时priority会小于doc.priority
                    AddDocumentToPriorityNode(doc, priority);
                }
                else // now no priority node exists with the same priority or lower
                // add the new document to the end
                {
                    // 1)第一次会进来或者当前priority以下的优先级都没有的情况也会进来
                    // 更小优先级的都没有,那么它就是最小优先级的
                    documentList.AddLast(doc);
                    // priorityNodes存放的时链表最后的那个元素,doc.Priority和documentList.Last上的优先级一样的
                    priorityNodes[doc.Priority] = documentList.Last;
                }
                return;
            }
            // 直接进来,或者递归进来,说明当前优先级或者递归减到的优先级有元素。
            else // a priority node exists
            {
                // 从priorityNodes获取的是当前优先级,最后一个节点的元素
                LinkedListNode<Document> prioNode = priorityNodes[priority];
                // 1)直接进来时候,如果优先级相等,如果是递归进来的不会到这里因为priority变小了
                if (priority == doc.Priority)
                // priority node with the same priority exists
                {
                    // 是当前优先级直接添加到末尾
                    documentList.AddAfter(prioNode, doc);
                    // set the priority node to the last document with the same priority
                    // 当前优先级存的是优先级最后的节点,doc.Priority和prioNode.Next;上的优先级一样的
                    priorityNodes[doc.Priority] = prioNode.Next;
                }
                // 2)递归时候进来的,因为priority 小于了doc.Priority,且priority有值,所以要放到priority前面
                else // only priority node with a lower priority exists
                {
                    // get the first node of the lower priority
                    LinkedListNode<Document> firstPrioNode = prioNode;

                    while (firstPrioNode.Previous != null &&
                       firstPrioNode.Previous.Value.Priority == prioNode.Value.Priority)
                    {
                        firstPrioNode = prioNode.Previous;
                        prioNode = firstPrioNode;
                    }

                    //没有放到前面,为了链表按照优先级大在前面
                    documentList.AddBefore(firstPrioNode, doc);
                    // set the priority node to the new value
                    // 当前优先级存的是优先级最后的节点,doc.Priority和prioNode.Next;上的优先级一样的
                    priorityNodes[doc.Priority] = firstPrioNode.Previous;
                }
            }
        }

        // 按照从大优先级,相同优先级先来优先级高的顺序排序
        public void DisplayAllNodes()
        {
            foreach (Document doc in documentList)
            {
                Console.WriteLine("priority: {0}, title {1}", doc.Priority, doc.Title);
            }
        }

        // returns the document with the highest priority
        // (that's first in the linked list)
        // 优先级高的出链表,并且删除该document
        public Document GetDocument()
        {
            Document doc = documentList.First.Value;
            documentList.RemoveFirst();
            return doc;
        }

    }

}
Program.cs:

namespace Wrox.ProCSharp.Collections  
2.{  
3.    class Program  
4.    {  
5.        static void Main()  
6.        {  
7.            PriorityDocumentManager pdm = new PriorityDocumentManager();  
8.            // 传入时候就排序好了,LinkList<T>结构方便优先级类型的插入操作(利于插入和删除)  
9.            pdm.AddDocument(new Document("one", "Sample", 8));  
10.            pdm.AddDocument(new Document("two", "Sample", 3));  
11.            pdm.AddDocument(new Document("three", "Sample", 4));  
12.            pdm.AddDocument(new Document("four", "Sample", 8));  
13.            pdm.AddDocument(new Document("five", "Sample", 1));  
14.            pdm.AddDocument(new Document("six", "Sample", 9));  
15.            pdm.AddDocument(new Document("seven", "Sample", 1));  
16.            pdm.AddDocument(new Document("eight", "Sample", 1));  
17.  
18.            // 展示排序好的  
19.            pdm.DisplayAllNodes();  
20.  
21.        }  
22.    }  
23.}  


namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main()
        {
            PriorityDocumentManager pdm = new PriorityDocumentManager();
            // 传入时候就排序好了,LinkList<T>结构方便优先级类型的插入操作(利于插入和删除)
            pdm.AddDocument(new Document("one", "Sample", 8));
            pdm.AddDocument(new Document("two", "Sample", 3));
            pdm.AddDocument(new Document("three", "Sample", 4));
            pdm.AddDocument(new Document("four", "Sample", 8));
            pdm.AddDocument(new Document("five", "Sample", 1));
            pdm.AddDocument(new Document("six", "Sample", 9));
            pdm.AddDocument(new Document("seven", "Sample", 1));
            pdm.AddDocument(new Document("eight", "Sample", 1));

            // 展示排序好的
            pdm.DisplayAllNodes();

        }
    }
}

四.Dictionary<TKey,TValue>、多键值ILookup<TKey,TValue>、SortedDictionary<TKey,TValue>、HashSet<T>和SortedSet<T>

1.Dictionary<TKey,TValue>

.net提供了几个字典类,其中最主要的类是Dictionary<TKey,TValue>。
字典基于hash_map存储结构,提供了快速的查找方法,查找效率是O(1),但是也不是绝对的因为要解决hash映射函数计算和解决冲突。
也可以自由的添加和删除元素,有点像List<T>但是没有内存元素挪动性能开销。
Dictionary数据结构很类似C++中的hash_map/unordered_map工作方式,或者就是这样的实现:
hash_map其插入过程是:
    得到key
    通过hash函数得到hash值
    得到桶号(一般都为hash值对桶数求模)
    存放key和value在桶内。

其取值过程是:
    得到key
    通过hash函数得到hash值
    得到桶号(一般都为hash值对桶数求模)
    比较桶的内部元素是否与key相等,若都不相等,则没有找到。
    取出相等的记录的value。
因此C#中要用Dictionary类,键类型需要重写:
1)哈希函数:Object类的GetHashCode()方法,GetHashCode()返回int值用于计算键对应位置放置的hashCode用作元素索引。
GetHashCode()实现要求:
相同的键总是返回相同的int值,不同的键可以返回相同的int值。
它应该执行得比较快,计算开销不大,hashCode应该尽量平均分布在int可以存储的整个数字范围上。
不能抛出异常。
至少使用一个键对象的字段,hashCode最好在键对象的生存期中不发生变化。

2)解决冲突:键类型必须实现IEquatable<T>.Equals()方法,或者重写Object类的Equals()方法,因为不同的键值需要返回不同的hashCode,相同的键返回相同hashCode。
默认没有重写,那么Equals方法比较的是引用无论是值类型还是引用类型,GetHashCode()是根据对象的地址计算hashCode,所以默认是基于引用的比较。
相同的int类型传入,只要不是相同的int引用,就会导致无法返回结果。
所以基础类型都重写了上述两个方法,基础类型中string比较通过字符串值有较好的散列平均分布,int也是通过值比较但是很难平均分布。

如果重写了一个Equals方法(一般是值比较),但是没有重写GetHashCode()方法(一般也是基于值的获取hashCode)那么获取hashCode的方法将是获取引用,使用字典类就会导致诡异的行为,将对象放入了字典中,但是取不出来了(因为键引用不同),或者取出来的是一个错误的结果,所以编译器会给一个编译警告!
如果键类型没有重写GetHashCode()和Equals()方法;也可以实现IEqualityComparer<T>接口的比较器它定义了GetHashCode()和Equals()方法,并将传递的对象作为参数,将比较器传入Dictionary<TKey,TValue>一个重载版本的构造函数即可。
Employee.cs:

using System;  
2.namespace Wrox.ProCSharp.Collections  
3.{  
4.    // 将类的一个实例序列化为一个文件  
5.    [Serializable]  
6.    public class Employee  
7.    {  
8.        private string name;  
9.        private decimal salary;  
10.        private readonly EmployeeId id;  
11.  
12.        public Employee(EmployeeId id, string name, decimal salary)  
13.        {  
14.            this.id = id;  
15.            this.name = name;  
16.            this.salary = salary;  
17.        }  
18.  
19.        public override string ToString()  
20.        {  
21.            return String.Format("{0}: {1, -20} {2:C}",  
22.                  id.ToString(), name, salary);  
23.        }  
24.    }  
25.}  


EmployeeId.cs:

using System;  
2.  
3.namespace Wrox.ProCSharp.Collections  
4.{  
5.    [Serializable]  
6.    public class EmployeeIdException : Exception  
7.    {  
8.        public EmployeeIdException(string message) : base(message)  { }  
9.    }  
10.  
11.    [Serializable]  
12.    public struct EmployeeId : IEquatable<EmployeeId>  
13.    {  
14.        private readonly char prefix;  
15.        private readonly int number;  
16.  
17.        public EmployeeId(string id)  
18.        {  
19.            if (id == null) throw new ArgumentNullException("id");  
20.  
21.            prefix = (id.ToUpper())[0];  
22.            int numLength = id.Length - 1;  
23.            try  
24.            {  
25.                // 截取前面6位,有可能提供的number一样,这样多个键会产生一个相同的GetHashCode()。  
26.                // 但是后面会通过Equals方法进行解决冲突。  
27.                number = int.Parse(id.Substring(1, numLength > 6 ? 6 : numLength));  
28.            }  
29.            catch (FormatException)  
30.            {  
31.                throw new EmployeeIdException("Invalid EmployeeId format");  
32.            }  
33.        }  
34.  
35.        public override string ToString()  
36.        {  
37.            return prefix.ToString() + string.Format("{0,6:000000}", number);  
38.        }  
39.        // 获取确定的,int类型上均匀分配的,高性能的产生hashCode方法  
40.        public override int GetHashCode()  
41.        {  
42.            return (number ^ number << 16) * 0x15051505;  
43.        }  
44.  
45.        public bool Equals(EmployeeId other)  
46.        {  
47.            if (other == null) return false;  
48.            // number相同情况下,如果prefix也相同,那么就会导致完全相同了  
49.            return (prefix == other.prefix && number == other.number);  
50.        }  
51.  
52.        public override bool Equals(object obj)  
53.        {  
54.            return Equals((EmployeeId)obj);  
55.        }  
56.  
57.        public static bool operator ==(EmployeeId left, EmployeeId right)  
58.        {  
59.            return left.Equals(right);  
60.        }  
61.  
62.        public static bool operator !=(EmployeeId left, EmployeeId right)  
63.        {  
64.            return !(left == right);  
65.        }  
66.    }  
67.}  


Program.cs:

using System;  
2.using System.Collections.Generic;  
3.  
4.namespace Wrox.ProCSharp.Collections  
5.{  
6.    class Program  
7.    {  
8.        static void Main()  
9.        {  
10.            // capacity是素数  
11.            var employees = new Dictionary<EmployeeId, Employee>(31);  
12.  
13.            var idKyle = new EmployeeId("T3755");  
14.            var kyle = new Employee(idKyle, "Kyle Bush", 5443890.00m);  
15.            employees.Add(idKyle, kyle);  
16.            Console.WriteLine(kyle);  
17.  
18.            var idCarl = new EmployeeId("F3547");  
19.            var carl = new Employee(idCarl, "Carl Edwards", 5597120.00m);  
20.            employees.Add(idCarl, carl);  
21.            Console.WriteLine(carl);  
22.  
23.            var idJimmie = new EmployeeId("C3386");  
24.            var jimmie = new Employee(idJimmie, "Jimmie Johnson", 5024710.00m);  
25.            var jimmie2 = new Employee(idJimmie, "Jimmie Cen", 5024710.00m);  
26.            employees.Add(idJimmie, jimmie);  
27.            //employees.Add(idJimmie, jimmie2); // 相同key,用Add不会覆盖,但是会抛出异常  
28.            Console.WriteLine(jimmie);  
29.  
30.            var idDale = new EmployeeId("C3323");  
31.            var dale = new Employee(idDale, "Dale Earnhardt Jr.", 3522740.00m);  
32.            employees[idDale] = dale;  
33.            Console.WriteLine(dale);  
34.  
35.            var idJeff = new EmployeeId("C3234");  
36.            var jeff = new Employee(idJeff, "Jeff Burton", 3879540.00m);  
37.            var jeff2 = new Employee(idJeff, "Jeff Cen", 3879540.00m);  
38.            // 下标索引方式添加元素  
39.            employees[idJeff] = jeff;  
40.            employees[idJeff] = jeff2; // 相同key,用下标索引会覆盖  
41.            Console.WriteLine(jeff);  
42.  
43.            while (true)  
44.            {  
45.                Console.Write("Enter employee id (X to exit)> ");  
46.                var userInput = Console.ReadLine();  
47.                userInput = userInput.ToUpper();  
48.                if (userInput == "X") break;  
49.  
50.                EmployeeId id;  
51.                try  
52.                {  
53.                    // 第一位字符会去掉,用后面的数字作为真正的key  
54.                    id = new EmployeeId(userInput);  
55.  
56.                    Employee employee;  
57.                    // 如果用下标访问的话,不存在会抛出异常NotFoundException  
58.                    if (!employees.TryGetValue(id, out employee))  
59.                    {  
60.                        Console.WriteLine("Employee with id {0} does not exist", id);  
61.                    }  
62.                    else  
63.                    {  
64.                        Console.WriteLine(employee);  
65.                    }  
66.                }  
67.                catch (EmployeeIdException ex)  
68.                {  
69.                    Console.WriteLine(ex.Message);  
70.                }  
71.            }  
72.        }  
73.    }  
74.}  

using System;
using System.Collections.Generic;

namespace Wrox.ProCSharp.Collections
{
    class Program
    {
        static void Main()
        {
            // capacity是素数
            var employees = new Dictionary<EmployeeId, Employee>(31);

            var idKyle = new EmployeeId("T3755");
            var kyle = new Employee(idKyle, "Kyle Bush", 5443890.00m);
            employees.Add(idKyle, kyle);
            Console.WriteLine(kyle);

            var idCarl = new EmployeeId("F3547");
            var carl = new Employee(idCarl, "Carl Edwards", 5597120.00m);
            employees.Add(idCarl, carl);
            Console.WriteLine(carl);

            var idJimmie = new EmployeeId("C3386");
            var jimmie = new Employee(idJimmie, "Jimmie Johnson", 5024710.00m);
            var jimmie2 = new Employee(idJimmie, "Jimmie Cen", 5024710.00m);
            employees.Add(idJimmie, jimmie);
            //employees.Add(idJimmie, jimmie2); // 相同key,用Add不会覆盖,但是会抛出异常
            Console.WriteLine(jimmie);

            var idDale = new EmployeeId("C3323");
            var dale = new Employee(idDale, "Dale Earnhardt Jr.", 3522740.00m);
            employees[idDale] = dale;
            Console.WriteLine(dale);

            var idJeff = new EmployeeId("C3234");
            var jeff = new Employee(idJeff, "Jeff Burton", 3879540.00m);
            var jeff2 = new Employee(idJeff, "Jeff Cen", 3879540.00m);
            // 下标索引方式添加元素
            employees[idJeff] = jeff;
            employees[idJeff] = jeff2; // 相同key,用下标索引会覆盖
            Console.WriteLine(jeff);

            while (true)
            {
                Console.Write("Enter employee id (X to exit)> ");
                var userInput = Console.ReadLine();
                userInput = userInput.ToUpper();
                if (userInput == "X") break;

                EmployeeId id;
                try
                {
                    // 第一位字符会去掉,用后面的数字作为真正的key
                    id = new EmployeeId(userInput);

                    Employee employee;
                    // 如果用下标访问的话,不存在会抛出异常NotFoundException
                    if (!employees.TryGetValue(id, out employee))
                    {
                        Console.WriteLine("Employee with id {0} does not exist", id);
                    }
                    else
                    {
                        Console.WriteLine(employee);
                    }
                }
                catch (EmployeeIdException ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }
    }
}

2.ILookup<TKey,TValue>

非常类似于Dictionary<TKey,TValue>,会把键映射到一个值集上,但是ILookup<TKey,TValue>是在System.Core命名空间中,用System.Linq命名空间定义。
ILookup<TKey,TValue>是一个拓展结构,不能像其它容器那样直接创建,需要从实现了IEnumerable<T>接口的容器中用ToLookup函数获取。
ToLookup函数需要传递一个Func<TSource, TKey>类型的键委托,可以用Lambda表达式来实现,例如:

static void Main()  
2.        {  
3.            var racers = new List<Racer>();  
4.            racers.Add(new Racer(26, "Jacques", "Villeneuve", "Canada", 11));  
5.            racers.Add(new Racer(18, "Alan", "Jones", "Australia", 12));  
6.            racers.Add(new Racer(11, "Jackie", "Stewart", "United Kingdom", 27));  
7.            racers.Add(new Racer(15, "James", "Hunt", "United Kingdom", 10));  
8.            racers.Add(new Racer(5, "Jack", "Brabham", "Australia", 14));  
9.           //public static ILookup<TKey, TSource> ToLookup<TSource, TKey>  
10.           //(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);  
11.           //创建一个1:n 的映射。 它可以方便的将数据分类成组,并生成一个字典供查询使用。  
12.            //System.Linq.Enumerable::ToLookup  
13.            //public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);  
14.            // this IEnumerable<TSource> source?,是.net的拓展方法机制,用实例调用静态方法,但是编译器是将实例作为静态方法的第一个参数来调用静态方法的。  
15.            var lookupRacers = racers.ToLookup(x => x.Country);  
16.            foreach (Racer r in lookupRacers["Australia"])  
17.            {  
18.                Console.WriteLine(r);  
19.            }  
20.        }  


3.SortedDictionary<TKey,TValue>

是一个二叉树,其中元素根据键来排序,该键类型必须实现IComparable<TKey>接口,或者需要传递一个IComparer<TKey>接口的比较器用作有序字典的构造函数的一个参数。其实类似于C++中

的map类型了,类似java中的TreeMap类型。
SortedDictionary<TKey,TValue>和SortedList<TKey,TValue>,但SortedDictionary<TKey,TValue>类插入和删除元素比较快,查找速度比较慢,内存开销比SortedList<TKey,TValue>大。

SortedList<TKey,TValue>适用很少修改的情形,因为有更快的查找速度,用更小的内存。

4.HashSet<T>和SortedSet<T>

HashSet<T>是无序的和SortedSet<T>是有序的都实现了接口ISet<T>,ISet<T>提供了集合的交集,并集,判断集合关系等操作。
直接创建集合对象就可以了,为集合添加元素可以用Add()方法如果重复那么会返回false不会抛出异常。
IsSubsetOf和IsSupersetOf方法比较集合实现了IEnumerable<T>接口的集合,并返回一个布尔结果,Overlaps()是判断有交集。
UnionWith()方法将多个集合求并,ExceptWith()求差集。
SortedSet<T>如果是自定义类型,那么需要提供排序的委托实例。

ObservableCollection<T>类,是为WPF定义的,这样UI可以得到集合的变化,在命名空间:System.Collections.ObjectModel中定义。
ObservableCollection<T>派生自Collection<T>基类,所以集合类的很多操作该容器都满足,并在内部使用了List<T>类。
ObservableCollection<T>对象的CollectionChanged事件可以添加消息处理函数也就是委托实例,当集合发生变化时候可以回调到处理函数中。

五.BitArray/BitVector32、IProducerConsumerCollection<T>接口

1.BitArray/BitVector32用集合进行位操作

1).BitArray位于System.Collections命名空间中,用于不确定位大小的操作,可以包含非常多的位,应该是存储在堆中。
BitArray是一个引用类型,包含一个int数组,其中每32位使用一个新整数。
BitArray可以用索引器对数组中的位进行操作,索引器是bool类型,还可以用Get(),Set方法访问数组中的位。
BitArray的位操作,Not非,And()与,Or()或,Xor()异或操作。

2).BitVector32是值类型
BitVector32位于System.Collections.Specialized中,32位的操纵,存储在栈中,速度很快。
BitVector32属性方法,Data返回二进制数据的整型大小.
BitVector32的访问可以使用索引器,索引器是重载的,可以使用掩码或BitVector32.Section类型的片段来获取和设置值。
CreateMask()为结构中的特定位创建掩码。
CreateSection()用于创建32位中的几个片段。
位操作例子:

using System;  
2.using System.Collections;  
3.using System.Collections.Specialized;  
4.using System.Text;  
5.namespace BitArraySample  
6.{  
7.    class Program  
8.    {  
9.        static void Main()  
10.        {  
11.            BitArrayDemo();  
12.            BitVectorDemo();  
13.        }  
14.  
15.        static void BitArrayDemo()  
16.        {  
17.            var bits1 = new BitArray(8);  
18.            // 全部设置为1  
19.            bits1.SetAll(true);  
20.            // 索引1设置为false  
21.            bits1.Set(1, false);  
22.            // 设置用下标索引器设置值  
23.            bits1[5] = false;  
24.            bits1[7] = false;  
25.            Console.Write("initialized: ");  
26.            DisplayBits(bits1);  
27.            Console.WriteLine();  
28.  
29.            // 位的一些运算  
30.            DisplayBits(bits1);  
31.            bits1.Not();  
32.            Console.Write(" not ");  
33.            DisplayBits(bits1);  
34.            Console.WriteLine();  
35.  
36.            var bits2 = new BitArray(bits1);  
37.            bits2[0] = true;  
38.            bits2[1] = false;  
39.            bits2[4] = true;  
40.            DisplayBits(bits1);  
41.  
42.            Console.Write(" or ");  
43.            DisplayBits(bits2);  
44.            Console.Write(" : ");  
45.            bits1.Or(bits2);  
46.            DisplayBits(bits1);  
47.            Console.WriteLine();  
48.  
49.            DisplayBits(bits2);  
50.            Console.Write(" and ");  
51.            DisplayBits(bits1);  
52.            Console.Write(" : ");  
53.            bits2.And(bits1);  
54.            DisplayBits(bits2);  
55.            Console.WriteLine();  
56.  
57.            DisplayBits(bits1);  
58.            Console.Write(" xor ");  
59.            DisplayBits(bits2);  
60.            bits1.Xor(bits2);  
61.            Console.Write(" : ");  
62.            DisplayBits(bits1);  
63.            Console.WriteLine();  
64.        }  
65.  
66.        static void BitVectorDemo()  
67.        {  
68.  
69.            var bits1 = new BitVector32();  
70.            // 书面写法,不考虑大小端,最后一个位的掩码(考虑是小端也就是地址读到的第一个)  
71.            int bit1 = BitVector32.CreateMask();  
72.            // 基于bit1位的上一个位的掩码,其实bit1是1  
73.            int bit2 = BitVector32.CreateMask(bit1);  
74.            // 基于bit2位的上一个位的掩码,其实bit2是2  
75.            int bit3 = BitVector32.CreateMask(bit2);  
76.            int bit4 = BitVector32.CreateMask(bit3);  
77.            int bit5 = BitVector32.CreateMask(bit4);  
78.  
79.            // 用索引器将末尾取值为1,其实bit1是1  
80.            bits1[bit1] = true;  
81.            // 用索引器将倒数第二位取值为0,其实bit2是2  
82.            bits1[bit2] = false;  
83.            bits1[bit3] = true;  
84.            bits1[bit4] = true;  
85.            Console.WriteLine(bits1);  
86.            // 可以一次性的把有1下标的赋值为true  
87.            bits1[0xabcdef] = true;  
88.            Console.WriteLine(bits1);  
89.  
90.            int received = 0x79abcdef;  
91.            // 一次性的把有1下标的赋值为true,为0的赋值位0  
92.            var bits2 = new BitVector32(received);  
93.            Console.WriteLine(bits2);  
94.            // sections: FF EEE DDD CCCC BBBBBBBB AAAAAAAAAAAA  
95.            // 从底地址开始截取0xfff片段的索引值  
96.            BitVector32.Section sectionA = BitVector32.CreateSection(0xfff);  
97.            // 基于sectionA偏移,取0xff位数上的索引值  
98.            BitVector32.Section sectionB = BitVector32.CreateSection(0xff, sectionA);  
99.            BitVector32.Section sectionC = BitVector32.CreateSection(0xf, sectionB);  
100.            BitVector32.Section sectionD = BitVector32.CreateSection(0x7, sectionC);  
101.            BitVector32.Section sectionE = BitVector32.CreateSection(0x7, sectionD);  
102.            BitVector32.Section sectionF = BitVector32.CreateSection(0x3, sectionE);  
103.  
104.            // 用索引片段,访问bits2在该片段上的元素  
105.            Console.WriteLine("Section A: " + IntToBinaryString(bits2[sectionA], true));  
106.            Console.WriteLine("Section B: " + IntToBinaryString(bits2[sectionB], true));  
107.            Console.WriteLine("Section C: " + IntToBinaryString(bits2[sectionC], true));  
108.            Console.WriteLine("Section D: " + IntToBinaryString(bits2[sectionD], true));  
109.            Console.WriteLine("Section E: " + IntToBinaryString(bits2[sectionE], true));  
110.            Console.WriteLine("Section F: " + IntToBinaryString(bits2[sectionF], true));  
111.        }  
112.  
113.        static string IntToBinaryString(int bits, bool removeTrailingZero)  
114.        {  
115.            var sb = new StringBuilder(32);  
116.            for (int i = 0; i < 32; i++)  
117.            {  
118.                // 从左边读起,读完后丢弃左边位数,所以与上0x80000000  
119.                if ((bits & 0x80000000) != 0)  
120.                {  
121.                    sb.Append("1");  
122.                }  
123.                else  
124.                {  
125.                    sb.Append("0");  
126.                }  
127.                bits = bits << 1;  
128.            }  
129.            string s = sb.ToString();  
130.            if (removeTrailingZero)  
131.                return s.TrimStart('0');  
132.            else  
133.                return s;  
134.        }  
135.  
136.        static void DisplayBits(BitArray bits)  
137.        {  
138.            // 可以直接迭代输出  
139.            foreach (bool bit in bits)  
140.            {  
141.                Console.Write(bit ? 1 : 0);  
142.            }  
143.        }  
144.    }  
145.}  


using System;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
namespace BitArraySample
{
    class Program
    {
        static void Main()
        {
            BitArrayDemo();
            BitVectorDemo();
        }

        static void BitArrayDemo()
        {
            var bits1 = new BitArray(8);
            // 全部设置为1
            bits1.SetAll(true);
            // 索引1设置为false
            bits1.Set(1, false);
            // 设置用下标索引器设置值
            bits1[5] = false;
            bits1[7] = false;
            Console.Write("initialized: ");
            DisplayBits(bits1);
            Console.WriteLine();

            // 位的一些运算
            DisplayBits(bits1);
            bits1.Not();
            Console.Write(" not ");
            DisplayBits(bits1);
            Console.WriteLine();

            var bits2 = new BitArray(bits1);
            bits2[0] = true;
            bits2[1] = false;
            bits2[4] = true;
            DisplayBits(bits1);

            Console.Write(" or ");
            DisplayBits(bits2);
            Console.Write(" : ");
            bits1.Or(bits2);
            DisplayBits(bits1);
            Console.WriteLine();

            DisplayBits(bits2);
            Console.Write(" and ");
            DisplayBits(bits1);
            Console.Write(" : ");
            bits2.And(bits1);
            DisplayBits(bits2);
            Console.WriteLine();

            DisplayBits(bits1);
            Console.Write(" xor ");
            DisplayBits(bits2);
            bits1.Xor(bits2);
            Console.Write(" : ");
            DisplayBits(bits1);
            Console.WriteLine();
        }

        static void BitVectorDemo()
        {

            var bits1 = new BitVector32();
            // 书面写法,不考虑大小端,最后一个位的掩码(考虑是小端也就是地址读到的第一个)
            int bit1 = BitVector32.CreateMask();
            // 基于bit1位的上一个位的掩码,其实bit1是1
            int bit2 = BitVector32.CreateMask(bit1);
            // 基于bit2位的上一个位的掩码,其实bit2是2
            int bit3 = BitVector32.CreateMask(bit2);
            int bit4 = BitVector32.CreateMask(bit3);
            int bit5 = BitVector32.CreateMask(bit4);

            // 用索引器将末尾取值为1,其实bit1是1
            bits1[bit1] = true;
            // 用索引器将倒数第二位取值为0,其实bit2是2
            bits1[bit2] = false;
            bits1[bit3] = true;
            bits1[bit4] = true;
            Console.WriteLine(bits1);
            // 可以一次性的把有1下标的赋值为true
            bits1[0xabcdef] = true;
            Console.WriteLine(bits1);

            int received = 0x79abcdef;
            // 一次性的把有1下标的赋值为true,为0的赋值位0
            var bits2 = new BitVector32(received);
            Console.WriteLine(bits2);
            // sections: FF EEE DDD CCCC BBBBBBBB AAAAAAAAAAAA
            // 从底地址开始截取0xfff片段的索引值
            BitVector32.Section sectionA = BitVector32.CreateSection(0xfff);
            // 基于sectionA偏移,取0xff位数上的索引值
            BitVector32.Section sectionB = BitVector32.CreateSection(0xff, sectionA);
            BitVector32.Section sectionC = BitVector32.CreateSection(0xf, sectionB);
            BitVector32.Section sectionD = BitVector32.CreateSection(0x7, sectionC);
            BitVector32.Section sectionE = BitVector32.CreateSection(0x7, sectionD);
            BitVector32.Section sectionF = BitVector32.CreateSection(0x3, sectionE);

            // 用索引片段,访问bits2在该片段上的元素
            Console.WriteLine("Section A: " + IntToBinaryString(bits2[sectionA], true));
            Console.WriteLine("Section B: " + IntToBinaryString(bits2[sectionB], true));
            Console.WriteLine("Section C: " + IntToBinaryString(bits2[sectionC], true));
            Console.WriteLine("Section D: " + IntToBinaryString(bits2[sectionD], true));
            Console.WriteLine("Section E: " + IntToBinaryString(bits2[sectionE], true));
            Console.WriteLine("Section F: " + IntToBinaryString(bits2[sectionF], true));
        }

        static string IntToBinaryString(int bits, bool removeTrailingZero)
        {
            var sb = new StringBuilder(32);
            for (int i = 0; i < 32; i++)
            {
                // 从左边读起,读完后丢弃左边位数,所以与上0x80000000
                if ((bits & 0x80000000) != 0)
                {
                    sb.Append("1");
                }
                else
                {
                    sb.Append("0");
                }
                bits = bits << 1;
            }
            string s = sb.ToString();
            if (removeTrailingZero)
                return s.TrimStart('0');
            else
                return s;
        }

        static void DisplayBits(BitArray bits)
        {
            // 可以直接迭代输出
            foreach (bool bit in bits)
            {
                Console.Write(bit ? 1 : 0);
            }
        }
    }
}

2.IProducerConsumerCollection<T>接口

.net4.0中包含了新的命名空间System.Collections.Concurrent,定义了一些线程安全的集合,这些集合实现了IProducerConsumerCollection<T>接口(生产消费者模式)。
这样多线程访问这些集合就不需要Lock{}操作了。只需要用相应集合的TryAdd和TryTake方法,这两个方法分为阻塞的和非阻塞的,如果失败会返回false否则返回true。
这些集合有:
ConcurrentQueue<T>集合,Enqueue(),TryDequeue()和TryPeek方法。
ConcurrentStack<T>类。
ConcurrentBag<T>类。
ConcurrentDictionary<TKey, TValue>这个是线程安全的键值集合,TryAdd,TryGetValue, TryRemove,TryUpdate方法以非阻塞方式访问成员。因为基于键和值ConcurrentDictionary<TKey,

TValue>没有实现IProducerConsumerCollection<T>接口,但是也是支持线程安全的。
ConcurrentXXX类型的集合是线程安全的。

BlockingCollection<T>定义了集合操作阻塞的接口,使用令牌机制,可以指定等待的最长时间。BlockingCollection<T>是针对实现了IProducerConsumerCollection<T>接口的任意类型修饰器,默认是使用了ConcurrentQueue<T>类。
实现例子:
static void Main()
        {
            BlockingDemoSimple();
        }

        static void BlockingDemoSimple()
        {
            // 阻塞的容器
            var sharedCollection = new BlockingCollection<int>();
            
            // 定义两个事件对象和等待事件完成的重置消息句柄
            var events = new ManualResetEventSlim[2];
            var waits = new WaitHandle[2];
            for (int i = 0; i < 2; i++)
			{
			    events[i] = new ManualResetEventSlim(false);
                waits[i] = events[i].WaitHandle;
			}

            var producer = new Thread(obj =>
            {
                // 解析传入的集合容器和事件对象
                var state = (Tuple<BlockingCollection<int>, ManualResetEventSlim>)obj;
                var coll = state.Item1;
                var ev = state.Item2;
                var r = new Random();

                for (int i = 0; i < 300; i++)
                {
                    // 阻塞函数,var coll前面强转确定类型,可以添加元素
                    coll.Add(r.Next(3000));
                }
                // 事件完成,释放信号
                ev.Set();
            });
            producer.Start(Tuple.Create<BlockingCollection<int>, ManualResetEventSlim>(sharedCollection, events[0]));

            var consumer = new Thread(obj =>
            {
                // 解析传入的集合容器和事件对象
                var state = (Tuple<BlockingCollection<int>, ManualResetEventSlim>)obj;
                var coll = state.Item1;
                var ev = state.Item2;

                for (int i = 0; i < 300; i++)
                {
                    // 阻塞函数,前面强转确定类型,提取元素
                    int result = coll.Take();
                }
                // 事件完成,释放信号
                ev.Set();
            });
            consumer.Start(Tuple.Create<BlockingCollection<int>, ManualResetEventSlim>(sharedCollection, events[1]));

            // 主线程会阻塞一直等待信号到来
            if (!WaitHandle.WaitAll(waits))
                Console.WriteLine("wait failed");
            else
                Console.WriteLine("reading/writing finished");

        }


static void Main()
        {
            BlockingDemoSimple();
        }

        static void BlockingDemoSimple()
        {
            // 阻塞的容器
            var sharedCollection = new BlockingCollection<int>();
            
            // 定义两个事件对象和等待事件完成的重置消息句柄
            var events = new ManualResetEventSlim[2];
            var waits = new WaitHandle[2];
            for (int i = 0; i < 2; i++)
			{
			    events[i] = new ManualResetEventSlim(false);
                waits[i] = events[i].WaitHandle;
			}

            var producer = new Thread(obj =>
            {
                // 解析传入的集合容器和事件对象
                var state = (Tuple<BlockingCollection<int>, ManualResetEventSlim>)obj;
                var coll = state.Item1;
                var ev = state.Item2;
                var r = new Random();

                for (int i = 0; i < 300; i++)
                {
                    // 阻塞函数,var coll前面强转确定类型,可以添加元素
                    coll.Add(r.Next(3000));
                }
                // 事件完成,释放信号
                ev.Set();
            });
            producer.Start(Tuple.Create<BlockingCollection<int>, ManualResetEventSlim>(sharedCollection, events[0]));

            var consumer = new Thread(obj =>
            {
                // 解析传入的集合容器和事件对象
                var state = (Tuple<BlockingCollection<int>, ManualResetEventSlim>)obj;
                var coll = state.Item1;
                var ev = state.Item2;

                for (int i = 0; i < 300; i++)
                {
                    // 阻塞函数,前面强转确定类型,提取元素
                    int result = coll.Take();
                }
                // 事件完成,释放信号
                ev.Set();
            });
            consumer.Start(Tuple.Create<BlockingCollection<int>, ManualResetEventSlim>(sharedCollection, events[1]));

            // 主线程会阻塞一直等待信号到来
            if (!WaitHandle.WaitAll(waits))
                Console.WriteLine("wait failed");
            else
                Console.WriteLine("reading/writing finished");

        }
版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/blues1021。 https://blog.csdn.net/Blues1021/article/details/48898905 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C#版本数据结构,用C#的同志们有福啦 本书节选: 第1章 绪论 数据是外部世界信息的计算机化,是计算机加工处理的对象。运用计算机处 理数据时,必须解决四个方面的问题:一是如何在计算机中方便、高效地表示和 组织数据;二是如何在计算机存储器(内存和外存)中存储数据;三是如何对存 储在计算机中的数据进行操作,可以有哪些操作,如何实现这些操作以及如何对 同一问题的不同操作方法进行评价;四是必须理解每种数据结构的性能特征,以 便选择一个适合于某个特定问题的数据结构。这些问题就是数据结构这门课程所 要研究的主要问题。本章首先说明学习数据结构的必要性和本书的目的,然后解 释数据结构及其有关概念,接着讨论算法的相关知识,最后简单介绍本书所要用 到的相关数学知识和C#知识。 1.1 数据结构 1.1.1 学习数据结构的必要性 我们知道,虽然每个人都懂得英语的语法与基本类型,但是对于同样的题目, 每个人写出的作文,水平却高低不一。程序设计也和写英语作文一样,虽然程序 员都懂得语言的语法与语义,但是对于同样的问题,程序员写出来的程序不一样。 有的人写出来的程序效率很高,有的人却用复杂的方法来解决一个简单的问题。 当然,程序设计水平的提高仅仅靠看几本程序设计书是不行的。只有多思索、 多练习,才能提高自己的程序设计水平;否则,书看得再多,提高也不大。记得 刚学程序设计时,常听人说程序设计水平要想提高,最重要的是多看别人写的程 序,多去思考问题。从别人写的程序中,我们可以发现效率更高的解决方法;从 思考问题的过程中,我们可以了解解决问题的方法常常不只一个。运用先前解决 问题的经验,来解决更复杂更深入的问题,是提高程序设计水平的最有效途径。 数据结构正是前人在思索问题的过程中所想出的解决方法。一般而言,在学 习程序设计一段时间后,学习“数据结构”便能让你的程序设计水平上一个台阶。 如果只学会了程序设计的语法和语义,那么你只能解决程序设计三分之一的问 题,而且运用的方法并不是最有效的。但如果学会了数据结构的概念,就能在程 序设计上,运用最有效的方法来解决绝大多数的问题。 《数据结构》这门课程的目的有三个。第一个是讲授常用的数据结构,这些 数据结构形成了程序员基本数据结构工具箱(toolkit)。对于许多常见的问题,工 具箱里的数据结构是理想的选择。就像.NET Framework 中Windows应用程序开 发中的工具箱,程序员可以直接拿来或经过少许的修改就可以使用,非常方便。 第二个是讲授常用的算法,这和数据结构一样,是人们在长期实践过程中的总结, 程序员可以直接拿来或经过少许的修改就可以使用。可以通过算法训练来提高程 序设计水平。第三个目的是通过程序设计的技能训练促进程序员综合能力的提 高。 1.1.2 基本概念和术语 在本小节中,将对一些常用的概念和术语进行介绍,这些概念和术语在以后 的章节中会多次出现。 1、数据(Data) 数据是外部世界信息的载体,它能够被计算机识别、存储和加工处理,是计 算机程序加工的原料。计算机程序处理各种各样的数据,可以是数值数据,如整 数、实数或复数;也可以是非数值数据,如字符、文字、图形、图像、声音等。 2、数据元素(Data Element)和数据项(Data Item) 数据结构C#语言版) 1.1 数据结构2 数据元素是数据的基本单位,在计算机程序中通常被作为一个整体进行考虑 和处理。数据元素有时也被称为元素、结点、顶点、记录等。一个数据元素可由 若干个数据项(Data Item)组成。数据项是不可分割的、含有独立意义的最小数据 单位,数据项有时也称为字段(Field)或域(Domain)。例如,在数据库信息处理系 统中,数据表中的一条记录就是一个数据元素。这条记录中的学生学号、姓名、 性别、籍贯、出生年月、成绩等字段就是数据项。数据项分为两种,一种叫做初 等项,如学生的性别、籍贯等,在处理时不能再进行分割;另一种叫做组合项, 如学生的成绩,它可以再分为数学、物理、化学等更小的项。 3、数据对象(Data Object) 数据对象是性质相同的数据元素的集合,是数据的一个子集。例如,整数数 据对象是{0,±1,±2,±3,…},字符数据对象是{a,b,c,…}。 4、数据类型(Data Type) 数据类型是高级程序设计语言中的概念,是数据的取值范围和对数据进行操 作的总和。数据类型规定了程序中对象的特性。程序中的每个变量、常量或表达 式的结果都应该属于某种确定的数据类型。例如,C#语言中的字符串类型(String, 经常写为string)。一 个String表示一个恒定不变的字符序列集合,所有的字符序 列集合构成String的取值范围。我们可以对String进行求长度、复制、连接两个 字符串等操作。 数据类型可分为两类:一类是非结构的原子类型,如C#语言中的基本类型 (整型、实型、字符型等);另一类是结构类型,它的成分可以由多个结构类型 组成,并可以分解。结构类型的成分可以是非结构的,也可以是结构的。例如, C#语言中数组的成分可以是整型等基本类型,也可以是数组等结构类型。 5、数据结构(Data Structure) 数据结构是相互之间存在一种或多种特定关系的数据元素的集合。在任何问 题中,数据元素之间都不是孤立的,而是存在着一定的关系,这种关系称为结构 (Structure)。根据数据元素之间关系的不同特性,通常有4类基本数据结构: (1) 集合(Set):如图1.1(a)所示,该结构中的数据元素除了存在“同属于一个集 合”的关系外,不存在任何其它关系。 (2) 线性结构(Linear Structure):如图1.1(b)所示,该结构中的数据元素存在着一 对一的关系。 (3) 树形结构(Tree Structure):如图1.1(c)所示,该结构中的数据元素存在着一对 多的关系。 (4) 图状结构(Graphic Structure):如图1.1(d)所示,该结构中的数据元素存在着 多对多的关系。 (a) 集合 (b) 线性结构 (c) 树形结构 (d)图状结构 图 1.1 4 类基本数据结构关系图 由于集合中的元素的关系极为松散,可用其它数据结构来表示,所以本书不 做专门介绍。关于集合的概念在1.3.1小节中有介绍。 数据结构的形式化定义为: 数据结构C#语言版) 1.1 数据结构3 数据结构(Data Structure)简记为DS,是一个二元组, DS = (D,R) 其中:D是数据元素的有限集合, R是数据元素之间关系的有限集合。 下面通过例题来进一步理解后3类数据结构。 【例1-1】 学生信息表(如表1.1所示.)是一个线性的数据结构,表中的每 一行是一个记录(在数据库信息处理系统中,表中的一个数据元素称为一个记 录)。一条记录由学号、姓名、行政班级、性别和出生年月等数据项组成。表中 数据元素之间的关系是一对一的关系。 表 1.1 学生信息表 学号 姓名 行政班级 性别 出生年月 040303001 雷洪 软件04103 男 1986.12 040303002 李春 软件04103 女 1987.3 040303003 周刚 软件04103 男 1986.9 【例1-2】 家族关系是典型的树形结构,图1.2是一个三代的家族关系。在 图中,爷爷、儿子、女儿、孙子、孙女或外孙女是一个结点(在树形结构中,数 据元素称为结点),他们之间是一对多的关系。其中,爷爷有两个儿子和一个女 儿,这是一对三的关系;一个儿子有两个儿子(爷爷的孙子),这是一对二的关 系;另一个儿子有一个儿子(爷爷的孙子)和一个女儿(爷爷的孙女),这是一 对二的关系;女儿有三个女儿(爷爷的外孙女),这是一对三的关系。树形结构 具有严格的层次关系,爷爷在树形结构的最上层,中间层是儿子和女儿,最下层 是孙子、孙女和外孙女。不能把这种关系倒过来,因为绝对不会先有儿子或女儿 再有爷爷,也不会先有孙子或孙女再有儿子、先有外孙女再有女儿。 外孙女 爷爷 儿子 儿子 女儿 孙子 孙子 孙子 孙女 外孙女 外孙女 图 1.2 家族关系图 【例1-3】 图1.3是四个城市的公路交通图,这是一个典型的图状结构。在 图中,每个城市是一个顶点(在图状结构中,数据元素称为顶点),它们之间是 多对多的关系。成都与都江堰、雅安直接通公路,都江堰与成都、青城山直接通 公路,青城山与都江堰、成都及雅安直接通公路,雅安与成都、青城山直接通公 路。这些公路构成了一个公路交通网,所以,又把图状结构称为网状结构(Network Structure) 数据结构C#语言版) 1.2 算法4 成都 都江堰 青城山 雅安 图 1.3 四城市交通图 从数据类型和数据结构的概念可知,二者的关系非常密切。数据类型可以看 作是简单的数据结构。数据的取值范围可以看作是数据元素的有限集合,而对数 据进行操作的集合可以看作是数据元素之间关系的集合。 数据结构包括数据的逻辑结构和物理结构。上述数据结构的定义就是数据的 逻辑结构(Logic Structure),数据的逻辑结构是从具体问题抽象出来的数学模型, 是为了讨论问题的方便,与数据在计算机中的具体存储没有关系。然而,我们讨 论数据结构的目的是为了在计算机中实现对它的操作,因此还需要研究在计算机 中如何表示和存储数据结构,即数据的物理结构(Physical Structure)。数据的物理 结构又称为存储结构(Storage Structure),是数据在计算机中的表示(又叫映像) 和存储,包括数据元素的表示和存储以及数据元素之间关系的表示和存储。 数据的存储结构包括顺序存储结构和链式存储结构两种。顺序存储结构 (Sequence Storage Structure)是通过数据元素在计算机存储器中的相对位置来表 示出数据元素的逻辑关系,一般把逻辑上相邻的数据元素存储在物理位置相邻的 存储单元中。在C#语言中用数组来实现顺序存储结构。因为数组所分配的存储 空间是连续的,所以数组天生就具有实现数据顺序存储结构的能力。链式存储结 构(Linked Storage Structure)对逻辑上相邻的数据元素不要求其存储位置必须相 邻。链式存储结构中的数据元素称为结点(Node),在结点中附设地址域(Address Domain)来存储与该结点相邻的结点的地址来实现结点间的逻辑关系。这个地址 称为引用(Reference),这个地址域称为引用域(Reference Domain)。 从20世纪60年代末到70年代初,出现了大型程序,软件也相对独立,人 们越来越重视数据结构,认为程序设计的实质是确定数据结构,加上设计一个好 的算法,这就是人们常说的“程序=数据结构+算法”。下一节谈谈算法的问题。 1.2 算法 从上节我们知道,算法与数据结构和程序的关系非常密切。进行程序设计时, 先确定相应的数据结构,然后再根据数据结构和问题的需要设计相应的算法。由 于篇幅所限,下面只从算法的特性、算法的评价标准和算法的时间复杂度等三个 方面进行介绍。 1.2.1 算法的特性 算法(Algorithm)是对某一特定类型的问题的求解步骤的一种描述,是指令的 有限序列。其中的每条指令表示一个或多个操作。一个算法应该具备以下5个特 性: 1、有穷性(Finity):一个算法总是在执行有穷步之后结束,即算法的执行时间是 有限的。 2、确定性(Unambiguousness):算法的每一个步骤都必须有确切的含义,即无二 义,并且对于相同的输入只能有相同的输出。 3、输入(Input):一个算法具有零个或多个输入。它即是在算法开始之前给出的 数据结构C#语言版) 1.2 算法5 量。这些输入是某数据结构中的数据对象。 4、 输出(Output):一个算法具有一个或多个输出,并且这些输出与输入之间存 在着某种特定的关系。 5、 能行性(realizability):算法中的每一步都可以通过已经实现的基本运算的有 限次运行来实现。 算法的含义与程序非常相似,但二者有区别。一个程序不一定满足有穷性。 例如操作系统,只要整个系统不遭破坏,它将永远不会停止。还有,一个程序只 能用计算机语言来描述,也就是说,程序中的指令必须是机器可执行的,而算法 不一定用计算机语言来描述,自然语言、框图、伪代码都可以描述算法。 在本书中我们尽可能采用C#语言来描述和实现算法,使读者能够阅读或上 机执行,以便更好地理解算法。 1.2.2 算法的评价标准 对于一个特定的问题,采用的数据结构不同,其设计的算法一般也不同,即 使在同一种数据结构下,也可以采用不同的算法。那么,对于解决同一问题的不 同算法,选择哪一种算法比较合适,以及如何对现有的算法进行改进,从而设计 出更适合于数据结构的算法,这就是算法评价的问题。评价一个算法优劣的主要 标准如下: 1、正确性(Correctness)。算法的执行结果应当满足预先规定的功能和性能的要求, 这是评价一个算法的最重要也是最基本的标准。算法的正确性还包括对于输入、 输出处理的明确而无歧义的描述。 2、可读性(Readability)。算法主要是为了人阅读和交流,其次才是机器的执行。 所以,一个算法应当思路清晰、层次分明、简单明了、易读易懂。即使算法已转 变成机器可执行的程序,也需要考虑人能较好地阅读理解。同时,一个可读性强 的算法也有助于对算法中隐藏错误的排除和算法的移植。 3、健壮性(Robustness)。一个算法应该具有很强的容错能力,当输入不合法的数 据时,算法应当能做适当的处理,使得不至于引起严重的后果。健壮性要求表明 算法要全面细致地考虑所有可能出现的边界情况和异常情况,并对这些边界情况 和异常情况做出妥善的处理,尽可能使算法没有意外的情况发生。 4、运行时间(Running Time)。运行时间是指算法在计算机上运行所花费的时间, 它等于算法中每条语句执行时间的总和。对于同一个问题如果有多个算法可供选 择,应尽可能选择执行时间短的算法。一般来说,执行时间越短,性能越好。 5、占用空间(Storage Space)。占用空间是指算法在计算机上存储所占用的存储空 间,包括存储算法本身所占用的存储空间、算法的输入及输出数据所占用的存储 空间和算法在运行过程中临时占用的存储空间。算法占用的存储空间是指算法执 行过程中所需要的最大存储空间,对于一个问题如果有多个算法可供选择,应尽 可能选择存储量需求低的算法。实际上,算法的时间效率和空间效率经常是一对 矛盾,相互抵触。我们要根据问题的实际需要进行灵活的处理,有时需要牺牲空 间来换取时间,有时需要牺牲时间来换取空间。 通常把算法在运行过程中临时占用的存储空间的大小叫算法的空间复杂度 (Space Complexity)。算法的空间复杂度比较容易计算,它主要包括局部变量所占 用的存储空间和系统为实现递归所使用的堆栈占用的存储空间。 如果算法是用计算机语言来描述的,还要看程序代码量的大小。对于同一个 问题,在用上面5条标准评价的结果相同的情况下,代码量越少越好。实际上, 代码量越大,占用的存储空间会越多,程序的运行时间也可能越长,出错的可能 数据结构C#语言版) 1.2 算法6 性也越大,阅读起来也越麻烦。 在以上标准中,本书主要考虑程序的运行时间,也考虑执行程序所占用的空 间。影响程序运行时间的因素很多,包括算法本身、输入的数据以及运行程序的 计算机系统等。计算机的性能由以下因素决定: 1、硬件条件。包括所使用的处理器的类型和速度(比如,使用双核处理器还是 单核处理器)、可使用的内存(缓存和RAM)以及可使用的外存等。 2、实现算法所使用的计算机语言。实现算法的语言级别越高,其执行效率相对 越低。 3、所使用的语言的编译器/解释器。一般而言,编译的执行效率高于解释,但解 释具有更大的灵活性。 4、所使用的操作系统软件。操作系统的功能主要是管理计算机系统的软件和硬 件资源,为计算机用户方便使用计算机提供一个接口。各种语言处理程序如编译 程序、解释程序等和应用程序都在操作系统的控制下运行。 1.2.3 算法的时间复杂度 一个算法的时间复杂度(Time Complexity)是指该算法的运行时间与问题规 模的对应关系。一个算法是由控制结构和原操作构成的,其执行的时间取决于二 者的综合效果。为了便于比较同一问题的不同算法,通常把算法中基本操作重复 执行的次数(频度)作为算法的时间复杂度。算法中的基本操作一般是指算法中 最深层循环内的语句,因此,算法中基本操作语句的频度是问题规模n的某个函 数f(n),记作:T(n)=O(f(n))。其中“O”表示随问题规模n的增大,算法执行时 间的增长率和f(n)的增长率相同,或者说,用“O”符号表示数量级的概念。例 如,如 )1n(n 2 1 )n(T −= ,则 )1n(n 2 1 −的数量级与n2 相同,所以T(n)=O(n2 )。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值