並べ替えられたJavaScript配列があり、配列にもう1つの項目を挿入して、結果の配列が並べ替えられたままになるようにします。簡単なクイックソートスタイルの挿入関数を確実に実装できます。
var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
array.splice(locationOf(element, array) + 1, 0, element);
return array;
}
function locationOf(element, array, start, end) {
start = start || 0;
end = end || array.length;
var pivot = parseInt(start + (end - start) / 2, 10);
if (end-start <= 1 || array[pivot] === element) return pivot;
if (array[pivot] < element) {
return locationOf(element, array, pivot, end);
} else {
return locationOf(element, array, start, pivot);
}
}
console.log(insert(element, array));
[警告]このコードには、配列の先頭に挿入しようとしたときにバグがあります。 insert(2, [3, 7 ,9]
)は、誤った[3、2、7、9]を生成します。
ただし、Array.sort関数の実装は、これをネイティブに実行する可能性があることに気付きました。
var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
array.Push(element);
array.sort(function(a, b) {
return a - b;
});
return array;
}
console.log(insert(element, array));
2番目よりも最初の実装を選択する正当な理由はありますか?
Edit:一般的な場合、O(log(n))挿入(最初の例)は一般的な並べ替えアルゴリズムよりも高速になりますが、特にJavaScriptの場合は必ずしもそうではありません。
単一のデータポイントとして、キックのために、Windows 7でChrome=
First Method:
~54 milliseconds
Second Method:
~57 seconds
したがって、少なくともこのセットアップでは、ネイティブメソッドはそれを補いません。これは、小さなデータセットにも当てはまり、1000の配列に100個の要素を挿入します。
First Method:
1 milliseconds
Second Method:
34 milliseconds
シンプル( デモ ):
function sortedIndex(array, value) {
var low = 0,
high = array.length;
while (low < high) {
var mid = (low + high) >>> 1;
if (array[mid] < value) low = mid + 1;
else high = mid;
}
return low;
}
非常に興味深い議論を交えた非常に優れた注目すべき質問です!また、数千のオブジェクトを持つ配列内の単一の要素をプッシュした後、Array.sort()
関数を使用していました。
複雑なオブジェクトがあるため、Array.sort()
のような比較関数が必要なため、目的のためにlocationOf
関数を拡張する必要がありました。
function locationOf(element, array, comparer, start, end) {
if (array.length === 0)
return -1;
start = start || 0;
end = end || array.length;
var pivot = (start + end) >> 1; // should be faster than dividing by 2
var c = comparer(element, array[pivot]);
if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;
switch (c) {
case -1: return locationOf(element, array, comparer, start, pivot);
case 0: return pivot;
case 1: return locationOf(element, array, comparer, pivot, end);
};
};
// sample for objects like {lastName: 'Miller', ...}
var patientCompare = function (a, b) {
if (a.lastName < b.lastName) return -1;
if (a.lastName > b.lastName) return 1;
return 0;
};
コードにバグがあります。次のようになります。
function locationOf(element, array, start, end) {
start = start || 0;
end = end || array.length;
var pivot = parseInt(start + (end - start) / 2, 10);
if (array[pivot] === element) return pivot;
if (end - start <= 1)
return array[pivot] > element ? pivot - 1 : pivot;
if (array[pivot] < element) {
return locationOf(element, array, pivot, end);
} else {
return locationOf(element, array, start, pivot);
}
}
この修正がないと、コードは配列の先頭に要素を挿入できなくなります。
挿入関数は、指定された配列がソートされていることを前提とし、通常は配列内のいくつかの要素を調べるだけで、新しい要素を挿入できる場所を直接検索します。
配列の一般的な並べ替え機能では、これらのショートカットを使用できません。明らかに、少なくとも配列内のすべての要素を検査して、それらが既に正しく順序付けされているかどうかを確認する必要があります。この事実だけでも、一般的なソートは挿入関数よりも遅くなります。
一般的なソートアルゴリズムは通常平均O(n⋅log(n))であり、実装によっては実際には配列が最悪の場合すでにソートされているため、O(n2)。代わりに、挿入位置を直接検索するのはO(log(n))の複雑さだけなので、常にずっと高速になります。
私はこれがすでに答えを持っている古い質問であることを知っています、そして、他の多くのまともな答えがあります。私はあなたがO(log n)で正しい挿入インデックスを検索することでこの問題を解決できることを提案するいくつかの答えがあります-できますが、その時間に挿入することはできません。スペース。
一番下の行:ソートされた配列に本当にO(log n)の挿入と削除が必要な場合は、配列ではなく別のデータ構造が必要です。 B-Tree を使用する必要があります。大規模なデータセットにBツリーを使用することで得られるパフォーマンスの向上は、ここで提供される改善のどれよりも小さくなります。
配列を使用する必要がある場合。挿入ソートに基づいて次のコードを提供します。これは機能します。if and only if配列はすでにソートされています。これは、すべての挿入後に再ソートする必要がある場合に役立ちます。
function addAndSort(arr, val) {
arr.Push(val);
for (i = arr.length - 1; i > 0 && arr[i] < arr[i-1]; i--) {
var tmp = arr[i];
arr[i] = arr[i-1];
arr[i-1] = tmp;
}
return arr;
}
O(n)で動作するはずです。これはあなたができる最善の方法だと思います。 jsが複数の割り当てをサポートしている場合、より良いでしょう。 ここで遊ぶ例:
これは速いかもしれません:
function addAndSort2(arr, val) {
arr.Push(val);
i = arr.length - 1;
item = arr[i];
while (i > 0 && item < arr[i-1]) {
arr[i] = arr[i-1];
i -= 1;
}
arr[i] = item;
return arr;
}
以下にいくつかの考えを示します。まず、コードの実行時間を本当に心配している場合は、組み込み関数を呼び出すとどうなるかを必ず確認してください。私はjavascriptで下からはわかりませんが、スプライス関数の簡単なグーグルは this を返しました。これは呼び出しごとにまったく新しい配列を作成していることを示しているようです!それが実際に重要かどうかはわかりませんが、確かに効率に関係しています。コメントでは、Bretonがすでにこれを指摘しているように見えますが、選択した配列操作関数には確実に当てはまります。
とにかく、実際に問題を解決します。
あなたがソートしたいことを読んだとき、私の最初の考えは 挿入ソート! を使用することです。 ソートされたリストまたはほぼソートされたリストで線形時間で実行されるため便利です。配列には1つの要素のみが順番に並んでいないため、ほぼ並べ替え済みと見なされます(サイズ2または3の配列、またはその時点ではc'monを除く)。さて、ソートの実装はそれほど悪くはありませんが、それはあなたが対処したくないかもしれない面倒です、そして、再び、私はjavascriptについてのことを知りません、そしてそれが簡単であるか困難であるかどうか。これにより、ルックアップ機能が不要になり、プッシュするだけです(Bretonの提案どおり)。
第二に、「クイックソート」ルックアップ関数は バイナリ検索 アルゴリズムのようです!これは非常に優れたアルゴリズムであり、直感的で高速ですが、キャッチが1つあります。正しく実装するのは難しいことです。私はあなたのものが正しいかどうかあえて言いません(もちろんそうであることを望みます!:))、しかしあなたがそれを使いたいなら用心してください。
とにかく、要約:挿入ソートで「プッシュ」を使用すると、線形時間で動作し(配列の残りがソートされていると仮定)、厄介なバイナリ検索アルゴリズムの要件を回避します。これが最善の方法であるかどうかはわかりません(配列の基本的な実装、多分、クレイジーな組み込み関数がそれをより良くするかもしれません)。 :)-アゴール。
少数のアイテムの場合、違いはごくわずかです。ただし、多くの項目を挿入する場合、または非常に大きな配列を操作する場合、挿入のたびに.sort()を呼び出すと、多大なオーバーヘッドが発生します。
最終的には、この正確な目的のためにかなり洗練されたバイナリ検索/挿入関数を作成することになったので、共有したいと思いました。再帰の代わりにwhile
ループを使用しているため、余分な関数呼び出しについて耳にすることはありません。したがって、パフォーマンスは最初に投稿されたメソッドのいずれよりもさらに良くなると思います。また、デフォルトでデフォルトのArray.sort()
コンパレーターをエミュレートしますが、必要に応じてカスタムコンパレーター関数を受け入れます。
function insertSorted(arr, item, comparator) {
if (comparator == null) {
// emulate the default Array.sort() comparator
comparator = function(a, b) {
if (typeof a !== 'string') a = String(a);
if (typeof b !== 'string') b = String(b);
return (a > b ? 1 : (a < b ? -1 : 0));
};
}
// get the index we need to insert the item at
var min = 0;
var max = arr.length;
var index = Math.floor((min + max) / 2);
while (max > min) {
if (comparator(item, arr[index]) < 0) {
max = index;
} else {
min = index + 1;
}
index = Math.floor((min + max) / 2);
}
// insert the item
arr.splice(index, 0, item);
};
他のライブラリを使用する場合は、lodashは sortedIndex および sortedLastIndex 関数を提供します。これらの関数は、while
ループの代わりに使用できます。 2つの潜在的な欠点は、1)パフォーマンスが私の方法ほど良くない(それがどれほど悪いかわからないと思った)と2)カスタムコンパレータ関数を受け入れず、比較する値を取得する方法のみ(デフォルトのコンパレータを使用して、私は仮定します)。
これを達成するための4つの異なるアルゴリズムの比較を次に示します。 https://jsperf.com/sorted-array-insert-comparison/1
アルゴリズム
ナイーブは常に恐ろしいです。配列サイズが小さい場合、他の3つの配列はそれほど違いはありませんが、配列が大きい場合、最後の2つの配列は単純な線形アプローチよりも優れています。
これはlodashを使用するバージョンです。
const _ = require('lodash');
sortedArr.splice(_.sortedIndex(sortedArr,valueToInsert) ,0,valueToInsert);
注:sortedIndexはバイナリ検索を行います。
function insertOrdered(array, elem) {
let _array = array;
let i = 0;
while ( i < array.length && array[i] < elem ) {i ++};
_array.splice(i, 0, elem);
return _array;
}
アイテムごとに再ソートしないでください。
挿入するアイテムが1つだけの場合、バイナリ検索を使用して挿入する場所を見つけることができます。次に、memcpyなどを使用して残りのアイテムを一括コピーし、挿入されたアイテム用のスペースを確保します。バイナリ検索はO(log n)であり、コピーはO(n)であり、合計でO(n + log n)になります。上記の方法を使用すると、すべての挿入後に再ソートを実行します。これはO(n log n)です。
それは重要ですか?ランダムにk個の要素を挿入するとします(k = 1000)。ソートされたリストは5000アイテムです。
Binary search + Move = k*(n + log n) = 1000*(5000 + 12) = 5,000,012 = ~5 million ops
Re-sort on each = k*(n log n) = ~60 million ops
挿入するk個のアイテムが常に到着する場合は、search + moveを実行する必要があります。ただし、ソート済みの配列に挿入するk個のアイテムのリストが提供されている場合(事前に)、さらに改善することができます。既にソートされたn配列とは別に、k個のアイテムをソートします。次に、スキャンソートを実行します。このソートでは、ソートされた両方のアレイを同時に下に移動し、一方を他方にマージします。 -ワンステップマージソート= k log k + n = 9965 + 5000 =〜15,000 ops
更新:質問について。First method = binary search+move = O(n + log n)
。 Second method = re-sort = O(n log n)
取得するタイミングを正確に説明します。