質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.49%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

アルゴリズム

アルゴリズムとは、定められた目的を達成するために、プログラムの理論的な動作を定義するものです。

検索

検索は、あるデータの集まりの中から 目的のデータを見つけ出すことです。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

Q&A

解決済

3回答

17820閲覧

ツリー構造(階層構造)のオブジェクトから要素を検索する方法

Peri

総合スコア12

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

アルゴリズム

アルゴリズムとは、定められた目的を達成するために、プログラムの理論的な動作を定義するものです。

検索

検索は、あるデータの集まりの中から 目的のデータを見つけ出すことです。

.NET Framework

.NET Framework は、Microsoft Windowsのオペレーティングシステムのために開発されたソフトウェア開発環境/実行環境です。多くのプログラミング言語をサポートしています。

0グッド

1クリップ

投稿2016/08/31 13:47

編集2016/08/31 14:06

やりたいこと

以下の様な、ツリー構造を想定したクラスが存在します。

c#

1public class Person { 2 public string PersonId { get; set;} 3 public string ParentId { get; set;} 4 public string PersonName { get; set;} 5 public List<Person> Children { get; set;} 6}

親となるPersonがあり、親は複数の子を持ち、子はさらに複数の子供を持ち…
という、よくある階層構造となるクラスです。

ツリー構造のオブジェクトの中からツリー構造を保ったまま要素の「検索」をしたいと思っていますが、スッキリとした方法が思い浮かびません。
(汚く効率の悪いロジックなら書けるのですが…)
このような場合の綺麗なロジックの書き方を教えてください。

※ 上に挙げたプロパティの他に必要なものがあれば、追加しても構いません

補足

このPersonクラスのオブジェクトは、最初は階層構造になっておらず単なる一次元のリストの状態です。
これを最終的に、PersonId(自分自身のID)ParentId(親要素のID)を頼りに階層構造となるように組み立てていきます。

検索について

検索を行う際は、以下のような仕組みを想定しています。

  • テキストボックスに入力されたキーワードを元に、PersonNameで検索(部分一致)をする
  • 検索の結果、ツリーの枝要素にヒットした場合は、その配下の枝・葉の要素はすべて表示する。ヒットした枝より上位の枝要素もすべて表示する。
  • 検索の結果、葉要素にヒットした場合は、それより上位の枝要素はすべて表示する
  • 葉要素が一つもヒットしなかった枝要素は画面に表示しない(葉のない枝は表示しない)

というものを考えています。
簡単に言えば、「検索結果に関係のあるものだけ表示をする」という、これまたよくありそうなツリー構造の検索の仕組みです。

検索の結果、「表示しない」と判断された要素はツリー構造のオブジェクトから削除されます。(または、ツリーを組み立てるときにそれを含まないようにします)
決して「非表示フラグを立てる」という方法ではありません。(これは仕様として絶対条件です)

例)
以下のようなツリーを例とします。
例えば「太郎」で検索をかけた場合、「田中太郎」にヒットします。
なので、配下の「田中二郎」「鈴木花子」も残ります。
また「田中太郎」より上の「山田一郎」も残ることになります。

「花子」で検索をかけた場合、ツリーに残るのは「山田一郎」「田中太郎」「鈴木花子」になります。

「一郎」で検索をかけた場合は、最上位の枝要素にヒットしているので、「すべて」の人物がツリーに残ったままになります。

同様に、「二郎」で検索をかけると、「鈴木花子」を除くすべての人物がツリーに残ります。

text

1山田一郎 ┓ 2____ ┣ 山田二郎 3____ ┗ 田中太郎 ┓ 4__________ ┣ 田中二郎 5__________ ┗ 鈴木花子 6

(図中のアンダーバーは、位置ずれを起こさないためのスペースの代わりです)

考えた方法

方法としては複数あるかと思います。

  • ツリー状に組み立てる前の一次元リストの状態で「検索」を行い、何かしらの処理をしてから組み立てる
  • 完成したツリーから、検索の結果不要となった要素を削除する
  • 検索の結果必要と判断された要素だけでツリーを構築する

ただし、どれも実現が難しく困っています。

それは、

  • 組み立てる前の一次元リストでは、直接ヒットしなかった要素が最終的に必要となるのかどうか組み立てるまで分からない
  • 木構造のリストから不要なものを削除するには非常に手間がかかる(foreachなどでループしながら要素を削除すると、かなり気を使わない要素数が減ったことでとエラーになる)
  • 親→子→孫→…の順に検索を行う必要があるため、この順で要素を作っていくことになるが、「孫を入れるための子オブジェクトを作ったが、孫が0人だったので子オブジェクトが不要になってしまった」として葉要素の無い枝オブジェクトが発生してしまう

といった問題となるからです。

さいごに

このように、「ツリーに対して検索をかけ、マッチした要素と関連するものだけ表示する」ように実装をする方法が分かりません。
どうにか綺麗に実装をする方法を教えてください。

なるべく外部のライブラリを使用したり(.NETに初めから含まれているものならOK)、巨大なツリー用のクラスを作ったり…ということは避けたいと思っています。
(前者は絶対に認められません。後者は簡易なものであれば良さそうですが。)

よろしくお願いします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

元のリストは壊れてよいようなので試しに書いてみました。
ただのバックトラックです。

Personは提示のまま。探索・ツリー再作成はSearchメソッドの20行くらい。あとは木を質問の形に初期化したり結果出したりしてるだけです。
Childrenを作り直してるので元のPersonは維持されません(結果のPersonだけ残る)
なにもマッチしないとき、nullで表示のところで落ちるかもしれません。

C#

1namespace tt46294 { 2 class Program { 3 public class Person { 4 public string PersonId { get; set; } 5 public string ParentId { get; set; } 6 public string PersonName { get; set; } 7 public List<Person> Children { get; set; } 8 } 9 10 11 static void Main(string[] args) { 12 13 var tree = null as Person; 14 var result = null as Person; 15 16 tree = InitTree(); 17 PrintPerson(Search(tree, "花子"), 0, "花子"); 18 Console.WriteLine(); 19 20 tree = InitTree(); 21 PrintPerson(Search(tree, "一郎"), 0, "一郎"); 22 Console.WriteLine(); 23 24 25 tree = InitTree(); 26 PrintPerson(Search(tree, "二郎"), 0, "二郎"); 27 Console.WriteLine(); 28 29 Console.ReadKey(); 30 } 31 32 33 private static void PrintPerson(Person p, int depth = 0, string word="") { 34 if (depth == 0) Console.WriteLine("Search Word: " + word); 35 Console.WriteLine(new String(' ', depth * 4) + p.PersonName); 36 foreach(var c in (p.Children??new List<Person>())) { 37 PrintPerson(c, depth + 1); 38 } 39 } 40 41 private static Person InitTree() { 42 var root = new Person { 43 PersonName = "山田一郎", 44 Children = new List<Person> { 45 new Person { 46 PersonName = "山田二郎" 47 }, 48 new Person { 49 PersonName = "田中太郎", 50 Children = new List<Person> { 51 new Person { 52 PersonName = "田中二郎" 53 }, 54 new Person { 55 PersonName = "鈴木花子" 56 } 57 } 58 } 59 } 60 61 }; 62 return root; 63 } 64 65 66 private static Person Search(Person current, string v) { 67 if (current.PersonName.Contains(v)) return current; // 自分がマッチしたら自分と配下はすべて必要 68 if (current.Children == null || current.Children.Count ==0) { 69 // 自分もマッチしないし、子もいないのでこのインスタンスはいらない 70 return null; 71 }else { 72 // 子を作り直すので自身をコピー。 73 var result = new Person(); 74 result.PersonName = current.PersonName; 75 result.PersonId = current.PersonId; 76 result.ParentId = current.ParentId; 77 result.Children = new List<Person>(); 78 // 子について探索 79 foreach( var c in current.Children) { 80 var childResult = Search(c, v); 81 // 子が返ってきたらそれは必要。 82 if (childResult != null) result.Children.Add(childResult); 83 } 84 if (result.Children.Count > 0) return result; // 子に必要なものがあった 85 return null; // 子も自分も不要 86 } 87 } 88 } 89}

結果:

Search Word: 花子 山田一郎 田中太郎 鈴木花子 Search Word: 一郎 山田一郎 山田二郎 田中太郎 田中二郎 鈴木花子 Search Word: 二郎 山田一郎 山田二郎 田中太郎 田中二郎

答えは書かない方針なんだけど、勢いで書いてしまった。

投稿2016/08/31 15:32

編集2016/08/31 15:35
flied_onion

総合スコア2604

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

こんにちは。

完成したツリーから、検索の結果不要となった要素を削除する

これが一番簡単と思います。
深さ優先探索(Depth First Search)で良いと思いますよ。

分岐点をスタックに積みながら、探索を進めるイメージです。
ルートAから子をたどる時、ルートをスタックに積みます。
その最初の子Bが葉ならば、スタック先頭の分岐点へ戻り、次の子Cをたどります。
次の子Cが更に子Dを持つなら、Cをスタックに積んでDを辿ります。Cの子全てを検索したらスタックの先頭要素をポップして破棄し、現在のスタック先頭要素の指す分岐へ戻るという感じですね。
意外にスマートに書けますよ。

上記のスタックの代わりにキューを使えば、幅優先探索(Width First Search)も実装できます。
幅優先探索はメモリを多量に消費するので必要ない限りは使わない方が良いです。

投稿2016/08/31 15:12

編集2016/08/31 15:13
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

このような場合はリストのまま検索しようとせず、Treeを最初に構築してから、Treeを深さ優先探索で走査するように検索していくと綺麗に実装できると思います。

それっぽいサンプルコードを書いてみました。質問にあるコードとPersonのプロパティ名が違うのはPersonクラスのプロパティにPersonと付いているのは冗長だからです。それとPersonにコンストラクタとコピーコンストラクタを追加しています。

csharp

1using System; 2using System.Collections.Generic; 3using System.Linq; 4 5public class SearchTree 6{ 7 public static void Main() 8 { 9 var persons = new List<Person>() 10 { 11 new Person("1", null, "山田一郎"), 12 new Person("2", "1", "山田二郎"), 13 new Person("3", "1", "田中太郎"), 14 new Person("4", "3", "田中二郎"), 15 new Person("5", "3", "鈴木花子"), 16 }; 17 var searcher = new PersonSearcher(persons); 18 var searched = searcher.Search("二郎"); 19 ShowPersons(searched, ""); 20 } 21 22 static void ShowPersons(List<Person> persons, string indent = "") 23 { 24 foreach (var person in persons) 25 { 26 Console.WriteLine(indent + "- " + person.Name); 27 if (person.Children != null) 28 { 29 ShowPersons(person.Children, indent + " "); 30 } 31 } 32 } 33} 34 35class PersonSearcher 36{ 37 // 親のいないPersonのリスト 38 private List<Person> Founders = new List<Person>(); 39 // Person.IdからPersonを参照するためのディクショナリ 40 private Dictionary<String, Person> PersonDictionary = new Dictionary<String, Person>(); 41 42 public PersonSearcher(List<Person> persons) 43 { 44 // PersonDictionaryとFoundersの構築 45 foreach (var person in persons) 46 { 47 if (person.ParentId == null) 48 { 49 Founders.Add(person); 50 } 51 PersonDictionary.Add(person.Id, person); 52 } 53 54 // Personのツリーを構築 55 foreach (var person in persons) 56 { 57 if (person.ParentId != null) 58 { 59 var parent = PersonDictionary[person.ParentId]; 60 if (parent.Children == null) 61 { 62 parent.Children = new List<Person>(); 63 } 64 parent.Children.Add(person); 65 } 66 } 67 } 68 69 public List<Person> Search(string query) 70 { 71 // 親のいないPersonから検索を始める 72 return SearchInternal(Founders, query); 73 } 74 75 private List<Person> SearchInternal(List<Person> persons, string query) 76 { 77 return persons.Select(person => 78 { 79 // 名前に含まれている場合はツリーをそのまま返す 80 if (person.Name.Contains(query)) 81 { 82 return person; 83 } 84 // 子供がいる場合はそれも検索する 85 if (person.Children != null) 86 { 87 var newChildren = SearchInternal(person.Children, query); 88 // 検索結果が0ではなければその検索結果を含んだPersonを新規に作る 89 if (newChildren.Count() > 0) 90 { 91 var newPerson = new Person(person); 92 newPerson.Children = newChildren; 93 return newPerson; 94 } 95 } 96 return null; 97 }).Where(person => person != null).ToList(); 98 } 99} 100 101class Person { 102 public string Id { get; set;} 103 public string ParentId { get; set;} 104 public string Name { get; set;} 105 public List<Person> Children { get; set;} 106 107 public Person(string id, string parentId, string name) 108 { 109 Id = id; 110 ParentId = parentId; 111 Name = name; 112 } 113 114 public Person(Person person) 115 { 116 Id = person.Id; 117 ParentId = person.ParentId; 118 Name = person.Name; 119 } 120}

何かコードについて分からないところがあったらコメントしてください。

投稿2016/08/31 15:08

編集2016/08/31 15:45
MakeNowJust

総合スコア545

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問