Sıralı bir sayı dizisine sayı eklemenin verimli yolu?


143

Sıralı bir JavaScript dizisi var ve ortaya çıkan dizi sıralanmış kalır diziye bir öğe daha eklemek istiyorum. Kesinlikle basit bir quicksort tarzı ekleme işlevi uygulayabilir:

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));

[UYARI] dizinin başına eklenmeye çalışılırken bu kodda bir hata vardır, örn. insert(2, [3, 7 ,9]) Yanlış [3, 2, 7, 9] üretir.

Ancak, Array.sort işlevinin uygulamalarının bunu benim için ve yerel olarak yapabileceğini fark ettim:

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));

İkinci uygulamayı ikinci uygulamadan seçmek için iyi bir neden var mı?

Düzenleme : Genel durumda, bir O (log (n)) ekleme (ilk örnekte uygulandığı gibi) genel bir sıralama algoritmasından daha hızlı olacağını unutmayın; ancak bu özellikle JavaScript için geçerli değildir. Bunu not et:

  • Birkaç ekleme algoritması için en iyi durum O (n) 'dir, bu hala O (log (n))' den hala önemli ölçüde farklıdır, ancak aşağıda belirtildiği gibi O (n log (n)) kadar kötü değildir. Kullanılan belirli sıralama algoritmasına gelir ( Javascript Array.sort uygulamasına bakın? )
  • JavaScript'teki sıralama yöntemi yerel bir işlevdir, bu nedenle potansiyel olarak büyük faydalar - büyük bir katsayılı O (log (n)), makul boyutlardaki veri setleri için O (n) 'den çok daha kötü olabilir.

İkinci uygulamada ek yeri kullanmak biraz boşa gider. Neden push kullanmıyorsunuz?
Breton

İyi bir nokta, sadece ilkinden kopyaladım.
Elliot Kroo

4
İçeren herhangi bir şey splice()(örneğin 1. örneğiniz) zaten O (n) 'dir. Tüm dizinin dahili olarak yeni bir kopyasını oluşturmasa bile, öğe 0 konumuna eklenecekse, potansiyel olarak tüm n öğeyi 1 konuma geri çevirmek zorunda olabilir. Belki de hızlıdır çünkü yerel bir işlevdir ve sabit düşük, ama yine de O (n).
j_random_hacker

6
ayrıca, bu kodu kullanan kişiler için ileride başvurmak üzere, dizinin başına ekleme yapmaya çalışırken kodda bir hata vardır. Düzeltilmiş kod için aşağıya bakın.
Pinokyo

3
Bunun yerine kullanımı parseIntkullanmayın Math.floor. Math.floordaha hızlıdır parseInt: jsperf.com/test-parseint-and-math-floor
Hubert Schölnast

Yanıtlar:


58

Tek bir veri noktası olarak, tekmeler için bunu, Windows 7'de Chrome'u kullanarak iki yöntemi kullanarak 100.000 önceden sıralanmış sayı dizisine 1000 rastgele öğe ekleyerek test ettim:

First Method:
~54 milliseconds
Second Method:
~57 seconds

Yani, en azından bu kurulumda, yerel yöntem bunu telafi etmez. Bu, 1000 veri dizisine 100 öğe ekleyerek küçük veri kümeleri için bile geçerlidir:

First Method:
1 milliseconds
Second Method:
34 milliseconds

1
arrays.sort oldukça korkunç geliyor
njzk2

2
Array.splice öğesinin, 54 mikrosaniye içinde tek bir öğe eklemek için gerçekten akıllı bir şey yapıyor olması gerekir.
gnasher729

@ gnasher729 - Javascript dizilerinin C'deki gibi fiziksel olarak sürekli dizilerle gerçekten aynı olduğunu düşünmüyorum. Ben JS motorlarının hızlı ekleme sağlayan bir karma harita / sözlük olarak uygulayabileceğini düşünüyorum.
Ian

1
ile bir karşılaştırma işlevi kullandığınızda Array.prototype.sort, JS işlevi çok denir çünkü C ++ avantajlarını kaybedersiniz.
aleclarson

İlk Yöntem, Chrome'un TimSort kullandığından şimdi nasıl karşılaştırılıyor ? Gönderen TimSort Wikipedia : "girdi zaten, [TimSort] doğrusal zamanda çalışır sıralanır oluşur iyi durumda,".
poshest

47

Basit ( Demo ):

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;
}

4
Güzel dokunuş. İki sayının orta değerini bulmak için bitsel operatörler kullandığımı hiç duymadım. Normalde 0,5 ile çarparım. Bu şekilde önemli bir performans artışı var mı?
Jackson

2
@Jackson x >>> 1, 1 pozisyona kadar ikili sağ kaymadır , bu da 2'ye etkili bir şekilde bölünür . Örneğin 11: 1011-> 101sonuçları 5'e
Qwerty

3
Zaten bu yolda @Qwerty @Web_Designer Varlığı, arasındaki farkı açıklayabilir >>> 1ve ( görülme burada ve orada ) >> 1?
yckart

4
>>>işaretsiz bir sağ kaymadır, oysa >>işaret uzar - hepsi negatif sayıların bellek içi temsiline kadar kaybolur; burada yüksek bit negatifse ayarlanır. Yani vardiya halinde 0b1000doğru 1 yerde >>elde edersiniz 0b1100yerine kullanırsanız, >>>elde edersiniz 0b0100. Cevapta verilen durumda gerçekten önemli olmasa da (kaydırılan sayı ne işaretli bir 32 bit pozitif tamsayının maksimum değerinden ne de negatiften daha büyük değildir), bu iki durumda doğru olanı kullanmak önemlidir (siz hangi durumda ele almanız gerektiğini seçmeniz gerekir).
asherkin

2
@asherkin - Bu doğru değil: "eğer 0b10001 yeri sağa kaydırırsanız >>alırsınız 0b1100". Hayır, anladın 0b0100. Farklı sağ kaydırma operatörlerinin sonucu, negatif sayılar ve 2 ^ 31'den büyük sayılar (yani, ilk bitte 1 olan sayılar) dışındaki tüm değerler için aynı olacaktır.
gilly3

29

Çok ilginç bir tartışma ile çok iyi ve dikkat çekici bir soru! Ayrıca Array.sort()binlerce nesne içeren bir dizide tek bir öğeyi ittikten sonra işlevi kullanıyordum .

locationOfKarmaşık nesneler ve bu nedenle aşağıdaki gibi bir karşılaştırma işlevine ihtiyaç duyduğum için işlevimi amacım için genişletmek zorunda kaldım Array.sort():

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;
};

7
Kayıt için, bu sürümün dizinin başına eklenmeye çalışılırken doğru çalıştığını belirtmek gerekir. (Orijinal sorudaki sürümde bir hata olduğu ve bu durumda doğru çalışmadığı için bahsetmeye değer.)
garyrob

3
Uygulamamın farklı olup olmadığından emin değilim, ancak return c == -1 ? pivot : pivot + 1;doğru dizini döndürmek için üçlüyü değiştirmem gerekiyordu . Aksi takdirde 1 uzunluğu olan bir dizi için işlev -1 veya 0
değerini

3
@James: Başlangıç ​​ve bitiş parametreleri yalnızca özyinelemeli çağrıda kullanılır ve inital çağrıda kullanılmaz. Bunlar dizinin dizin değerleri olduğundan tamsayı türünde olmalı ve özyinelemeli çağrıda bu örtük olarak verilmiştir.
kwrl

1
@TheRedPea: hayır, demek istediğimden >> 1daha hızlı (ya da daha yavaş değil)/ 2
kwrl 17:17

1
comparerİşlevin sonucu ile ilgili olası bir sorun görüyorum . Bu algoritmada karşılaştırılır +-1ancak keyfi değer <0/ olabilir >0. Karşılaştırma fonksiyonuna bakınız . Sorunlu kısım sadece switchifade değil, aynı zamanda doğrudur: if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;nerede cde karşılaştırılır -1.
eXavier

19

Kodunuzda bir hata var. Bunu şöyle olmalıdır:

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);
  }
}

Bu düzeltme olmadan kod dizinin başına asla bir öğe ekleyemez.


neden 0 ile int yapıyorsun? yani ne başlıyor || 0 mı?
Pinokyo

3
@Pinocchio: başla || 0, kısa bir eşdeğerdir: if (! Start) start = 0; - Bununla birlikte, "daha uzun" versiyon daha etkilidir, çünkü kendisine bir değişken atamaz.
SuperNova

11

Bunun zaten cevabı olan eski bir soru olduğunu biliyorum ve bir dizi başka iyi cevap var. O (log n) 'de doğru ekleme dizinini arayarak bu sorunu çözmenizi öneren bazı yanıtlar görüyorum - bunu yapabilirsiniz, ancak o zaman ekleyemezsiniz, çünkü dizi yapmak için kısmen kopyalanması gerekiyor Uzay.

Alt satır: Gerçekten O (log n) eklenmiş ve sıralı bir dizi siler, farklı bir veri yapısı gerekir - bir dizi değil. Bir B-Ağacı kullanmalısınız . Büyük bir veri kümesi için bir B-Ağacı kullanarak elde edeceğiniz performans kazanımları, burada sunulan iyileştirmelerden herhangi birini gölgeleyecektir.

Bir dizi kullanmanız gerekiyorsa. Ben sadece dizi zaten sıralanmış ise çalışır, ekleme sıralama dayalı aşağıdaki kodu sunuyoruz . Bu, her uçtan sonra başvurmanız gerektiğinde kullanışlıdır:

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 yapabileceğiniz en iyi şey olduğunu düşünüyorum O (n) 'de çalışması gerekir. Js çoklu atamayı destekleseydi daha iyi olurdu. İşte oynamak için bir örnek:

Güncelleme:

bu daha hızlı olabilir:

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;
}

Güncellenmiş JS Bin bağlantısı


JavaScript'te, önerdiğiniz ekleme sıralaması ikili arama ve ekleme yönteminden daha yavaş olacaktır, çünkü ekleme işlemi hızlı bir uygulamaya sahiptir.
trincot

javascript bir şekilde zaman karmaşıklığı yasalarını kıramazsa, şüpheliyim. İkili arama ve ekleme yönteminin ne kadar hızlı olduğuna dair runnable bir örneğiniz var mı?
domoarigato

İkinci yorumumu geri alıyorum ;-) Gerçekten de, bir B-ağacı çözümünün ekleme çözümünden daha iyi performans göstereceği bir dizi boyutu olacaktır.
trincot

9

Ekleme işleviniz verilen dizinin sıralandığını varsayar, genellikle dizideki öğelerin birkaçına bakarak yeni öğenin eklenebileceği konumu arar.

Bir dizinin genel sıralama işlevi bu kısayolları alamaz. Açıkçası, en azından dizideki tüm öğeleri doğru sıralanıp sıralanmadığını görmek için incelemelidir. Sadece bu gerçek genel sıralamayı ekleme fonksiyonundan daha yavaş yapar.

Genel bir sıralama algoritması genellikle ortalama O (n ⋅ log (n)) şeklindedir ve uygulamaya bağlı olarak, dizi zaten sıralanmışsa O (n 2 ) karmaşıklığına yol açan en kötü durum olabilir . Doğrudan yerleştirme pozisyonunu aramak sadece O (log (n)) karmaşıklığına sahiptir , bu yüzden her zaman çok daha hızlı olacaktır.


Bir diziye bir öğe eklemenin karmaşık bir O (n) değerine sahip olduğunu belirtmek gerekir, bu nedenle sonuç yaklaşık aynı olmalıdır.
NemPlayer

5

Az sayıda öğe için fark oldukça önemsizdir. Ancak, çok fazla öğe ekliyorsanız veya çok geniş bir dizi ile çalışıyorsanız, her ekleme işleminden sonra .sort () yönteminin çağrılması büyük miktarda ek yüke neden olur.

Bu amaç için oldukça kaygan bir ikili arama / ekleme fonksiyonu yazmayı bitirdim, bu yüzden paylaşacağımı düşündüm. whileÖzyineleme yerine bir döngü kullandığından, ekstra işlev çağrıları için kulak misafiri yoktur, bu nedenle performansın orijinal olarak gönderilen yöntemlerden herhangi birinden daha iyi olacağını düşünüyorum. Varsayılan varsayılan Array.sort()karşılaştırıcıyı öykünür , ancak istenirse özel bir karşılaştırıcı işlevini kabul eder.

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);
};

Diğer kitaplıkları kullanmaya açıksanız, lodash , döngü yerine kullanılabilen sortIndex ve sortLastIndex işlevlerini sağlar while. İki potansiyel dezavantajı 1) performans benim yöntemim kadar iyi değil (ne kadar kötü olduğundan emin değilim) ve 2) özel bir karşılaştırıcı işlevini kabul etmiyor, sadece karşılaştırılacak değeri elde etmek için bir yöntem (varsayılan karşılaştırıcıyı kullanarak, sanırım).


çağrı arr.splice()kesinlikle O (n) zaman karmaşıklığıdır.
domoarigato

4

İşte birkaç düşünce: İlk olarak, kodunuzun çalışma zamanı hakkında gerçekten endişe duyuyorsanız, yerleşik işlevleri çağırdığınızda ne olacağını bildiğinizden emin olun! Javascript aşağıdan bilmiyorum, ama splice işlevi hızlı bir google bu döndürdü , hangi her çağrı tamamen yeni bir dizi oluşturduğunu gösterir gibi görünüyor! Gerçekten önemli olup olmadığını bilmiyorum, ama kesinlikle verimlilikle ilgili. Görüyorum ki Breton, yorumlarda zaten bunu işaret etti, ancak kesinlikle seçtiğiniz dizi manipüle etme işlevi için geçerlidir.

Her neyse, aslında problemi çözmek üzerine.

Sıralamak istediğinizi okuduğumda, ilk düşüncem ekleme sıralama kullanmaktır ! . Sıralı veya neredeyse sıralı listelerde doğrusal zamanda çalıştığı için kullanışlıdır . Dizileriniz sıra dışı olarak yalnızca 1 öğeye sahip olacağından, neredeyse sıralanmış olarak sayılır (2 veya 3 boyutlu diziler ya da her neyse, ancak bu noktada, hadi). Şimdi, sıralamayı uygulamak çok kötü değil, ama uğraşmak istemeyeceğiniz bir güçlük, ve yine, javascript hakkında bir şey bilmiyorum ve kolay ya da zor olup olmayacağı ya da ne olursa olsun. Bu, arama işlevinize olan ihtiyacı ortadan kaldırır ve sadece itersiniz (Breton'un önerdiği gibi).

İkincisi, "quicksort-esque" arama fonksiyonu bir ikili arama algoritması gibi görünüyor ! Çok güzel bir algoritma, sezgisel ve hızlı, ancak tek bir yakalama ile: doğru bir şekilde uygulanması herkes için zordur. Seninkinin doğru olup olmadığını söylemeye cesaret edemem (umarım öyle!)), Ama kullanmak istiyorsan dikkatli ol.

Her neyse, özet: ekleme sıralaması ile "push" kullanmak doğrusal zamanda çalışır (dizinin geri kalanının sıralandığı varsayılarak) ve dağınık ikili arama algoritması gereksinimlerinden kaçının. Bunun en iyi yol olup olmadığını bilmiyorum (dizilerin altında yatan uygulama, belki çılgın bir yerleşik işlev daha iyi yapar, kim bilir), ama benim için makul görünüyor. :) - Agor.


1
+1 çünkü içerdiği her şey splice() her zaten O (n) 'dir. Tüm dizinin dahili olarak yeni bir kopyasını oluşturmasa bile, öğe 0 konumuna eklenecekse, potansiyel olarak tüm n öğeyi 1 konuma geri
çevirmek zorundadır.

Ekleme sıralaması da O (n) en iyi durum ve O (n ^ 2) en kötü durum (OP'nin kullanım durumu muhtemelen en iyi durum olsa da) olduğuna inanıyorum.
domoarigato

Biri OP ile konuţtuđu için. İlk paragraf, splice'ın kaputun altında nasıl çalıştığını bilmediği için gereksiz bir uyarı gibi geldi
Matt Zera

2

İşte bunu gerçekleştirmek için dört farklı algoritmanın karşılaştırması: https://jsperf.com/sorted-array-insert-comparison/1

Algoritmalar

Naif her zaman korkunçtur. Küçük dizi boyutları için görünüyor, diğer üçü çok fazla farklılık göstermiyor, ancak daha büyük diziler için son 2 basit doğrusal yaklaşımdan daha iyi performans gösteriyor.


Hızlı ekleme ve arama yapmak için tasarlanmış veri yapılarını neden test etmiyorsunuz? ex. listeleri ve BST'leri atla. stackoverflow.com/a/59870937/3163618
qwr

Native, Chrome'un TimSort kullandığını şimdi nasıl karşılaştırıyor ? Gönderen TimSort Wikipedia : "girdi zaten sıralanır oluşur iyi durumda, bu doğrusal zamanda çalışır".
poshest

2

İşte lodash kullanan bir sürüm.

const _ = require('lodash');
sortedArr.splice(_.sortedIndex(sortedArr,valueToInsert) ,0,valueToInsert);

not: sortIndex bir ikili arama yapar.


1

Aklıma gelen en iyi veri yapısı indeksli bir atlama listesidir , bağlantılı listelerin ekleme özelliklerini günlük süresi işlemlerini sağlayan bir hiyerarşi yapısına sahip tutan . Ortalama olarak, arama, ekleme ve rastgele erişim aramaları O (log n) zamanında yapılabilir.

Bir sipariş istatistik ağacı , bir sıralama işleviyle günlük zaman dizinini etkinleştirir.

Rastgele erişime ihtiyacınız yoksa, ancak O (log n) girmeniz ve anahtarları aramanız gerekiyorsa , dizi yapısını silebilir ve her türlü ikili arama ağacını kullanabilirsiniz .

array.splice()Ortalama O (n) zamanı olduğu için kullanılan cevapların hiçbiri etkili değildir. Google Chrome'daki array.splice () yönteminin zaman karmaşıklığı nedir?


Bu cevap nasılIs there a good reason to choose [splice into location found] over [push & sort]?
greybeard

1
@greybeard Başlığa cevap veriyor. alaycı olarak her iki seçim de etkili değildir.
qwr

Her iki seçenek de bir dizinin birçok öğesinin üzerine kopyalanmasını içeriyorsa etkili olamaz.
qwr

1

İşte benim işlevi, öğeyi bulmak için ikili arama kullanır ve sonra uygun şekilde ekler:

function binaryInsert(val, arr){
    let mid, 
    len=arr.length,
    start=0,
    end=len-1;
    while(start <= end){
        mid = Math.floor((end + start)/2);
        if(val <= arr[mid]){
            if(val >= arr[mid-1]){
                arr.splice(mid,0,val);
                break;
            }
            end = mid-1;
        }else{
            if(val <= arr[mid+1]){
                arr.splice(mid+1,0,val);
                break;
            }
            start = mid+1;
        }
    }
    return arr;
}

console.log(binaryInsert(16, [
    5,   6,  14,  19, 23, 44,
   35,  51,  86,  68, 63, 71,
   87, 117
 ]));


0

Her öğeden sonra yeniden sıralamayın, aşırıya kaçması ..

Eklenecek tek bir öğe varsa, ikili aramayı kullanarak eklenecek konumu bulabilirsiniz. Ardından, eklenen öğeye yer açmak için kalan öğeleri toplu kopyalamak için memcpy veya benzeri bir yöntem kullanın. İkili arama O (log n) 'dir ve kopya O (n)' dir ve O (n + log n) toplamını verir. Yukarıdaki yöntemleri kullanarak, her ekleme işleminden sonra O (n log n) olan bir yeniden sıralama yapıyorsunuz.

Önemli mi? Diyelim ki rastgele k elemanları ekliyorsunuz, burada k = 1000. Sıralanan liste 5000 öğedir.

  • 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

Eklenecek k öğeleri her zaman ulaşırsa, arama + taşıma işlemi yapmanız gerekir. Ancak, sıralı bir diziye önceden eklenecek k öğelerinin bir listesi verilirse, daha da iyisini yapabilirsiniz. K öğelerini, zaten sıralanmış n dizisinden ayrı olarak sıralayın. Ardından, her iki sıralı diziyi aynı anda aşağıya doğru hareket ettirerek birini diğerine birleştirdiğiniz bir tarama sıralaması yapın. - Tek Adımlı Birleştirme sıralaması = k günlüğü k + n = 9965 + 5000 = ~ 15.000 ops

Güncelleme: Sorunuzla ilgili.
First method = binary search+move = O(n + log n). Second method = re-sort = O(n log n)Aldığınız zamanlamaları tam olarak açıklar.


evet, ama hayır, sıralama algoritmanıza bağlı. Ters sırada bir kabarcık sıralaması kullanarak, son öğe sıralanmamışsa sıralamanız her zaman o (n) 'dir
njzk2

-1
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;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.