【问题标题】:Reducing nested loop complexity with help of Dictionary (or other data structure) in C#借助 C# 中的 Dictionary(或其他数据结构)降低嵌套循环的复杂性
【发布时间】:2020-10-03 22:21:18
【问题描述】:

我有兴趣在字典(或可能是其他数据结构)的帮助下降低下面这段代码的时间复杂度。

据我了解,我的蛮力解决方案的时间复杂度为 O(n^2),但是,可能可以在 O(n) 中完成(非嵌套循环的 n 次)。

任务是针对每一天和位置打印该天和位置的观察结果百分比,即哺乳动物观察结果。

foreach (var day in EachDay(GetDateTimeForFirstObservation(animalObservations),
GetDateTimeForLastObservation(animalObservations)))
{
    Console.WriteLine("Day: {0}", day.ToString("dd/MM/yyyy"));

    foreach (var location in EachLocation(animalObservations
        .Where(ao => ao.Datetime.Day == day.Day).ToList()))
    {
        Console.WriteLine("Location: {0}", location);

        numOfAllAnimalsInLocationAndDay = animalObservations
            .Where(aob => aob.Location == location &&
                aob.Datetime.Date == day).Count();

        numOfMammalsAnimalsInLocationAndDay = animalObservations
            .Where(aob => aob.Location == location &&
            aob.Datetime.Date == day && aob.Animal.IsMammal).Count();

        Console.WriteLine("Percentage of Mammals in location and day: {0:N2}%",
            numOfMammalsAnimalsInLocationAndDay/numOfAllAnimalsInLocationAndDay * 100);
    }
}

输入看起来像这样:

[
{"DateTime":"2020-02-22 10:10:15", "Location":"Backyard", "Animal": {"Species":"Camel", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-22 11:10:15", "Location":"Backyard", "Animal": {"Species":"Camel", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-22 12:10:15", "Location":"Backyard", "Animal": {"Species":"Ant", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-22 22:10:15", "Location":"Sky", "Animal": {"Species":"Flamingo", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-22 23:10:15", "Location":"Sky", "Animal": {"Species":"Bee", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-23 13:11:15", "Location":"City", "Animal": {"Species":"Racoon", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-24 15:10:00", "Location":"City", "Animal": {"Species":"Dog", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-24 19:10:00", "Location":"City", "Animal": {"Species":"Fly", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-24 19:10:15", "Location":"City", "Animal": {"Species":"Butterfly", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-24 19:10:20", "Location":"City", "Animal": {"Species":"Cat", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-24 19:10:30", "Location":"City", "Animal": {"Species":"Flee", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-25 21:10:15", "Location":"Village", "Animal": {"Species":"Horse", "IsMammal": "TRUE"}},
{"DateTime":"2020-02-25 22:10:15", "Location":"Village", "Animal": {"Species":"Fly", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-25 23:10:15", "Location":"Village", "Animal": {"Species":"Bee", "IsMammal": "FALSE"}},
{"DateTime":"2020-02-25 10:10:15", "Location":"Home", "Animal": {"Species":"Iguana", "IsMammal": "FALSE"}}
]

以及想要的输出:

Day: 22.02.2020
Location: Backyard
Percentage of Mammals in location and day: 66,67%
Location: Sky
Percentage of Mammals in location and day: 50,00%
Day: 23.02.2020
Location: City
Percentage of Mammals in location and day: 100,00%
Day: 24.02.2020
Location: City
Percentage of Mammals in location and day: 40,00%
Day: 25.02.2020
Location: Village
Percentage of Mammals in location and day: 33,33%
Location: Home
Percentage of Mammals in location and day: 0,00%

【问题讨论】:

    标签: c# dictionary time-complexity nested-loops


    【解决方案1】:

    一种解决方案是使用字典,键类型包括日期和位置,值类型保存哺乳动物/非哺乳动物的数量。

    试试下面的代码。您需要将顶部附近的字符串常量指向 JSON 文件的位置。您还需要将 NewtonSoft JSON 库添加到您的项目中。请注意,我已经覆盖了用作字典键类型的类中的 Equals 和 GetHashCode 方法。

    namespace Mammals
    {
        using System;
        using System.Collections.Generic;
        using System.IO;
        using System.Linq;
    
        using Newtonsoft.Json.Linq;
    
        class Program
        {
            static void Main(string[] args)
            {
                const string filePath = "C:\\temp\\mammals.json";
    
                // Read input from JSON:
                string jsonInput;
                using (var file = new StreamReader(new FileStream(filePath, FileMode.Open)))
                {
                    jsonInput = file.ReadToEnd();
                }
    
                var json = JArray.Parse(jsonInput);
                var sightings = json.Select(j => j.ToObject<Sighting>());
    
                // Set up dictionary, using day/location as key:
                var sightingDictionary = new Dictionary<SightingDayAndPlace, SightingCounter>();
    
                // Loop through sightings in O(n):
                foreach (var sighting in sightings)
                {
                    var sightingTimeAndPlace = sighting.GetSightingDayAndPlace;
                    if (!sightingDictionary.ContainsKey(sightingTimeAndPlace))
                    {
                        sightingDictionary.Add(sightingTimeAndPlace, new SightingCounter());
                    }
    
                    if (sighting.IsMammal)
                    {
                        sightingDictionary[sightingTimeAndPlace].MammalCount++;
                    }
                    else
                    {
                        sightingDictionary[sightingTimeAndPlace].NonMammalCount++;
                    }
                }
    
                // Print output:
                var currentDay = default(DateTime);
                foreach (var item in sightingDictionary)
                {
                    var key = item.Key;
                    if (key.Day != currentDay)
                    {
                        Console.WriteLine($"Day: {key.Day:dd.MM.yyyy}");
                    }
    
                    Console.WriteLine($"Location: {key.Location}");
                    Console.WriteLine($"Percentage of Mammals in location and day: {item.Value.MammalPercentage:F}%");
                }
    
                Console.ReadKey();
            }
    
            private class SightingDayAndPlace
            {
                public SightingDayAndPlace(DateTime day, string location)
                {
                    this.Day = day;
                    this.Location = location;
                }
    
                public DateTime Day { get; }
    
                public string Location { get; }
    
                public override bool Equals(object obj)
                {
                    if (obj == null || !(obj is SightingDayAndPlace that))
                    {
                        return false;
                    }
    
                    return this.Day == that.Day
                           && this.Location == that.Location;
                }
    
                public override int GetHashCode()
                {
                    // Consider a different implementation if memory or performance is relevant.
                    return new { this.Day, this.Location }.GetHashCode();
                }
            }
    
            private class Sighting
            {
                public DateTime DateTime { get; set; }
                public string Location { get; set; }
                public Animal Animal { get; set; }
                public string Species => Animal.Species;
                public bool IsMammal => Animal.IsMammal;
                public DateTime Day => DateTime.Date;
    
                public SightingDayAndPlace GetSightingDayAndPlace => new SightingDayAndPlace(this.Day, this.Location);
            }
    
            private class Animal
            {
                public string Species { get; set; }
                public bool IsMammal { get; set; }
            }
    
            private class SightingCounter
            {
                public int MammalCount { get; set; }
                public int NonMammalCount { get; set; }
    
                public double MammalPercentage => (MammalCount / ((double)MammalCount + NonMammalCount)) * 100;
            }
        }
    }
    

    【讨论】:

    • 感谢您的回答!它清晰、独立,我很欣赏使用小型、易于测试的方法将其编写为 TDD 的努力。
    【解决方案2】:

    我认为您的解决方案实际上是 O(n^3) 的复杂性,因为您正在进行 3 次嵌套迭代:

    1. 每个不同的日子
    2. 当天每个不同的位置
    3. 计算日-位置对的动物和哺乳动物的数量 --- 这是您在 Linq 表达式中给出的,所以不是很明显

    假设您具有以下类结构:

    public class AnimalObservation {
        public DateTime DateTime { get; set; }
        public string Location { get; set; }
        public Animal Animal { get; set; }
    }
    
    public class Animal {
        public string Species { get; set; }
        public bool IsMammal { get; set; }
    }
    

    您可以在O(n) 中执行此操作,方法是使用两个字典 --- 一个用于动物,一个用于哺乳动物 --- 将日期-位置对作为键,将计数器作为值

        IDictionary<ValueTuple<DateTime, string>, int> animals = new Dictionary<ValueTuple<DateTime, string>, int>(new DayLocationComparer());
        IDictionary<ValueTuple<DateTime, string>, int> mammals = new Dictionary<ValueTuple<DateTime, string>, int>(new DayLocationComparer());
        foreach (AnimalObservation ao in aos) {
            ValueTuple<DateTime, string> dayLocation = new ValueTuple<DateTime, string>(ao.DateTime, ao.Location);
    
            if (!animals.ContainsKey(dayLocation)) {
                animals.Add(dayLocation, 1);
            } else {
                animals[dayLocation] = animals[dayLocation] + 1;
            }
    
    
            if (!mammals.ContainsKey(dayLocation) && ao.Animal.IsMammal) {
                mammals.Add(dayLocation, 1);
            } else if (!mammals.ContainsKey(dayLocation) && !ao.Animal.IsMammal) {
                mammals.Add(dayLocation, 0);
            } else if (mammals.ContainsKey(dayLocation) && ao.Animal.IsMammal) {
                animals[dayLocation] = animals[dayLocation] + 1;
            }
        }
    
        foreach (ValueTuple<DateTime, string> dayLocation in animals.Keys) {
            int nrOfAnimals = animals[dayLocation];
            int nrOfMammals = mammals[dayLocation];
            Console.WriteLine((double)nrOfMammals / nrOfAnimals * 100);
        }
    

    其中DayLocationComparer 是一个忽略DateTime 的时间部分的比较器

    public class DayLocationComparer : EqualityComparer<ValueTuple<DateTime, string>> {
        public override bool Equals(ValueTuple<DateTime, string> x, ValueTuple<DateTime, string> y) => x.Item1.Date == y.Item1.Date && x.Item2 == y.Item2;
        public override int GetHashCode(ValueTuple<DateTime, string> x) => x.Item1.GetHashCode();
    }
    

    当然,我建议使用一个类作为日期-位置对以获得更易读的代码。

    【讨论】:

    • 谢谢,感谢您的回答。我没有对此进行测试 - 很可能这工作得很好。对我来说,如果我需要与接受的答案进行比较,这似乎有点复杂。虽然我喜欢你使用更通用的 EqualityComparer 并强制执行 GetHash 和 Equals 覆盖。
    【解决方案3】:

    有很多非常疯狂的技巧可以降低时间复杂度,但大多数时候处理数据必须依赖于数据的初始排序方式。如果它没有被订购,我们可以通过一些复合键来订购它。在您的情况下,键是 Tuple&lt;DateTime, Location&gt; 其中 Item1 是日期,Item2 是位置。这将花费 n*log(n),然后使用线性时间来遍历使用线性时间产生结果的数据。

    因此,查看您的数据已经排序。所以我们可以跳过那部分,直接走过去。我们初始化一些状态并检查它是否改变我们产生结果的基本思想。在我们的例子中,状态是当前日期、当前位置,我们在两个变量中跟踪信息,包括动物总数、哺乳动物总数。

        public static void PrintPopulation(List<AnimalObservations> animalObservations)
        {
            if (animalObservations.Count == 0)
                return;
            var item = animalObservations[0];
            string currentLocation = item.Location;
            DateTime currentDate = item.DateTime.Date;
            int totalAnimals = 1;
            int totalMammals = item.Animal.IsMammal ? 1 : 0;
            for (int i = 1; i < animalObservations.Count; i++)
            {
                item = animalObservations[i];
                if (currentLocation != item.Location ||
                    currentDate != item.DateTime.Date)
                {
                    PrintResult(currentDate, currentLocation, totalAnimals, totalMammals);
                    totalMammals = 0;
                    totalAnimals = 0;
                    currentLocation = item.Location;
                    currentDate = item.DateTime.Date;
                }
    
                totalAnimals++;
                totalMammals += item.Animal.IsMammal ? 1 : 0;
            }
            PrintResult(currentDate, currentLocation, totalAnimals, totalMammals);
        }
    
        public static void PrintResult(DateTime date, string location, int totalAnimals, int totalMammals)
        {
            Console.WriteLine($"{date} {location} {(double) totalMammals / totalAnimals}");
        }
    

    我认为

    public class AnimalObservations
    {
        public DateTime DateTime { get; set; }
        public string Location { get; set; }
        public Animal Animal { get; set; }
    }
    
    public class Animal
    {
        public bool IsMammal { get; set; }
    }
    

    【讨论】:

    • 这不仅假定数据是按时间顺序排列的,而且还假定永远不会重新访问位置。 OP 肯定会知道,但这似乎不太可能,即使对于小样本数据集也是如此。
    • @NeilT 看起来可能会查看 2020-02-25 的最后一条记录
    • 查看其余数据,我怀疑这是“2020-02-26”的拼写错误!但是当然,如​​果我们可以假设预先排序,那么是的,这是一个更简单的问题。
    • 这不是一个错字,因为他的输出中应该有额外的结果。但是是的,我也想过使用字典来解决它
    • @Irdis 感谢您提供这个好的解决方案!很高兴您提供了有关排序方法以及稍后如何使用状态来查找更改的有价值信息。如果 NeilT 不发布后一个答案,包括考虑未排序的数据,我会接受这一点。
    猜你喜欢
    • 1970-01-01
    • 2012-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多