译自:http://www.objc.io/issue-7/collections.html
(注:我不是全文翻译,只译重要部分)
NSArray
NSArray是最常使用的有序集合,还有自己的语法糖@[...]取代[NSArray arrayWithObjects:..., nil]。
NSArray实现了objectAtIndexedSubscript,从而我们可以使用类C语法array[0]取代[array objectAtIndex:0]。
Useful Methods
大多数NSArray函数使用isEqual来判断数组元素相等(例如containsObject)。但有个函数
indexOfObjectIdenticalTo是用来做指针比较的,若某个对象是该集合的元素,调用这个函数可以加快检索。
ios7新引入了firstObject(其实ios4起就有了,只是ios7公开化了,ios4以上都可以调用),空数组调用会返回nil,而array[0]则会抛出越界异常。
创建可变数组的简化方式细节:如果用一个可能为nil的源数组来创建可变数组,通常代码是这样:
NSMutableArray *mutableObjects = [array mutableCopy];
if (!mutableObjects) {
mutableObjects = [NSMutableArray array];
}
或者三目运算符:
NSMutableArray *mutableObjects = [array mutableCopy] ?: [NSMutableArray array];
更好的方式是使用 arrayWithArray,即使源数组为nil(那mutableObjects也为nil)
NSMutableArray *mutableObjects = [NSMutableArray arrayWithArray:array];
反转数组相当简单:array.reverseObjectEnumerator.allObjects,但并没有randomObjectEnumerator这样的函数,可以自定义enumerator或者使用开源类(我以前做牌类游戏,洗牌算法有用到java的Collections.shuffle函数,还专门去看了下函数源代码如何实现,挺简单的)
Sorting Arrays(排序)
排序有若干个函数。
若元素是字符串,首选sortedArrayUsingSelector:
NSArray *array = @[@"John Appleseed", @"Tim Cook", @"Hair Force One", @"Michael Jurewitz"];
NSArray *sortedArray = [array sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
对于 NSNumber同样适用:
NSArray *numbers = @[@9, @5, @11, @3, @1];
NSArray *sortedNumbers = [numbers sortedArrayUsingSelector:@selector(compare:)];
也可以使用基于函数指针的函数:
- (NSData *)sortedArrayHint;
- (NSArray *)sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))comparator
context:(void *)context;
- (NSArray *)sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))comparator
context:(void *)context hint:(NSData *)hint;
这里要重点说下 sortedArrayHint,其可以加快排序:
若对一个元素较多的数组(n个元素)排序一次后,之后较少的改动(p次增删操作,p << n);这种情况下,第一次调用排序函数后,可调用sortedArrayHint保存其返回的NSData,之后增删操作后再次调用排序函数可以传入这个data,排序会快些。
另外还有块语法的排序函数:
- (NSArray *)sortedArrayUsingComparator:(NSComparator)cmptr;
- (NSArray *)sortedArrayWithOptions:(NSSortOptions)opts
usingComparator:(NSComparator)cmptr;
几种类型的排序函数没有明显的性能差异,非得要找个最快的那就是 sortedArrayUsingSelector了,github有个benchmark示例:
Sorting 1000000 elements. selector: 4947.90[ms] function: 5618.93[ms] block: 5082.98[ms].
Binary Search(二分查找)
从ios4/mac雪豹系统起NSArray集成了二分查找。
typedef NS_OPTIONS(NSUInteger, NSBinarySearchingOptions) {
NSBinarySearchingFirstEqual = (1UL << 8),
NSBinarySearchingLastEqual = (1UL << 9),
NSBinarySearchingInsertionIndex = (1UL << 10),
};
- (NSUInteger)indexOfObject:(id)obj
inSortedRange:(NSRange)r
options:(NSBinarySearchingOptions)opts
usingComparator:(NSComparator)cmp;
我专门去运行使用了下这个函数,代码如下:
NSArray *bsArr = @[@(0), @(2), @(3), @(3), @(4), @(5), @(6)];
NSUInteger index = [bsArr indexOfObject:@(3) inSortedRange:NSMakeRange(0, bsArr.count)
options:NSBinarySearchingInsertionIndex | NSBinarySearchingLastEqual
usingComparator:^NSComparisonResult(id obj1, id obj2)
{
NSInteger value1 = [obj1 integerValue];
NSInteger value2 = [obj2 integerValue];
return value1 > value2 ? NSOrderedDescending :
(value1 == value2 ? NSOrderedSame : NSOrderedAscending);
}];
NSLog(@"binary search index=%d",index);
几种options传参情况的输出如下:
NSBinarySearchingInsertionIndex | NSBinarySearchingLastEqual
输出:binary search index=4
NSBinarySearchingInsertionIndex | NSBinarySearchingFirstEqual 或者NSBinarySearchingInsertionIndex
输出:binary search index=2
NSBinarySearchingFirstEqual或0
输出:binary search index=2
NSBinarySearchingLastEqual
输出:binary search index=3
若第一个参数obj传入的对象是@7,
options为0或
NSBinarySearchingFirstEqual或
NSBinarySearchingLastEqual
输出
binary search index=
2147483647(
NSNotFound)
options为
NSBinarySearchingInsertionIndex或外加FirstEqual或外加LastEqual
输出:
binary search index=7
为何使用该函数?诸如containsObject和indexOfObject的函数都是从0开始顺序遍历每个元素直到匹配为止。这类函数不需要事先排序好的数组,但时间复杂度为O(n);另一方面,二分查找需要数组先排序,时间复杂度仅为O(log n)。若有一百万个元素的数组,二分查找至多需要21次比较,而线性查找平均需要500000次比较。
为何使用该函数?诸如containsObject和indexOfObject的函数都是从0开始顺序遍历每个元素直到匹配为止。这类函数不需要事先排序好的数组,但时间复杂度为O(n);另一方面,二分查找需要数组先排序,时间复杂度仅为O(log n)。若有一百万个元素的数组,二分查找至多需要21次比较,而线性查找平均需要500000次比较。
二分查找benchmark:
Time to search for 1000 entries within 1000000 objects. Linear: 54130.38[ms]. Binary: 7.62[ms]
作个比较,用NSOrderedSet查找某个索引只需要0.23毫秒,比二分查找还快30多倍。
排序是费时的,Apple使用归并排序,时间复杂度为O(n*log n),如果仅需调用一次indexOfObject,那没必要使用二分查找。
指定参数NSBinarySearchingInsertionIndex,向已排序数组插入新元素时可以得到插入索引(上面例子已经说明)
Enumeration and Higher-Order Messaging
让我们看个常例,从数组筛选出元素到新数组。这个测试使用了若干种遍历方法,以及专门的一个过滤函数:// First variant, using `indexesOfObjectsWithOptions:passingTest:`.
NSIndexSet *indexes = [randomArray indexesOfObjectsWithOptions:NSEnumerationConcurrent
passingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
return testObj(obj);
}];
NSArray *filteredArray = [randomArray objectsAtIndexes:indexes];
// Filtering using predicates (block-based or text)
NSArray *filteredArray2 = [randomArray filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
return testObj(obj);
}]];
// Block-based enumeration
NSMutableArray *mutableArray = [NSMutableArray array];
[randomArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (testObj(obj)) {
[mutableArray addObject:obj];
}
}];
// Classic enumeration
NSMutableArray *mutableArray = [NSMutableArray array];
for (id obj in randomArray) {
if (testObj(obj)) {
[mutableArray addObject:obj];
}
}
// Using NSEnumerator, old school.
NSMutableArray *mutableArray = [NSMutableArray array];
NSEnumerator *enumerator = [randomArray objectEnumerator];
id obj = nil;
while ((obj = [enumerator nextObject]) != nil) {
if (testObj(obj)) {
[mutableArray addObject:obj];
}
}
// Using objectAtIndex: (via subscripting)
NSMutableArray *mutableArray = [NSMutableArray array];
for (NSUInteger idx = 0; idx < randomArray.count; idx++) {
id obj = randomArray[idx];
if (testObj(obj)) {
[mutableArray addObject:obj];
}
}
运行消耗时间:
Enumeration Method / Time [ms] | 10.000.000 elements | 10.000 elements |
---|---|---|
indexesOfObjects: , concurrent | 1844.73 | 2.25 |
NSFastEnumeration (for in ) | 3223.45 | 3.21 |
indexesOfObjects: | 4221.23 | 3.36 |
enumerateObjectsUsingBlock: | 5459.43 | 5.43 |
objectAtIndex: | 5282.67 | 5.53 |
NSEnumerator | 5566.92 | 5.75 |
filteredArrayUsingPredicate: | 6466.95 | 6.31 |
indexesOfObjectsWithOptions:passingTest:由于每次遍历都要调用block,因此相对于传统的NSFastEnumeration遍历方式效率要低些。但是若第一个参数传入NSEnumerationConcurrent,则其速度会快近两倍。如果集合元素较少,使用哪种方式都无所谓,但对于NSEnumerationConcurrent额外的线程管理反而会偏慢。
最差劲的当属filteredArrayUsingPredicate。但NSPredicate还是要提一下,其可以实现较复杂的过滤表达式,用过Core Data的对此应该较为熟悉。
另外NSEnumerator这个开发中避免使用,尽管它要比filteredArrayUsingPredicate还快些,其只是为了向后兼容而存在。
Should I Use arrayWithCapacity:?
使用array构造函数即可,指定capacity并没有多少性能变化。NSDictionary
// Using keysOfEntriesWithOptions:passingTest:,optionally concurrent
NSSet *matchingKeys = [randomDict keysOfEntriesWithOptions:NSEnumerationConcurrent
passingTest:^BOOL(id key, id obj, BOOL *stop)
{
return testObj(obj);
}];
NSArray *keys = matchingKeys.allObjects;
NSArray *values = [randomDict objectsForKeys:keys notFoundMarker:NSNull.null];
__unused NSDictionary *filteredDictionary = [NSDictionary dictionaryWithObjects:values
forKeys:keys];
// Block-based enumeration.
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionary];
[randomDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (testObj(obj)) {
mutableDictionary[key] = obj;
}
}];
// NSFastEnumeration
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionary];
for (id key in randomDict) {
id obj = randomDict[key];
if (testObj(obj)) {
mutableDictionary[key] = obj;
}
}
// NSEnumeration
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionary];
NSEnumerator *enumerator = [randomDict keyEnumerator];
id key = nil;
while ((key = [enumerator nextObject]) != nil) {
id obj = randomDict[key];
if (testObj(obj)) {
mutableDictionary[key] = obj;
}
}
// C-based array enumeration via getObjects:andKeys:
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionary];
id __unsafe_unretained objects[numberOfEntries];
id __unsafe_unretained keys[numberOfEntries];
[randomDict getObjects:objects andKeys:keys];
for (int i = 0; i < numberOfEntries; i++) {
id obj = objects[i];
id key = keys[i];
if (testObj(obj)) {
mutableDictionary[key] = obj;
}
}
Filtering/Enumeration Method | Time [ms], 50.000 elements | 1.000.000 elements |
---|---|---|
keysOfEntriesWithOptions: , concurrent | 16.65 | 425.24 |
getObjects:andKeys: | 30.33 | 798.49* |
keysOfEntriesWithOptions: | 30.59 | 856.93 |
enumerateKeysAndObjectsUsingBlock: | 36.33 | 882.93 |
NSFastEnumeration | 41.20 | 1043.42 |
NSEnumeration | 42.21 | 1113.08 |
Shared Keys
sharedKeySetForKeys用来创建预定义好的key集合,返回的值作为NSMutableDictionary的dictionaryWithSharedKeySet参数传入,可以重用key,从而节约copy操作,节省内存。NSOrderedSet
排序的set集合,但添加元素操作较费时
NSHashTable
可以包含弱引用对象的set集合,通常使用 weakObjectsHashTable构造函数NSMapTable
对应NSDictionary的弱引用版本,添加元素和存取操作都比NSMutableDictionary慢点