Tìm các bản sao trong O (n) thời gian và O (1) không gian


121

Dữ liệu vào: Cho một mảng n phần tử chứa các phần tử từ 0 đến n-1, với bất kỳ số nào trong số này xuất hiện với số lần bất kỳ.

Mục tiêu: Tìm các số lặp lại này trong O (n) và chỉ sử dụng không gian bộ nhớ không đổi.

Ví dụ: đặt n là 7 và mảng là {1, 2, 3, 1, 3, 0, 6}, câu trả lời phải là 1 & 3. Tôi đã kiểm tra các câu hỏi tương tự ở đây nhưng câu trả lời sử dụng một số cấu trúc dữ liệu như HashSetv.v.

Bất kỳ thuật toán hiệu quả cho cùng một?

Câu trả lời:


164

Đây là những gì tôi đã nghĩ ra, không yêu cầu thêm bit dấu:

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

Vòng lặp đầu tiên hoán vị mảng để nếu phần tử xcó mặt ít nhất một lần, thì một trong các mục đó sẽ ở vị trí A[x].

Lưu ý rằng thoạt đầu nó có thể trông không đỏ mặt, nhưng nó - mặc dù nó có một vòng lặp lồng nhau, nó vẫn chạy O(N)đúng lúc. Một hoán đổi chỉ xảy ra nếu có một inhư vậy A[i] != i, và mỗi hoán đổi bộ ít nhất một yếu tố như vậy A[i] == i, nơi đó là không đúng sự thật trước. Điều này có nghĩa là tổng số lần hoán đổi (và do đó tổng số lần thực thi của phần whilethân vòng lặp) là nhiều nhất N-1.

Vòng lặp thứ hai in các giá trị của xA[x]không bằng x- kể từ khi đảm bảo vòng lặp đầu tiên mà nếu xtồn tại ít nhất một lần trong mảng, một trong những trường hợp sẽ có mặt tại A[x], điều này có nghĩa rằng nó sẽ in những giá trị củax mà không phải là trong hiện tại mảng.

(Liên kết Ideone để bạn có thể chơi với nó)


10
@arasmussen: Vâng. Tuy nhiên, tôi đã nghĩ ra một phiên bản bị hỏng đầu tiên. Những điều trái ngược của vấn đề cung cấp một chút manh mối cho giải pháp - thực tế là mọi giá trị mảng hợp lệ cũng là một chỉ số mảng hợp lệ gợi ý a[a[i]]và ràng buộc không gian O (1) gợi ý swap()thao tác là khóa.
caf

2
@caf: Vui lòng chạy mã của bạn với mảng {3,4,5,3,4} nó không thành công.
NirmalGeo

6
@NirmalGeo: Đó không phải là đầu vào hợp lệ, vì 5không nằm trong phạm vi 0..N-1( Ntrong trường hợp này là 5).
caf

2
@caf đầu ra cho {1,2,3,1,3,0,0,0,0,6} là 3 1 0 0 0 hoặc trong bất kỳ trường hợp nào mà số lần lặp lại nhiều hơn 2. Có đúng o / p không?
Nhà ga

3
Thật đáng kinh ngạc! Tôi đã thấy một số biến thể cho câu hỏi này, thường bị hạn chế hơn và đây là cách chung nhất để giải quyết nó mà tôi đã thấy. Tôi sẽ chỉ đề cập đến việc thay đổi printcâu lệnh để print ibiến điều này thành một giải pháp cho stackoverflow.com/questions/5249985/… và (giả sử "bag" là một mảng có thể sửa đổi) Qk của stackoverflow.com/questions/3492302/… .
j_random_hacker

35

câu trả lời tuyệt vời của caf in ra mỗi số xuất hiện k lần trong mảng k-1 lần. Đó là hành vi hữu ích, nhưng câu hỏi được cho là yêu cầu mỗi bản sao chỉ được in một lần và anh ta ám chỉ đến khả năng thực hiện điều này mà không vượt qua giới hạn thời gian / không gian tuyến tính. Điều này có thể được thực hiện bằng cách thay thế vòng lặp thứ hai của anh ta bằng mã giả sau:

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

Điều này khai thác thuộc tính mà sau khi vòng lặp đầu tiên chạy, nếu bất kỳ giá trị nào mxuất hiện nhiều hơn một lần, thì một trong những lần xuất hiện đó được đảm bảo ở đúng vị trí, cụ thể làA[m] . Nếu cẩn thận, chúng ta có thể sử dụng vị trí "nhà riêng" đó để lưu trữ thông tin về việc có bản sao nào đã được in hay chưa.

Trong phiên bản của caf, khi chúng ta xem qua mảng, A[i] != ingụ ý rằng đó A[i]là một bản sao. Trong phiên bản của tôi, tôi dựa trên một bất biến hơi khác: A[i] != i && A[A[i]] == A[i]ngụ ý rằng đó A[i]là một bản sao mà chúng ta chưa từng thấy trước đây . (Nếu bạn bỏ phần "mà chúng tôi chưa từng thấy trước đây", phần còn lại có thể được thấy là ngụ ý bởi sự thật bất biến của caf và đảm bảo rằng tất cả các bản sao đều có một số bản sao ở vị trí nhà riêng.) Thuộc tính này có tại đầu tiên (sau khi kết thúc vòng lặp đầu tiên của caf) và tôi chỉ ra bên dưới rằng nó được duy trì sau mỗi bước.

Khi chúng ta đi qua mảng, thành công trong A[i] != imột phần của thử nghiệm ngụ ý rằng đó A[i] có thể là một bản sao chưa từng thấy trước đây. Nếu chúng tôi chưa từng nhìn thấy nó trước đây, thì chúng tôi hy vọng A[i]vị trí nhà của nó sẽ tự chỉ về chính nó - đó là những gì được kiểm tra trong nửa sau củaif điều kiện. Nếu đúng như vậy, chúng tôi sẽ in nó và thay đổi vị trí nhà để quay lại bản sao được tìm thấy đầu tiên này, tạo ra một "chu kỳ" gồm 2 bước.

Để thấy rằng thao tác này không làm thay đổi tính bất biến của chúng ta, hãy giả sử m = A[i]đối với một vị trí cụ thể ithỏa mãn A[i] != i && A[A[i]] == A[i]. Rõ ràng là thay đổi mà chúng tôi thực hiện ( A[A[i]] = i) sẽ hoạt động để ngăn các trường hợp không phải ở nhà khác xuất hiện mdưới dạng bản sao bằng cách khiến nửa sau của các ifđiều kiện của chúng không thành công, nhưng liệu nó có hoạt động khi iđến vị trí chính mkhông? Đúng vậy, bởi vì bây giờ, mặc dù ở thời điểm mới này, ichúng tôi thấy rằng nửa đầu của ifđiều kiện A[i] != i, là đúng, nhưng nửa sau sẽ kiểm tra xem vị trí mà nó trỏ đến có phải là vị trí nhà hay không và nhận thấy rằng nó không phải. Trong tình huống này, chúng ta không còn biết liệu mhoặcA[m] là giá trị trùng lặp, nhưng chúng ta biết rằng một trong hai cách,nó đã được báo cáo , bởi vì 2 chu kỳ này được đảm bảo không xuất hiện trong kết quả của vòng lặp đầu tiên của caf. (Lưu ý rằng nếu m != A[m]sau đó chính xác một trong số mA[m]xảy ra nhiều lần, và trường hợp kia hoàn toàn không xảy ra.)


1
Vâng, điều đó rất giống với điều mà tôi đã nghĩ ra. Thật thú vị khi một vòng lặp đầu tiên giống hệt nhau lại hữu ích cho một số vấn đề khác nhau, chỉ với một vòng lặp in khác.
caf

22

Đây là mã giả

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

Mã mẫu trong C ++


3
Rất thông minh - mã hóa câu trả lời trong bit dấu của mục nhập được lập chỉ mục!
holtavolt

3
@sashang: Không được đâu. Kiểm tra đặc tả vấn đề. "Với một mảng n phần tử , trong đó có các yếu tố từ 0 đến n-1 "
Prasoon Saurav

5
Điều này sẽ không phát hiện các số 0 trùng lặp và sẽ phát hiện cùng một số giống như là một số trùng lặp nhiều lần.
Null Set

1
@Null Set: Bạn chỉ có thể thay thế -bằng ~cho vấn đề số không.
user541686

26
Đây có thể là câu trả lời cho vấn đề, nhưng về mặt kỹ thuật, nó sử dụng O(n)không gian ẩn - các nbit dấu hiệu. Nếu mảng được định nghĩa sao cho mỗi phần tử chỉ có thể giữ các giá trị giữa 0n-1, thì rõ ràng nó không hoạt động.
caf,

2

Đối với N tương đối nhỏ, chúng ta có thể sử dụng các phép toán div / mod

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

Không phải C / C ++ nhưng dù sao

http://ideone.com/GRZPI


+1 Giải pháp tốt. Việc ngừng thêm n vào một mục sau hai lần sẽ chứa n lớn hơn .
Apshir

1

Không thực sự đẹp nhưng ít nhất bạn cũng dễ dàng nhìn thấy thuộc tính O (N) và O (1). Về cơ bản, chúng tôi quét mảng và đối với mỗi số, chúng tôi xem liệu vị trí tương ứng đã được gắn cờ là đã nhìn thấy một lần (N) hay đã được nhìn thấy nhiều lần (N + 1). Nếu nó được gắn cờ đã-xem-một lần, chúng tôi in nó và gắn cờ nó đã-xem-nhiều lần. Nếu nó không được gắn cờ, chúng tôi gắn cờ nó đã-nhìn thấy một lần và chúng tôi di chuyển giá trị ban đầu của chỉ mục tương ứng đến vị trí hiện tại (gắn cờ là một hoạt động phá hủy).

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

hoặc tốt hơn nữa (nhanh hơn, mặc dù có vòng lặp kép):

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, nó hoạt động tốt, nhưng phải mất một chút suy nghĩ để tìm ra chính xác lý do if (value > i) a[i--] = a[value];hoạt động: nếu value <= isau đó chúng tôi đã xử lý giá trị tại a[value]và có thể ghi đè lên nó một cách an toàn. Ngoài ra, tôi sẽ không nói bản chất O (N) là hiển nhiên! Viết chính tả: Vòng lặp chính chạy Nnhiều lần, cộng với tuy nhiên a[i--] = a[value];dòng chạy nhiều lần . Dòng đó chỉ có thể chạy nếu a[value] < Nvà mỗi lần nó chạy, ngay sau đó một giá trị mảng chưa được Nđặt thành N, vì vậy nó có thể chạy nhiều Nlần nhất , với tổng số 2Nlần lặp vòng lặp nhiều nhất .
j_random_hacker

1

Một giải pháp trong C là:

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

Đó là độ phức tạp thời gian O (n) và không gian O (1).


1
Độ phức tạp không gian của điều này là O (N), vì nó sử dụng N bit dấu bổ sung. Thuật toán sẽ hoạt động theo giả định rằng kiểu phần tử mảng chỉ có thể chứa các số từ 0 đến N-1.
caf

vâng, điều đó đúng nhưng đối với biệt danh được hỏi, nó hoàn hảo vì họ muốn biệt danh chỉ dành cho các số từ 0 đến n-1 và tôi cũng đã kiểm tra lời giải của bạn, nó vượt quá O (n) nên tôi nghĩ đến điều này
Anshul Garg

1

Giả sử rằng chúng ta trình bày mảng này dưới dạng cấu trúc dữ liệu đồ thị đơn hướng - mỗi số là một đỉnh và chỉ số của nó trong mảng hướng đến một đỉnh khác tạo thành một cạnh của đồ thị.

Để đơn giản hơn, chúng ta có các chỉ số từ 0 đến n-1 và phạm vi số từ 0..n-1. ví dụ

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

0 (3) -> 3 (3) là một chu kỳ.

Trả lời: Chỉ cần duyệt qua mảng dựa vào các chỉ số. nếu a [x] = a [y] thì đó là một chu kỳ và do đó sẽ trùng lặp. Bỏ qua chỉ mục tiếp theo và tiếp tục lặp lại và tiếp tục như vậy cho đến khi kết thúc mảng. Độ phức tạp: O (n) thời gian và O (1) không gian.


0

Một mã python nhỏ để chứng minh phương pháp của caf ở trên:

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

Lưu ý rằng hoán đổi có thể phải xảy ra nhiều lần cho một igiá trị - hãy lưu ý whilecâu trả lời của tôi.
caf

0

Có thể dễ dàng nhìn thấy thuật toán trong hàm C sau đây. Truy xuất mảng ban đầu, mặc dù không bắt buộc, sẽ có thể thực hiện với mỗi mục nhập modulo 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');
}

Liên kết Ideone để thử nghiệm.


Tôi e rằng điều này là "gian lận" về mặt kỹ thuật, vì làm việc với các số lên đến 2 * n yêu cầu thêm 1 bit không gian lưu trữ cho mỗi mục nhập mảng so với những gì cần thiết để lưu các số ban đầu. Trên thực tế, bạn cần gần log2 (3) = 1,58 bit bổ sung cho mỗi mục nhập, bởi vì bạn đang lưu trữ các số lên đến 3 * n-1.
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

Tôi đã nhanh chóng tạo một ứng dụng sân chơi mẫu để tìm kiếm các bản sao với độ phức tạp thời gian 0 (n) và không gian thừa không đổi. Vui lòng kiểm tra url Tìm bản sao

Giải pháp IMP ở trên hoạt động khi một mảng chứa các phần tử từ 0 đến n-1, với bất kỳ số nào trong số này xuất hiện bất kỳ số lần nào.


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

    }

Giải pháp trên sẽ đạt được như nhau về độ phức tạp thời gian của O (n) và không gian không đổi.
dùng12704811

3
Cảm ơn bạn vì đoạn mã này, đoạn mã này có thể cung cấp một số trợ giúp ngắn hạn có giới hạn. Một lời giải thích phù hợp sẽ cải thiện đáng kể giá trị lâu dài của nó bằng cách chỉ ra lý do tại sao đây là một giải pháp tốt cho vấn đề và sẽ làm cho nó hữu ích hơn cho những người đọc trong tương lai với những câu hỏi tương tự khác. Vui lòng chỉnh sửa câu trả lời của bạn để thêm một số giải thích, bao gồm cả những giả định bạn đã đưa ra.
Toby Speight

3
BTW, độ phức tạp thời gian dường như là O (n²) ở đây - ẩn vòng lặp bên trong không thay đổi điều đó.
Toby Speight

-2

Nếu mảng không quá lớn, giải pháp này đơn giản hơn, Nó tạo một mảng khác có cùng kích thước để đánh dấu.

1 Tạo một bitmap / mảng có cùng kích thước với mảng đầu vào của bạn

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

2 quét mảng đầu vào của bạn và tăng số lượng của nó trong mảng trên

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

3 Bây giờ, hãy quét mảng check_list và in bản sao một lần hoặc nhiều lần chúng đã được sao chép

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

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

Tất nhiên, nó tốn gấp đôi không gian tiêu thụ bởi giải pháp đưa ra ở trên, nhưng hiệu suất thời gian là O (2n) về cơ bản là O (n).


Đây không phải là không O(1)gian.
Daniel Kamil Kozar

Giáo sư ...! không nhận thấy rằng ... xấu của tôi.
Suy nghĩ sâu sắc

@nikhil nó thế nào rồi O (1) ?. Danh sách check_list mảng của tôi phát triển tuyến tính khi kích thước của đầu vào tăng lên, vậy nó như thế nào là O (1) nếu vậy heuristics bạn đang sử dụng để gọi nó là O (1).
Suy nghĩ sâu sắc

Đối với một đầu vào nhất định, bạn cần không gian không đổi, đó có phải là O (1) không? Tôi cũng có thể là sai :)
nikhil

Giải pháp của tôi cần nhiều không gian hơn khi đầu vào tăng lên. Hiệu quả (không gian / thời gian) của một thuật toán không được đo lường cho một đầu vào cụ thể. (Trong trường hợp như vậy, hiệu quả thời gian của mọi thuật toán tìm kiếm sẽ không đổi, tức là phần tử được tìm thấy trong chỉ mục đầu tiên nơi chúng tôi tìm kiếm). Nó được đo cho bất kỳ đầu vào nào, đó là lý do tại sao chúng tôi có trường hợp tốt nhất, trường hợp xấu nhất và trường hợp trung bình.
Suy nghĩ sâu sắc
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.