Tìm số nguyên nhỏ nhất không có trong danh sách


87

Một câu hỏi phỏng vấn thú vị mà một đồng nghiệp của tôi sử dụng:

Giả sử rằng bạn được cung cấp một danh sách rất dài, không được sắp xếp gồm các số nguyên 64 bit không dấu. Làm thế nào bạn có thể tìm số nguyên không âm nhỏ nhất không xuất hiện trong danh sách?

SAU: Bây giờ giải pháp rõ ràng bằng cách sắp xếp đã được đề xuất, bạn có thể thực hiện nó nhanh hơn O (n log n) không?

THEO DÕI: Thuật toán của bạn phải chạy trên máy tính có bộ nhớ 1GB

XÁC NHẬN: Danh sách nằm trong RAM, mặc dù nó có thể tiêu thụ một lượng lớn RAM. Bạn được cung cấp trước kích thước của danh sách, chẳng hạn như N.


6
Tôi nghĩ bạn có thể bỏ đi phần không âm, xem cách bạn đang nói về một số nguyên không dấu.
KevenDenen

4
Câu hỏi này khá cơ bản, trừ khi tôi không có cơ sở, IMO, nhưng, như những người khác đã đề cập, có những câu hỏi cần đặt ra hoặc những giả định cần được nêu ra.
James Black

8
@paxdiablo: Đây là trường hợp nói O (n) không có nhiều ý nghĩa. Ngay cả khi bạn lưu trữ mảng 2 ^ 64 bit của mình trên các viên đất sét trên Đảo Phục Sinh và truy cập nó bằng chim bồ câu mang, thuật toán vẫn là O (n).
IJ Kennedy

6
Thay đổi yêu cầu bộ nhớ nửa chừng làm cho này một câu hỏi phỏng vấn lớn ;-)
Chris Ballance

1
Tôi nghĩ thật thú vị khi tất cả các câu trả lời đều có cùng một giải pháp chung (sắp xếp mảng và tìm giá trị đầu tiên phá vỡ chuỗi), nhưng tất cả chúng đều sử dụng một cách sắp xếp khác nhau. (Quicksort Modified, sắp xếp cơ số, ...) Câu trả lời được chấp nhận là tương đương với một đếm loại mà vứt bỏ các yếu tố trên N.
Joren

Câu trả lời:


121

Nếu cơ cấu dữ liệu có thể được thay đổi tại chỗ và hỗ trợ truy cập ngẫu nhiên thì bạn có thể thực hiện trong thời gian O (N) và O (1) không gian bổ sung. Chỉ cần đi qua mảng một cách tuần tự và với mọi chỉ mục, hãy ghi giá trị tại chỉ mục vào chỉ mục được chỉ định bởi giá trị, đặt một cách đệ quy bất kỳ giá trị nào tại vị trí đó vào vị trí của nó và loại bỏ các giá trị> N. Sau đó, quay lại mảng tìm kiếm vị trí trong đó giá trị không khớp với chỉ mục - đó là giá trị nhỏ nhất không có trong mảng. Điều này dẫn đến nhiều nhất là 3N so sánh và chỉ sử dụng một vài giá trị của không gian tạm thời.

# Pass 1, move every value to the position of its value
for cursor in range(N):
    target = array[cursor]
    while target < N and target != array[target]:
        new_target = array[target]
        array[target] = target
        target = new_target

# Pass 2, find first location where the index doesn't match the value
for cursor in range(N):
    if array[cursor] != cursor:
        return cursor
return N

9
Nitpick nhỏ. Bạn đã bỏ qua một trường hợp nhỏ: khi danh sách là {0, ..., N-1}. Trong trường hợp đó, truyền 1 không làm gì và trong 2 mảng [con trỏ] == con trỏ cho tất cả các mục trong danh sách, vì vậy thuật toán không trả về. Vì vậy, bạn cần một câu lệnh 'return N' ở cuối.
Alex

12
Giải pháp của bạn cấu hình miền và phạm vi (mục tiêu vừa là giá trị vừa là chỉ mục). Phạm vi bị giới hạn bởi bộ nhớ khả dụng ở 128M phần tử, nhưng miền có kích thước 2G. Nó sẽ không thành công với một mục nhập duy nhất có giá trị lớn hơn số mục nhập có thể được phân bổ vào mảng. Nếu câu hỏi không chỉ định 'rất dài', thì câu trả lời là thanh lịch, ngay cả khi nó hủy đầu vào. Sự đánh đổi không gian thời gian là rất rõ ràng trong vấn đề này và một giải pháp O (N) có thể không khả thi theo các ràng buộc được cung cấp.
Pekka

2
Lần vượt qua thứ hai có thể sử dụng tìm kiếm nhị phân thay vì tìm kiếm tuyến tính.
user448810

4
Giải pháp này chỉ hoạt động nếu phạm vi giá trị và chỉ số có thể so sánh được.
Dubby

7
Nó sẽ hoạt động tốt với các giá trị lớn hơn. Các giá trị lớn hơn có thể bị bỏ qua vì chúng không thể liên quan gì đến giá trị nhỏ nhất không có trong mảng. Đối với ví dụ của bạn, đường truyền đầu tiên sẽ lặp qua mảng bỏ qua tất cả các giá trị do target <N và sau đó sẽ trả về 0 trong lần lặp đầu tiên của lượt truyền thứ hai.
Kiến Aasma

89

Đây là một O(N)giải pháp đơn giản sử dụng O(N)không gian. Tôi giả định rằng chúng tôi đang giới hạn danh sách đầu vào ở các số không âm và chúng tôi muốn tìm số không âm đầu tiên không có trong danh sách.

  1. Tìm độ dài của danh sách; hãy nói rằng nó là N.
  2. Phân bổ một mảng Nboolean, được khởi tạo cho tất cả false.
  3. Đối với mỗi số Xtrong danh sách, nếu Xnhỏ hơn N, hãy đặt X'thphần tử của mảng thành true.
  4. Quét mảng bắt đầu từ chỉ mục 0, tìm kiếm phần tử đầu tiên false. Nếu bạn tìm thấy đầu tiên falseở chỉ mục I, thì đó Ilà câu trả lời. Nếu không (tức là khi tất cả các phần tử đều là true) thì câu trả lời là N.

Trong thực tế, "mảng Nboolean" có thể sẽ được mã hóa dưới dạng "bitmap" hoặc "bitet" được biểu diễn dưới dạng một bytehoặc intmảng. Điều này thường sử dụng ít dung lượng hơn (tùy thuộc vào ngôn ngữ lập trình) và cho phép quét lần đầu tiên falseđược thực hiện nhanh hơn.


Đây là cách / tại sao thuật toán hoạt động.

Giả sử rằng các Nsố trong danh sách không khác biệt, hoặc một hoặc nhiều trong số chúng lớn hơn N. Điều này có nghĩa là phải có ít nhất một số trong phạm vi 0 .. N - 1không có trong danh sách. Vì vậy bài toán tìm số bị thiếu nhỏ nhất do đó phải rút gọn thành bài toán tìm số bị thiếu nhỏ nhất nhỏ hơnN . Điều này có nghĩa là chúng ta không cần phải theo dõi những con số lớn hơn hoặc bằng N... bởi vì chúng sẽ không phải là câu trả lời.

Thay thế cho đoạn trước là danh sách là một hoán vị của các số từ 0 .. N - 1. Trong trường hợp này, bước 3 đặt tất cả các phần tử của mảng thành truevà bước 4 cho chúng ta biết rằng số "bị thiếu" đầu tiên là N.


Độ phức tạp tính toán của thuật toán là O(N)với một hằng số tương đối nhỏ. Nó làm cho hai tuyến tính đi qua danh sách hoặc chỉ một lần vượt qua nếu độ dài danh sách được biết là bắt đầu bằng. Không cần phải biểu diễn việc lưu giữ toàn bộ danh sách trong bộ nhớ, vì vậy việc sử dụng bộ nhớ tiệm cận của thuật toán chỉ là những gì cần thiết để biểu diễn mảng boolean; tức là O(N)các bit.

(Ngược lại, các thuật toán dựa vào sắp xếp hoặc phân vùng trong bộ nhớ giả định rằng bạn có thể đại diện cho toàn bộ danh sách trong bộ nhớ. Ở dạng câu hỏi đã được hỏi, điều này sẽ yêu cầu các từ O(N)64 bit.)


@Jorn nhận xét rằng từ bước 1 đến bước 3 là một biến thể về sắp xếp đếm. Theo một nghĩa nào đó thì anh ấy đúng, nhưng sự khác biệt rất đáng kể:

  • Sắp xếp đếm yêu cầu một mảng (ít nhất) Xmax - Xminbộ đếm trong đó Xmaxlà số lớn nhất trong danh sách và Xminlà số nhỏ nhất trong danh sách. Mỗi bộ đếm phải có thể đại diện cho N trạng thái; nghĩa là giả sử một biểu diễn nhị phân, nó phải có kiểu số nguyên (ít nhất) ceiling(log2(N))bit.
  • Để xác định kích thước mảng, một sắp xếp đếm cần thực hiện một lần chuyển đầu tiên qua danh sách để xác định XmaxXmin.
  • Do đó, yêu cầu không gian tối thiểu trong trường hợp xấu nhất là ceiling(log2(N)) * (Xmax - Xmin)bit.

Ngược lại, thuật toán được trình bày ở trên chỉ đơn giản yêu cầu Ncác bit trong trường hợp xấu nhất và tốt nhất.

Tuy nhiên, phân tích này dẫn đến trực giác rằng nếu thuật toán thực hiện lần đầu tiên đi qua danh sách để tìm số 0 (và đếm các phần tử trong danh sách nếu được yêu cầu), nó sẽ đưa ra câu trả lời nhanh hơn mà không sử dụng khoảng trắng nào nếu nó tìm thấy số 0. Điều này chắc chắn là đáng làm nếu khả năng cao là bạn tìm thấy ít nhất một số 0 trong danh sách. Và vượt qua này không thay đổi độ phức tạp tổng thể.


CHỈNH SỬA: Tôi đã thay đổi mô tả của thuật toán để sử dụng "mảng boolean" vì mọi người dường như thấy mô tả ban đầu của tôi sử dụng bit và bitmap là khó hiểu.


3
@ adi92 Nếu bước 3 cung cấp cho bạn một bitmap với tất cả các bit được đặt thành 1, thì danh sách chứa mọi giá trị từ 0 đến N-1. Điều đó có nghĩa là số nguyên không âm nhỏ nhất trong danh sách là N. Nếu có bất kỳ giá trị nào từ 0 đến N-1 KHÔNG nằm trong danh sách, thì bit tương ứng sẽ không được đặt. Giá trị nhỏ nhất như vậy là câu trả lời.
lặn biển

4
@ adi92 Trong ví dụ của bạn, danh sách sẽ chứa 300 phần tử. Điều đó có nghĩa là nếu có bất kỳ giá trị "bị thiếu" nào, nó phải nhỏ hơn 300. Chạy thuật toán, chúng tôi sẽ tạo một trường bit với 300 vị trí, sau đó lặp lại đặt các bit trong các vị trí 1, 2 và 3, để lại tất cả các khe khác - 0 và 4 đến 299 - rõ ràng. Khi quét trường bit, chúng tôi sẽ thấy cờ ở vị trí 0 rõ ràng, vì vậy chúng tôi biết 0 là câu trả lời.
lặn biển

4
Lưu ý rằng thuật toán này có thể được hiểu đơn giản hơn mà không có chút gì khúc mắc: "Tạo một mảng Boolean có kích thước N", v.v. Một khi bạn hiểu theo cách đó, việc chuyển sang phiên bản bitwise về mặt khái niệm rất dễ dàng.
Jon Skeet

2
Khi đưa ra một giải pháp trừu tượng, hãy sử dụng cách đơn giản nhất về mặt khái niệm mà hiệu quả và đừng quá chuyên môn hóa. Giải pháp của bạn yêu cầu sử dụng một mảng boolean (trừu tượng), vì vậy hãy gọi nó như vậy. Việc bạn có thể triển khai mảng này bằng bool[]hoặc bằng một bitmap không liên quan đến giải pháp chung.
Joren

2
Tôi nghĩ rằng giải pháp này có thể được mô tả tốt nhất bằng cách "Sử dụng sắp xếp đếm bỏ qua các phần tử trên N, sau đó tìm phần tử bị thiếu đầu tiên bằng cách thực hiện tìm kiếm tuyến tính ngay từ đầu."
Joren

13

Vì OP hiện đã chỉ định rằng danh sách ban đầu được lưu trữ trong RAM và máy tính chỉ có 1GB bộ nhớ, tôi sẽ đi ra ngoài và dự đoán rằng câu trả lời là 0.

1GB RAM có nghĩa là danh sách có thể có nhiều nhất 134.217.728 số trong đó. Nhưng có 2 64 = 18.446.744.073.709.551.616 số có thể. Vì vậy, xác suất để số 0 trong danh sách là 1 trong 137.438.953.472.

Ngược lại, tỷ lệ bị sét đánh của tôi trong năm nay là 1 trên 700.000. Và tỷ lệ tôi bị thiên thạch rơi vào khoảng 1 trên 10 nghìn tỷ. Vì vậy, khả năng tôi được viết trên tạp chí khoa học do cái chết không đúng lúc bởi một thiên thể cao hơn khoảng mười lần so với câu trả lời không phải là số không.


11
Tính toán của bạn chỉ đúng nếu các giá trị được phân phối đồng đều và được chọn ngẫu nhiên. Chúng cũng có thể được tạo tuần tự.
lặn biển

1
Tất nhiên là bạn đúng. Nhưng tôi là tất cả về tối ưu hóa cho trường hợp phổ biến. :)
Barry Brown

10
Vì vậy, khả năng người được phỏng vấn được chọn với câu trả lời này là bao nhiêu?
Amarghosh

6
Câu hỏi không cho biết các con số được chọn ngẫu nhiên đồng nhất. Họ được chọn bởi người đặt câu hỏi này. Vì điều này, xác suất từ 0 mệnh nào trong danh sách này là nhiều lớn hơn 1 trong 137.438.953.472, có lẽ thậm chí lớn hơn 1 trong 2. :-)
ShreevatsaR

8
@Amarghosh Câu trả lời cho câu hỏi đó cũng bằng không.
PeterAllenWebb

10

Như đã chỉ ra trong các câu trả lời khác, bạn có thể sắp xếp, và sau đó chỉ cần quét lên cho đến khi bạn tìm thấy khoảng trống.

Bạn có thể cải thiện độ phức tạp của thuật toán thành O (N) và giữ khoảng trống O (N) bằng cách sử dụng QuickSort đã sửa đổi, nơi bạn loại bỏ các phân vùng không phải là ứng cử viên tiềm năng để chứa khoảng trống.

  • Trong giai đoạn phân vùng đầu tiên, hãy loại bỏ các bản sao.
  • Sau khi phân vùng hoàn tất, hãy xem số lượng các mục trong phân vùng dưới
  • Giá trị này có bằng với giá trị được sử dụng để tạo phân vùng không?
    • Nếu vậy thì nó có nghĩa là khoảng trống nằm trong phân vùng cao hơn.
      • Tiếp tục với quicksort, bỏ qua phân vùng dưới
    • Nếu không thì khoảng trống nằm ở phân vùng dưới
      • Tiếp tục với quicksort, bỏ qua phân vùng cao hơn

Điều này tiết kiệm một số lượng lớn các tính toán.


Điều đó khá tiện lợi. Nó sẽ giả sử bạn có thể tính toán độ dài của phân vùng trong thời gian ít hơn thời gian tuyến tính, điều này có thể được thực hiện nếu nó được lưu trữ cùng với mảng phân vùng. Nó cũng giả định danh sách ban đầu được giữ trong RAM.
Barry Brown

2
Nếu bạn biết độ dài của danh sách, bạn cũng có thể chọn bất kỳ giá trị nào lớn hơn len (list). Theo nguyên tắc chuồng chim bồ câu, bất kỳ 'lỗ' nào phải nhỏ hơn len (danh sách).
lặn biển

1
Tôi không nghĩ đó là O (n) ... Thứ nhất, tôi không chắc bạn có thể xóa các bản sao cho đến khi danh sách được sắp xếp đầy đủ. Thứ hai, trong khi bạn có thể đảm bảo loại bỏ một nửa không gian tìm kiếm mỗi lần lặp (vì bạn đã chia thành dưới và trên điểm giữa), bạn vẫn có nhiều lần vượt qua (phụ thuộc vào n) đối với dữ liệu phụ thuộc vào n.
paxdiablo

1
paxdiablo: Bạn có thể xây dựng một danh sách mới chỉ có các giá trị duy nhất bằng cách sử dụng phương pháp bitmap như những gì Stephen C đã đề xuất. Điều này chạy trong O (n) thời gian và không gian. Tôi không chắc liệu nó có thể được thực hiện tốt hơn thế không.
Nic

9

Để minh họa một trong những cạm bẫy của O(N)tư duy, đây là một O(N)thuật toán sử dụng O(1)không gian.

for i in [0..2^64):
  if i not in list: return i

print "no 64-bit integers are missing"

1
Ý chí là đúng. Đây không phải là O (n) bởi vì bạn thực sự có hai vòng lặp ở đây, nhưng một vòng lặp là ẩn. Việc xác định xem một giá trị có trong danh sách hay không là một phép toán O (n) và bạn đang thực hiện điều đó n lần trong vòng lặp for của mình. Điều đó làm cho nó trở thành O (n ^ 2).
Nic

6
Nic, Will, đó là O (n * N) trong đó n là kích thước của danh sách và N là kích thước của miền (số nguyên 64 bit). Mặc dù N là một số rất lớn, nó vẫn là một hằng số nên về mặt hình thức, độ phức tạp của bài toán như đã nêu là O (n).
Ants Aasma

1
Kiến, tôi đồng ý rằng đó là O (n N), nhưng N không phải là hằng số. Vì thuật toán kết thúc khi nó tìm thấy câu trả lời, nên số lần lặp hoàn chỉnh qua vòng lặp ngoài bằng với câu trả lời, bản thân nó bị ràng buộc bởi kích thước của danh sách. Vì vậy, O (N n) là O (n ^ 2) trong trường hợp này.
Will Harris

12
Tìm một số trong danh sách có N phần tử rõ ràng là O (N). Chúng tôi làm điều này 2 ^ 64 lần. Trong khi lớn, 2 ^ 64 là một CONSTANT. Do đó thuật toán là C * O (N), vẫn là O (N).
IJ Kennedy

3
Tôi phải rút lại tuyên bố trước đây của mình; theo định nghĩa chặt chẽ nhất, phép toán này thực sự là O (n).
Nic

8

Vì tất cả các số đều dài 64 bit, chúng ta có thể sử dụng sắp xếp cơ số trên chúng, là O (n). Sắp xếp 'chúng, sau đó quét' chúng cho đến khi bạn tìm thấy những gì bạn đang tìm kiếm.

nếu số nhỏ nhất bằng 0, hãy quét về phía trước cho đến khi bạn tìm thấy khoảng trống. Nếu số nhỏ nhất không phải là số 0, thì câu trả lời là số không.


Đúng, nhưng các yêu cầu về bộ nhớ có thể khá cao đối với sắp xếp cơ số.
PeterAllenWebb

1
Sắp xếp Radix sẽ không hoạt động cho các tập dữ liệu rất lớn. Nhưng phân vùng và sắp xếp cơ số có thể hoạt động.
DarthVader

5

Đối với một phương pháp hiệu quả về không gian và tất cả các giá trị đều khác biệt, bạn có thể thực hiện theo không gian O( k )và thời gian O( k*log(N)*N ). Đó là không gian hiệu quả và không có dữ liệu di chuyển và tất cả các hoạt động là cơ bản (cộng trừ).

  1. bộ U = N; L=0
  2. Đầu tiên phân vùng không gian số trong kcác vùng. Như thế này:
    • 0->(1/k)*(U-L) + L, 0->(2/k)*(U-L) + L, 0->(3/k)*(U-L) + L...0->(U-L) + L
  3. Tìm xem có bao nhiêu số ( count{i}) trong mỗi vùng. ( N*kcác bước)
  4. Tìm vùng đầu tiên ( h) chưa đầy. Có nghĩa là count{h} < upper_limit{h}. ( kcác bước)
  5. nếu h - count{h-1} = 1bạn có câu trả lời của mình
  6. bộ U = count{h}; L = count{h-1}
  7. goto 2

điều này có thể được cải thiện bằng cách sử dụng băm (cảm ơn Nic vì ý tưởng này).

  1. tương tự
  2. Đầu tiên phân vùng không gian số trong kcác vùng. Như thế này:
    • L + (i/k)->L + (i+1/k)*(U-L)
  3. inc count{j} sử dụng j = (number - L)/k (if L < number < U)
  4. tìm vùng đầu tiên ( h) không có k phần tử trong đó
  5. nếu count{h} = 1h là câu trả lời của bạn
  6. bộ U = maximum value in region h L = minimum value in region h

Điều này sẽ chạy trong O(log(N)*N).


Tôi thực sự thích câu trả lời này. Nó hơi khó đọc, nhưng nó rất giống với những gì tôi có trong đầu khi đọc câu hỏi.
Nic

cũng tại một số điểm nó sẽ được thông minh để chuyển sang rằng giải pháp bitmap bởi Stephen C. lẽ khiU-L < k
Egon

Điều này không chạy trong O (log (N) * N) mà trong O (N). Câu trả lời của bạn là tổng quát của câu trả lời @cdiggins và nó chạy ở O (N) vì sum (1 / k ** i với tôi trong khoảng (ceil (log_k (n)))) <= 2
Lapinot

Trên mỗi lần lặp bạn đi qua các số O (N), cần tổng số lần lặp là O (log_k (N)). Do đó O (log_k (N) * N) == O (log (N) * N). Các số ban đầu không được sắp xếp / nhóm và bạn cần phải xem qua tất cả chúng.
Egon

Nhưng nếu bạn phân vùng danh sách ban đầu thành k vùng (kích thước n / k) thì bạn chọn vùng đầu tiên chưa đầy. Do đó, trong lần lặp tiếp theo, bạn chỉ cần xem xét vùng đã chọn và chia nó thành k vùng mới (có kích thước n / k ** 2), v.v. Thực tế bạn không lặp lại trên toàn bộ danh sách mỗi lần (khác điểm phân vùng là gì ?).
Lapinot

3

Tôi chỉ sắp xếp chúng sau đó chạy qua chuỗi cho đến khi tôi tìm thấy một khoảng trống (bao gồm khoảng cách ở đầu giữa số 0 và số đầu tiên).

Về mặt thuật toán, một cái gì đó như thế này sẽ làm được:

def smallest_not_in_list(list):
    sort(list)
    if list[0] != 0:
        return 0
    for i = 1 to list.last:
        if list[i] != list[i-1] + 1:
            return list[i-1] + 1
    if list[list.last] == 2^64 - 1:
        assert ("No gaps")
    return list[list.last] + 1

Tất nhiên, nếu bạn có nhiều bộ nhớ hơn CPU grunt, bạn có thể tạo bitmask của tất cả các giá trị 64 bit có thể có và chỉ cần đặt bit cho mọi số trong danh sách. Sau đó, tìm bit 0 đầu tiên trong bitmask đó. Điều đó biến nó thành một hoạt động O (n) về mặt thời gian nhưng khá tốn kém về yêu cầu bộ nhớ :-)

Tôi nghi ngờ bạn có thể cải thiện O (n) vì tôi không thể thấy một cách làm điều đó mà không liên quan đến việc xem từng số ít nhất một lần.

Thuật toán cho thuật toán đó sẽ dọc theo các dòng của:

def smallest_not_in_list(list):
    bitmask = mask_make(2^64) // might take a while :-)
    mask_clear_all (bitmask)
    for i = 1 to list.last:
        mask_set (bitmask, list[i])
    for i = 0 to 2^64 - 1:
        if mask_is_clear (bitmask, i):
            return i
    assert ("No gaps")

Từ mô tả, nó dường như loại trừ 0 đến phần tử đầu tiên, vì nó là phần tử nhỏ nhất không có trong danh sách. Nhưng, đó là một giả định mà tôi đã đưa ra, tôi có thể sai.
James Black

Suy nghĩ của tôi là nếu dãy được sắp xếp là 4,5,6, thì 0 sẽ là dãy nhỏ nhất không có trong danh sách.
paxdiablo

Tôi hy vọng rằng 2, 3, 5, câu trả lời phải là 4, nhưng, tôi có thể đã sai.
James Black

Một câu hỏi cần được trả lời bởi OP. Không gian tìm kiếm có phải là "tất cả các số nguyên 64-bit không dấu" hoặc "tất cả các số từ thấp nhất đến cao nhất trong danh sách" không?
paxdiablo

Tôi đồng ý rằng trong trường hợp xấu nhất, bạn phải xem ít nhất một lần, trừ khi nó đã được sắp xếp trong một cây nhị phân.
James Black

2

Sắp xếp danh sách, xem xét yếu tố đầu tiên và yếu tố thứ hai, và bắt đầu đi lên cho đến khi có một khoảng trống.


Phụ thuộc vào cách bạn xác định, Không có trong danh sách.
James Black

@PeterAllenWebb - Sẽ có, nhưng các số theo thứ tự ngẫu nhiên hay được sắp xếp?
James Black

1

Bạn có thể thực hiện trong O (n) thời gian và O (1) không gian bổ sung, mặc dù hệ số ẩn là khá lớn. Đây không phải là một cách thực tế để giải quyết vấn đề, nhưng nó có thể rất thú vị.

Đối với mỗi số nguyên 64-bit không dấu (theo thứ tự tăng dần), hãy lặp lại danh sách cho đến khi bạn tìm thấy số nguyên đích hoặc bạn đến cuối danh sách. Nếu bạn đến cuối danh sách, số nguyên đích là số nguyên nhỏ nhất không có trong danh sách. Nếu bạn đạt đến cuối số nguyên 64 bit, mọi số nguyên 64 bit đều có trong danh sách.

Đây là một hàm Python:

def smallest_missing_uint64(source_list):
    the_answer = None

    target = 0L
    while target < 2L**64:

        target_found = False
        for item in source_list:
            if item == target:
                target_found = True

        if not target_found and the_answer is None:
            the_answer = target

        target += 1L

    return the_answer

Hàm này cố tình không hiệu quả để giữ nó là O (n). Đặc biệt lưu ý rằng hàm liên tục kiểm tra các số nguyên đích ngay cả sau khi câu trả lời đã được tìm thấy. Nếu hàm trả về ngay sau khi câu trả lời được tìm thấy, số lần vòng lặp ngoài chạy sẽ bị ràng buộc bởi kích thước của câu trả lời, được ràng buộc bởi n. Thay đổi đó sẽ làm cho thời gian chạy O (n ^ 2), mặc dù nó sẽ nhanh hơn rất nhiều.


Thật. Thật là buồn cười khi một số thuật toán không gian O (1) và O (n) thời gian lại thất bại trong thực tế với câu hỏi này.
PeterAllenWebb

1

Cảm ơn egon, swilden và Stephen C đã truyền cảm hứng cho tôi. Trước tiên, chúng tôi biết các giới hạn của giá trị mục tiêu vì nó không thể lớn hơn kích thước của danh sách. Ngoài ra, danh sách 1GB có thể chứa tối đa 134217728 (128 * 2 ^ 20) số nguyên 64 bit.

Phần băm
Tôi đề xuất sử dụng phép băm để giảm đáng kể không gian tìm kiếm của chúng tôi. Đầu tiên, căn bậc hai kích thước của danh sách. Đối với danh sách 1GB, đó là N = 11,586. Thiết lập một mảng số nguyên có kích thước N. Lặp lại danh sách và lấy căn bậc hai * của mỗi số bạn tìm thấy làm hàm băm. Trong bảng băm của bạn, hãy tăng bộ đếm cho hàm băm đó. Tiếp theo, lặp lại bảng băm của bạn. Nhóm đầu tiên bạn thấy không bằng kích thước tối đa của nó xác định không gian tìm kiếm mới của bạn.

Phần bitmap
Bây giờ hãy thiết lập một bản đồ bit thông thường bằng với kích thước của không gian tìm kiếm mới của bạn và lặp lại qua danh sách nguồn, điền vào bitmap khi bạn tìm thấy từng số trong không gian tìm kiếm của mình. Khi bạn hoàn tất, bit chưa đặt đầu tiên trong bitmap của bạn sẽ cho bạn câu trả lời.

Điều này sẽ được hoàn thành trong thời gian O (n) và không gian O (sqrt (n)).

(* Bạn có thể sử dụng một cái gì đó như dịch chuyển bit để làm điều này hiệu quả hơn rất nhiều và chỉ cần thay đổi số lượng và kích thước của các nhóm cho phù hợp.)


1
Tôi thích ý tưởng chia không gian tìm kiếm thành các nhóm Root-N để giảm dung lượng bộ nhớ, nhưng các bản sao trong danh sách sẽ phá vỡ phương pháp này. Tôi tự hỏi nếu nó có thể được sửa chữa.
PeterAllenWebb

Bạn nói đúng, tôi đã sơ ý xem xét các mục trùng lặp. Tôi không chắc điều đó có thể được giải quyết.
Nic

1

Nếu chỉ có một số bị thiếu trong danh sách các số, thì cách dễ nhất để tìm số bị thiếu là tính tổng chuỗi và trừ từng giá trị trong danh sách. Giá trị cuối cùng là số còn thiếu.


Vâng. Đó là một câu hỏi phỏng vấn kinh điển khác.
PeterAllenWebb

1
Thậm chí dễ dàng hơn là XOR các số trong danh sách với nhau, XOR các số trong phạm vi với nhau và XOR kết quả với nhau.
John Kurlak

1
 int i = 0;
            while ( i < Array.Length)
            {

                if (Array[i] == i + 1)
                {
                    i++;
                }

                if (i < Array.Length)
                {
                    if (Array[i] <= Array.Length)
                    {//SWap

                        int temp = Array[i];
                        int AnoTemp = Array[temp - 1];
                        Array[temp - 1] = temp;
                        Array[i] = AnoTemp;

                    }
                    else
                       i++;



                }
            }

            for (int j = 0; j < Array.Length; j++)
            {
                if (Array[j] > Array.Length)
                {
                    Console.WriteLine(j + 1);
                    j = Array.Length;
                }
                else
                    if (j == Array.Length - 1)
                        Console.WriteLine("Not Found !!");

            }
        }

1

Chúng ta có thể sử dụng một bảng băm để chứa các số. Sau khi tất cả các số được thực hiện, chạy bộ đếm từ 0 cho đến khi chúng tôi tìm thấy số thấp nhất. Một hàm băm hợp lý sẽ băm và lưu trữ trong thời gian không đổi, và truy xuất trong thời gian không đổi.

for every i in X         // One scan Θ(1)
   hashtable.put(i, i);  // O(1)

low = 0;

while (hashtable.get(i) <> null)   // at most n+1 times
   low++;

print low;

Trường hợp xấu nhất nếu có ncác phần tử trong mảng, và {0, 1, ... n-1}trong trường hợp đó, câu trả lời sẽ nhận được tại n, vẫn giữ nguyên nó O(n).


1

Đây là câu trả lời của tôi được viết bằng Java:

Ý tưởng cơ bản: 1- Lặp lại mảng để loại bỏ các số dương, số không và số âm trùng lặp trong khi tính tổng các phần còn lại, nhận số dương tối đa cũng như giữ các số dương duy nhất trong một Bản đồ.

2- Tính tổng dưới dạng max * (max + 1) / 2.

3- Tìm sự khác biệt giữa các tổng được tính ở bước 1 & 2

4- Lặp lại từ 1 đến giá trị tối thiểu [tổng số chênh lệch, tối đa] và trả về số đầu tiên không có trong bản đồ được điền ở bước 1.

public static int solution(int[] A) {
    if (A == null || A.length == 0) {
        throw new IllegalArgumentException();
    }

    int sum = 0;
    Map<Integer, Boolean> uniqueNumbers = new HashMap<Integer, Boolean>();
    int max = A[0];
    for (int i = 0; i < A.length; i++) {
        if(A[i] < 0) {
            continue;
        }
        if(uniqueNumbers.get(A[i]) != null) {
            continue;
        }
        if (A[i] > max) {
            max = A[i];
        }
        uniqueNumbers.put(A[i], true);
        sum += A[i];
    }
    int completeSum = (max * (max + 1)) /  2;
    for(int j = 1; j <= Math.min((completeSum - sum), max); j++) {
        if(uniqueNumbers.get(j) == null) { //O(1)
            return j;
        }
    }
    //All negative case
    if(uniqueNumbers.isEmpty()) {
        return 1;
    }
    return 0;
}

0

Như Stephen C đã chỉ ra một cách thông minh, câu trả lời phải là một số nhỏ hơn độ dài của mảng. Sau đó tôi sẽ tìm câu trả lời bằng cách tìm kiếm nhị phân. Điều này tối ưu hóa trường hợp xấu nhất (vì vậy người phỏng vấn không thể bắt bạn trong tình huống bệnh lý 'điều gì xảy ra nếu'). Trong một cuộc phỏng vấn, hãy chỉ ra rằng bạn đang làm điều này để tối ưu hóa cho trường hợp xấu nhất.

Cách sử dụng tìm kiếm nhị phân là trừ số bạn đang tìm kiếm cho mỗi phần tử của mảng và kiểm tra kết quả âm.


0

Tôi thích thẩm định "đoán số không". Nếu các con số là ngẫu nhiên, rất có thể xảy ra số 0. Nếu "giám khảo" đặt danh sách không ngẫu nhiên, sau đó thêm một danh sách và đoán lại:

LowNum=0
i=0
do forever {
  if i == N then leave /* Processed entire array */
  if array[i] == LowNum {
     LowNum++
     i=0
     }
   else {
     i++
   }
}
display LowNum

Trường hợp xấu nhất là n * N với n = N, nhưng trong thực tế n rất có thể là một số nhỏ (ví dụ: 1)


0

Tôi không chắc nếu tôi nhận được câu hỏi. Nhưng nếu đối với danh sách 1,2,3,5,6 và số bị thiếu là 4, thì số bị thiếu có thể được tìm thấy trong O (n) bằng: (n + 2) (n + 1) / 2- (n + 1) n / 2

EDIT: xin lỗi, tôi đoán tôi đã suy nghĩ quá nhanh tối qua. Dù sao, Phần thứ hai thực sự nên được thay thế bằng sum (danh sách), đó là nơi xuất hiện của O (n). Công thức cho thấy ý tưởng đằng sau nó: đối với n số nguyên liên tiếp, tổng phải là (n + 1) * n / 2. Nếu thiếu một số, tổng sẽ bằng tổng (n + 1) số nguyên tuần tự trừ đi số bị thiếu.

Cảm ơn vì đã chỉ ra thực tế rằng tôi đã đặt một số mảnh giữa vào tâm trí của mình.


1
Thoạt nhìn tôi không thấy điều này sẽ hoạt động như thế nào. Trong trường hợp của bạn n = 5 và formulera sẽ được cố định, bất kể số nào trong nó bị thiếu.
sisve

Simon: bây giờ bạn có thể vui lòng loại bỏ phiếu bầu xuống theo bản chỉnh sửa của tôi không?
Codism

0

Làm tốt lắm Kiến Aasma! Tôi đã suy nghĩ về câu trả lời trong khoảng 15 phút và độc lập đưa ra câu trả lời theo cách suy nghĩ tương tự như của bạn:

#define SWAP(x,y) { numerictype_t tmp = x; x = y; y = tmp; }
int minNonNegativeNotInArr (numerictype_t * a, size_t n) {
    int m = n;
    for (int i = 0; i < m;) {
        if (a[i] >= m || a[i] < i || a[i] == a[a[i]]) {
            m--;
            SWAP (a[i], a[m]);
            continue;
        }
        if (a[i] > i) {
            SWAP (a[i], a[a[i]]);
            continue;
        }
        i++;
    }
    return m;
}

m đại diện cho "đầu ra tối đa hiện tại có thể có với những gì tôi biết về đầu vào thứ i đầu tiên và không giả định gì khác về các giá trị cho đến mục nhập tại m-1".

Giá trị này của m sẽ chỉ được trả về nếu (a [i], ..., a [m-1]) là một hoán vị của các giá trị (i, ..., m-1). Do đó nếu a [i]> = m hoặc nếu a [i] <i hoặc nếu a [i] == a [a [i]] chúng ta biết rằng m là đầu ra sai và phải thấp hơn ít nhất một phần tử. Vì vậy, giảm m và hoán đổi a [i] với a [m] chúng ta có thể đệ quy.

Nếu điều này không đúng nhưng a [i]> i thì khi biết rằng a [i]! = A [a [i]] chúng ta biết rằng hoán đổi [i] với [a [i]] sẽ làm tăng số lượng phần tử ở vị trí của riêng họ.

Nếu không thì a [i] phải bằng i, trong trường hợp đó chúng ta có thể tăng i khi biết rằng tất cả các giá trị lên đến và bao gồm chỉ số này đều bằng chỉ số của chúng.

Bằng chứng rằng điều này không thể đi vào một vòng lặp vô hạn được để lại như một bài tập cho người đọc. :)


0

Đoạn Dafny từ câu trả lời của Người Kiến cho thấy lý do tại sao thuật toán tại chỗ có thể thất bại. Điều kiện requirestrước mô tả rằng các giá trị của mỗi mục không được vượt quá giới hạn của mảng.

method AntsAasma(A: array<int>) returns (M: int)
  requires A != null && forall N :: 0 <= N < A.Length ==> 0 <= A[N] < A.Length;
  modifies A; 
{
  // Pass 1, move every value to the position of its value
  var N := A.Length;
  var cursor := 0;
  while (cursor < N)
  {
    var target := A[cursor];
    while (0 <= target < N && target != A[target])
    {
        var new_target := A[target];
        A[target] := target;
        target := new_target;
    }
    cursor := cursor + 1;
  }

  // Pass 2, find first location where the index doesn't match the value
  cursor := 0;
  while (cursor < N)
  {
    if (A[cursor] != cursor)
    {
      return cursor;
    }
    cursor := cursor + 1;
  }
  return N;
}

Dán mã vào trình xác thực có và không có forall ...điều khoản để xem lỗi xác minh. Lỗi thứ hai là do trình xác minh không thể thiết lập điều kiện kết thúc cho vòng lặp Vượt qua 1. Việc chứng minh điều này được giao cho người hiểu công cụ hơn.


0

Đây là một câu trả lời trong Java không sửa đổi đầu vào và sử dụng thời gian O (N) và N bit cộng với chi phí bộ nhớ không đổi nhỏ (trong đó N là kích thước của danh sách):

int smallestMissingValue(List<Integer> values) {
    BitSet bitset = new BitSet(values.size() + 1);
    for (int i : values) {
        if (i >= 0 && i <= values.size()) {
            bitset.set(i);
        }
    }
    return bitset.nextClearBit(0);
}

0
def solution(A):

index = 0
target = []
A = [x for x in A if x >=0]

if len(A) ==0:
    return 1

maxi = max(A)
if maxi <= len(A):
    maxi = len(A)

target = ['X' for x in range(maxi+1)]
for number in A:
    target[number]= number

count = 1
while count < maxi+1:
    if target[count] == 'X':
        return count
    count +=1
return target[count-1] + 1

Đã nhận 100% cho giải pháp trên.


0

1) Lọc âm và 0

2) Sắp xếp / phân biệt

3) Truy cập mảng

Độ phức tạp : O (N) hoặc O (N * log (N))

sử dụng Java8

public int solution(int[] A) {
            int result = 1;
    boolean found = false;
    A = Arrays.stream(A).filter(x -> x > 0).sorted().distinct().toArray();
    //System.out.println(Arrays.toString(A));
    for (int i = 0; i < A.length; i++) {
        result = i + 1;
        if (result != A[i]) {
            found = true;
            break;
        }
    }
    if (!found && result == A.length) {
        //result is larger than max element in array
        result++;
    }
    return result;
}

0

Một tập hợp không có thứ tự có thể được sử dụng để lưu trữ tất cả các số dương và sau đó chúng ta có thể lặp lại từ 1 đến độ dài của tập hợp không có thứ tự và xem số đầu tiên không xảy ra.

int firstMissingPositive(vector<int>& nums) {

    unordered_set<int> fre;
    // storing each positive number in a hash.
    for(int i = 0; i < nums.size(); i +=1)
    {
        if(nums[i] > 0)
            fre.insert(nums[i]);
     }

    int i = 1;
    // Iterating from 1 to size of the set and checking 
    // for the occurrence of 'i'

    for(auto it = fre.begin(); it != fre.end(); ++it)
    {
        if(fre.find(i) == fre.end())
            return i;
        i +=1;
    }

    return i;
}

0

Giải pháp thông qua javascript cơ bản

var a = [1, 3, 6, 4, 1, 2];

function findSmallest(a) {
var m = 0;
  for(i=1;i<=a.length;i++) {
    j=0;m=1;
    while(j < a.length) {
      if(i === a[j]) {
        m++;
      }
      j++;
    }
    if(m === 1) {
      return i;
    }
  }
}

console.log(findSmallest(a))

Hy vọng điều này sẽ giúp ích cho ai đó.


0

Với python, nó không phải là hiệu quả nhất, nhưng chính xác

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import datetime

# write your code in Python 3.6

def solution(A):
    MIN = 0
    MAX = 1000000
    possible_results = range(MIN, MAX)

    for i in possible_results:
        next_value = (i + 1)
        if next_value not in A:
            return next_value
    return 1

test_case_0 = [2, 2, 2]
test_case_1 = [1, 3, 44, 55, 6, 0, 3, 8]
test_case_2 = [-1, -22]
test_case_3 = [x for x in range(-10000, 10000)]
test_case_4 = [x for x in range(0, 100)] + [x for x in range(102, 200)]
test_case_5 = [4, 5, 6]
print("---")
a = datetime.datetime.now()
print(solution(test_case_0))
print(solution(test_case_1))
print(solution(test_case_2))
print(solution(test_case_3))
print(solution(test_case_4))
print(solution(test_case_5))

0
def solution(A):
    A.sort()
    j = 1
    for i, elem in enumerate(A):
        if j < elem:
            break
        elif j == elem:
            j += 1
            continue
        else:
            continue
    return j

0

điều này có thể giúp:

0- A is [5, 3, 2, 7];
1- Define B With Length = A.Length;                            (O(1))
2- initialize B Cells With 1;                                  (O(n))
3- For Each Item In A:
        if (B.Length <= item) then B[Item] = -1                (O(n))
4- The answer is smallest index in B such that B[index] != -1  (O(n))

Điều này có khác với câu trả lời của Stephen C không? Làm sao?
greybeard,
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.