面接で聞いた。整数のリストが与えられた場合、与えられたリストにすべてのメンバーが含まれる最大の区間をどのように見つけることができますか?
例えば。リスト1、3、5、7、4、6、10が与えられた場合、答えは[3、7]になります。それは3と7の間のすべての要素を持っているからです。
私は答えようとしましたが、説得力がありませんでした。私が採用したアプローチは、最初にリストを並べ替えてから、最大の間隔でリストをチェックすることでした。しかし私はO(n)
でそうするように頼まれました。
私はハッシュと動的計画法に基づく解決策を知っています。 f(x)をハッシュ関数とします。秘訣はハッシュテーブルの値です。リストに含まれる最長の間隔で、xで開始または終了することを考慮してください。次にh [f(x)] = y、ここでyはその間隔のもう一方の端。その間隔の長さはabs(x --y)+1。アルゴリズムの説明により、その値を格納する理由が明確になります。
リストの上に移動します。 iを現在のインデックス、x:= list [i]-現在の番号。今
1。h [f(x)]が空でない場合は、以前に番号xに会いました。何もすることはありません、続けてください。
2。チェックh [f(x-1)]およびh [f(x + 1)]。
2.1。両方が空でない場合、それはすでに会ったことを意味しますx-1およびx + 1、そしていくつかの間隔を知っています[a..x-1]および[x + 1..b ]これはすでにリストで出会っています。 a= h [f(x-1)]およびb= h [f(x + 1) ]hの定義による。 xを取得すると、間隔全体が満たされたことを意味します[a、b]したがって、値を次のように更新します。h [f(a)]:=bおよびh [f( b)]:=a。
また、h [f(x)]をある値に設定します(たとえばx、答えに影響を与えない)、次回会うときにx リストでは、無視します。 xはすでに彼の仕事をしています。
2.2。 1つだけが設定されている場合、たとえばh [f(x-1)] =a、つまり、すでに一定の間隔に達していることを意味します[a。 x-1]、そして今ではxで拡張されています。更新はh [f(a)]:=xおよびh [f(x)]:=a。
2.3。いずれも設定されていない場合は、どちらにも会っていないことを意味しますx-1 =、またはx + 1、およびを含む最大の間隔xすでに会ったのはシングルです[x]それ自体。したがって、h [f(x)]:=x。
最後に、答えを得るには、リスト全体を渡して、maximumabs(x-h [f(x)])+1すべてのx。
並べ替えが望ましくない場合は、ハッシュマップと 素集合データ構造 の組み合わせを使用できます。
リスト内の要素ごとにノードを作成し、key =要素の値を使用してハッシュマップに挿入します。次に、ハッシュマップでvalue +1とvalue-1をクエリします。何かが見つかった場合は、現在のノードを隣接ノードが属するセットと組み合わせます。リストが終了すると、最大のセットが最大の間隔に対応します。
時間計算量はO(N *α(N))です。ここで、α(N)は逆アッカーマン関数です。
編集:実際には素集合はこの単純なタスクには強力すぎます。 GrigorGevorgyanによるソリューションはそれを使用しません。したがって、よりシンプルで効率的です。
スペースをトレードオフして、線形時間でこれを取得できます。
最初のステップはリスト内で直線的です。最後の値はAのサイズが線形であり、遠く離れた値がいくつかある場合は、リストに比べて大きくなる可能性があります。しかし、intを扱っているので、Aは有界です。
HashSet
を使用して非常に簡単なソリューションを作成しました。 contains
とremove
はO(1)操作であるため、ランダムなセット項目から新しい間隔を作成し、間隔を「拡張」するだけです。あなたはそのフルサイズを発見し、あなたが進むにつれてセットからアイテムを削除します。これがあなたがどんな間隔でも「繰り返す」ことを妨げるので、削除は重要です。
このように考えると役立つ場合があります。リストにはK個の間隔があり、そのサイズは合計でNになります。次に、間隔や項目を繰り返さずに、これらの間隔が何であるかを検出することがタスクです。これが、HashSetが仕事に最適な理由です。間隔を広げると、セットからアイテムを効率的に削除できます。次に、あなたがする必要があるのは、あなたが進むにつれて最大の間隔を追跡することです。
HashSet
に入れますi = interval.start-1
を定義しますi
が含まれている間に、セットからi
を削除し、i
とinterval.start
の両方をデクリメントします。interval.end
から展開します)Javaでの解決策は次のとおりです。
public class BiggestInterval {
static class Interval {
int start;
int end;
public Interval(int base) {
this(base,base);
}
public Interval(int start, int end) {
this.start = start;
this.end = end;
}
public int size() {
return 1 + end - start;
}
@Override
public String toString() {
return "[" + start + "," + end + "]";
}
}
/**
* @param args
*/
public static void main(String[] args) {
System.out.println(biggestInterval(Arrays.asList(1,3,5,7,4,6,10)));
}
public static Interval biggestInterval(List<Integer> list) {
HashSet<Integer> set = new HashSet<Integer>(list);
Interval largest = null;
while(set.size() > 0) {
Integer item = set.iterator().next();
set.remove(item);
Interval interval = new Interval(item);
while(set.remove(interval.start-1)) {
interval.start--;
}
while(set.remove(interval.end+1)) {
interval.end++;
}
if (largest == null || interval.size() > largest.size()) {
largest = interval;
}
}
return largest;
}
}
これがGrigorのソリューションに似たソリューションです。 2つの主な違いは、このソリューションが他のインデックスの代わりにシーケンシャルセットの長さを格納することと、これにより最後のハッシュセットの反復が不要になることです。
配列を反復処理します
隣接するセットエンドポイントを探して更新することにより、ハッシュマップを作成します。
Key-配列値
Value-キーがシーケンシャルセットのエンドポイントである場合、そのセットの長さを格納します。それ以外の場合は、物事を一度だけ検討するように、それを真実に保ちます。
現在のセットサイズが最も長い場合は、最長のセットサイズと最長のセット開始を更新します。
わかりやすくするためのJavaScriptの実装と、実際の動作を確認するための fiddle を次に示します。
var array = [1,3,5,7,4,6,10];
//Make a hash of the numbers - O(n) assuming O(1) insertion
var longestSetStart;
var longestSetSize = 0;
var objArray = {};
for(var i = 0; i < array.length; i++){
var num = array[i];
if(!objArray[num]){//Only consider numbers once
objArray[num] = 1;//Initialize to 1 item in the set by default
//Get the updated start and end of the current set
var currentSetStart = num;//Starting index of the current set
var currentSetEnd = num;//Ending index of the current set
//Get the updated start of the set
var leftSetSize = objArray[num - 1];
if(leftSetSize){
currentSetStart = num - leftSetSize;
}
//Get the updated end of the set
var rightSetSize = objArray[num + 1];
if(rightSetSize){
currentSetEnd = num + rightSetSize;
}
//Update the endpoints
var currentSetSize = currentSetEnd - currentSetStart + 1;
objArray[currentSetStart] = currentSetSize;
objArray[currentSetEnd] = currentSetSize;
//Update if longest set
if(currentSetSize > longestSetSize){
longestSetSize = currentSetSize;
longestSetStart = currentSetStart;
}
}
}
var longestSetEnd = longestSetStart + longestSetSize - 1;
これは、平均O(1)ハッシュテーブルで作成された辞書を考慮すると線形になります。
L = [1,3,5,7,4,6,10]
a_to_b = {}
b_to_a = {}
for i in L:
if i+1 in a_to_b and i-1 in b_to_a:
new_a = b_to_a[i-1]
new_b = a_to_b[i+1]
a_to_b[new_a] = new_b
b_to_a[new_b] = new_a
continue
if i+1 in a_to_b:
a_to_b[i] = a_to_b[i+1]
b_to_a[a_to_b[i]] = i
if i-1 in b_to_a:
b_to_a[i] = b_to_a[i-1]
a_to_b[b_to_a[i]] = i
if not (i+1 in a_to_b or i-1 in b_to_a):
a_to_b[i] = i
b_to_a[i] = i
max_a_b = max_a = max_b = 0
for a,b in a_to_b.iteritems():
if b-a > max_a_b:
max_a = a
max_b = b
max_a_b = b-a
print max_a, max_b
秘訣は、アイテムをリストではなくセットとして考えることです。セットを使用すると、item-1またはitem + 1が存在するかどうかを確認できるため、これにより、連続する範囲の開始または終了にあるアイテムを識別できます。これにより、線形時間と空間で問題を解決できます。
擬似コード:
C#コード:
static Tuple<int, int> FindLargestContiguousRange(this IEnumerable<int> items) {
var itemSet = new HashSet<int>(items);
// find contiguous ranges by identifying their starts and scanning for ends
var ranges = from item in itemSet
// is the item at the start of a contiguous range?
where !itemSet.Contains(item-1)
// find the end by scanning upward as long as we stay in the set
let end = Enumerable.Range(item, itemSet.Count)
.TakeWhile(itemSet.Contains)
.Last()
// represent the contiguous range as a Tuple
select Tuple.Create(item, end);
// return the widest contiguous range that was found
return ranges.MaxBy(e => e.Item2 - e.Item1);
}
注:MaxByは MoreLinq からのものです
テスト
小さなサニティチェック:
new[] {3,6,4,1,8,5}.FindLargestContiguousRange().Dump();
// prints (3, 6)
大きな連続リスト:
var zeroToTenMillion = Enumerable.Range(0, (int)Math.Pow(10, 7)+1);
zeroToTenMillion.FindLargestContiguousRange().Dump();
// prints (0, 10000000) after ~1 seconds
大きな断片化されたリスト:
var tenMillionEvens = Enumerable.Range(0, (int)Math.Pow(10, 7)).Select(e => e*2);
var evensWithAFewOdds = tenMillionEvens.Concat(new[] {501, 503, 505});
evensWithAFewOdds.FindLargestContiguousRange().Dump();
// prints (500, 506) after ~3 seconds
複雑さ
このアルゴリズムには、O(N)時間とO(N)スペースが必要です。ここで、Nはリスト内のアイテムの数です。ただし、セット操作は次のようになります。一定の時間。
セットが入力として指定された場合、アルゴリズムによって構築されるのではなく、O(1)スペースのみが必要になることに注意してください。
(一部のコメントでは、これは2次時間であると言われています。範囲の先頭にあるアイテムだけでなく、すべてのアイテムがスキャンをトリガーすると想定していました。アルゴリズムがそのように機能した場合、実際には2次になります。)