Tìm một lỗ trống trong một danh sách các số


14

Cách nhanh nhất để tìm số nguyên đầu tiên (nhỏ nhất) không tồn tại trong danh sách các số nguyên chưa được sắp xếp (và giá trị này lớn hơn giá trị nhỏ nhất của danh sách) là gì?

Cách tiếp cận nguyên thủy của tôi là sắp xếp chúng và bước qua danh sách, có cách nào tốt hơn không?


6
@Jodrell Tôi nghĩ rằng việc sắp xếp một sự tiến triển vô hạn sẽ khó khăn ;-)
maple_shaft

3
@maple_shaft đồng ý, có thể mất một lúc.
Jodrell

4
Làm thế nào để bạn xác định đầu tiên cho một danh sách chưa sắp xếp?
Jodrell

1
Tôi mới nhận ra điều này có lẽ thuộc về StackOverflow, vì nó không thực sự là một vấn đề khái niệm.
JasonTrue

2
@JasonTrue Từ FAQ, If you have a question about… •algorithm and data structure conceptsnó thuộc chủ đề IMHO.
maple_shaft

Câu trả lời:


29

Giả sử rằng bạn có nghĩa là "số nguyên" khi bạn nói "số", bạn có thể sử dụng một bitvector có kích thước 2 ^ n, trong đó n là số phần tử (giả sử phạm vi của bạn bao gồm các số nguyên từ 1 đến 256, sau đó bạn có thể sử dụng 256- bit, hoặc 32 byte, bitvector). Khi bạn gặp một số nguyên ở vị trí n trong phạm vi của bạn, hãy đặt bit thứ n.

Khi bạn hoàn thành việc liệt kê bộ sưu tập số nguyên, bạn lặp lại các bit trong bitvector của mình, tìm vị trí của bất kỳ bit nào được đặt 0. Bây giờ chúng khớp với vị trí n của số nguyên bị thiếu của bạn.

Đây là O (2 * N), do đó O (N) và có lẽ hiệu quả bộ nhớ hơn so với việc sắp xếp toàn bộ danh sách.


6
Vâng, như là một so sánh trực tiếp, nếu bạn có tất cả các số nguyên 32 bit không dấu dương nhưng 1, bạn có thể giải bài toán số nguyên bị thiếu trong khoảng nửa gigabyte bộ nhớ. Nếu bạn đã sắp xếp thay thế, bạn sẽ phải sử dụng hơn 8 gigabyte bộ nhớ. Và sắp xếp, ngoại trừ trong các trường hợp đặc biệt như thế này (danh sách của bạn được sắp xếp một khi bạn có bitvector) gần như luôn luôn là log n hoặc tệ hơn, vì vậy trừ trường hợp hằng số vượt quá mức độ phức tạp về chi phí, phương pháp tuyến tính sẽ thắng.
JasonTrue

1
Điều gì nếu bạn không biết phạm vi tiên nghiệm?
Blrfl

2
Nếu bạn có kiểu dữ liệu số nguyên, Blrfl, bạn chắc chắn biết phạm vi tối đa của phạm vi, ngay cả khi bạn không có đủ thông tin để thu hẹp hơn nữa. Nếu bạn tình cờ biết đó là một danh sách nhỏ, nhưng không biết kích thước chính xác, sắp xếp có thể là một giải pháp đơn giản hơn.
JasonTrue

1
Hoặc thực hiện một vòng lặp khác trước thông qua danh sách để tìm phần tử nhỏ nhất và lớn nhất. Sau đó, bạn có thể phân bổ một mảng có kích thước chính xác với giá trị nhỏ nhất làm phần bù cơ bản. Vẫn là O (N).
Bảo mật

1
@JPatrick: Không phải bài tập về nhà, kinh doanh, tôi đã tốt nghiệp CS năm trước :).
Fabian Zeindl

4

Nếu bạn sắp xếp toàn bộ danh sách trước, thì bạn đảm bảo thời gian chạy trong trường hợp xấu nhất. Ngoài ra, sự lựa chọn của bạn về thuật toán sắp xếp là rất quan trọng.

Đây là cách tôi tiếp cận vấn đề này:

  1. Sử dụng một loại heap , tập trung vào các yếu tố nhỏ nhất trong danh sách.
  2. Sau mỗi lần trao đổi, hãy xem bạn có khoảng cách không.
  3. Nếu bạn tìm thấy một khoảng trống, thì return: Bạn đã tìm thấy câu trả lời của mình.
  4. Nếu bạn không tìm thấy một khoảng trống, tiếp tục trao đổi.

Đây là một hình dung của một loại heap .


Một câu hỏi, làm thế nào để bạn xác định các yếu tố "nhỏ nhất" của danh sách?
Jodrell

4

Chỉ cần bí mật và "thông minh", trong trường hợp đặc biệt của mảng chỉ có một "lỗ", bạn có thể thử một giải pháp dựa trên XOR:

  • Xác định phạm vi của mảng của bạn; điều này được thực hiện bằng cách đặt biến "max" và "min" thành phần tử đầu tiên của mảng và với mỗi phần tử sau đó, nếu phần tử đó nhỏ hơn min hoặc lớn hơn max, đặt min hoặc max thành giá trị mới.
  • Nếu phạm vi nhỏ hơn một lần so với số lượng của tập hợp, chỉ có một "lỗ" để bạn có thể sử dụng XOR.
  • Khởi tạo một biến số nguyên X thành không.
  • Đối với mỗi số nguyên từ tối thiểu đến tối đa, XOR giá trị đó với X và lưu kết quả vào X.
  • Bây giờ XOR từng số nguyên trong mảng với X, lưu trữ từng kết quả liên tiếp vào X như trước.
  • Khi bạn hoàn thành, X sẽ là giá trị của "lỗ" của bạn.

Điều này sẽ chạy trong khoảng 2N thời gian tương tự như giải pháp bitvector, nhưng cần ít không gian bộ nhớ hơn cho bất kỳ N> sizeof (int) nào. Tuy nhiên, nếu mảng có nhiều "lỗ", X sẽ là "tổng" XOR của tất cả các lỗ, sẽ khó hoặc không thể tách thành các giá trị lỗ thực tế. Trong trường hợp đó, bạn quay lại một số phương pháp khác, chẳng hạn như phương pháp "trục" hoặc "bitvector" từ các câu trả lời khác.

Bạn có thể tái diễn điều này bằng cách sử dụng một cái gì đó tương tự như phương pháp trục để giảm thêm độ phức tạp. Sắp xếp lại mảng dựa trên điểm trục (sẽ là tối đa của bên trái và tối thiểu của bên phải; sẽ rất đơn giản để tìm tối đa và tối thiểu của toàn bộ mảng trong khi xoay vòng). Nếu bên trái của trục có một hoặc nhiều lỗ, chỉ lặp lại ở bên đó; mặt khác tái diễn vào phía bên kia Tại bất kỳ điểm nào mà bạn có thể xác định chỉ có một lỗ, hãy sử dụng phương pháp XOR để tìm lỗ đó (nên rẻ hơn về tổng thể so với việc tiếp tục xoay vòng xuống một tập hợp hai phần tử có lỗ đã biết, đó là trường hợp cơ bản cho thuật toán trục thuần túy).


Điều đó thật thông minh và tuyệt vời! Bây giờ bạn có thể đưa ra một cách để làm điều này với một số lượng lỗ khác nhau không? :-D

2

Phạm vi của các số bạn sẽ gặp phải là gì? Nếu phạm vi đó không lớn lắm, bạn có thể giải quyết điều này bằng hai lần quét (thời gian tuyến tính O (n)) bằng cách sử dụng một mảng có nhiều phần tử như bạn có số, không gian giao dịch theo thời gian. Bạn có thể tìm thấy phạm vi linh hoạt với một lần quét nữa. Để giảm dung lượng, bạn có thể gán 1 bit cho mỗi số, cung cấp cho bạn 8 số lưu trữ cho mỗi byte.

Tùy chọn khác của bạn có thể tốt hơn cho các kịch bản ban đầu và sẽ là nội dung thay vì sao chép bộ nhớ là sửa đổi loại lựa chọn để thoát sớm nếu số phút tìm thấy trong thẻ quét không vượt quá 1 lần so với phút cuối được tìm thấy.


1

Không thật sự lắm. Vì bất kỳ số nào chưa được quét luôn có thể là một số lấp đầy một "lỗ" nhất định, bạn không thể tránh quét từng số ít nhất một lần và sau đó so sánh nó với các hàng xóm có thể. Bạn có thể có thể tăng tốc mọi thứ bằng cách xây dựng một cây nhị phân hoặc sau đó di chuyển nó từ trái sang phải cho đến khi tìm thấy một lỗ, nhưng về cơ bản nó có độ phức tạp tương tự như sắp xếp, vì nó đang được sắp xếp. Và bạn có thể sẽ không nghĩ ra thứ gì nhanh hơn Timsort .


1
Bạn đang nói rằng việc duyệt qua một danh sách có cùng độ phức tạp như sắp xếp không?
maple_shaft

@maple_shaft: Không, tôi đang nói xây dựng cây nhị phân từ dữ liệu ngẫu nhiên và sau đó di chuyển từ trái sang phải tương đương với việc sắp xếp và sau đó di chuyển từ nhỏ sang lớn.
thuốc

1

Hầu hết các ý tưởng ở đây không chỉ là sắp xếp. Phiên bản bitvector là Bucketsort đơn giản. Heap sort cũng được đề cập. Về cơ bản, nó tập trung vào việc lựa chọn thuật toán sắp xếp phù hợp phụ thuộc vào yêu cầu thời gian / không gian và cũng tùy thuộc vào phạm vi và số lượng phần tử.

Theo quan điểm của tôi, sử dụng cấu trúc heap có lẽ là giải pháp tổng quát nhất (heap về cơ bản cung cấp cho bạn các yếu tố nhỏ nhất một cách hiệu quả mà không cần sắp xếp hoàn chỉnh).

Bạn cũng có thể phân tích các cách tiếp cận tìm số nhỏ nhất trước rồi quét từng số nguyên lớn hơn số đó. Hoặc bạn tìm thấy 5 con số nhỏ nhất với hy vọng sẽ có một khoảng cách.

Tất cả các thuật toán này có sức mạnh của chúng tùy thuộc vào đặc điểm đầu vào và yêu cầu chương trình.


0

Một giải pháp không sử dụng bộ nhớ bổ sung hoặc giả sử chiều rộng (32 bit) của số nguyên.

  1. Trong một tuyến tính tìm số nhỏ nhất. Hãy gọi đây là "tối thiểu". Độ phức tạp thời gian O (n).

  2. Chọn một phần tử trục ngẫu nhiên và thực hiện phân vùng kiểu quicksort.

  3. Nếu trục kết thúc ở vị trí = ("trục" - "tối thiểu"), sau đó lặp lại ở phía bên phải của phân vùng, khác sẽ lặp lại ở phía bên trái của phân vùng. Ý tưởng ở đây là nếu không có lỗ ngay từ đầu, trục sẽ ở vị trí ("trục" - "tối thiểu"), vì vậy lỗ đầu tiên phải nằm ở bên phải của phân vùng và ngược lại.

  4. Trường hợp cơ sở là một mảng gồm 1 phần tử và lỗ nằm giữa phần tử này và phần tử tiếp theo.

Độ phức tạp tổng thời gian chạy dự kiến ​​là O (n) (8 * n với các hằng số) và trường hợp xấu nhất là O (n ^ 2). Phân tích độ phức tạp thời gian cho một vấn đề tương tự có thể được tìm thấy ở đây .


0

Tôi tin rằng tôi đã đưa ra một cái gì đó nên hoạt động chung và hiệu quả nếu bạn được đảm bảo không có các bản sao * (tuy nhiên, nó có thể mở rộng cho bất kỳ số lượng lỗ và bất kỳ phạm vi số nguyên nào).

Ý tưởng đằng sau phương pháp này giống như quicksort, trong đó chúng ta tìm thấy một trục và phân vùng xung quanh nó, sau đó lặp lại ở một bên có một lỗ. Để xem bên nào có lỗ, chúng tôi tìm các số thấp nhất và cao nhất, và so sánh chúng với trục và số giá trị ở bên đó. Giả sử trục là 17 và số tối thiểu là 11. Nếu không có lỗ, cần có 6 số (11, 12, 13, 14, 15, 16, 17). Nếu có 5, chúng ta biết có một lỗ ở bên đó và chúng ta có thể tái diễn ở bên đó để tìm thấy nó. Tôi đang gặp khó khăn khi giải thích nó rõ ràng hơn thế, vì vậy hãy lấy một ví dụ.

15 21 10 13 18 16 22 23 24 20 17 11 25 12 14

Trục:

10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25

15 là trục, được biểu thị bằng ống ( ||). Có 5 số ở bên trái của trục, vì sẽ có (15 - 10) và 9 ở bên phải, trong đó nên có 10 (25 - 15). Vì vậy, chúng tôi tái diễn ở phía bên phải; chúng tôi sẽ lưu ý rằng giới hạn trước là 15 trong trường hợp lỗ liền kề với nó (16).

[15] 18 16 17 20 |21| 22 23 24 25

Bây giờ có 4 số ở bên trái nhưng nên có 5 (21 - 16). Vì vậy, chúng tôi tái diễn ở đó và một lần nữa chúng tôi sẽ lưu ý ràng buộc trước đó (trong ngoặc).

[15] 16 17 |18| 20 [21]

Bên trái có đúng 2 số (18 - 16), nhưng bên phải có 1 thay vì 2 (20 - 18). Tùy thuộc vào điều kiện kết thúc của chúng tôi, chúng tôi có thể so sánh 1 số với hai bên (18, 20) và thấy rằng 19 bị thiếu hoặc lặp lại một lần nữa:

[18] |20| [21]

Phía bên trái có kích thước bằng 0, với một khoảng cách giữa trục (20) và ràng buộc trước đó (18), vì vậy 19 là lỗ.

*: Nếu có trùng lặp, có thể bạn có thể sử dụng bộ băm để loại bỏ chúng trong thời gian O (N), giữ nguyên phương thức O (N), nhưng điều đó có thể mất nhiều thời gian hơn so với sử dụng một số phương pháp khác.


1
Tôi không tin OP nói bất cứ điều gì về việc chỉ có một lỗ hổng. Đầu vào là một danh sách các số chưa được sắp xếp - chúng có thể là bất cứ thứ gì. Không rõ từ mô tả của bạn về cách bạn xác định có bao nhiêu số "nên có".
Caleb

@caleb Không quan trọng có bao nhiêu lỗ hổng, chỉ là không có bản sao (có thể được loại bỏ trong O (N) bằng một bộ băm, mặc dù trong thực tế có thể có nhiều chi phí hơn các phương pháp khác). Tôi đã thử cải thiện mô tả, xem nó có tốt hơn không.
Kevin

Đây không phải là tuyến tính, IMO. Nó giống như (logN) ^ 2. Ở mỗi bước, bạn xoay vòng tập hợp con của bộ sưu tập mà bạn quan tâm (một nửa của phân đoạn trước mà bạn đã xác định là có "lỗ" đầu tiên), sau đó lặp lại vào bên trái nếu nó có "lỗ", hoặc bên phải nếu bên trái không. (logN) ^ 2 vẫn tốt hơn tuyến tính; nếu N tăng gấp 10 lần, bạn chỉ thực hiện theo thứ tự 2 (log (N) -1) + 1 bước nữa.
KeithS

@Keith - thật không may, bạn phải xem tất cả các số ở mỗi cấp để xoay vòng chúng, do đó, sẽ mất khoảng n + n / 2 + n / 4 + ... = 2n (về mặt kỹ thuật, 2 (nm)) .
Kevin
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.