Помогите отсортировать NSArray по двум свойствам (с помощью NSSortDescriptor?)

Я немного NSSortDescriptor n00b. Я думаю, однако, что это правильный инструмент для того, что мне нужно сделать:

У меня есть NSArray, состоящий из объектов с ключами, скажем, «имя» и «время». Вместо словесного выражения приведу пример:

input:

name: time
B: 4
C: 8
B: 5
C: 4
A: 3
C: 2
A: 1
A: 7
B: 6


desired output:

name: time
A: 1 <---
A: 3
A: 7
C: 2 <---
C: 4
C: 8
B: 4 <---
B: 5
B: 6

Таким образом, значения сортируются по «времени» и группируются по «имени». Первым идет А, потому что у него была наименьшая временная стоимость, а все значения для А идут одно за другим. Затем идет C, у него было второе наименьшее значение времени из всех его значений. Я указал значения, определяющие способ сортировки имен; внутри каждой группы имен сортировка по времени.

Как наиболее эффективно получить от ввода до вывода NSArray? (с точки зрения процессора и памяти, не обязательно с точки зрения кода.) Как мне создать для этого NSSortDescriptors или использовать любой другой метод? Я не хочу сворачивать свои собственные, если это не самый эффективный способ.


person Jaanus    schedule 03.02.2010    source источник


Ответы (6)


sortedArrayUsingDescriptors: NSArray делает большую часть того, что вам нужно:

Первый дескриптор указывает путь первичного ключа, который будет использоваться при сортировке содержимого получателя. Любые последующие дескрипторы используются для дальнейшего уточнения сортировки объектов с повторяющимися значениями. Дополнительные сведения см. в разделе NSSortDescriptor.

Также требуется некоторая фильтрация с помощью NSPredicate:

NSSortDescriptor *timeSD = [NSSortDescriptor sortDescriptorWithKey: @"time" ascending: YES];

NSMutableArray *sortedByTime = [UnsortedArray sortedArrayUsingDescriptors: timeSD];
NSMutableArray *sortedArray = [NSMutableArray arrayWithCapacity:[sortedByTime count]];

while([sortedByTime count]) 
{
        id groupLead = [sortedByTime objectAtIndex:0];  
        NSPredicate *groupPredicate = [NSPredicate predicateWithFormat:@"name = %@", [groupLead name]];

        NSArray *group = [sortedByTime filteredArrayUsingPredicate: groupPredicate];

        [sortedArray addObjectsFromArray:group];
        [sortedByTime removeObjectsInArray:group];
}

Я понятия не имею, является ли это наиболее эффективным методом, но пока у вас нет оснований полагать, что он вызывает проблемы, не стоит беспокоиться о последствиях для производительности. Это преждевременная оптимизация. Я бы не беспокоился о производительности этого метода. Вы должны доверять фреймворку, иначе вы в конечном итоге перепишете его (тем самым подорвав суть фреймворка) из-за необоснованной паранойи.

person Benedict Cohen    schedule 03.02.2010
comment
Это не отвечает на вопрос: моя ситуация сложнее, чем простая сортировка по имени. - person Jaanus; 03.02.2010
comment
@Яанус. О, я вижу. Я не заметил, что порядок групп зависит от времени. - person Benedict Cohen; 03.02.2010
comment
@Яанус. Я обновил код, чтобы он действительно отвечал на вопрос! - person Benedict Cohen; 03.02.2010
comment
Спасибо, похоже, это то, что мне нужно. Я попробую несколько подходов из ответов и отчитаюсь. - person Jaanus; 03.02.2010
comment
Проголосовал за ответ. Хотел бы я вычесть четверть балла за комментарий о влиянии на производительность. Рассмотрение эффективности ваших алгоритмов не является преждевременной оптимизацией. Эта фраза звучит все чаще и чаще, и она не о том. Потратить время на то, чтобы рассмотреть сложность ваших алгоритмов и узнать, есть ли лучший способ, — ​​это просто хорошая инженерия. - person DougW; 05.06.2010
comment
Теперь у него есть какой-то баг. - person Rajesh Maurya; 05.04.2016

Мое решение:

    NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:@"time" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, sortDescriptor2, nil];

Можешь попробовать

person Ha Duyen Hoa    schedule 24.12.2010
comment
Это решило мои проблемы, гибко добавляя больше sortDescriptor. - person Linh Nguyen; 11.04.2017

Я бы создал новый класс с именем ItemGroup, а затем добавил бы дополнительный ivar с именем group к вашему классу элементов:

@interface ItemGroup : NSObject
{
    NSNumber * time;
}
@property (nonatomic, copy) time;
@end

@interface ItemClass : NSobject
{
    NSString * name;
    NSNumber * time;
    ItemGroup * group;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSNumber * time;
@property (nonatomic, assign) ItemClass * group; // note: must be assign
@end

Затем вы можете сделать следующее:

NSMutableDictionary * groups = [NSMutableDictionary dictionaryWithCapacity:0];
for (ItemClass * item in sourceData)
{
    ItemGroup * group = [groups objectForKey:item.name];
    if (group == nil)
    {
        group = [[ItemGroup alloc] init];
        [groups setObject:group forKey:item.name];
        [group release];

        group.time = item.time;
    }
    else if (item.time < group.time)
    {
        group.time = item.time;
    }
    item.group = group;
}

Этот код перебирает несортированный массив, отслеживая минимальное время для каждой группы, а также устанавливая группу для каждого элемента. После этого вы просто сортируете по group.time и time:

NSSortDescriptor * groupSorter;
groupSort = [NSSortDescriptor sortDescriptorWithKey:@"group.time" ascending:YES];

NSSortDescriptor * timeSorter;
timeSort = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];

NSArray * sortDescriptors = [NSArray arrayWithObjects:groupSort, timeSort, nil];

NSArray * sorted = [sourceData sortedArrayUsingDescriptors:sortDescriptors];

И это должно сработать!

ОБНОВЛЕНИЕ. Обратите внимание, что вы могли бы добиться гораздо более высокой производительности, если бы могли назначать группы сразу после запуска. Что-то вроде этого:

@interface ItemGroup : NSObject
{
    NSString * name;
    NSNumber * time;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSSNumber * time;
@end

@interface ItemClass : NSObject
{
    ItemGroup * group;
    NSNumber * time;
}
@property (nonatomic, retain) ItemGroup * group;
@property (nonatomic, copy) NSNumber * time;
@end

Теперь, если вы где-то ведете список групп (при необходимости они могут даже находиться где-то в массиве):

ItemGroup * group_A = [[ItemGroup alloc] init];
group_A.name = @"A";
ItemGroup * group_B = [[ItemGroup alloc] init];
group_B.name = @"B";
...

И вместо того, чтобы устанавливать имена ваших элементов данных, вы устанавливаете их группу:

someItem.group = group_A;
someItem.time = GetSomeRandomTimeValue();
[sourceData addObject:someItem];
....

Это значительно упростило бы цикл, используемый для установки группового времени:

for (ItemClass * item in sourceData)
{
    if (item.time < group.time) { group.time = item.time; }
}

И, если вы действительно хотите быть молниеносным в этом, вы можете даже изменить установщик свойства для вашего свойства time, чтобы установить групповое время на лету:

@implementation ItemClass
- (void)setTime:(NSNumber *)newTime
{
    if (newTime < group.time) { group.time = newTime; }
    time = [newTime copy];
}
@end

Обратите внимание, что вы должны быть уверены, что group было установлено, прежде чем устанавливать время. При этом вам вообще не понадобится этот цикл сортировки. Дескрипторов sortDescriptors было бы достаточно.

person e.James    schedule 03.02.2010
comment
Я понимаю это, но я не думаю, что это отвечает на вопрос. Я не сортирую по имени, мне нужно ранжировать и группировать имена на основе наименьшего значения времени для данного имени. - person Jaanus; 03.02.2010
comment
Ах. Теперь я вижу. Это намного интереснее. - person e.James; 03.02.2010
comment
Определяете ли вы тип объекта, который сохраняется в исходном массиве? то есть это пользовательский класс, к которому вы можете добавить ivars? - person e.James; 03.02.2010
comment
Да, это мой пользовательский класс, и я могу добавить ивары. Хотя я сам не понимаю, как это может помочь. Данные изменчивы, и новые значения могут поступать во время выполнения. В любой момент времени у меня просто есть моментальный снимок данных, которые мне нужно отсортировать таким образом. - person Jaanus; 03.02.2010
comment
Из всех ответов на данный момент мне больше всего нравятся эти, особенно групповой подход, о котором я сообщу. Я думаю, что это имеет сложность O (n), намного лучше, чем мой текущий наивный O (n * n). Назначение вне ворот было бы здорово, но реальная ситуация сложнее и зависит от среды, которая неизвестна во время сохранения. - person Jaanus; 03.02.2010
comment
Как вы получаете исходный список данных? Вы начинаете с пустого массива, добавляете элементы (имя и время) на лету, сортируете результаты, а затем очищаете массив и начинаете заново, или элементы случайным образом добавляются и удаляются из массива во время выполнения? - person e.James; 03.02.2010
comment
Исходный список загружается из бэкэнда Core Data. Дополнительные записи будут поступать во время выполнения из сети, и их время будет не текущим, а временем, когда они были созданы в какой-то другой системе. - person Jaanus; 03.02.2010
comment
Хм. Да, это затрудняет настройку групп заранее. Я уверен, что еще есть способ сделать это, но это, вероятно, не стоит усилий. Как прошло тестирование? - person e.James; 04.02.2010
comment
До выходных не смогу протестировать, отпишусь, когда доберусь. - person Jaanus; 04.02.2010

Я сделал небольшой код (не пытался его запускать и не пересматривал, так что может быть пара ошибок, но в нем есть общая идея), чтобы сделать то, что вы ищете. С точки зрения производительности, это, вероятно, не будет лучшим, если вы начнете работать с огромными объемами данных. Я уверен, что есть лучший способ сделать это, но мне хотелось сделать это самым простым способом в качестве ответа «временного исправления».

NSMutableArray *copiedarray = [YourFirstArray mutableCopy];
NSMutableArray *sortedarray = [[NSMutableArray alloc] init];
NSMutableArray *tempgroup = nil;
NSSortDescriptor * groupSorter = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];

NSInteger i;
NSInteger savedlowest = -1;
NSString *savedname = @"";


while ([copiedarray count] > 0) {
    ///reset lowest time and group
    savedlowest = -1;
    savedname = @"";

    ///grab the lowest time and group name
    for (ii = 0;ii < [copiedarray count]; ii++) {
        if (savedlowest==-1 || ((YourClass *)([copiedarray objectAtIndex:ii])).time<savedlowest)) {
            savedname = ((YourClass *)([copiedarray objectAtIndex:ii])).name;
            savedlowest = ((YourClass *)([copiedarray objectAtIndex:ii])).time;
        }
    }

    //we have the lowest time and the type so we grab all those items from the group
    tempgroup = [[NSMutableArray alloc] init];
    for (ii = [copiedarray count]-1;ii > -1; ii--) {
        if ([((YourClass *)([copiedarray objectAtIndex:ii])).name isEqualToString:savedname]) {
            ///the item matches the saved group so we'll add it to our temporary array
            [tempgroup addObject:[copiedarray objectAtIndex:ii]];
            ///remove it from the main copied array for "better performance"
            [copiedarray removeObjectAtIndex:ii];
        }
    }

    [tempgroup sortUsingDescriptors:[NSArray arrayWithObject:groupSorter]];
    [sortedarray addObjectsFromArray:tempgroup];

    [tempgroup release];
    tempgroup = nil;

}

В конце концов вы получите то, что ищете в sortedarray.

person mjdth    schedule 03.02.2010
comment
Я думаю, что это в основном то же самое, что и ответ Бенедикта Коэна, но самостоятельно, а не сортировка и предикаты. - person Jaanus; 04.02.2010

Вы можете использовать NSSortDescriptor. Эти дескрипторы очень полезны, поскольку они позволяют выполнять сортировку по нескольким ключам, а также сортировку по одному ключу. Чувствительность к регистру и нечувствительность также легко достижимы. Я нашел подробный пример ЗДЕСЬ

person guPra    schedule 23.08.2011

Если вам нужно выполнить более сложную сортировку, о которой может позаботиться только «возрастание» (скажем, sort NSString, как если бы они были с плавающей запятой), вы можете сделать что-то вроде этого:

    NSDictionary *d = [self dictionaryFromURL:[NSURL URLWithString:urlStringValue]];    

    NSSortDescriptor *distanceSort = [[NSSortDescriptor alloc] initWithKey:@"distance" ascending:YES comparator:^(id left, id right) {
        float v1 = [left floatValue];
        float v2 = [right floatValue];
        if (v1 < v2)
            return NSOrderedAscending;
        else if (v1 > v2)
            return NSOrderedDescending;
        else
            return NSOrderedSame;
    }];
    NSSortDescriptor *nameSort = [NSSortDescriptor sortDescriptorWithKey:@"company_name" ascending:YES];

    NSArray *sortDescriptors = [NSArray arrayWithObjects:distanceSort, nameSort, nil];

    [distanceSort release];

    NSArray *sortedObjects = [[d allValues] sortedArrayUsingDescriptors:sortDescriptors];

    ILog();
    return sortedObjects;
person james_womack    schedule 16.06.2011