O (n) zamanda ve O (1) uzayında kopyaları bulma


121

Girdi: 0'dan n-1'e kadar elemanlar içeren n elemanlı bir dizi verildiğinde, bu sayılardan herhangi biri herhangi bir sayıda görünmektedir.

Amaç: Bu tekrar eden sayıları O (n) cinsinden bulmak ve yalnızca sabit bellek alanını kullanmak.

Örneğin, n 7 ve dizi {1, 2, 3, 1, 3, 0, 6} olsun, cevap 1 & 3 olmalıdır. Benzer soruları burada kontrol ettim ama cevaplarda bazı veri yapıları kullanıldı. HashSet vb. .

Aynı şey için verimli bir algoritma var mı?

Yanıtlar:


164

Benim bulduğum şey bu, ek işaret biti gerektirmiyor:

for i := 0 to n - 1
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 0 to n - 1
    if A[i] != i then 
        print A[i]
    end if
end for

İlk döngü diziye izin verir, böylece öğe xen az bir kez mevcutsa, o zaman bu girişlerden biri konumunda olacaktır A[x].

İlk bakışta O (n) gibi görünmeyebileceğine dikkat edin, ancak öyle - iç içe bir döngüye sahip olmasına rağmen, yine de O(N)zamanında çalışıyor. Bir takas yalnızca iböyle bir şey varsa gerçekleşir A[i] != ive her takas A[i] == i, daha önce doğru olmayan bir yerde en az bir öğeyi ayarlar . Bu, toplam takas sayısının (ve dolayısıyla whiledöngü gövdesinin toplam yürütme sayısının ) en fazla olduğu anlamına gelir.N-1 .

İkinci çevrim değerlerini yazdırır xolan A[x]eşit değildir x, eğer birinci döngü garantilediği - xen azından dizi bir kez bulunur, bu durumda biri olacaktır A[x], bu, bu değerleri basar, bu araçlar x, bu değildir olan dizi.

(Fikir bağlantısı ile oynayabilmeniz için)


10
@arasmussen: Evet. Yine de önce bozuk bir versiyon buldum. Problemin a[a[i]]kısıtlamaları çözüme bir miktar ipucu verir - her geçerli dizi değerinin aynı zamanda geçerli bir dizi indeksi olduğu ve O (1) uzay kısıtlaması swap()işlemin anahtar olduğuna dair ipuçları verir .
cafe

2
@caf: Lütfen kodunuzu {3,4,5,3,4} başarısız olduğu için dizi ile çalıştırın.
NirmalGeo

6
@NirmalGeo: Bu geçerli bir girdi değil, çünkü 5aralıkta değil 0..N-1( Nbu durumda 5).
cafe

2
@caf {1,2,3,1,3,0,0,0,0,6} için çıktı 3 1 0 0 0'dır veya her durumda tekrarın 2'den fazla olduğu durumlarda doğru mu o / p?
Terminal

3
Bu harika! Bu soruda, genellikle daha kısıtlı bir dizi varyant gördüm ve bu, bunu çözmenin en genel yoludur. Sadece değişen söz edeceğiz printetmek deyimi print iiçin bir çözüm haline döner bu stackoverflow.com/questions/5249985/... ve Qk ( "torba" varsayarak bir değiştirilebilir dizidir) stackoverflow.com/questions/3492302/... .
j_random_hacker

35

caf'in parlak cevabı , dizide k-1 kez görünen her sayıyı yazdırır. Bu yararlı bir davranış, ancak soru tartışmalı olarak her bir kopyasının yalnızca bir kez basılmasını gerektiriyor ve bunu doğrusal zaman / sabit uzay sınırlarını aşmadan yapma olasılığını ima ediyor. Bu, ikinci döngüsünü aşağıdaki sözde kodla değiştirerek yapılabilir:

for (i = 0; i < N; ++i) {
    if (A[i] != i && A[A[i]] == A[i]) {
        print A[i];
        A[A[i]] = i;
    }
}

Bu, ilk döngü çalıştıktan sonra herhangi bir değer mbirden fazla görünürse, bu görünümlerden birinin doğru konumda olması garantilenmesi özelliğinden yararlanır, yaniA[m] . Dikkatli olursak, herhangi bir kopyasının henüz yazdırılıp yazdırılmadığına ilişkin bilgileri depolamak için bu "ev" konumunu kullanabiliriz.

Caf'in versiyonunda, dizide A[i] != iilerlerken, A[i]bunun bir kopya olduğunu ima etti . Benim versiyonumda, biraz farklı bir değişmeze güveniyorum: bu A[i] != i && A[A[i]] == A[i], daha önce görmediğimizA[i] bir kopya olduğunu ima ediyor . ("Daha önce görmediğimiz" bölümünü bırakırsanız, geri kalanı caf değişmez gerçeği ve tüm kopyaların bir ev konumunda bir kopyasına sahip olma garantisi tarafından ima edilmiş olarak görülebilir.) Bu mülk, başlangıç ​​(caf'in 1. döngüsü bittikten sonra) ve aşağıda her adımdan sonra korunduğunu gösteriyorum.

Dizide ilerlerken A[i] != i, test kısmındaki başarı, daha önce görülmemiş bir kopya A[i] olabileceğini ima eder . Daha önce görmediysek, o zaman A[i]ev konumunun kendisine işaret etmesini bekleriz - ikinci yarısında test edilen şey budur.if durumun . Durum buysa, onu yazdırır ve ev konumunu bu ilk bulunan kopyaya işaret edecek şekilde değiştiririz ve 2 adımlı bir "döngü" oluştururuz.

Bu işlemin değişmezimizi değiştirmediğini görmek m = A[i]için, belirli bir pozisyonun itatmin edici olduğunu varsayalım A[i] != i && A[A[i]] == A[i]. Yaptığımız değişikliğin ( A[A[i]] = i), koşullarının m2. yarısının ifbaşarısız olmasına neden olarak , diğer ev dışı olayların kopya olarak çıkmasını önlemek için çalışacağı açıktır , ancak iev konumuna geldiğinde işe yarayacak mmı? Evet, çünkü şimdi, bu iyenide ifkoşulun 1. yarısının A[i] != idoğru olduğunu bulsak da, 2. yarı işaret ettiği konumun bir ev konumu olup olmadığını test eder ve olmadığını bulur. Bu durumda artık olmadığını bilmek mveya A[m]yinelenen bir değer, ama biz her iki şekilde biliyoruz,zaten rapor edilmiştirçünkü bu 2 döngü, caf'in 1. döngü sonucunda görünmeyeceği garantilidir. (O m != A[m]zaman tam olarak biri mve A[m]birden fazla meydana gelirse ve diğerinin hiç meydana gelmediğini unutmayın.)


1
Evet, benim bulduğuma çok benziyor. Özdeş bir ilk döngünün, sadece farklı bir baskı döngüsüyle birkaç farklı sorun için yararlı olması ilginçtir.
kafe

22

İşte sözde kod

for i <- 0 to n-1:
   if (A[abs(A[i])]) >= 0 :
       (A[abs(A[i])]) = -(A[abs(A[i])])
   else
      print i
end for

C ++ 'da örnek kod


3
Çok zekice - cevabı indekslenmiş girişin işaret bitinde kodlamak!
holtavolt

3
@sashang: Olamaz. Sorun spesifikasyonuna bakın. " 0'dan n-1'e kadar elemanlar içeren n elemanlı bir dizi verildiğinde "
Prasoon Saurav

5
Bu, yinelenen 0'ları algılamaz ve aynı sayıyı birden çok kez yinelenen olarak tespit eder.
Null Set

1
@Null Seti: Sadece yerini alabilir -ile ~sıfır sorun için.
user541686

26
Bu sorunun çözüldüğü cevap olabilir, ancak teknik olarak O(n)gizli alan kullanır - nişaret bitleri. Dizi, her öğenin yalnızca 0ve arasındaki değerleri tutabileceği şekilde tanımlanmışsa n-1, o zaman açıkça çalışmaz.
cafe

2

Nispeten küçük N için div / mod işlemlerini kullanabiliriz

n.times do |i|
  e = a[i]%n
  a[e] += n
end

n.times do |i| 
  count = a[i]/n
  puts i if count > 1
end

C / C ++ değil ama yine de

http://ideone.com/GRZPI


+1 Güzel çözüm. İki kez sonra bir girişe n eklemeyi durdurmak daha büyük n'yi barındıracaktır .
Apshir

1

Pek hoş değil ama en azından O (N) ve O (1) özelliklerini görmek kolay. Temel olarak diziyi tararız ve her sayı için karşılık gelen konumun bir kez görülmüş mü (N) yoksa birden çok kez görülmüş mü (N + 1) olarak işaretlenmiş olup olmadığını görürüz. Zaten bir kez görülmüş olarak işaretlenmişse, onu yazdırır ve birden çok kez görülmüş olarak işaretleriz. İşaretlenmemişse, onu bir kez görülmüş olarak işaretleriz ve ilgili dizinin orijinal değerini geçerli konuma taşırız (işaretleme yıkıcı bir işlemdir).

for (i=0; i<a.length; i++) {
  value = a[i];
  if (value >= N)
    continue;
  if (a[value] == N)  {
    a[value] = N+1; 
    print value;
  } else if (a[value] < N) {
    if (value > i)
      a[i--] = a[value];
    a[value] = N;
  }
}

veya daha iyisi (çift döngüye rağmen daha hızlı):

for (i=0; i<a.length; i++) {
  value = a[i];
  while (value < N) {
    if (a[value] == N)  {
      a[value] = N+1; 
      print value;
      value = N;
    } else if (a[value] < N) {
      newvalue = value > i ? a[value] : N;
      a[value] = N;
      value = newvalue;
    }
  }
}

+1, güzel çalışıyor, ancak tam olarak neden if (value > i) a[i--] = a[value];işe yaradığını anlamak biraz düşündü : eğer value <= io zaman değerinde zaten a[value]işledik ve güvenle üzerine yazabilirsek. Ayrıca O (N) doğasının apaçık olduğunu söylemem! Bunu hecelemek: Ana döngü Nzamanları çalıştırır , artı a[i--] = a[value];hat kaç kez çalışır. Bu satır, ancak a[value] < Nve her çalıştığında, hemen ardından, daha önceden Nayarlanmayan bir dizi değeri ile Nçalışabilir, böylece Ntoplamda en fazla 2Ndöngü yinelemesi için çoğu zaman çalışabilir .
j_random_hacker

1

C'de bir çözüm şudur:

#include <stdio.h>

int finddup(int *arr,int len)
{
    int i;
    printf("Duplicate Elements ::");
    for(i = 0; i < len; i++)
    {
        if(arr[abs(arr[i])] > 0)
          arr[abs(arr[i])] = -arr[abs(arr[i])];
        else if(arr[abs(arr[i])] == 0)
        {
             arr[abs(arr[i])] = - len ;
        }
        else
          printf("%d ", abs(arr[i]));
    }

}
int main()
{   
    int arr1[]={0,1,1,2,2,0,2,0,0,5};
    finddup(arr1,sizeof(arr1)/sizeof(arr1[0]));
    return 0;
}

O (n) zamanı ve O (1) uzay karmaşıklığıdır.


1
Bunun uzay karmaşıklığı O (N) 'dir, çünkü N ek işaret biti kullanır. Algoritma, dizi öğesi türünün yalnızca 0 ile N-1 arasındaki sayıları tutabileceği varsayımı altında çalışmalıdır .
cafe

evet bu doğru ama algo için mükemmel, çünkü sadece 0'dan n-1'e kadar olan sayılar için algo istiyorlar ve ayrıca çözümünüzün O (n) 'nin üzerine çıktığını kontrol ettim, bu yüzden şunu düşündüm
Anshul

1

Bu diziyi tek yönlü bir grafik veri yapısı olarak sunduğumuzu varsayalım - her sayı bir tepe noktasıdır ve dizideki dizini grafiğin bir kenarını oluşturan başka bir tepe noktasına işaret eder.

Daha da fazla basitlik için 0'dan n-1'e kadar indislerimiz ve 0..n-1 arasındaki sayı aralığımız var. Örneğin

   0  1  2  3  4 
 a[3, 2, 4, 3, 1]

0 (3) -> 3 (3) bir döngüdür.

Cevap: Sadece diziyi indislere dayanarak çaprazlayın. a [x] = a [y] ise bir döngüdür ve dolayısıyla kopyalanır. Bir sonraki dizine atlayın ve bir dizinin sonuna kadar tekrar tekrar devam edin. Karmaşıklık: O (n) zaman ve O (1) uzay.


0

Yukarıdaki caf yöntemini göstermek için küçük bir python kodu:

a = [3, 1, 1, 0, 4, 4, 6] 
n = len(a)
for i in range(0,n):
    if a[ a[i] ] != a[i]: a[a[i]], a[i] = a[i], a[a[i]]
for i in range(0,n):
    if a[i] != i: print( a[i] )

Takasın tek bir ideğer için birden fazla yapılması gerekebileceğini unutmayın - cevabımda şunu not edin while.
35'te kafe

0

Algoritma aşağıdaki C fonksiyonunda kolayca görülebilir. Şart olmamakla birlikte, orijinal dizi alma, her giriş modül alınarak mümkün olacaktır n .

void print_repeats(unsigned a[], unsigned n)
{
    unsigned i, _2n = 2*n;
    for(i = 0; i < n; ++i) if(a[a[i] % n] < _2n) a[a[i] % n] += n;
    for(i = 0; i < n; ++i) if(a[i] >= _2n) printf("%u ", i);
    putchar('\n');
}

Test için Ideone Link.


Korkarım bu teknik olarak "hile yapmaktır", çünkü 2 * n'ye kadar olan sayılarla çalışmak, orijinal sayıları depolamak için gerekenler yerine dizi girişi başına fazladan 1 bit depolama alanı gerektirir. Aslında, giriş başına log2 (3) = 1.58 ekstra bit değerine daha yakın olmanız gerekir, çünkü 3 * n-1'e kadar sayıları depoluyorsunuz.
j_random_hacker

0
static void findrepeat()
{
    int[] arr = new int[7] {0,2,1,0,0,4,4};

    for (int i = 0; i < arr.Length; i++)
    {
        if (i != arr[i])
        {
            if (arr[i] == arr[arr[i]])
            {
                Console.WriteLine(arr[i] + "!!!");
            }

            int t = arr[i];
            arr[i] = arr[arr[i]];
            arr[t] = t;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();

    for (int j = 0; j < arr.Length; j++)
    {
        if (j == arr[j])
        {
            arr[j] = 1;
        }
        else
        {
            arr[arr[j]]++;
            arr[j] = 0;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();
}

0

0 (n) zaman karmaşıklığı ve sabit fazladan boşlukta kopyaları bulmak için hızlı bir şekilde örnek bir oyun alanı uygulaması yarattım. Lütfen Yinelenenleri Bulma url'sini kontrol edin

IMP Above çözümü, bir dizi 0'dan n-1'e kadar öğeler içerdiğinde çalıştı ve bu sayılardan herhangi biri herhangi bir sayıda göründü.


0
private static void printRepeating(int arr[], int size) {
        int i = 0;
        int j = 1;
        while (i < (size - 1)) {
            if (arr[i] == arr[j]) {
                System.out.println(arr[i] + " repeated at index " + j);
                j = size;
            }
            j++;
            if (j >= (size - 1)) {
                i++;
                j = i + 1;
            }
        }

    }

Yukarıdaki çözüm, O (n) ve sabit uzayın zaman karmaşıklığında aynı sonuca ulaşacaktır.
user12704811

3
Kısa vadeli sınırlı yardım sağlayabilecek bu kod parçacığı için teşekkür ederiz. Uygun bir açıklama , bunun neden soruna iyi bir çözüm olduğunu göstererek uzun vadeli değerini büyük ölçüde artıracak ve diğer benzer sorularla gelecekteki okuyucular için daha yararlı hale getirecektir. Yaptığınız varsayımlar da dahil olmak üzere bazı açıklamalar eklemek için lütfen cevabınızı düzenleyin .
Toby Speight

3
BTW, burada zaman karmaşıklığı O (n²) gibi görünüyor - iç döngüyü gizlemek bunu değiştirmez.
Toby Speight

-2

Dizi çok büyük değilse bu çözüm daha basittir, aynı boyutta başka bir dizi oluşturur.

1 Giriş dizinizle aynı boyutta bir bitmap / dizi oluşturun

 int check_list[SIZE_OF_INPUT];
 for(n elements in checklist)
     check_list[i]=0;    //initialize to zero

2 giriş dizinizi tarayın ve yukarıdaki dizideki sayısını artırın

for(i=0;i<n;i++) // every element in input array
{
  check_list[a[i]]++; //increment its count  
}  

3 Şimdi check_list dizisini tarayın ve kopyayı bir kez veya çoğaltıldıklarında yazdırın

for(i=0;i<n;i++)
{

    if(check_list[i]>1) // appeared as duplicate
    {
        printf(" ",i);  
    }
}

Elbette yukarıda verilen çözüm tarafından tüketilen alanın iki katını alır, ancak zaman verimliliği temelde O (n) olan O (2n) 'dir.


Bu O(1)uzay değil .
Daniel Kamil Kozar

oops ...! bunu fark etmedim ... benim hatam.
Deepthought

@nikhil nasıl O (1) ?. Dizim kontrol_ listem, girdi boyutu büyüdükçe doğrusal olarak büyür, öyleyse nasıl O (1) eğer öyleyse, onu O (1) olarak adlandırmak için kullandığınız buluşsal yöntemler nelerdir?
Deepthought

Belirli bir girdi için sabit alana ihtiyacınız var, bu O (1) değil mi? Yanılıyor olabilirim :)
nikhil

Girdi büyüdükçe çözümümün daha fazla alana ihtiyacı var. Belirli bir girdi için ölçülmeyen bir algoritmanın verimliliği (uzay / zaman). (Böyle bir durumda her arama algoritmasının zaman verimliliği sabit olacaktır, yani aradığımız 1. indekste bulunan eleman) Herhangi bir girdi için ölçülür, yani en iyi duruma, en kötü duruma ve ortalama duruma sahip olmamızın nedeni.
Deepthought
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.