【C#入門】配列、Listをソートする方法(降順、カスタマイズも解説)

Listや配列のソートって使っていますか?

C#ではListや配列の要素を昇順で並び替えるメソッドが用意されています。また、要素が文字列や日付時刻であっても並び替えることができます。

この記事では、Listや配列のソートについて

  • 配列、Listのソートとは
  • Array.Sortで配列のソート
  • List.SortでListのソート
  • ラムダ式で降順ソート
  • LINQで降順ソート

という基本的な内容から、処理速度の比較や文字列、日付時刻のソート、Comparisonデリゲートでカスタムソート、Compare、CompareToを使用する場合など応用的な内容についても解説していきます。

今回はListや配列のソートについて、使い方をわかりやすく解説します!

目次

配列、Listのソートとは

配列やList型のオブジェクトで要素を順番に並び替えることをソートと言います。小さい値から大きい値の順に並べることを昇順、逆に大きい値から小さい値に並べることを降順と言います。

C#では数値の要素に限らず、文字列やDateTime型の日付時刻もソートすることができるメソッドが用意されています。文字列の場合はアルファベット順や五十音順に並び替えます。ソートの方法はいくつかあるので、一つずつ解説していきます!

Array.Sortで配列のソート

Array.Sortで配列をソートする方法についてみていきましょう。Array.Sortメソッドは以下のように定義されています。

public static void Sort(
    Array array
)

引数arrayには、並べ替えの対象となる1次元の配列を指定します。サンプルコードで確認しましょう。

using System;

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      int[] src = {3, 1, 5, 4, 2};
      
      Array.Sort(src);
      
      Console.WriteLine("[{0}]", string.Join(", ", src));
      Console.ReadKey();
    }
  }
}

実行結果:

[1, 2, 3, 4, 5]

このサンプルコードでは、Array.Sortメソッドを使ってint型の配列srcの要素を昇順に並び替えています。

List.SortでListのソート

配列と同じようにList型のオブジェクトもソートすることができます。List型のオブジェクトからSortメソッドを呼び出すことで、昇順に並び替えることができます。サンプルコードで確認しましょう。

using System;
using System.Collections.Generic;

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      int[] src = {3, 1, 5, 4, 2};
      var list = new List<int>();
      
      // listに要素を追加
      list.AddRange(src);
      
      // listをソート
      list.Sort();
      
      Console.WriteLine("[{0}]", string.Join(", ", list));
      Console.ReadKey();
    }
  }
}

実行結果:

[1, 2, 3, 4, 5]

このサンプルコードでは、まずAddRangeメソッドを使ってList型のオブジェクトlistに配列srcの要素を追加しています。そして、Sortメソッドを使ってlistの要素を昇順に並び替えています。

ラムダ式で降順ソート

List.Sortメソッドの引数にソートの条件をラムダ式で簡単に指定することができます。サンプルコードで確認しましょう。

using System;
using System.Collections.Generic;

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      int[] src = {3, 1, 5, 4, 2};
      var list = new List<int>();
      
      // listに要素を追加
      list.AddRange(src);
      
      // listをラムダ式でソート
      list.Sort((a, b) => b - a);
      
      Console.WriteLine("[{0}]", string.Join(", ", list));
      Console.ReadKey();
    }
  }
}

実行結果:

[5, 4, 3, 2, 1]

このサンプルコードでは、Sortメソッドの引数にラムダ式を指定しています。

LINQで降順ソート

LINQを使ってソートすることもできます。ソートするにはOrderByメソッドを使用します。サンプルコードで確認しましょう。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      int[] src = {3, 1, 5, 4, 2};
      
      // LINQでソート
      var dst = src.OrderBy(a => -a);
      
      Console.WriteLine("[{0}]", string.Join(", ", dst));
      Console.ReadKey();
    }
  }
}

実行結果:

[5, 4, 3, 2, 1]

このサンプルコードでは、OrderByメソッドを使って降順に並び替えています。

処理速度の比較

これまでArray.Sortメソッド、List.Sortメソッド、ラムダ式、LINQそれぞれでソートする方法についてお伝えしてきました。これらの処理速度についてサンプルコードで比較してみましょう。

なお、乱数を使って配列の要素の値を決めて、それを昇順でソートすることにします。サンプルコードで確認しましょう。

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

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      Stopwatch sw = new Stopwatch();
      long nums = 100000;
      double[] src = new double[nums];
      Random r = new Random();
      for(int i = 0; i < nums; i++) {
        src[i] = r.NextDouble();
      }
      
      double[] dst1 = new double[nums];
      Array.Copy(src, dst1, src.Length);
      sw.Start();
      Array.Sort(dst1);
      sw.Stop();
      Console.WriteLine("Array.Sortでは{0}ミリ秒", sw.Elapsed.TotalMilliseconds);
      
      var list1 = new List<double>();
      list1.AddRange(src);
      sw.Start();
      list1.Sort();
      sw.Stop();
      Console.WriteLine("List.Sortでは{0}ミリ秒", sw.Elapsed.TotalMilliseconds);
      
      var list2 = new List<double>();
      list2.AddRange(src);
      sw.Start();
      list2.Sort((a, b) => a.CompareTo(b));
      sw.Stop();
      Console.WriteLine("ラムダ式では{0}ミリ秒", sw.Elapsed.TotalMilliseconds);
      
      sw.Start();
      var dst2 = src.OrderBy(a => a);
      sw.Stop();
      Console.WriteLine("LINQでは{0}ミリ秒", sw.Elapsed.TotalMilliseconds);
      
      Console.ReadKey();
    }
  }
}

実行結果:

Array.Sortでは32.1564ミリ秒
List.Sortでは57.7862ミリ秒
ラムダ式では92.5847ミリ秒
LINQでは93.1272ミリ秒

このサンプルコードでは、それぞれの方法を10万回繰り返すのにかかった時間をDiagnostics.Stopwatchクラスを使って計測しています。Array.Sortメソッドでソートした場合が最も処理速度が速く、ラムダ式やLINQを使った場合は処理が遅くなっています。

ちなみにラムダ式の記述の部分でCompareToメソッドを使っています。CompareToメソッドはこのメソッドを呼び出すオブジェクトと引数に指定したオブジェクトを比較します。

メソッドを呼び出すオブジェクトの方が、小さいもしくは順番が前の場合にマイナスの値、同じ場合に0(ゼロ)、大きいもしくは順番が後ろの場合にプラスの値を返します。

文字列、日付時刻のソート

C#では要素が数値以外の文字列やDateTime型であってもソートすることができます。

文字列要素のソート

要素が文字列の場合のソートについて、サンプルコードで確認しましょう。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      string[] src = {"Taro", "Jiro", "Saburo"};
      
      string[] dst1 = new string[src.Length];
      Array.Copy(src, dst1, src.Length);
      Array.Sort(dst1);
      Console.WriteLine("[{0}]", string.Join(", ", dst1));
      
      var list1 = new List<string>();
      list1.AddRange(src);
      list1.Sort();
      Console.WriteLine("[{0}]", string.Join(", ", list1));
      
      var list2 = new List<string>();
      list2.AddRange(src);
      list2.Sort((a, b) => a.CompareTo(b));
      Console.WriteLine("[{0}]", string.Join(", ", list2));
      
      var dst2 = src.OrderBy(a => a);
      Console.WriteLine("[{0}]", string.Join(", ", dst2));
      
      Console.ReadKey();
    }
  }
}

実行結果:

[Jiro, Saburo, Taro]
[Jiro, Saburo, Taro]
[Jiro, Saburo, Taro]
[Jiro, Saburo, Taro]

このサンプルコードでは、Array.Sortメソッド、List.Sortメソッド、ラムダ式、LINQそれぞれを使って文字列要素を昇順で並び変えています。

日付時刻要素のソート

要素がDateTime型の場合のソートについて、サンプルコードで確認しましょう。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Sample
{
  class Sample
  {
    static void Main()
    {
      DateTime[] src = {DateTime.Parse("2017/12/23"), 
                        DateTime.Parse("2018/01/01"), 
                        DateTime.Parse("2017/11/03")};
      
      DateTime[] dst1 = new DateTime[src.Length];
      Array.Copy(src, dst1, src.Length);
      Array.Sort(dst1);
      Console.WriteLine("[{0}]", string.Join(", ", dst1));
      
      var list1 = new List<DateTime>();
      list1.AddRange(src);
      list1.Sort();
      Console.WriteLine("[{0}]", string.Join(", ", list1));
      
      var list2 = new List<DateTime>();
      list2.AddRange(src);
      list2.Sort((a, b) => a.CompareTo(b));
      Console.WriteLine("[{0}]", string.Join(", ", list2));
      
      var dst2 = src.OrderBy(a => a);
      Console.WriteLine("[{0}]", string.Join(", ", dst2));
      
      Console.ReadKey();
    }
  }
}

実行結果:

[11/03/2017 00:00:00, 12/23/2017 00:00:00, 01/01/2018 00:00:00]
[11/03/2017 00:00:00, 12/23/2017 00:00:00, 01/01/2018 00:00:00]
[11/03/2017 00:00:00, 12/23/2017 00:00:00, 01/01/2018 00:00:00]
[11/03/2017 00:00:00, 12/23/2017 00:00:00, 01/01/2018 00:00:00]

このサンプルコードでは、Array.Sortメソッド、List.Sortメソッド、ラムダ式、LINQそれぞれを使ってDateTime型の要素を昇順で並び変えています。

Comparisonデリゲートでカスタムソート

List.Sortメソッドの引数に並び替えの条件を定義したメソッドを表すComparison<T>デリゲートを指定することで、定義した条件によって並び替えることができます。

複数のキーで並び替えを行いたい場合に、キーの指定と並び替えの条件を定義できるので便利です。Comparison<T>デリゲートは以下のように定義されています。

public delegate int Comparison<in T>(
    T x,
    T y
)

引数xには比較する最初のオブジェクトを指定します。引数yには比較する2番目のオブジェクトを指定します。xがyより小さい値の場合マイナスの値を、等しい場合にゼロ(0)を、大きい値の場合にプラスの値を返します。

ちなみに、デリゲートとは委譲という意味で、関数ポインタのようにメソッドへの参照を使って参照先のメソッドを呼び出すことができます。サンプルコードで確認しましょう。

using System;
using System.Collections.Generic;

namespace Sample
{
  class Info {
    public string name;
    public int age;
    
    public Info(string name, int age) {
      this.name = name;
      this.age = age;
    }
  }
  
  class Sample
  {
    static void Main()
    {
      Info[] src = new Info[]{new Info("Taro", 40), 
                              new Info("Jiro", 30),
                              new Info("Saburo", 20)};
                              
      var list = new List<Info>();
      
      // listに要素を追加
      list.AddRange(src);
      
      // listをソート
      var c = new Comparison<Info>(Compare);
      list.Sort(c);
      
      foreach (Info i in list) {
        Console.WriteLine("{0}, {1}", i.name, i.age);
      }
      Console.ReadKey();
    }
    
    static int Compare(Info a, Info b) {
      return a.age - b.age;
    }
  }
}

実行結果:

Saburo, 20
Jiro, 30
Taro, 40

このサンプルコードでは、Comparisonデリゲートを使ってList.Sortメソッドの並び替えの条件を指定しています。まず、2つのメンバ変数を持つInfoクラスを定義しています。

また、CompareメソッドでInfoクラスのメンバ変数ageが昇順に並び替えられるように定義しています。

Compareメソッドを引数に指定したComparisonオブジェクトcを作成し、List.Sortメソッドの引数に指定することで、list内のInfo型の要素を並び替えています。

Compare、CompareToを使用する場合

先ほどはComparisonデリゲートを使って複数キーでもソートを定義できることをお伝えしました。

Comparisonデリゲートを使わずに、クラス内でソートを定義する方法もあります。その場合、IComparerインターフェースやIComparableインターフェースを実装する必要があります。

IComparerインターフェースを実装したクラスではCompareメソッドを実装することでソートを定義します。IComparableインターフェースを実装したクラスではCompareToメソッドを実装することでソートを定義します。

CompareToでカスタムソート

IComparableインターフェースを実装したクラスでCompareToメソッドを定義します。CompareToメソッドはobject型の引数を1つ指定する必要があります。

この引数からメンバ変数を呼び出し、IComparableインターフェースを実装したクラスのメンバ変数と比較して並び替えの順番をきめます。

IComparableインターフェースのCompareToメソッドもstringクラスなどのCompareToメソッドと同じように、マイナスの値か0(ゼロ)もしくはプラスの値を返します。サンプルコードで確認しましょう。

using System;

namespace Sample
{
  class Info : IComparable {
    public string name;
    public int age;
    
    public Info(string name, int age) {
      this.name = name;
      this.age = age;
    }
    
    public int CompareTo(object obj) {
      Info i = obj as Info;
      return this.name.CompareTo(i.name);
    }
  }
  
  class Sample
  {
    static void Main()
    {
      Info[] src = new Info[]{new Info("Taro", 40), 
                              new Info("Jiro", 30),
                              new Info("Saburo", 20)};
                              
      Array.Sort(src);
      
      foreach (Info i in src) {
        Console.WriteLine("{0}, {1}", i.name, i.age);
      }
      Console.ReadKey();
    }
  }
}

実行結果:

Jiro, 30
Saburo, 20
Taro, 40

このサンプルコードでは、IComparableインターフェースを実装したInfoクラスを定義しています。Infoクラス内ではCompareToメソッドを実装し、引数のオブジェクトのメンバ変数nameとInfoクラスのメンバ変数nameとを比較するように定義しています。

Infoクラスの配列srcをArray.Sortメソッドの引数に指定することで、ソートが実行されています。

Compareでカスタムソート

Comparerインターフェースを実装したクラスでCompareメソッドを定義します。Compareメソッドはobject型の引数を2つ指定する必要があります。

この2つの引数からそれぞれメンバ変数を呼び出し、比較して並び替えの順番を決めます。ComparerインターフェースのCompareメソッドもstringクラスなどのCompareToメソッドと同じように、マイナスの値か0(ゼロ)もしくはプラスの値を返します。

サンプルコードで確認しましょう。

using System;
using System.Collections;

namespace Sample
{
  class Info {
    public string name;
    public int age;
    
    public Info(string name, int age) {
      this.name = name;
      this.age = age;
    }
  }
  
  class MyClass : IComparer {
    public int Compare(object a, object b) {
      Info ia = a as Info;
      Info ib = b as Info;
      return ia.name.CompareTo(ib.name);
    }
  }
  
  class Sample
  {
    static void Main()
    {
      Info[] src = new Info[]{new Info("Taro", 40), 
                              new Info("Jiro", 30),
                              new Info("Saburo", 20)};
                              
      var mc = new MyClass();         
      Array.Sort(src, mc);
      
      foreach (Info i in src) {
        Console.WriteLine("{0}, {1}", i.name, i.age);
      }
      Console.ReadKey();
    }
  }
}

実行結果:

Jiro, 30
Saburo, 20
Taro, 40

このサンプルコードでは、IComparerインターフェースを実装したMyClassクラスを定義しています。MyClassクラス内ではCompareメソッドを実装し、引数のそれぞれのオブジェクトのメンバ変数nameを比較するように定義しています。

Infoクラスの配列srcとMyClassクラスのインスタンスをArray.Sortメソッドの引数に指定することで、ソートが実行されています。

IComparerインターフェースのCompareメソッドの場合2つの引数を指定できるので、Infoクラスとは別のクラスで定義することができます。

定義を後で変更することが予想される場合は、このようにソートを定義するメソッドを別のクラスで定義するようにしておいた方が変更するのが楽になります。

まとめ

ここでは、配列やList型のオブジェクトのソートについて説明しました。

ソートする方法として、Array.Sortメソッド、List.Sortメソッド、ラムダ式、LINQを使う方法などいくつかの方法がありました。それぞれ使い方が異なりますので、目的に合わせて使い分けるようにしましょう。

また、カスタマイズして複数キーでソートする方法についてもいくつかご紹介しました。使いこなすことができるように、この記事を何度も参考にして下さいね!

この記事を書いた人

熊本在住のフリープログラマ兼ライターです。C/C++/C#、Java、Python、HTML/CSS、PHPを使ってプログラミングをしています。専門は画像処理で最近は機械学習、ディープラーニングにはまっています。幅広くやってきた経験を活かしてポイントをわかりやすくお伝えしようと思います。
お問合せはこちらでも受け付けています。
info@sss-lab.com

目次