iOS開發·必會的演算法操作:字串陣列排序+模型物件陣列排序

陳滿iOS發表於2018-04-12

傳送門:排序演算法演示小DEMO

前面的話

為了給字串陣列排序,除了用C/C++的基本辦法,iOS開發者更應該學會利用蘋果專門為NSArray 排序提供的sortedArrayUsingComparator 方法:

- (NSArray<ObjectType> *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr NS_AVAILABLE(10_6, 4_0);
複製程式碼

其中,需要設定一個NSComparator 引數,它是一個block,檢視定義如下:

typedef NSComparisonResult (^NSComparator)(id obj1, id obj2);
複製程式碼

這個block體返回的NSComparisonResult 是一個列舉型別,它的定義是:

typedef NS_ENUM(NSInteger, NSComparisonResult) {NSOrderedAscending = -1L, NSOrderedSame, NSOrderedDescending};
複製程式碼

問題來了,怎麼設定?

  • 為了設定這個NSComparator 引數的block體,你可以在設定其block體的時候,手動返回一個NSComparisonResult 列舉型別的某個具體值(NSOrderedAscending, NSOrderedSame, NSOrderedDescending 三選一):

image.png

  • 如果陣列裡面是字串,在設定其block體的時候,你也可以利用蘋果專門為NSString 提供的字串比較方法,獲得一個NSComparisonResult 型別,將其自動返回。
- (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToCompare locale:(nullable id)locale; // locale arg used to be a dictionary pre-Leopard. We now accept NSLocale. Assumes the current locale if non-nil and non-NSLocale. nil continues to mean canonical compare, which doesn't depend on user's locale choice.
複製程式碼

image.png

這時候,就需要了解NSStringCompareOptions 的意思。但如果你搜尋一下NSStringCompareOptions ,會發現很多文章中的翻譯或者中文解釋在誤導,或者很難看清什麼意思?例如下面這篇部落格:

image.png

然後,相同的解釋文案還以訛傳訛的傳開來了,例如你看下面這個部落格:

image.png

於是,筆者決定寫此本文,好好展示他們的用途。

1. 第一種:陣列的字串元素裡面是基本資料型別


1.1 字串陣列排序示例

1.1.1 實驗程式碼
  • main.m
void handleSortingForIntStrArray(void){
    NSArray *originalArray = @[@"00",@"0",@"00",@"01",@"10",@"21",@"12",@"11",@"22"];
    //block比較方法,陣列中可以是NSInteger,NSString(需要轉換)
    NSComparator finderSort = ^(id string1,id string2){
        if ([string1 integerValue] > [string2 integerValue]) {
            return (NSComparisonResult)NSOrderedDescending;
        }else if ([string1 integerValue] < [string2 integerValue]){
            return (NSComparisonResult)NSOrderedAscending;
        }else{
            return (NSComparisonResult)NSOrderedSame;
        }
    };
    //陣列排序:
    NSArray *resultArray = [originalArray sortedArrayUsingComparator:finderSort];
    NSLog(@"第一種排序結果:%@",resultArray);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Results of handleSortingForIntArray()**********************");
        handleSortingForIntStrArray();
    }
    return 0;
}
複製程式碼
1.1.2 執行結果

image.png

1.1.3 實驗結論
  • 依據陣列元素的數值大小返回升序陣列

1.2 NSComparator與NSComparisonResult

上面的程式碼中用到了NSComparator與NSComparisonResult,在本文的“前面的話”中已經介紹過,這裡重新列一下定義。

1.2.1 NSComparator
typedef NSComparisonResult (^NSComparator)(id obj1, id obj2);
複製程式碼
1.2.2 NSComparisonResult
typedef NS_ENUM(NSInteger, NSComparisonResult) {NSOrderedAscending = -1L, NSOrderedSame, NSOrderedDescending};
複製程式碼

2. 第二種:陣列的字串元素裡面不是基本資料型別


2.1 示例:字串陣列排序

2.1.1 實驗程式碼
  • main.m
//
//  main.m
//  SortingForArray
//
//  Created by ChenMan on 2017/12/20.
//  Copyright © 2017年 ChenMan. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <stdio.h>

void handleSortingForStrArray(void){
       NSArray *stringsArray = [NSArray arrayWithObjects:
                             @"string b",
                             @"string A",
                             @"string a",
                             @"string \uFF41",
                             @"string a",
                             @"string A",
                             @"string c",
                             @"string d0030",
                             @"string d2",
                             @"アいろはアイウエイウエ",
                             @"アいろはアイウエイウエ",
                             @"アいろはアイウエイウエ",nil];
    
    NSLocale *currentLocale = [NSLocale currentLocale];
    NSComparator finderSortBlock = ^(id string1,id string2) {
        
        NSRange string1Range =NSMakeRange(0, [string1 length]);
        return [string1 compare:string2 options:nil range:string1Range locale:currentLocale];
    };
    
    NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];
    NSLog(@"finderSortArray: %@", finderSortArray);
    
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Results of handleSortingForStrArray()**********************");
        handleSortingForStrArray();
    }
    return 0;
}
複製程式碼
2.1.2 執行結果:

image.png

2.1.3 實驗結論:

如上實驗程式碼中,有這樣一行程式碼:

return [string1 compare:string2 options:nil range:string1Range locale:currentLocale];
複製程式碼

根據執行結果,可知如下結論:

  • 即使在- (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask range:(NSRange)rangeOfReceiverToCompare locale:(nullable id)locale;中將(NSStringCompareOptions)列舉型別的引數設定為nil,也可以執行。但一般不這麼做,這裡只是為了觀察不指定該列舉引數時候系統的預設設定,並與本文接下來指定該列舉引數的排序結果對比。
  • 可以發現:
    • 預設同一字元的全形字元看做半形字元。不區分同一個字元(如日文的片假字)的半形與全形狀態。相同元素,維持原序。
    • 預設區分字母大小寫,同一個字元小寫在前,大寫在後。
    • 字母並非按unicode碼的大小升序排列。例如,全形a的unicode為FF41,半形a的unicode為0061,半形A的unicode為0041,半形b的unicode為0062,但排序結果是 全形a = 半形a < 半形A < 半形b
    • 預設不識別含有數字字元的數值大小,0030雖然數學意義比2大,但是,僅從字串的角度看,第一個字元0比2小,所以d0030排在d2前面。
2.1.4 知識擴充:

半形與全形字元

  • 全形佔兩個位元組,半形佔一個位元組。通常我們碰到的英文字母、數字鍵、符號鍵這種ASCII碼系統裡面的字元大多數情況下是半形的。

  • 國內漢字輸入法輸入的漢字為全形,字母數字為半形,但是標點則預設為全形,可切換為半形(可以通過輸入法工具條上的相應按鈕來切換標點符號的全形半形狀態)。

  • 日文裡面的有漢字,也有片假字。這個片假字有兩套編碼,同一個片假字分別有半形和全形兩種編碼。例如:看起來像一樣的片假字組成的句子,全形狀態字元開頭的為アいろはアイウエイウエ,半形狀態字元開頭的為アいろはアイウエイウエ。可以看到,明顯同一個片假字的全形狀態半形狀態 “胖”一圈。

  • 英文字母其實也有全形字母,例如小寫的a,其半形形式的unicode碼為0061,其全形形式的unicode碼為FF41。可查閱Unicode®字元百科官網。

2.2 NSStringCompareOptions

NSStringCompareOptions是一個列舉型別,並非一個類。開啟NSStringCompareOptions的定義,可檢視如下

typedef NS_OPTIONS(NSUInteger, NSStringCompareOptions) {
    NSCaseInsensitiveSearch = 1,
    NSLiteralSearch = 2,		/* Exact character-by-character equivalence */
    NSBackwardsSearch = 4,		/* Search from end of source string */
    NSAnchoredSearch = 8,		/* Search is limited to start (or end, if NSBackwardsSearch) of source string */
    NSNumericSearch = 64,		/* Added in 10.2; Numbers within strings are compared using numeric value, that is, Foo2.txt < Foo7.txt < Foo25.txt; only applies to compare methods, not find */
    NSDiacriticInsensitiveSearch API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 128, /* If specified, ignores diacritics (o-umlaut == o) */
    NSWidthInsensitiveSearch API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 256, /* If specified, ignores width differences ('a' == UFF41) */
    NSForcedOrderingSearch API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 512, /* If specified, comparisons are forced to return either NSOrderedAscending or NSOrderedDescending if the strings are equivalent but not strictly equal, for stability when sorting (e.g. "aaa" > "AAA" with NSCaseInsensitiveSearch specified) */
    NSRegularExpressionSearch API_AVAILABLE(macos(10.7), ios(3.2), watchos(2.0), tvos(9.0)) = 1024    /* Applies to rangeOfString:..., stringByReplacingOccurrencesOfString:..., and replaceOccurrencesOfString:... methods only; the search string is treated as an ICU-compatible regular expression; if set, no other options can apply except NSCaseInsensitiveSearch and NSAnchoredSearch */
};
複製程式碼
2.2.1 NSNumericSearch

官方解釋:Added in 10.2; Numbers within strings are compared using numeric value, that is, Foo2.txt < Foo7.txt < Foo25.txt; only applies to compare methods, not find

  • 假設,將上例中的部分程式碼修改為
void handleSortingForStrArray(void){
    NSArray *stringsArray = [NSArray arrayWithObjects:
                             @"string b",
                             @"string A",
                             @"string a",
                             @"string \uFF41",
                             @"string a",
                             @"string A",
                             @"string c",
                             @"string d0030",
                             @"string d2",
                             @"アいろはアイウエイウエ",
                             @"アいろはアイウエイウエ",
                             @"アいろはアイウエイウエ",nil];
    NSStringCompareOptions comparisonOptions = NSNumericSearch;
    NSLocale *currentLocale = [NSLocale currentLocale];
    NSComparator finderSortBlock = ^(id string1,id string2) {
        
        NSRange string1Range =NSMakeRange(0, [string1 length]);
        return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];
    };
    
    NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];
    NSLog(@"finderSortArray: %@", finderSortArray);
}
複製程式碼
  • 執行結果

image.png

  • 結論 NSStringCompareOptions指定為NSNumericSearch,當字串中含有數字時,從數值大小的角度按升序排序。
2.2.2 NSCaseInsensitiveSearch

官方解釋:無。英文字面解釋:不區分字母大小寫。

  • 假設,將上例中的部分程式碼修改為
NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch; 
複製程式碼
  • 執行結果

image.png

  • 結論 NSStringCompareOptions指定為NSCaseInsensitiveSearch,不區分同一個字母的大小寫狀態,如aA看做相同元素,若其它條件也一致則保持原序。
2.2.3 NSLiteralSearch

官方解釋:Exact character-by-character equivalence

  • 假設,將上例中的部分程式碼修改為
NSStringCompareOptions comparisonOptions = NSLiteralSearch;
複製程式碼
  • 執行結果

image.png

  • 結論

    • 區分 同一個字元(如日文的片假字)的半形與全形狀態,同一片假字的全形狀態小於半形狀態。
    • 其它規則,繼續按系統預設排序規則排序,包括預設區分 字母大小寫,以及其它預設排序規則。
    • 按照官方英文說明,這個規則是指區分每個字元的等效狀態。只要unicode不同的字元,就不認可他們“等效”,即使他們的語言上的含義相同。
  • 題外話

    • 所以,有的文獻說NSLiteralSearch 是區分大小寫是誤導,系統本就預設區分 字母大小寫,這些人以為蘋果公司提供這個功能來畫蛇添足幹嘛?而且可以看看官方英文說明,也不是這個意思。只有指定不區分 字母大小寫的NSCaseInsensitiveSearch,要麼不寫,即預設區分
2.2.4 NSWidthInsensitiveSearch

官方解釋:If specified, ignores width differences ('a' == UFF41)

  • 假設,將上例中的部分程式碼修改為
NSStringCompareOptions comparisonOptions = NSWidthInsensitiveSearch;
複製程式碼
  • 執行結果

image.png

  • 結論
    • 不區分 同一個字元(如日文的片假字)的半形與全形狀態,同一片假字的全形狀態等於半形狀態。
    • 其它規則,繼續按系統預設排序規則排序,包括預設區分 字母大小寫,以及其它預設排序規則。
    • 同時指定兩個時,NSWidthInsensitiveSearchNSLiteralSearch 的優先順序高,綜合起來的結果是不區分 半形全形。
    • 官方英文說明中的UFF41是指全形a'a' 是指半形a,如果指定NSWidthInsensitiveSearch,則不區分字元的全形半形,即使你同時指定了NSLiteralSearch

即,當有如下程式碼

NSStringCompareOptions comparisonOptions = NSWidthInsensitiveSearch | NSLiteralSearch;
複製程式碼

其作用相當於沒有NSLiteralSearch的程式碼

NSStringCompareOptions comparisonOptions = NSWidthInsensitiveSearch;
複製程式碼
2.2.5 NSForcedOrderingSearch

官方解釋:If specified, comparisons are forced to return either NSOrderedAscending or NSOrderedDescending if the strings are equivalent but not strictly equal, for stability when sorting (e.g. "aaa" > "AAA" with NSCaseInsensitiveSearch specified)

  • 假設,將上例中的部分程式碼修改為
NSStringCompareOptions comparisonOptions = NSForcedOrderingSearch;
複製程式碼
  • 執行結果

image.png

  • 結論
    • 不存在字元等不等效相不相等的概念了,只要unicode不一樣的字元,必須區分,必須返回一個誰大誰小的結果(NSOrderedAscending or NSOrderedDescending)。
    • 從英文說明也可以看出,NSForcedOrderingSearch 的優先順序最高,即如果你同時指定了其它有可能作用衝突的列舉型別,也以NSForcedOrderingSearch 的作用為準。
2.2.6 綜合應用
  • 一個比較多的應用示例是,區分字母大小寫,區分數值大小,區分半形全形,並強制性指定區分unicode不一樣的字元。綜合這些條件,寫起來就是:
NSStringCompareOptions comparisonOptions = NSNumericSearch|NSWidthInsensitiveSearch|NSForcedOrderingSearch;
複製程式碼
  • 執行結果

image.png

2.2.7 誤導用法
  • 我看過有很多其它部落格用了這樣的誤導示例:
NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch|NSNumericSearch|NSWidthInsensitiveSearch|NSForcedOrderingSearch;
複製程式碼

這裡面,NSCaseInsensitiveSearch是為了不區分大小寫字母,但是後面再加個NSForcedOrderingSearch想強制區分字元又是怎麼回事?雖然,這樣寫並不會報錯,執行效果跟上面的綜合示例一摸一樣。但這樣誤導的想法是個邏輯矛盾。不信,你看看它執行的結果:

image.png

3. 陣列裡面是類的物件


需求:假設我們根據後臺返回的JSON字典陣列用MJExtension轉換成模型陣列,現在我們需要根據ID或者Age對模型陣列進行排序。

  • Pesson.m
#import <Foundation/Foundation.h>  
  
@interface Person : NSObject  
@property (nonatomic,copy) NSString *ID;  
@property (nonatomic,copy) NSString *name;  
@property (nonatomic,assign) int age;  
@end  
複製程式碼
  • 根據int型別的屬性對模型陣列進行排序
NSArray *sortArrayByAgeInt = [self.dataArray sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {  
      
    Person *pModel1 = obj1;  
    Person *pModel2 = obj2;  
    
    if (pModel1.age > pModel2.age) { 
        return NSOrderedDescending;//降序  
    }else if (pModel1.name < pModel2.name){  
        return NSOrderedAscending;//升序  
    }else {  
        return NSOrderedSame;//相等  
    }  
      
}];
複製程式碼
  • 根據str型別的屬性對模型陣列進行排序
NSArray *sortArrayByIDStr = [self.dataArray sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {  
      
    Person *pModel1 = obj1;  
    Person *pModel2 = obj2;  
    
    if ([pModel1.ID intValue]> [pModel2.ID intValue]) { 
        return NSOrderedDescending;//降序  
    }else if (pModel1.name < pModel2.name){  
        return NSOrderedAscending;//升序  
    }else {  
        return NSOrderedSame;//相等  
    }  
      
}];
複製程式碼

4. 花樣玩法:例題


在OC的高階用法中,經常需要檢視系統類或者某個自定義類中的私有屬性以及私有成員變數,並通過KVC的辦法強制修改這些私有成員變數的值,以取代系統或者自定義類中的預設設定。所以,如果你懶得建立一些假資料的陣列,可以想到運用執行時的辦法獲取成員變數的陣列,並進行排序操作訓練。

題1. 請取出NSString類的全部公有 屬性 並存放到一個陣列,並利用NSArraysortedArrayUsingComparator的方法給這個陣列進行升序排序操作。要求:排序過程中需要區分字元全形半形狀態,其它可按系統預設條件。

  • 參考程式碼: main.m
void handlePrintingOfProperties(void){
    unsigned int count;// 記錄屬性個數
    objc_property_t *properties = class_copyPropertyList([NSString class], &count);
    // 生成一個屬性名稱組成的陣列
    NSMutableArray *propertyNameArray = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        // An opaque type that represents an Objective-C declared property.
        // objc_property_t 屬性型別
        objc_property_t property = properties[i];
        // 獲取屬性的名稱 C語言字串
        const char *cName = property_getName(property);
        // 轉換為Objective C 字串
        NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
        [propertyNameArray addObject:name];
    }
    NSLog(@"排序前的屬性列表 = %@",propertyNameArray);
    
    NSComparator cmptr = ^(NSString *obj1, NSString *obj2){
        return [obj1 compare:obj2 options:NSLiteralSearch];
    };
    NSArray *afterSort = [propertyNameArray sortedArrayUsingComparator:cmptr];
    NSLog(@"排序後的屬性列表 = %@",afterSort);
    
    //C語言中,用完copy,create的東西之後,最好釋放
    free(properties);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"handlePrintingOfProperties()**********************");
        handlePrintingOfProperties();
    }
    return 0;
}
複製程式碼
  • 執行結果

image.png

題2. 請取出NSURL類中包括私有 在內的全部 成員變數,並存放到一個陣列,並利用NSArraysortedArrayUsingComparator的方法給這個陣列進行升序排序操作。要求:排序過程中需要區分字元全形半形狀態,其它可按系統預設條件。

  • 參考程式碼:
void handlePrintingOfIvars(void){
    unsigned int count;// 記錄屬性個數
    Ivar *properties = class_copyIvarList([NSURL class], &count);
    // 生成一個屬性名稱組成的陣列
    NSMutableArray *propertyNameArray = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        // An opaque type that represents an Objective-C declared property.
        // objc_property_t 屬性型別
        Ivar property = properties[i];
        // 獲取屬性的名稱 C語言字串
        const char *cName = ivar_getName(property);
        // 轉換為Objective C 字串
        NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
        [propertyNameArray addObject:name];
    }
    NSLog(@"排序前的成員變數列表 = %@",propertyNameArray);
    
    NSComparator cmptr = ^(NSString *obj1, NSString *obj2){
        return [obj1 compare:obj2 options:NSLiteralSearch];
    };
    NSArray *afterSort = [propertyNameArray sortedArrayUsingComparator:cmptr];
    NSLog(@"排序後的成員變數列表 = %@",afterSort);
    
    //C語言中,用完copy,create的東西之後,最好釋放
    free(properties);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"handlePrintingOfIvars()**********************");
        handlePrintingOfIvars();
    }
    return 0;
}
複製程式碼
  • 執行結果

image.png

5. 附錄:本實驗中建立工程說明


任何能在計算機上執行的專案稱之為程式,其中,有圖形化使用者介面的程式稱之為應用 ,沒有圖形介面的程式可以是守護程式 ,還有一種稱之為命令列工具。本文這裡關注的是演算法和資料結果,不關注圖形介面,所以新建一個命令列工具即可。建立方法:新建一個macOS工程,選擇Command Line Tool型別,點選下一步配置工程資訊即可。

建立一個命令列工具

工程建立成功

相關文章