Tương đương O lớn cho LINQ chọn


10

Tôi đang cố gắng xác định xem có sự thay đổi nào trong tương đương Big O của vòng lặp lồng nhau hay không khi tôi sử dụng lựa chọn LINQ thay thế.

public void myFunc(List<Foo> fooList, List<Bar> barList)
{
    foreach(Foo foo in fooList)
    {
        foreach(Bar bar in barList)
        {
            if(foo.PropA == bar.PropA && bar.PropZ.HasValue)
                foo.PropC = foo.PropB * bar.PropZ;
        }
    }
}

Tôi tin rằng ví dụ vòng lặp lồng nhau này là O (n ^ 2) vì độ phức tạp.

Tôi đã thay thế vòng lặp lồng nhau bằng một LINQ chọn như thế này:

public void myFunc(List<Foo> fooList, List<Bar> barList)
{
    foreach(Foo foo in fooList)
    {
        Bar bar = (from b in barList
                    where foo.PropA == b.PropA
                    select b).FirstOrDefault();

        if(bar.PropZ.HasValue)
            foo.PropC = foo.PropB * bar.PropZ;
    }
}

Nếu không có gì khác, mã dường như sạch hơn một chút để đọc vì nó tuyên bố rõ ràng "chúng tôi đang tìm kiếm đặc biệt này Barđể làm việc với."

Câu hỏi của tôi là : Việc sử dụng LINQ có làm giảm độ phức tạp Big O không?

c#  big-o 

3
Sẽ rất thú vị khi so sánh IL được tạo bởi trình biên dịch cho mỗi thứ này.
Dan Pichelman

@DanPichelman - Thoát khỏi mọt sách! Vâng, tôi đồng ý rằng sẽ rất thú vị để tìm hiểu. Chúng có tương đương hay không?

Có thể nó tiết kiệm thời gian để lặp lại bars và lọc bar.PropZ.HasValuetrước, nếu bạn mong đợi nhiều hơn một lượng nhỏ để đánh giá thành sai? Không thực sự trả lời câu hỏi của bạn, tôi chỉ đang xem lại mã của bạn.

1
Có phải hai vòng lặp này thậm chí làm cùng một việc, đặc biệt trong trường hợp foo.PropA == bar.PropAđúng với nhiều mục trong barList? Chỉnh sửa: chắc chắn không phải là thứ hai sẽ ném một NullReferenceExceptionkhi lựa chọn trở lại null.
Philip Kendall

1
Tôi đoán rằng .FirstOrDefault()sẽ làm cho vòng lặp linq thoát ra sớm nếu một trận đấu được tìm thấy, trong khi các vòng lặp lồng nhau câm lặng của bạn không thoát ra sớm, vì vậy, tôi nghĩ rằng linq sẽ có một trận đấu lớn hơn. Nhưng tôi không chắc chắn, do đó một bình luận và không một câu trả lời.
Mike Nakis

Câu trả lời:


14

Big O là như nhau, vì cả hai mã sẽ (trong trường hợp xấu nhất) phải thực hiện so sánh cho từng kết hợp các mục từ cả hai danh sách. Ví dụ: nếu không có kết quả trùng khớp giữa các danh sách, không tối ưu hóa trong triển khai Linq sẽ tránh phải kiểm tra tất cả các mục.

Nhưng này, bạn không thực sự muốn biết về big-O, phải không? Bạn muốn biết nếu cái thứ hai nhanh hơn . Câu trả lời là có, giải pháp dựa trên linq nhanh hơn (miễn là đủ lớn) vì nó chỉ phải thực hiện kiểm tra cho đến khi tìm thấy kết quả khớp đầu tiên trong danh sách, trong khi giải pháp lồng nhau luôn phải truy cập tất cả các mục . Các Wherephương pháp không quét toàn bộ danh sách ngay lập tức nhưng Rater tạo ra một iterator lười biếng. Các FirstOrDefaultphương pháp sau đó thực hiện lặp cho đến khi trận đấu đầu tiên được tìm thấy, có nghĩa là trong trường hợp thông thường nó không nhất thiết phải đi qua toàn bộ danh sách. Tất nhiên có một số chi phí trong giải pháp Linq, vì vậy nếu n đủ thấp thì vòng lặp lồng nhau sẽ nhanh hơn.

Bạn có thể tự kiểm tra nguồn: https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Enumerable.cs

(Nhưng nếu bạn đã thêm một breakmẫu trong mẫu đầu tiên, hai mã sẽ tương đương về mặt ngữ nghĩa và mã đầu tiên sẽ nhanh hơn do ít chi phí hơn.)


"Giải pháp dựa trên linq nhanh hơn" Ai biết? LINQ có chi phí nhân tố không đổi có thể dễ dàng vượt quá 2 lần.
usr

@usr: Bạn nói đúng, nó phụ thuộc vào tần suất trùng khớp chứ không phải kích thước của danh sách. Tần suất trùng khớp càng cao, lợi thế tương đối lớn hơn của giải pháp dựa trên linq.
JacquesB

5

Không có sự khác biệt về độ phức tạp, cả hai đều là O (n²) bởi vì O (n) của Linq.
Việc sử dụng FirstOrDefault tương đương để làm điều này:

if(foo.PropA == bar.PropA && bar.PropZ.HasValue) 
{
    foo.PropC = foo.PropB * bar.PropZ;
    break;
}

Đây là một sự cải thiện về thời gian tính toán, nhưng không phức tạp.

Bạn có thể làm tốt hơn bằng cách sử dụng Hashmap cho một trong hai danh sách.
Các gia của LINQ điều hành sẽ tạo ra một HashMap cho một trong 2 danh sách, vì vậy bạn sẽ đạt được hiệu suất khi tìm kiếm hai yếu tố phù hợp:

    public void myFunc(List<Foo> fooList, List<Bar> barList)
    {
        var rows = fooList.Join(
                        barList, 
                        f => f.PropA, 
                        b => b.PropA, 
                        (f, b) => new { Foo = f, Bar = b }
                   );

        foreach (var row in rows)
        {
            if (row.Bar.PropZ.HasValue)
                row.Foo.PropC = row.Foo.PropB * row.Bar.PropZ.Value;
        }
    }

Điều này sẽ chạy trong O (n log (n))


Bạn có thể giải thích tại sao nó sẽ chạy O (n log (n)) không? Theo mã nguồn, nó dường như tạo ra một Lookup(Of TKey, TElement), lặp đi lặp lại barList, lấy Groupingcho mỗi mục và thêm các mục vào Grouping. Sau đó, nó lặp đi lặp lại fooList, lấy Groupingvà trả về từng phần tử trong nhóm. Trường hợp `log (n) đến từ đâu?
Zach Mierzejewski
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.