2009-05-05 31 views
74

Bất cứ ai có thể giải thích cho tôi tại sao hàm Contains() của danh sách generics quá chậm?
Tôi có Danh sách có khoảng một triệu số và mã liên tục kiểm tra xem có số cụ thể nào trong các số này hay không.
Tôi đã thử làm điều tương tự bằng cách sử dụng từ điển và hàm ContainsKey() và nhanh hơn khoảng 10-20 lần so với danh sách.
Tất nhiên, tôi không thực sự muốn sử dụng từ điển cho mục đích đó, bởi vì nó không có nghĩa là được sử dụng theo cách đó.
Vì vậy, câu hỏi thực sự ở đây là, có bất kỳ thay thế cho List.Contains(), nhưng không phải là whacky như Dictionary.ContainsKey()?
Cảm ơn bạn trước!C#, Danh sách <T> .Contains() - quá chậm?

+2

Vấn đề gì với từ điển? Nó được thiết kế để sử dụng trong trường hợp như của bạn. – Kamarey

+4

@Kamarey: HashSet có thể là một lựa chọn tốt hơn. –

+0

HashSet là những gì tôi đang tìm kiếm. – DSent

Trả lời

134

Nếu bạn chỉ kiểm tra cho sự tồn tại, HashSet<T> trong .NET 3.5 là lựa chọn tốt nhất của bạn - hiệu suất từ ​​điển giống như, nhưng không có cặp khóa/giá trị - chỉ các giá trị:

HashSet<int> data = new HashSet<int>(); 
    for (int i = 0; i < 1000000; i++) 
    { 
     data.Add(rand.Next(50000000)); 
    } 
    bool contains = data.Contains(1234567); // etc 
5

Từ điển không phải là xấu, bởi vì các phím trong từ điển được thiết kế để được tìm thấy nhanh chóng. Để tìm một số trong danh sách cần phải lặp qua toàn bộ danh sách.

Tất nhiên từ điển chỉ hoạt động nếu các số của bạn là duy nhất và không được đặt hàng.

Tôi nghĩ rằng đó cũng là lớp học HashSet<T> trong .NET 3.5, nó cũng chỉ cho phép các phần tử duy nhất.

+0

Từ điển có thể lưu trữ hiệu quả các đối tượng không phải là duy nhất - sử dụng số nguyên để đếm số lượng các bản sao. Ví dụ: bạn lưu trữ danh sách {a, b, a} là {a = 2, b = 1}. Dĩ nhiên, nó không làm mất đi sự xuất sắc. – MSalters

25

List.Contains là một hoạt động O (n).

Dictionary.ContainsKey là một hoạt động O (1), vì nó sử dụng mã băm của các đối tượng dưới dạng khóa, cung cấp cho bạn khả năng tìm kiếm nhanh hơn.

Tôi không nghĩ rằng nên có một Danh sách chứa hàng triệu mục nhập. Tôi không nghĩ rằng lớp List được thiết kế cho mục đích đó. :)

Chẳng phải có thể lưu các thực thể millon đó vào RDBMS và thực hiện truy vấn trên cơ sở dữ liệu đó không?

Nếu không thể, thì tôi sẽ sử dụng từ điển.

+12

Tôi không nghĩ có bất cứ điều gì không phù hợp về một danh sách với hàng triệu mục, nó chỉ là bạn có thể không muốn tiếp tục chạy các tìm kiếm tuyến tính trên nó. –

+0

Tôi đã kết thúc bằng cách sử dụng HashSet cho công việc của mình, nhưng cảm ơn! – DSent

+0

Đồng ý, không có gì sai với một danh sách cũng không phải là một mảng với nhiều mục. Chỉ cần không quét các giá trị. –

2

Một SortedList sẽ nhanh hơn để tìm kiếm (nhưng chậm hơn để chèn bài)

1

Tại sao là một cuốn từ điển phù hợp?

Để xem liệu một giá trị cụ thể có nằm trong danh sách bạn cần để đi bộ toàn bộ danh sách hay không. Với một từ điển (hoặc thùng chứa dựa trên băm khác), bạn sẽ nhanh chóng thu hẹp số lượng đối tượng cần so sánh. Chìa khóa (trong trường hợp của bạn, số) được băm và cung cấp cho từ điển tập con của các đối tượng phân đoạn để so sánh.

0

Tôi đang sử dụng này trong Compact Framework, nơi không có hỗ trợ cho HashSet, tôi đã chọn cho một từ điển mà cả hai chuỗi là giá trị tôi đang tìm kiếm.

Điều đó có nghĩa là tôi nhận được danh sách <> chức năng có hiệu suất từ ​​điển. Đó là một chút hacky, nhưng nó hoạt động.

+1

Nếu bạn đang sử dụng một từ điển thay cho một HashSet, bạn cũng có thể đặt giá trị thành "" chứ không phải là chuỗi giống như khóa. Bằng cách đó bạn sẽ sử dụng ít bộ nhớ hơn. Hoặc bạn thậm chí có thể sử dụng từ điển và đặt tất cả thành true (hoặc false). Tôi không biết cái nào sẽ sử dụng ít bộ nhớ hơn, một chuỗi rỗng hoặc một cái bool. Đoán của tôi sẽ là bool. – TTT

+0

Trong từ điển, tham chiếu 'chuỗi' và giá trị' bool' tạo ra sự khác biệt 3 hoặc 7 byte, tương ứng với hệ thống 32 hoặc 64 bit. Tuy nhiên, lưu ý rằng kích thước của mỗi mục nhập được làm tròn lên đến bội số của 4 hoặc 8, tương ứng. Sự lựa chọn giữa 'chuỗi' và' bool' có thể không tạo ra bất kỳ sự khác biệt nào về kích thước. Chuỗi rỗng '" "' luôn luôn tồn tại trong bộ nhớ đã là thuộc tính tĩnh 'string.Empty', vì vậy nó không tạo ra bất kỳ sự khác biệt nào cho dù bạn sử dụng nó trong từ điển hay không. (Và nó được sử dụng ở những nơi khác.) – Wormbo

2

Đây không phải là câu trả lời chính xác cho câu hỏi của bạn, nhưng tôi có một lớp làm tăng hiệu suất của Chứa() trên một bộ sưu tập. Tôi đã phân lớp một Hàng đợi và thêm một Dictionary để ánh xạ hashcodes vào danh sách các đối tượng.Hàm Dictionary.Contains() là O (1) trong khi List.Contains(), Queue.Contains()Stack.Contains() là O (n).

Loại giá trị của từ điển là hàng đợi chứa đối tượng có cùng mã băm. Người gọi có thể cung cấp một đối tượng lớp tùy chỉnh thực hiện IEqualityComparer. Bạn có thể sử dụng mẫu này cho Ngăn xếp hoặc Danh sách. Mã sẽ chỉ cần một vài thay đổi.

/// <summary> 
/// This is a class that mimics a queue, except the Contains() operation is O(1) rather  than O(n) thanks to an internal dictionary. 
/// The dictionary remembers the hashcodes of the items that have been enqueued and dequeued. 
/// Hashcode collisions are stored in a queue to maintain FIFO order. 
/// </summary> 
/// <typeparam name="T"></typeparam> 
private class HashQueue<T> : Queue<T> 
{ 
    private readonly IEqualityComparer<T> _comp; 
    public readonly Dictionary<int, Queue<T>> _hashes; //_hashes.Count doesn't always equal base.Count (due to collisions) 

    public HashQueue(IEqualityComparer<T> comp = null) : base() 
    { 
     this._comp = comp; 
     this._hashes = new Dictionary<int, Queue<T>>(); 
    } 

    public HashQueue(int capacity, IEqualityComparer<T> comp = null) : base(capacity) 
    { 
     this._comp = comp; 
     this._hashes = new Dictionary<int, Queue<T>>(capacity); 
    } 

    public HashQueue(IEnumerable<T> collection, IEqualityComparer<T> comp = null) :  base(collection) 
    { 
     this._comp = comp; 

     this._hashes = new Dictionary<int, Queue<T>>(base.Count); 
     foreach (var item in collection) 
     { 
      this.EnqueueDictionary(item); 
     } 
    } 

    public new void Enqueue(T item) 
    { 
     base.Enqueue(item); //add to queue 
     this.EnqueueDictionary(item); 
    } 

    private void EnqueueDictionary(T item) 
    { 
     int hash = this._comp == null ? item.GetHashCode() :  this._comp.GetHashCode(item); 
     Queue<T> temp; 
     if (!this._hashes.TryGetValue(hash, out temp)) 
     { 
      temp = new Queue<T>(); 
      this._hashes.Add(hash, temp); 
     } 
     temp.Enqueue(item); 
    } 

    public new T Dequeue() 
    { 
     T result = base.Dequeue(); //remove from queue 

     int hash = this._comp == null ? result.GetHashCode() : this._comp.GetHashCode(result); 
     Queue<T> temp; 
     if (this._hashes.TryGetValue(hash, out temp)) 
     { 
      temp.Dequeue(); 
      if (temp.Count == 0) 
       this._hashes.Remove(hash); 
     } 

     return result; 
    } 

    public new bool Contains(T item) 
    { //This is O(1), whereas Queue.Contains is (n) 
     int hash = this._comp == null ? item.GetHashCode() : this._comp.GetHashCode(item); 
     return this._hashes.ContainsKey(hash); 
    } 

    public new void Clear() 
    { 
     foreach (var item in this._hashes.Values) 
      item.Clear(); //clear collision lists 

     this._hashes.Clear(); //clear dictionary 

     base.Clear(); //clear queue 
    } 
} 

Thử nghiệm đơn giản của tôi cho thấy rằng HashQueue.Contains() chạy nhanh hơn nhiều so với Queue.Contains(). Chạy mã thử nghiệm với số lượng được đặt là 10.000 sẽ mất 0,00045 giây đối với phiên bản HashQueue và 0,37 giây đối với phiên bản Hàng đợi. Với số lượng 100.000, phiên bản HashQueue mất 0,0031 giây trong khi Hàng đợi mất 36,38 giây!

Đây là mã thử nghiệm của tôi:

static void Main(string[] args) 
{ 
    int count = 10000; 

    { //HashQueue 
     var q = new HashQueue<int>(count); 

     for (int i = 0; i < count; i++) //load queue (not timed) 
      q.Enqueue(i); 

     System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); 
     for (int i = 0; i < count; i++) 
     { 
      bool contains = q.Contains(i); 
     } 
     sw.Stop(); 
     Console.WriteLine(string.Format("HashQueue, {0}", sw.Elapsed)); 
    } 

    { //Queue 
     var q = new Queue<int>(count); 

     for (int i = 0; i < count; i++) //load queue (not timed) 
      q.Enqueue(i); 

     System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); 
     for (int i = 0; i < count; i++) 
     { 
      bool contains = q.Contains(i); 
     } 
     sw.Stop(); 
     Console.WriteLine(string.Format("Queue,  {0}", sw.Elapsed)); 
    } 

    Console.ReadLine(); 
} 
+0

Tôi vừa thêm trường hợp thử nghiệm thứ 3 cho HashSet mà dường như có được kết quả tốt hơn so với giải pháp của bạn: 'HashQueue, 00: 00: 00.0004029' ' Queue, 00: 00: 00.3901439 ' ' HashSet, 00: 00: 00.0001716' – psulek

7

Tôi nghĩ rằng tôi có câu trả lời! Có, đúng là Contains() trong một danh sách (mảng) là O (n), nhưng nếu mảng ngắn và bạn đang sử dụng các kiểu giá trị, nó vẫn phải khá nhanh. Nhưng bằng cách sử dụng CLR Profiler [tải xuống miễn phí từ Microsoft], tôi phát hiện ra rằng Chứa() là các giá trị quyền anh để so sánh chúng, yêu cầu phân bổ đống, rất tốn kém (chậm). [Ghi chú: Đây là .Net 2.0; các phiên bản .Net khác không được kiểm tra.]

Đây là câu chuyện và giải pháp đầy đủ. Chúng ta có một kiểu liệt kê gọi là "VI" và tạo một lớp có tên là "ValueIdList", đây là một kiểu trừu tượng cho một danh sách (mảng) của các đối tượng VI. Việc thực hiện ban đầu là ở cổ .Net 1.1 ngày, và nó đã sử dụng một ArrayList đóng gói. Chúng tôi phát hiện gần đây trong http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx rằng một danh sách chung (Danh sách < VI>) thực hiện tốt hơn nhiều so với ArrayList trên các loại giá trị (như enum VI của chúng tôi) vì các giá trị không phải được đóng hộp. Đó là sự thật và nó hoạt động ... gần như.

CLR Profiler tiết lộ một bất ngờ. Dưới đây là một phần của Graph Allocation:

  • ValueIdList :: Có bool (VI) 5.5Mb (34,81%)
  • Generic.List :: Có bool (< UNKNOWN>) 5.5Mb (34,81%)
  • Generic.ObjectEqualityComparer < T> :: Equals bool (< UNKNOWN> < UNKNOWN>) 5.5Mb (34,88%)
  • Values.VI 7.7MB (49,03%)

Như bạn thấy, có() surpri đơn giản gọi Generic.ObjectEqualityComparer.Equals(), mà dường như đòi hỏi quyền anh của một giá trị VI, đòi hỏi phân bổ đống tốn kém. Thật kỳ lạ khi Microsoft loại bỏ quyền anh trong danh sách, chỉ cần yêu cầu nó một lần nữa cho một thao tác đơn giản như thế này.

Giải pháp của chúng tôi là viết lại thực thi Contains(), trong trường hợp của chúng tôi dễ thực hiện vì chúng tôi đã đóng gói đối tượng danh sách chung (_items). Đây là mã đơn giản:

public bool Contains(VI id) 
{ 
    return IndexOf(id) >= 0; 
} 

public int IndexOf(VI id) 
{ 
    int i, count; 

    count = _items.Count; 
    for (i = 0; i < count; i++) 
    if (_items[i] == id) 
     return i; 
    return -1; 
} 

public bool Remove(VI id) 
{ 
    int i; 

    i = IndexOf(id); 
    if (i < 0) 
    return false; 
    _items.RemoveAt(i); 

    return true; 
} 

So sánh các giá trị VI hiện đang được thực hiện trong phiên bản của riêng chúng ta IndexOf() không yêu cầu đấm bốc và rất nhanh. Chương trình đặc biệt của chúng tôi tăng lên 20% sau khi viết lại đơn giản này. O (n) ... không sao cả! Chỉ cần tránh việc sử dụng bộ nhớ bị lãng phí!

+0

Cảm ơn lời khuyên, tôi đã bị bắt bởi màn trình diễn quyền anh xấu. Việc triển khai 'Contains' tùy chỉnh là cách nhanh hơn cho trường hợp sử dụng của tôi. –

Các vấn đề liên quan