C # Sắp xếp và Thứ tự bằng cách so sánh


105

Tôi có thể sắp xếp một danh sách bằng cách sử dụng Sort hoặc OrderBy. Cái nào nhanh hơn? Cả hai đều hoạt động trên cùng một thuật toán?

List<Person> persons = new List<Person>();
persons.Add(new Person("P005", "Janson"));
persons.Add(new Person("P002", "Aravind"));
persons.Add(new Person("P007", "Kazhal"));

1.

persons.Sort((p1,p2)=>string.Compare(p1.Name,p2.Name,true));

2.

var query = persons.OrderBy(n => n.Name, new NameComparer());

class NameComparer : IComparer<string>
{
    public int Compare(string x,string y)
    {
      return  string.Compare(x, y, true);
    }
}

22
Tôi không thể tin rằng không có câu trả lời nào đề cập đến điều này, nhưng sự khác biệt lớn nhất là ở đây: OrderBy tạo một bản sao được sắp xếp của Mảng hoặc Danh sách, trong khi Sort thực sự sắp xếp nó tại chỗ.
PRMan

2
như tiêu đề nói so sánh, tôi muốn thêm rằng OrderBy ổn định và sắp xếp ổn định tối đa 16 phần tử vì sắp xếp chèn tối đa 16 phần tử được sử dụng nếu các phần tử nhiều hơn thế thì nó chuyển sang các bí danh không ổn định khác Chỉnh sửa: ổn định có nghĩa là duy trì thứ tự tương đối của các phần tử có cùng một khóa.
Eklavyaa

@PRMan Không, OrderBy tạo một danh sách lười biếng. Chỉ khi bạn gọi một phương thức như ToList trên bảng liệt kê trả về, bạn mới nhận được một bản sao đã được sắp xếp.
Stewart

1
@Stewart, Bạn không coi Array.Copy hoặc Collection.Copy vào TElement [] trong Bộ đệm trong System.Core / System / Linq / Enumerable.cs là một bản sao? Và nếu bạn gọi ToList trên IEnumerable, ngay lập tức bạn có thể có 3 bản sao trong bộ nhớ cùng một lúc. Đây là một vấn đề đối với các mảng rất lớn, đó là một phần quan điểm của tôi. Ngoài ra, nếu bạn cần cùng một thứ tự được sắp xếp nhiều lần, thì việc gọi Sắp xếp tại chỗ một lần sẽ hiệu quả hơn nhiều so với việc sắp xếp nhiều lần Danh sách, vì tính lâu dài của nó.
PRMan

1
@PRMan Ồ, ý bạn là một bản sao được sắp xếp được tạo trong nội bộ. Điều đó vẫn không chính xác, vì OrderBy không tạo bản sao - theo những gì tôi có thể thấy, điều này được thực hiện bởi phương thức GetEnumerator khi bạn thực sự bắt đầu lặp lại bộ sưu tập. Tôi vừa thử xem qua mã của mình và nhận thấy rằng mã điền một biến từ biểu thức LINQ chạy gần như ngay lập tức, nhưng khi bạn vào vòng lặp foreach, nó sẽ dành thời gian để sắp xếp nó. Tôi đoán khi có thêm một chút thời gian, tôi nên dành một chút thời gian để tìm hiểu xem nó hoạt động như thế nào ở hậu trường.
Stewart

Câu trả lời:


90

Tại sao không đo lường nó:

class Program
{
    class NameComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return string.Compare(x, y, true);
        }
    }

    class Person
    {
        public Person(string id, string name)
        {
            Id = id;
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
    }

    static void Main()
    {
        List<Person> persons = new List<Person>();
        persons.Add(new Person("P005", "Janson"));
        persons.Add(new Person("P002", "Aravind"));
        persons.Add(new Person("P007", "Kazhal"));

        Sort(persons);
        OrderBy(persons);

        const int COUNT = 1000000;
        Stopwatch watch = Stopwatch.StartNew();
        for (int i = 0; i < COUNT; i++)
        {
            Sort(persons);
        }
        watch.Stop();
        Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

        watch = Stopwatch.StartNew();
        for (int i = 0; i < COUNT; i++)
        {
            OrderBy(persons);
        }
        watch.Stop();
        Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);
    }

    static void Sort(List<Person> list)
    {
        list.Sort((p1, p2) => string.Compare(p1.Name, p2.Name, true));
    }

    static void OrderBy(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToArray();
    }
}

Trên máy tính của tôi khi được biên dịch ở chế độ Phát hành, chương trình này sẽ in:

Sort: 1162ms
OrderBy: 1269ms

CẬP NHẬT:

Theo đề xuất của @Stefan, đây là kết quả của việc sắp xếp một danh sách lớn ít lần hơn:

List<Person> persons = new List<Person>();
for (int i = 0; i < 100000; i++)
{
    persons.Add(new Person("P" + i.ToString(), "Janson" + i.ToString()));
}

Sort(persons);
OrderBy(persons);

const int COUNT = 30;
Stopwatch watch = Stopwatch.StartNew();
for (int i = 0; i < COUNT; i++)
{
    Sort(persons);
}
watch.Stop();
Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

watch = Stopwatch.StartNew();
for (int i = 0; i < COUNT; i++)
{
    OrderBy(persons);
}
watch.Stop();
Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);

Bản in:

Sort: 8965ms
OrderBy: 8460ms

Trong trường hợp này, có vẻ như OrderBy hoạt động tốt hơn.


CẬP NHẬT2:

Và sử dụng các tên ngẫu nhiên:

List<Person> persons = new List<Person>();
for (int i = 0; i < 100000; i++)
{
    persons.Add(new Person("P" + i.ToString(), RandomString(5, true)));
}

Ở đâu:

private static Random randomSeed = new Random();
public static string RandomString(int size, bool lowerCase)
{
    var sb = new StringBuilder(size);
    int start = (lowerCase) ? 97 : 65;
    for (int i = 0; i < size; i++)
    {
        sb.Append((char)(26 * randomSeed.NextDouble() + start));
    }
    return sb.ToString();
}

Sản lượng:

Sort: 8968ms
OrderBy: 8728ms

Still OrderBy nhanh hơn


2
Tôi nghĩ, việc sắp xếp một danh sách rất nhỏ (3 mục) 1000000 lần hoặc bằng cách sắp xếp một danh sách rất lớn (1000000 mục) chỉ một vài lần sẽ khác nhiều. Cả hai đều rất phù hợp. Trong thực tế, kích thước trung bình của danh sách (trung bình là gì? ... giả sử 1000 mục bây giờ) là thú vị nhất. IMHO, sắp xếp danh sách có 3 mục không có ý nghĩa lắm.
Stefan Steinegger

25
Lưu ý rằng có sự khác biệt giữa "nhanh hơn" và "nhanh hơn đáng kể". Trong ví dụ cuối cùng của bạn, sự khác biệt là khoảng một phần tư giây. Người dùng có để ý không? Việc người dùng đợi kết quả gần 9 giây có phải là điều không thể chấp nhận được không? Nếu câu trả lời cho cả hai câu hỏi là "không" thì bạn chọn câu nào từ góc độ hiệu suất thực sự không quan trọng.
Eric Lippert

12
Cũng lưu ý rằng thử nghiệm ở đây sắp xếp danh sách trước khi bắt đầu đồng hồ bấm giờ, vì vậy chúng tôi đang so sánh cách hai thuật toán so sánh khi đối mặt với đầu vào được sắp xếp. Điều này có thể khá khác so với hiệu suất tương đối của chúng với đầu vào không được sắp xếp.
phoog

3
Những kết quả này khá đáng ngạc nhiên IMHO, xem xét thực tế là LINQphải tốn thêm bộ nhớ so với List<T>.Sortviệc triển khai tại chỗ . Tôi không chắc liệu họ có cải thiện điều này trong các phiên bản .NET mới hơn hay không, nhưng trên máy của tôi (bản phát hành i7 thế hệ thứ 3 64-bit .NET 4.5) hoạt động Sorttốt hơn OrderBytrong mọi trường hợp. Hơn nữa, bằng cách xem xét OrderedEnumerable<T>mã nguồn, có vẻ như nó tạo ra ba mảng bổ sung (đầu tiên là a Buffer<T>, sau đó là một mảng các khóa dự kiến, sau đó là một mảng chỉ số) trước khi gọi Quicksort để sắp xếp các mảng chỉ số tại chỗ.
Groo

2
... và sau đó, có một ToArraylệnh gọi tạo ra mảng kết quả. Các phép toán bộ nhớ và lập chỉ mục mảng là những hoạt động cực kỳ nhanh, nhưng tôi vẫn không thể tìm ra logic đằng sau những kết quả này.
Groo

121

Không, chúng không giống một thuật toán. Đối với người mới bắt đầu, LINQ OrderByđược ghi nhận là ổn định (nghĩa là nếu hai mục có cùng một mục Name, chúng sẽ xuất hiện theo thứ tự ban đầu).

Nó cũng phụ thuộc vào việc bạn đệm truy vấn hay lặp lại nhiều lần (LINQ-to-Objects, trừ khi bạn đệm kết quả, sẽ sắp xếp lại mỗi lần foreach).

Đối với OrderBytruy vấn, tôi cũng muốn sử dụng:

OrderBy(n => n.Name, StringComparer.{yourchoice}IgnoreCase);

(đối với {yourchoice}một trong CurrentCulture, Ordinalhoặc InvariantCulture).

List<T>.Sort

Phương pháp này sử dụng Array.Sort, sử dụng thuật toán QuickSort. Việc triển khai này thực hiện một loại không ổn định; nghĩa là, nếu hai phần tử bằng nhau, thứ tự của chúng có thể không được giữ nguyên. Ngược lại, một sắp xếp ổn định bảo toàn thứ tự của các phần tử bằng nhau.

Enumerable.OrderBy

Phương pháp này thực hiện một sắp xếp ổn định; nghĩa là, nếu khóa của hai phần tử bằng nhau, thứ tự của các phần tử được giữ nguyên. Ngược lại, một sắp xếp không ổn định không bảo toàn thứ tự của các phần tử có cùng một khóa. sắp xếp; nghĩa là, nếu hai phần tử bằng nhau, thứ tự của chúng có thể không được giữ nguyên. Ngược lại, một sắp xếp ổn định bảo toàn thứ tự của các phần tử bằng nhau.


5
Nếu bạn sử dụng .NET Reflector hoặc ILSpy để mở Enumerable.OrderByvà đi sâu vào triển khai bên trong của nó, bạn có thể thấy rằng thuật toán sắp xếp OrderBy là một biến thể của QuickSort thực hiện một loại ổn định. (Xem System.Linq.EnumerableSorter<TElement>.) Do đó, Array.SortEnumerable.OrderBycả hai đều có thể được mong đợi là có thời gian thực thi O (N log N) , trong đó N là số phần tử trong tập hợp.
John Beyer

@Marc Tôi không hoàn toàn theo dõi sự khác biệt sẽ là gì nếu hai phần tử bằng nhau và thứ tự của chúng không được giữ nguyên. Điều này chắc chắn không phải là một vấn đề đối với các kiểu dữ liệu nguyên thủy. Nhưng ngay cả đối với một loại tham chiếu, tại sao nó lại quan trọng, nếu tôi sắp xếp, người có tên Marc Gravell lại xuất hiện trước một người khác có tên Marc Gravell (ví dụ: :))? Tôi không đặt câu hỏi về câu trả lời / kiến ​​thức của bạn, thay vì tìm kiếm một ứng dụng của tình huống này.
Mukus

4
@Mukus hãy tưởng tượng bạn sắp xếp sổ địa chỉ công ty theo tên (hoặc thực sự là theo ngày sinh) - chắc chắn sẽ có các bản sao. Câu hỏi cuối cùng là: điều gì xảy ra cho họ? Thứ tự con có được xác định không?
Marc Gravell

55

Câu trả lời của Darin Dimitrov cho thấy nó OrderBynhanh hơn một chút so với List.Sortkhi đối mặt với đầu vào đã được sắp xếp. Tôi đã sửa đổi mã của anh ấy để nó liên tục sắp xếp dữ liệu chưa được sắp xếp và OrderBytrong hầu hết các trường hợp, nó hơi chậm hơn.

Hơn nữa, OrderBykiểm tra sử dụng ToArrayđể buộc liệt kê Linq, nhưng điều đó rõ ràng trả về một kiểu ( Person[]) khác với kiểu đầu vào ( List<Person>). Do đó, tôi đã chạy lại thử nghiệm bằng cách sử dụng ToListchứ không phải ToArrayvà nhận được sự khác biệt lớn hơn:

Sort: 25175ms
OrderBy: 30259ms
OrderByWithToList: 31458ms

Mật mã:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

class Program
{
    class NameComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return string.Compare(x, y, true);
        }
    }

    class Person
    {
        public Person(string id, string name)
        {
            Id = id;
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
        public override string ToString()
        {
            return Id + ": " + Name;
        }
    }

    private static Random randomSeed = new Random();
    public static string RandomString(int size, bool lowerCase)
    {
        var sb = new StringBuilder(size);
        int start = (lowerCase) ? 97 : 65;
        for (int i = 0; i < size; i++)
        {
            sb.Append((char)(26 * randomSeed.NextDouble() + start));
        }
        return sb.ToString();
    }

    private class PersonList : List<Person>
    {
        public PersonList(IEnumerable<Person> persons)
           : base(persons)
        {
        }

        public PersonList()
        {
        }

        public override string ToString()
        {
            var names = Math.Min(Count, 5);
            var builder = new StringBuilder();
            for (var i = 0; i < names; i++)
                builder.Append(this[i]).Append(", ");
            return builder.ToString();
        }
    }

    static void Main()
    {
        var persons = new PersonList();
        for (int i = 0; i < 100000; i++)
        {
            persons.Add(new Person("P" + i.ToString(), RandomString(5, true)));
        } 

        var unsortedPersons = new PersonList(persons);

        const int COUNT = 30;
        Stopwatch watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            Sort(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

        watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            OrderBy(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);

        watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            OrderByWithToList(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("OrderByWithToList: {0}ms", watch.ElapsedMilliseconds);
    }

    static void Sort(List<Person> list)
    {
        list.Sort((p1, p2) => string.Compare(p1.Name, p2.Name, true));
    }

    static void OrderBy(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToArray();
    }

    static void OrderByWithToList(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToList();
    }
}

2
Tôi chạy mã thử nghiệm ngay bây giờ trong LinqPad 5 (.net 5) và OrderByWithToListmất thời gian tương tự OrderBy.
dovid

38

Tôi nghĩ điều quan trọng cần lưu ý là sự khác biệt khác giữa SortOrderBy:

Giả sử tồn tại một Person.CalculateSalary()phương pháp, phương pháp này mất rất nhiều thời gian; có thể nhiều hơn cả thao tác sắp xếp một danh sách lớn.

Đối chiếu

// Option 1
persons.Sort((p1, p2) => Compare(p1.CalculateSalary(), p2.CalculateSalary()));
// Option 2
var query = persons.OrderBy(p => p.CalculateSalary()); 

Tùy chọn 2 có thể có hiệu suất cao hơn, vì nó chỉ gọi CalculateSalaryphương thức n lần, trong khi Sorttùy chọn có thể gọi CalculateSalarytối đa 2 n log ( n ) lần, tùy thuộc vào thành công của thuật toán sắp xếp.


4
Điều này đúng, mặc dù có một giải pháp cho vấn đề đó, đó là giữ dữ liệu trong một mảng và sử dụng quá tải Array.Sort chiếm hai mảng, một là các khóa và một là các giá trị. Khi điền vào mảng khóa, bạn sẽ gọi nthời gian tính toán . Điều này rõ ràng là không thuận tiện bằng việc sử dụng OrderBy.
phoog

14

Tóm lại:

Danh sách / Sắp xếp Mảng ():

  • Sắp xếp không ổn định.
  • Thực hiện tại chỗ.
  • Sử dụng Introsort / Quicksort.
  • So sánh tùy chỉnh được thực hiện bằng cách cung cấp một trình so sánh. Nếu so sánh là đắt, nó có thể chậm hơn OrderBy () (cho phép sử dụng các khóa, xem bên dưới).

OrderBy / ThenBy ():

  • Sắp xếp ổn định.
  • Không đúng chỗ.
  • Sử dụng Quicksort. Quicksort không phải là một loại ổn định. Đây là mẹo: khi sắp xếp, nếu hai phần tử có khóa bằng nhau, nó sẽ so sánh thứ tự ban đầu của chúng (đã được lưu trữ trước khi sắp xếp).
  • Cho phép sử dụng các khóa (sử dụng lambdas) để sắp xếp các phần tử theo giá trị của chúng (ví dụ x => x.Id:). Tất cả các khóa được trích xuất trước khi sắp xếp. Điều này có thể dẫn đến hiệu suất tốt hơn so với việc sử dụng Sort () và một trình so sánh tùy chỉnh.

Nguồn: MDSN , nguồn tham khảo và kho lưu trữ dotnet / coreclr (GitHub).

Một số câu lệnh được liệt kê ở trên dựa trên việc triển khai .NET framework hiện tại (4.7.2). Nó có thể thay đổi trong tương lai.


0

bạn nên tính độ phức tạp của các thuật toán được sử dụng bằng các phương pháp OrderBy và Sort. QuickSort có độ phức tạp n (log n) theo tôi nhớ, trong đó n là độ dài của mảng.

Tôi cũng đã tìm kiếm orderby's, nhưng tôi không thể tìm thấy bất kỳ thông tin nào ngay cả trong thư viện msdn. nếu bạn không có bất kỳ giá trị nào giống nhau và việc sắp xếp chỉ liên quan đến một thuộc tính, tôi thích sử dụng phương thức Sort () hơn; nếu không, hãy sử dụng OrderBy.


1
Theo tài liệu MSDN hiện tại, Sort sử dụng 3 thuật toán sắp xếp khác nhau dựa trên đầu vào. Trong số đó có QuickSort. Câu hỏi về thuật toán OrderBy () ở đây (Quicksort): stackoverflow.com/questions/2792074/…
Thor

-1

Tôi chỉ muốn thêm rằng orderby là cách hữu ích hơn.

Tại sao? Bởi vì tôi có thể làm điều này:

Dim thisAccountBalances = account.DictOfBalances.Values.ToList
thisAccountBalances.ForEach(Sub(x) x.computeBalanceOtherFactors())
thisAccountBalances=thisAccountBalances.OrderBy(Function(x) x.TotalBalance).tolist
listOfBalances.AddRange(thisAccountBalances)

Tại sao so sánh phức tạp? Chỉ cần sắp xếp dựa trên một trường. Ở đây tôi sắp xếp dựa trên TotalBalance.

Rất dễ.

Tôi không thể làm điều đó với sự sắp xếp. Tôi tự hỏi tại sao. Làm tốt với trật tựBy.

Đối với tốc độ, nó luôn luôn là O (n).


3
Câu hỏi: O (n) Time (tôi giả sử) trong câu trả lời của bạn đề cập đến OrderBy hay Comparer? Tôi không nghĩ rằng sắp xếp nhanh có thể đạt được thời gian O (N).
Kevman
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.