1. 程式人生 > >iOS7: 漫談基礎集合類(NSArray, NSSet, NSOrderedSet ,NSHashTable和 NSDictionary)

iOS7: 漫談基礎集合類(NSArray, NSSet, NSOrderedSet ,NSHashTable和 NSDictionary)

基礎集合類是每一個Mac/iOS應用的基本組成部分。在本文中,我們將對”老類”(NSArray, NSSet)和”新類”(NSMapTable, NSHashTable, NSPointerArray)進行一個深入的研究,探索每一個的效率細節,並討論其使用場景。

   提示:本文包含一些參照結果,但它們並不意味著絕對精確,也沒有進行多個、複雜的測試。這些結果的目的是給出一個快速和主要的執行時統計。所有的測試基於iPhone 5s,使用Xcode 5.1b1和iOS 7.1b1,64位的程式。編譯選項設定為-Ofast的釋出構建。Vectorize loops和unroll loops(預設設定)均設定為關閉。

大O符號

首先,我們需要一些理論知識。效率通常用大O符號描述。它定義了一個函式的 極限特徵 ,通常被用於描繪其演算法效率。O定義了函式增長率的上限。通過檢視通常使用的O符號和所需要的運算元來檢視差異的大小。

iOS7: <wbr>漫談基礎集合類(NSArray, <wbr>NSSet, <wbr>NSOrderedSet <wbr>和 <wbr>NSDictionary)

例如,如果用演算法複雜度為O(n2)的演算法對一個有50個元素的陣列排序,需要2500步的操作。而且,還有內部的系統開銷和方法呼叫 — 所以是2500個操作的時間常量。 O(1)是理想的複雜度,代表著恆定的時間。好的演算法通常需要O(n*log n)的時間

可變性

大多數的集合類存在兩個版本:可變和不可變(預設)。這和其他大多數的框架有非常大的不同,一開始會讓人覺得有一點奇怪。然而其他的框架現在也應用了這一特性:就在幾個月前,.

NET公佈了作為官方擴充套件的不可變集合

最大的好處是什麼?執行緒安全。不可變的集合完全是執行緒安全的,可以同時在多個執行緒中迭代,避免各種突變異常的風險。你的API 絕不 應該暴露可變集合。

當然從不可變到可變再變回來會有一定的代價 — 物件必須被拷貝兩次,所有集合內的物件將被retain/release。有時在內部使用一個可變的集合而在出口返回一個不可變的物件副本會更高效。

與其他框架不同,蘋果沒有提供一個執行緒安全的可變集合,NSCache是例外 — 但它真的算不上是集合類,因為它不是一個通用的容器。大多數時候,你的確想要高於集合級別的同步。想象一段程式碼,作用是檢查字典中一個key是否存在,並根據檢查結果決定設定一個新的key或者返回某些值 — 你通常需要把多個操作歸類,這時執行緒安全的可變變體並不能幫助你。

這裡有一些同步的,執行緒安全的可變集合有效的使用案例,只需要用幾行程式碼,通過子類和組合的方法建立諸如NSDictionaryNSArray

需要注意的是,一些更新式的集合類,如NSHashTable,NSMapTable和NSPointerArray預設就是可變的,它們並沒有對應的不可變的類。它們用於類的內部使用,或者某個你想要不常見的可變類的場景。

NSArray

NSArray作為一個儲存物件的有序集合,可能是被使用最多的集合類。這也是為什麼它有自己的比原來的[NSArray arrayWithObjects:..., nil]簡短得多的快速語法糖符號@[...]。

NSArray實現了objectAtIndexedSubscript:,因為我們可以使用類C的語法array[0]來代替原來的[array objectAtIndex:0]。

效能特徵

關於NSArray的內容比你想象的要多的多。基於儲存物件的多少,它使用各種內部的變體。最有趣的部分是蘋果對於個別的物件訪問並不保證O(1)的訪問時間 — 正如你在CFArray.h CoreFoundation header中的關於演算法複雜度的註解中可以讀到的:

The access time for a value in the array is guaranteed to be at worst O(lg N) for any implementation, current and future, but will often be O(1) (constant time). Linear search operations similarly have a worst case complexity of O(Nlg N), though typically the bounds will be tighter, and so on. Insertion or deletion operations will typically be linear in the number of values in the array, but may be O(Nlg N) clearly in the worst case in some implementations. There are no favored positions within the array for performance; that is, it is not necessarily faster to access values with low indices, or to insert or delete values with high indices, or whatever.

在測量的時候,NSArray產生了一些有趣的額外的效能特徵。在陣列的開頭和結尾插入/刪除元素通常是一個O(1)操作,而隨機的插入/刪除通常是 O(N)的。

有用的方法

NSArray的大多數方法使用isEqual:來檢查物件間的關係(例如containsObject:)。有一個特別的方法indexOfObjectIdenticalTo:用來檢查指標相等,如果你確保在同一個集合中搜索,那麼這個方法可以很大的提升搜尋速度。

在iOS 7中,我們最終得到了與lastObject對應的公開的firstObject方法,對於空陣列,這兩個方法都會返回nil — 而常規的訪問方法會丟擲一個NSRangeException異常。

關於構造(可變)陣列有一個漂亮的細節可以節省程式碼量。如果你通過一個可能為nil的陣列建立一個可變陣列,通常會這麼寫:

  1. NSMutableArray *mutableObjects [array mutableCopy]; 
  2. if (!mutableObjects) 
  3.     mutableObjects [NSMutableArray array]; 

或者通過更簡潔的三元運算子:

  1. NSMutableArray *mutableObjects [array mutableCopy] ?: [NSMutableArray array]; 

更好的解決方案是使用arrayWithArray:,即使原陣列為nil,該方法也會返回一個數組物件:

  1. NSMutableArray *mutableObjects [NSMutableArray arrayWithArray:array]; 

這兩個操作在效率上幾乎相等。使用copy會快一點點,不過話說回來,這不太可能是你應用的瓶頸所在。提醒:不要使用[@[] mutableCopy]。經典的[NSMutableArray array]可讀性更好。

翻轉一個數組非常簡單:array.reverseObjectEnumerator.allObjects。我們使用系統提供的reverseObjectEnumerator,每一個NSEnumerator都實現了allObjects,該方法返回一個新陣列。雖然沒有原生的randomObjectEnumerator方法,你可以寫一個自定義的打亂陣列順序的列舉器或者使用一些出色的開原始碼

陣列排序

有很多各種各樣的方法來對一個數組排序。如果陣列儲存的是字串物件,sortedArrayUsingSelector:是第一選擇:

  1. NSArray *array @[@"John Appleseed"@"Tim Cook"@"Hair Force One"@"Michael Jurewitz"]; 
  2. NSArray *sortedArray [array sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; 

下面的程式碼對儲存數字的內容同樣很好,因為NSNumber實現了compare::

  1. NSArray *numbers @[@9, @5, @11, @3, @1]; 
  2. NSArray *sortedNumbers [numbers sortedArrayUsingSelector:@selector(compare:)]; 

如果想更可控,可以使用基於函式指標的排序方法:

  1. (NSData *)sortedArrayHint; 
  2. (NSArray *)sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))comparator 
  3.                               context:(void *)context; 
  4. (NSArray *)sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))comparator 
  5.                               context:(void *)context hint:(NSData *)hint; 

蘋果增加了一個方法來加速使用sortedArrayHint的排序。

The hinted sort is most efficient when you have a large array (N entries) that you sort once and then change only slightly (P additions and deletions, where P is much smaller than N). You can reuse the work you did in the original sort by conceptually doing a merge sort between the N “old” items and the P “new” items. To obtain an appropriate hint, you use sortedArrayHint when the original array has been sorted, and keep hold of it until you need it (when you want to re-sort the array after it has been modified).

因為block的引入,也出現了一些基於block的排序方法:

  1. (NSArray *)sortedArrayUsingComparator:(NSComparator)cmptr; 
  2. (NSArray *)sortedArrayWithOptions:(NSSortOptions)opts 
  3.                     usingComparator:(NSComparator)cmptr; 

效能上來說,不同的方法間並沒有太多的不同。有趣的是,基於selector的方式是最快的。你可以在GitHub上找到測試用的原始碼:

  1. Sorting 1000000 elements. selector: 4947.90[ms] function5618.93[ms] block: 5082.98[ms]. 

二分查詢

NSArray從iOS 4/Snow Leopard開始內建了二分查詢

  1. typedef NS_OPTIONS(NSUInteger, NSBinarySearchingOptions
  2.         NSBinarySearchingFirstEqual     (1UL << 8), 
  3.         NSBinarySearchingLastEqual      (1UL << 9), 
  4.         NSBinarySearchingInsertionIndex (1UL << 10), 
  5. }; 
  6. (NSUInteger)indexOfObject:(id)obj 
  7.               inSortedRange:(NSRange)r 
  8.                     options:(NSBinarySearchingOptions)opts 
  9.             usingComparator:(NSComparator)cmp; 

為什麼要使用這個方法?類似containsObject:和indexOfObject:這樣的方法從0索引開始搜尋每個物件直到找到目標 — 不需要陣列被排序而且是O(n)的效率特性。換句話說,二分查詢需要陣列事先被排序,但只需要O(log n)的時間。因此,對於1,000,000的記錄,二分查詢法最多隻需要21次比較,而傳統的線性查詢則平均需要5000,000次的比較。

這是個簡單的衡量二分查詢有多快的資料:

  1. Time to search for 1000 entries within 1000000 objects. Linear: 54130.38[ms]. Binary: 7.62[ms] 

作為比較,查詢NSOrderedSet中的指定索引花費0.23毫秒 — 即使跟二分查詢相比也快了30多倍。

記住排序的開銷也是昂貴的。蘋果使用複雜度為O(n*log n)的歸併排序,所以如果執行過indexOfObject:一次,就沒有必要使用二分查找了。

通過指定NSBinarySearchingInsertionIndex,你可以獲得正確的插入索引,以確保在插入元素後仍然可以保證陣列的順序。

列舉和高階訊息

作為參照,我們來看一個普通的使用場景。從一個數組中過濾出另一個數組。測試了多個列舉方法和API特性:

  1. // First variant, using `indexesOfObjectsWithOptions:passingTest:`. 
  2. NSIndexSet *indexes [randomArray indexesOfObjectsWithOptions:NSEnumerationConcurrent 
  3.                                                    passingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) 
  4.     return testObj(obj); 
  5. }]; 
  6. NSArray *filteredArray [randomArray objectsAtIndexes:indexes]; 
  7. // Filtering using predicates (block-based or text)  
  8. NSArray *filteredArray2 [randomArray filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) 
  9.     return testObj(obj); 
  10. }]]; 
  11. // Block-based enumeration  
  12. NSMutableArray *mutableArray [NSMutableArray array]; 
  13. [randomArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
  14.     if (testObj(obj)) 
  15.         [mutableArray addObject:obj]; 
  16.     
  17. }]; 
  18. // Classic enumeration 
  19. NSMutableArray *mutableArray [NSMutableArray array]; 
  20. for (id obj in randomArray) 
  21.     if (testObj(obj)) 
  22.         [mutableArray addObject:obj]; 
  23.     
  24. // Using NSEnumerator, old school. 
  25. NSMutableArray *mutableArray [NSMutableArray array]; 
  26. NSEnumerator *enumerator [randomArray objectEnumerator]; 
  27. id obj nil; 
  28. while ((obj [enumerator nextObject]) != nil) 
  29.     if (testObj(obj)) 
  30.         [mutableArray addObject:obj]; 
  31.     
  32. // Using objectAtIndex: (via subscripting) 
  33. NSMutableArray *mutableArray [NSMutableArray array]; 
  34. for (NSUInteger idx 0; idx randomArray.count; idx++) 
  35.     id obj randomArray[idx]; 
  36.     if (testObj(obj)) 
  37.         [mutableArray addObject:obj]; 
  38.     

為了更好的理解這裡的效率測量,我們首先看一下陣列是如何迭代的。

indexesOfObjectsWithOptions:passingTest:必須每次都執行一次block因此比傳統的使用NSFastEnumeration技術的基於for迴圈的列舉要稍微低效一些。然後如果開啟了併發列舉,那麼前者的速度則會大大的超過後者幾乎2倍。iPhone 5s是雙核的,所以這說得通。這裡並沒有體現出來的是NSEnumerationConcurrent只對大量的物件有意義,如果你的集合中的物件數量很少,用哪個方法就真的無關緊要。甚至NSEnumerationConcurrent上額外的執行緒管理實際上會使結果變得更慢。

最大的輸家是filteredArrayUsingPredicate:。NSPredicate需要在這裡提及是因為,人們可以寫出,尤其是用不基於block的變體。使用Core Data的使用者應該會很熟悉。

為了比較的完整,我們也加入了NSEnumerator作為比較 — 雖然沒有任何理由再使用它了。然而它竟出人意料的快(比基於NSPredicate的查詢要快),它的執行時消耗無疑比快速列舉更多 — 現在它只用於向後相容。甚至沒有優化過的objectAtIndex:都要更快些。

NSFastEnumeration

在OSX 10.5和iOS的最初版本中,蘋果增加了NSFastEnumeration。在此之前,只有每次返回一個元素的NSEnumeration,每次迭代都有執行時開銷。而快速列舉,蘋果通過countByEnumeratingWithState:objects:count:返回一個數據塊。該資料塊被解析成ids型別的C陣列。這就是更快的速度的原因;迭代一個C陣列更快,而且可以被編譯器更深一步的優化。手動的實現快速列舉是十分難辦的,所以蘋果的FastEnumerationSample是一個不錯的開始,還有一篇Mike Ash的文章也很不錯。

應該用arrayWithCapacity:嗎?

初始化NSArray的時候,可以選擇指定陣列的預期大小。在檢測的時候,結果是在效率上沒有差別 — 測量的時間幾乎相等,且在統計不確定性的範圍內。有訊息透漏說實際上蘋果並沒有使用這個特性。然而使用arrayWithCapacity:仍然有用,在文件不清晰的程式碼中,它可以幫助理解程式碼:

  1. Adding 10.000.000 elements to NSArray. no count 1067.35[ms] with count: 1083.13[ms]. 

子類化注意事項

很少有理由去子類化基礎集合類。大多數時候,使用CoreFoundation級別的類並且自定義回撥函式定製自定義行為是更好的解決方案。

建立一個大小寫不敏感的字典,一種方法是子類化NSDictionary並且自定義訪問方法,使其將字串始終變為小寫(或大寫),並對排序也做類似的修改。更快更好的解決方案是提供一個不同的CFDictionaryKeyCallBacks集,你可以提供自定義的hash和isEqual:回撥。你可以在這裡找到一個例子。這種方法的優美之處(感謝toll-free bridging)在於它仍然是一個簡單的字典,可以被任何使用NSDictionary作為引數的API接受。

子類作用的一個例子是有序字典的用例。.NET提供了一個SortedDictionary,Java有TreeMap,C++有std::map。雖然你 可以 使用C++的STL容器,但卻無法使它自動的retain/release,這會使使用起來笨重的多。因為NSDictionary是一個類簇,所以子類化跟人們想象的相比非常不同。這已經超過了本文的討論範疇,這裡有一個真實的有序字典的例子。

NSDictionary

一個字典儲存任意的物件鍵值對。 由於歷史原因,初始化方法使用相反的物件到值的方法,[NSDictionary dictionaryWithObjectsAndKeys:object, key, nil],而新的快捷語法則從key開始,@{key : value, ...}。

相關推薦

iOS7: 漫談基礎集合(NSArray, NSSet, NSOrderedSet ,NSHashTable NSDictionary)

基礎集合類是每一個Mac/iOS應用的基本組成部分。在本文中,我們將對”老類”(NSArray, NSSet)和”新類”(NSMapTable, NSHashTable, NSPointerArray)進行一個深入的研究,探索每一個的效率細節,並討論其使用場景。

__block __weak漫談基礎集合(NSArray,NSSet,NSOrderedSet,NSDictionary,NSMapTable,NSHashTable, NSPointerArra

http://www.cnblogs.com/zhaoguowen/p/4273237.html http://blog.csdn.net/leikezhu1981/article/details/45009123

Java基礎--集合

最近在找工作,目前還沒有定下來,拿到了一個公司的offer,不過被當白菜了,正在商量薪資方面的事情。隨著百度面試的失敗,夢想再次破滅。想想這一年來的奮鬥,別是一番滋味在心頭。突然想起一句話:踏歌長行,夢想永在! 說程式設計師是幸福的,因為我們每天都會和大腦過意不

黑馬程式設計師——java基礎——集合

------- android培訓、java培訓、期待與您交流! ---------- 面嚮物件語言對事物的體現都是以物件的形式,所以為了方便對多個物件的操作,Java就提供了集合類。 集合的繼承

java基礎集合——ArrayList 源碼略讀

lec rst extend except ini cts 數據量 bound cif ArrayList是java的動態數組,底層是基於數組實現。 1. 成員變量 public class ArrayList<E> extends AbstractList&l

java基礎集合——LinkedList 源碼略讀

serializa 鏈表 ngs 功能 ray 方法 == memory () 1.概覽 LinkedList是java的動態數組另一種實現方式,底層是基於雙向鏈表,而不是數組。 public class LinkedList<E> extends Ab

Java集合中不實現CloneableSerializable原因

tran style 能夠 uid sta 工具 數據 類型 size  Java集合類中不實現Cloneable和Serializable原因      莫名的被問這麽一個問題,也答不上,記錄記錄,為何Java集合類不實現Cloneable和Serializable接口。

Java集合根介面:Collection Map

前言 在前文中我們瞭解了幾種常見的資料結構,這些資料結構有著各自的應用場景,並且被廣泛的應用於程式語言中,其中,Java中的集合類就是基於這些資料結構為基礎。 Java的集合類是一些非常實用的工具類,主要用於儲存和裝載資料 (包括物件),因此,Java的集合類也被成為容器。在Java中,所有的集合類都位於

Java--容器/集合(Collection)理解使用

、陣列和集合的比較 陣列:長度固定,用來存放基本型別的資料 集合:長度不固定,用來存放物件的引用   二、集合類的基本概念 1.java.util包中提供了一些集合類,這些集合類也被稱為容器。 常用的集合有List集合、Set集合、Map集合,他們的關係繼承如下:  

C# 集合Dictionary的遍歷修改(防止錯誤:集合已修改;可能無法執行列舉操作。)

       C#中直接對集合Dictionary進行遍歷並修改其中的值,會報錯,如下程式碼就會報錯:集合已修改;可能無法執行列舉操作。程式碼如下 public void ForeachDic() { Dictionary<String, In

背水一戰 Windows 10 (50) - 控件(集合): ItemsControl - 基礎知識, 數據綁定, ItemsPresenter, GridViewItemPresenter, ListViewItemPresenter

需要 emc rectangle ems sources mic navi schema mark 原文:背水一戰 Windows 10 (50) - 控件(集合類): ItemsControl - 基礎知識, 數據綁定, ItemsPresenter, GridViewI

背水一戰 Windows 10 (56) - 控件(集合): ListViewBase - 基礎知識, 拖動項

tar accepted 類型 idv .get footer ati model 變化 原文:背水一戰 Windows 10 (56) - 控件(集合類): ListViewBase - 基礎知識, 拖動項[源碼下載] 背水一戰 Windows 10 (56) - 控件

java 集合基礎問題匯總

應該 會計 queue 基本數據 actor 相同 快速查找 設置 非線程安全 1、Java集合類框架的基本接口有哪些? 參考答案 集合類接口指定了一組叫做元素的對象。集合類接口的每一種具體的實現類都可以選擇以它自己的方式對元素進行保存和排序。有的集合類允許重復的鍵

java基礎(17)、集合(1:Collection)

實例 array 9.png str 列表 println 方式 elements .com 一.  什麽是集合類? 二.  Collection接口   2.1  Collection子接口(List接口和Set接口)   2.1  Collection的常用方法 三. 

【python基礎集合

linux code 課程 xxd hone 插入元素 close 1.3 格式 集合類型:   作用:  --> 關系運算(交集,並集,差集)        --> 去重(有局限性) 定義方法:set() linuxers = {1,1.1,‘a‘,(1,2

黑馬程式設計師----Java基礎集合(一)

------- <a href="http://www.itheima.com" target="blank">android培訓</a>、<a href="http://www.itheima.com" target="blank">java培訓</a&g

【搞定Java基礎】之集合面試題整理

因為集合類在Java基礎知識中是非常重要的,也是面試中最常問到的,設計的問題也比較多,因此單獨拿出來做面試題的整理,方便自己複習,也希望給看到此篇文章的你帶來一定的幫助。文章內容均來自於網路,平時看到總結不錯的題目,就收集在此。持續更新....... 先推薦幾篇不錯的文章: 1、Java集合

黑馬程式設計師—13—java基礎:有關集合的學習筆記心得體會

 ------- <a href="http://www.itheima.com" target="blank">android培訓</a>、<a href="http://www.itheima.com" target="blank">

Java基礎面試題3-說說你知道的幾個Java集合:list、set、queue、map

關係這張圖簡單揭示了Set、List與Map之間的相對關係。 需要說明下的是,圖中的實現並不指這麼簡單的實現,這個稍後會說到。Collection介面Collection是Java中最基本的集合介面。它描述了一組有關集合操作的方法。int Size(); //集合大小 boo

Java基礎面試題3-說說你知道的幾個Java集合:list、set、map

關係 這張圖簡單揭示了Set、List與Map之間的相對關係。  需要說明下的是,圖中的實現並不指這麼簡單的實現,這個稍後會說到。 Collection介面 Collection是Java中最基本的集合介面。它描述了一組有關集合操作的方法。 int Size(); /