Làm cách nào để tìm kiếm một số trong mảng 2d được sắp xếp từ trái sang phải và từ trên xuống dưới?


90

Gần đây tôi đã nhận được câu hỏi phỏng vấn này và tôi tò mò không biết giải pháp tốt cho nó sẽ là gì.

Giả sử tôi được cung cấp một mảng 2d trong đó tất cả các số trong mảng theo thứ tự tăng dần từ trái sang phải và từ trên xuống dưới.

Cách tốt nhất để tìm kiếm và xác định xem một số đích có trong mảng không?

Bây giờ, xu hướng đầu tiên của tôi là sử dụng tìm kiếm nhị phân vì dữ liệu của tôi đã được sắp xếp. Tôi có thể xác định xem một số có nằm trong một hàng trong thời gian O (log N) hay không. Tuy nhiên, đó là 2 hướng mà tôi đi.

Một giải pháp khác mà tôi nghĩ có thể hiệu quả là bắt đầu ở đâu đó ở giữa. Nếu giá trị giữa nhỏ hơn mục tiêu của tôi, thì tôi có thể chắc chắn rằng nó nằm trong phần hình vuông bên trái của ma trận từ giữa. Sau đó, tôi di chuyển theo đường chéo và kiểm tra lại, giảm kích thước của hình vuông mà mục tiêu có thể ở trong đó cho đến khi tôi hoàn thiện được số mục tiêu.

Có ai có bất kỳ ý tưởng tốt về việc giải quyết vấn đề này?

Mảng ví dụ:

Đã sắp xếp từ trái sang phải, trên xuống dưới.

1  2  4  5  6  
2  3  5  7  8  
4  6  8  9  10  
5  8  9  10 11  

Câu hỏi đơn giản: bạn có thể có một người hàng xóm có cùng giá trị [[1 1][1 1]]:?
Matthieu M.

Câu trả lời:


115

Đây là một cách tiếp cận đơn giản:

  1. Bắt đầu ở góc dưới cùng bên trái.
  2. Nếu mục tiêu nhỏ hơn giá trị đó, nó phải ở trên chúng ta, vì vậy hãy di chuyển lên một mục tiêu .
  3. Nếu không, chúng tôi biết rằng mục tiêu không thể nằm trong cột đó, vì vậy hãy di chuyển sang phải .
  4. Goto 2.

Đối với một NxMmảng, điều này chạy trong O(N+M). Tôi nghĩ sẽ rất khó để làm tốt hơn. :)


Edit: Rất nhiều thảo luận hay. Tôi đã nói về trường hợp chung ở trên; rõ ràng, nếu Nhoặc M nhỏ, bạn có thể sử dụng phương pháp tìm kiếm nhị phân để thực hiện điều này trong một cái gì đó gần với thời gian logarit.

Dưới đây là một số chi tiết, dành cho những ai tò mò:

Lịch sử

Thuật toán đơn giản này được gọi là Tìm kiếm lưng yên . Nó đã tồn tại được một thời gian và nó là tối ưu khi nào N == M. Một số tài liệu tham khảo:

Tuy nhiên, khi nào N < M, trực giác gợi ý rằng tìm kiếm nhị phân có thể làm tốt hơn O(N+M): Ví dụ: khi N == 1, một tìm kiếm nhị phân thuần túy sẽ chạy theo logarit chứ không phải theo thời gian tuyến tính.

Trường hợp xấu nhất ràng buộc

Richard Bird đã kiểm tra trực giác rằng tìm kiếm nhị phân có thể cải thiện thuật toán Saddleback trong một bài báo năm 2006:

Sử dụng một kỹ thuật đàm thoại khá bất thường, Bird cho chúng ta thấy rằng đối với N <= M, vấn đề này có giới hạn thấp hơn Ω(N * log(M/N)). Giới hạn này có ý nghĩa, vì nó cho chúng ta hiệu suất tuyến tính khi N == Mvà hiệu suất logarit khi N == 1.

Các thuật toán cho mảng hình chữ nhật

Một cách tiếp cận sử dụng tìm kiếm nhị phân từng hàng trông giống như sau:

  1. Bắt đầu với một mảng hình chữ nhật ở đâu N < M. Giả sử Nlà hàng và Mlà cột.
  2. Thực hiện tìm kiếm nhị phân trên hàng giữa cho value. Nếu chúng tôi tìm thấy nó, chúng tôi đã hoàn thành.
  3. Nếu không, chúng tôi đã tìm thấy một cặp số liền kề sgở đâu s < value < g.
  4. Hình chữ nhật của các số ở trên và bên trái snhỏ hơn value, vì vậy chúng ta có thể loại bỏ nó.
  5. Hình chữ nhật bên dưới và bên phải glớn hơn value, vì vậy chúng ta có thể loại bỏ nó.
  6. Chuyển sang bước (2) cho mỗi hình chữ nhật còn lại.

Về độ phức tạp trong trường hợp xấu nhất, thuật toán này log(M)hoạt động để loại bỏ một nửa các giải pháp có thể, và sau đó gọi đệ quy chính nó hai lần cho hai bài toán nhỏ hơn. Chúng tôi phải lặp lại một phiên bản nhỏ hơn của log(M)công việc đó cho mỗi hàng, nhưng nếu số hàng nhỏ so với số cột, thì việc loại bỏ tất cả các cột đó theo thời gian logarit bắt đầu trở nên đáng giá .

Điều này làm cho thuật toán có độ phức tạp T(N,M) = log(M) + 2 * T(M/2, N/2), mà Bird cho thấy là O(N * log(M/N)).

Một cách tiếp cận khác được đăng bởi Craig Gidney mô tả một thuật toán tương tự như cách tiếp cận ở trên: nó kiểm tra một hàng tại một thời điểm bằng cách sử dụng kích thước bước là M/N. Phân tích của ông cho thấy điều này cũng dẫn đến O(N * log(M/N))hiệu suất.

So sánh hiệu suất

Phân tích Big-O đều tốt và tốt, nhưng những cách tiếp cận này hoạt động tốt như thế nào trong thực tế? Biểu đồ dưới đây kiểm tra bốn thuật toán cho các mảng ngày càng "vuông":

hiệu suất thuật toán so với tính bình phương

(Thuật toán "ngây thơ" chỉ đơn giản là tìm kiếm mọi phần tử của mảng. Thuật toán "đệ quy" được mô tả ở trên. Thuật toán "kết hợp" là cách triển khai thuật toán của Gidney . Đối với mỗi kích thước mảng, hiệu suất được đo bằng cách tính thời gian cho mỗi thuật toán trên tập hợp cố định trong số 1.000.000 mảng được tạo ngẫu nhiên.)

Một số điểm đáng chú ý:

  • Như mong đợi, các thuật toán "tìm kiếm nhị phân" cung cấp hiệu suất tốt nhất trên mảng hình chữ nhật và thuật toán Saddleback hoạt động tốt nhất trên mảng hình vuông.
  • Thuật toán Saddleback hoạt động kém hơn thuật toán "ngây thơ" cho mảng 1-d, có lẽ là do nó thực hiện nhiều phép so sánh trên mỗi mục.
  • Hiệu suất mà thuật toán "tìm kiếm nhị phân" thực hiện trên mảng vuông có lẽ là do chi phí chạy các tìm kiếm nhị phân lặp lại.

Tóm lược

Sử dụng khéo léo tìm kiếm nhị phân có thể cung cấp O(N * log(M/N)hiệu suất cho cả mảng hình chữ nhật và hình vuông. Các O(N + M)"Saddleback" thuật toán đơn giản hơn nhiều, nhưng bị xuống cấp hiệu suất như mảng ngày càng trở nên hình chữ nhật.


6
áp dụng tìm kiếm nhị phân cho bước đi theo đường chéo và bạn đã có O (logN) hoặc O (logM) tùy theo giá trị nào cao hơn.
Anurag

3
@Anurag - Tôi không nghĩ sự phức tạp hoạt động tốt như vậy. Tìm kiếm nhị phân sẽ cung cấp cho bạn một nơi tốt để bắt đầu, nhưng bạn sẽ phải đi hết chiều này hay chiều khác, và trong trường hợp xấu nhất, bạn vẫn có thể bắt đầu ở góc này và kết thúc ở góc kia.
Jeffrey L Whitledge

1
Nếu N = 1 và M = 1000000 tôi có thể làm tốt hơn O (N + M), Vì vậy, một giải pháp khác là áp dụng tìm kiếm nhị phân trong mỗi hàng mang lại O (N * log (M)) trong đó N <M trong trường hợp điều này mang lại hằng số nhỏ hơn.
Luka Rahne

1
Tôi đã thực hiện một số thử nghiệm bằng cách sử dụng cả phương pháp của bạn và phương pháp tìm kiếm nhị phân và đăng kết quả TẠI ĐÂY . Có vẻ như phương pháp zigzag là tốt nhất, trừ khi tôi không tạo đúng điều kiện trường hợp xấu nhất cho cả hai phương pháp.
The111

1
Sử dụng tốt các tài liệu tham khảo! Tuy nhiên, khi M==Nchúng ta muốn O(N)độ phức tạp, không phải O(N*log(N/N))vì cái sau bằng không. Một ràng buộc sắc nét "hợp nhất" chính xác là O(N*(log(M/N)+1))khi N<=M.
hardmath

35

Vấn đề này cần có Θ(b lg(t))thời gian, ở đâu b = min(w,h)t=b/max(w,h). Tôi thảo luận về giải pháp trong bài đăng trên blog này .

Chặn dưới

Kẻ thù có thể buộc một thuật toán thực hiện Ω(b lg(t))các truy vấn, bằng cách giới hạn chính nó trong đường chéo chính:

Đối thủ sử dụng đường chéo chính

Chú giải: các ô màu trắng là các mục nhỏ hơn, các ô màu xám là các mục lớn hơn, các ô màu vàng là các mục nhỏ hơn hoặc bằng và các ô màu cam là các mục lớn hơn hoặc bằng nhau. Kẻ thù buộc giải pháp phải là ô màu vàng hoặc màu cam nào mà thuật toán truy vấn cuối cùng.

Lưu ý rằng có bcác danh sách kích thước được sắp xếp độc lập t, yêu cầu Ω(b lg(t))các truy vấn để loại bỏ hoàn toàn.

Thuật toán

  1. (Giả sử mà không mất đi tính tổng quát rằng w >= h)
  2. So sánh mục đích với ô tở bên trái góc trên cùng bên phải của vùng hợp lệ
    • Nếu mục của ô khớp, hãy trả lại vị trí hiện tại.
    • Nếu mục của ô nhỏ hơn mục đích, hãy loại bỏ các tô còn lại trong hàng bằng tìm kiếm nhị phân. Nếu một mục phù hợp được tìm thấy trong khi thực hiện việc này, hãy quay lại với vị trí của nó.
    • Nếu không, mục của ô nhiều hơn mục đích, loại bỏ tcác cột ngắn.
  3. Nếu không còn khu vực hợp lệ, trả về lỗi
  4. Chuyển sang bước 2

Tìm một mục:

Tìm một mặt hàng

Xác định một mục không tồn tại:

Xác định một mục không tồn tại

Chú thích: các ô màu trắng là các mục nhỏ hơn, các ô màu xám là các mục lớn hơn và ô màu xanh lá cây là các mục bằng nhau.

Phân tích

b*tcác cột ngắn cần loại bỏ. Có bhàng dài để loại bỏ. Loại bỏ một hàng dài tốn O(lg(t))thời gian. Việc loại bỏ tcác cột ngắn sẽ tốn O(1)thời gian.

Trong trường hợp xấu nhất, chúng tôi sẽ phải loại bỏ mọi cột và mọi hàng, tốn thời gian O(lg(t)*b + b*t*1/t) = O(b lg(t)).

Lưu ý rằng tôi đang giả sử lgkẹp vào kết quả trên 1 (tức là lg(x) = log_2(max(2,x))). Đó là lý do tại sao khi w=h, nghĩa là t=1, chúng ta nhận được giới hạn mong đợi O(b lg(1)) = O(b) = O(w+h).

public static Tuple<int, int> TryFindItemInSortedMatrix<T>(this IReadOnlyList<IReadOnlyList<T>> grid, T item, IComparer<T> comparer = null) {
    if (grid == null) throw new ArgumentNullException("grid");
    comparer = comparer ?? Comparer<T>.Default;

    // check size
    var width = grid.Count;
    if (width == 0) return null;
    var height = grid[0].Count;
    if (height < width) {
        var result = grid.LazyTranspose().TryFindItemInSortedMatrix(item, comparer);
        if (result == null) return null;
        return Tuple.Create(result.Item2, result.Item1);
    }

    // search
    var minCol = 0;
    var maxRow = height - 1;
    var t = height / width;
    while (minCol < width && maxRow >= 0) {
        // query the item in the minimum column, t above the maximum row
        var luckyRow = Math.Max(maxRow - t, 0);
        var cmpItemVsLucky = comparer.Compare(item, grid[minCol][luckyRow]);
        if (cmpItemVsLucky == 0) return Tuple.Create(minCol, luckyRow);

        // did we eliminate t rows from the bottom?
        if (cmpItemVsLucky < 0) {
            maxRow = luckyRow - 1;
            continue;
        }

        // we eliminated most of the current minimum column
        // spend lg(t) time eliminating rest of column
        var minRowInCol = luckyRow + 1;
        var maxRowInCol = maxRow;
        while (minRowInCol <= maxRowInCol) {
            var mid = minRowInCol + (maxRowInCol - minRowInCol + 1) / 2;
            var cmpItemVsMid = comparer.Compare(item, grid[minCol][mid]);
            if (cmpItemVsMid == 0) return Tuple.Create(minCol, mid);
            if (cmpItemVsMid > 0) {
                minRowInCol = mid + 1;
            } else {
                maxRowInCol = mid - 1;
                maxRow = mid - 1;
            }
        }

        minCol += 1;
    }

    return null;
}

1
Thú vị và có thể một phần qua đầu tôi. Tôi không quen với phong cách phân tích độ phức tạp "đối thủ" này. Đối thủ có thực sự bằng cách nào đó tự động thay đổi mảng khi bạn tìm kiếm, hay anh ta chỉ là một cái tên được đặt cho những điều xui xẻo mà bạn gặp phải trong một lần tìm kiếm trong trường hợp xấu nhất?
The111

2
@ The111 Hên xui tương đương với việc ai đó chọn một con đường xấu mà không vi phạm những điều đã thấy từ trước đến nay, vì vậy cả hai định nghĩa đó đều hoạt động giống nhau. Tôi thực sự đang gặp khó khăn khi tìm các liên kết giải thích kỹ thuật cụ thể liên quan đến độ phức tạp của tính toán ... Tôi nghĩ đây là một ý tưởng nổi tiếng hơn nhiều.
Craig Gidney

Bởi vì log (1) = 0, nên ước lượng độ phức tạp nên được đưa ra O(b*(lg(t)+1))thay vì O(b*lg(t)). Viết rất hay, đặc biệt. để kêu gọi sự chú ý đến "kỹ thuật của đối thủ" trong việc hiển thị ràng buộc "trường hợp xấu nhất".
hardmath

@hardmath Tôi đề cập đến điều đó trong câu trả lời. Tôi đã làm rõ nó một chút.
Craig Gidney

17

Tôi sẽ sử dụng chiến lược chia để trị cho vấn đề này, tương tự như những gì bạn đã đề xuất, nhưng các chi tiết hơi khác một chút.

Đây sẽ là một tìm kiếm đệ quy trên các biên con của ma trận.

Ở mỗi bước, hãy chọn một phần tử ở giữa phạm vi. Nếu giá trị được tìm thấy là những gì bạn đang tìm kiếm, thì bạn đã hoàn thành.

Ngược lại, nếu giá trị tìm thấy nhỏ hơn giá trị bạn đang tìm kiếm, thì bạn biết rằng nó không nằm trong góc phần tư phía trên và bên trái vị trí hiện tại của bạn. Vì vậy, tìm kiếm đệ quy hai biên bản con: mọi thứ (độc quyền) bên dưới vị trí hiện tại và mọi thứ (độc quyền) ở bên phải bằng hoặc cao hơn vị trí hiện tại.

Nếu không, (giá trị tìm thấy lớn hơn giá trị bạn đang tìm kiếm) bạn biết rằng nó không nằm trong góc phần tư bên dưới và ở bên phải vị trí hiện tại của bạn. Vì vậy, hãy tìm kiếm đệ quy hai biên bản con: mọi thứ (độc quyền) ở bên trái vị trí hiện tại và mọi thứ (độc quyền) ở trên vị trí hiện tại nằm trên cột hiện tại hoặc một cột ở bên phải.

Và ba-da-bing, bạn đã tìm thấy nó.

Lưu ý rằng mỗi lệnh gọi đệ quy chỉ xử lý với dải con hiện tại, không phải (ví dụ) TẤT CẢ các hàng phía trên vị trí hiện tại. Chỉ những người trong dải con hiện tại.

Đây là một số mã giả dành cho bạn:

bool numberSearch(int[][] arr, int value, int minX, int maxX, int minY, int maxY)

if (minX == maxX and minY == maxY and arr[minX,minY] != value)
    return false
if (arr[minX,minY] > value) return false;  // Early exits if the value can't be in 
if (arr[maxX,maxY] < value) return false;  // this subrange at all.
int nextX = (minX + maxX) / 2
int nextY = (minY + maxY) / 2
if (arr[nextX,nextY] == value)
{
    print nextX,nextY
    return true
}
else if (arr[nextX,nextY] < value)
{
    if (numberSearch(arr, value, minX, maxX, nextY + 1, maxY))
        return true
    return numberSearch(arr, value, nextX + 1, maxX, minY, nextY)
}
else
{
    if (numberSearch(arr, value, minX, nextX - 1, minY, maxY))
        return true
    reutrn numberSearch(arr, value, nextX, maxX, minY, nextY)
}

+1: Đây là chiến lược O (log (N)), và do đó nó tốt cho một đơn đặt hàng mà người ta sẽ nhận được.
Rex Kerr

3
@Rex Kerr - Có vẻ như O (log (N)), vì đó là cách tìm kiếm nhị phân thông thường, tuy nhiên, lưu ý rằng có khả năng có hai lệnh gọi đệ quy ở mỗi cấp. Điều này có nghĩa là nó tệ hơn nhiều so với logarit đơn giản. Tôi không tin rằng trường hợp tồi tệ hơn là tốt hơn O (M + N) vì, có khả năng, mọi hàng hoặc mọi cột phải được tìm kiếm. Tuy nhiên, tôi đoán rằng thuật toán này có thể đánh bại trường hợp xấu nhất đối với rất nhiều giá trị. Và phần tốt nhất là nó có thể phân tích được, vì đó là nơi mà phần cứng đang phát triển gần đây.
Jeffrey L Whitledge

1
@JLW: Nó là O (log (N)) - nhưng thực ra nó là O (log_ (4/3) (N ^ 2)) hoặc đại loại như vậy. Xem câu trả lời của Svante bên dưới. Câu trả lời của bạn thực sự giống nhau (nếu bạn muốn đệ quy theo cách tôi nghĩ bạn đã làm).
Rex Kerr

1
@Svante - Các mảng con không chồng lên nhau. Trong tùy chọn đầu tiên, chúng không có phần tử y nào chung. Trong tùy chọn thứ hai, chúng không có phần tử x nào chung.
Jeffrey L Whitledge,

1
Tôi không chắc đây có phải là logarit hay không. Tôi đã tính toán độ phức tạp bằng cách sử dụng quan hệ lặp lại gần đúng T (0) = 1, T (A) = T (A / 2) + T (A / 4) + 1, trong đó A là vùng tìm kiếm và kết thúc bằng T ( A) = O (Fib (lg (A))), xấp xỉ O (A ^ 0,7) và kém hơn O (n + m) là O (A ^ 0,5). Có thể tôi đã mắc một sai lầm ngớ ngẩn nào đó, nhưng có vẻ như thuật toán đang lãng phí rất nhiều thời gian để đi xuống những cành không kết quả.
Craig Gidney

6

Hai câu trả lời chính đưa ra cho đến nay dường như được cho là O(log N)"phương pháp ZigZag" và O(N+M)phương pháp Tìm kiếm nhị phân. Tôi nghĩ rằng tôi sẽ thực hiện một số thử nghiệm so sánh hai phương pháp với một số thiết lập khác nhau. Đây là những thông tin chi tiết:

Mảng là N x N hình vuông trong mọi thử nghiệm, với N thay đổi từ 125 đến 8000 (đống JVM lớn nhất của tôi có thể xử lý). Đối với mỗi kích thước mảng, tôi chọn một vị trí ngẫu nhiên trong mảng để đặt một 2. Sau đó, tôi đặt một dấu 3ở mọi nơi có thể (ở bên phải và bên dưới của 2) và sau đó lấp đầy phần còn lại của mảng bằng1. Một số nhà bình luận trước đó dường như nghĩ rằng kiểu thiết lập này sẽ mang lại thời gian chạy trong trường hợp xấu nhất cho cả hai thuật toán. Đối với mỗi kích thước mảng, tôi chọn 100 vị trí ngẫu nhiên khác nhau cho 2 (mục tiêu tìm kiếm) và chạy thử nghiệm. Tôi đã ghi lại thời gian chạy trung bình và thời gian chạy trường hợp xấu nhất cho mỗi thuật toán. Bởi vì việc đọc ms tốt trong Java diễn ra quá nhanh và vì tôi không tin tưởng vào nanoTime () của Java, tôi đã lặp lại mỗi lần kiểm tra 1000 lần chỉ để thêm hệ số thiên vị đồng nhất vào mọi lúc. Đây là kết quả:

nhập mô tả hình ảnh ở đây

ZigZag đánh bại nhị phân trong mọi thử nghiệm cho cả thời gian trung bình và trường hợp xấu nhất, tuy nhiên, tất cả chúng đều nằm trong một thứ tự cường độ của nhau ít nhiều.

Đây là mã Java:

public class SearchSortedArray2D {

    static boolean findZigZag(int[][] a, int t) {
        int i = 0;
        int j = a.length - 1;
        while (i <= a.length - 1 && j >= 0) {
            if (a[i][j] == t) return true;
            else if (a[i][j] < t) i++;
            else j--;
        }
        return false;
    }

    static boolean findBinarySearch(int[][] a, int t) {
        return findBinarySearch(a, t, 0, 0, a.length - 1, a.length - 1);
    }

    static boolean findBinarySearch(int[][] a, int t,
            int r1, int c1, int r2, int c2) {
        if (r1 > r2 || c1 > c2) return false; 
        if (r1 == r2 && c1 == c2 && a[r1][c1] != t) return false;
        if (a[r1][c1] > t) return false;
        if (a[r2][c2] < t) return false;

        int rm = (r1 + r2) / 2;
        int cm = (c1 + c2) / 2;
        if (a[rm][cm] == t) return true;
        else if (a[rm][cm] > t) {
            boolean b1 = findBinarySearch(a, t, r1, c1, r2, cm - 1);
            boolean b2 = findBinarySearch(a, t, r1, cm, rm - 1, c2);
            return (b1 || b2);
        } else {
            boolean b1 = findBinarySearch(a, t, r1, cm + 1, rm, c2);
            boolean b2 = findBinarySearch(a, t, rm + 1, c1, r2, c2);
            return (b1 || b2);
        }
    }

    static void randomizeArray(int[][] a, int N) {
        int ri = (int) (Math.random() * N);
        int rj = (int) (Math.random() * N);
        a[ri][rj] = 2;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (i == ri && j == rj) continue;
                else if (i > ri || j > rj) a[i][j] = 3;
                else a[i][j] = 1;
            }
        }
    }

    public static void main(String[] args) {

        int N = 8000;
        int[][] a = new int[N][N];
        int randoms = 100;
        int repeats = 1000;

        long start, end, duration;
        long zigMin = Integer.MAX_VALUE, zigMax = Integer.MIN_VALUE;
        long binMin = Integer.MAX_VALUE, binMax = Integer.MIN_VALUE;
        long zigSum = 0, zigAvg;
        long binSum = 0, binAvg;

        for (int k = 0; k < randoms; k++) {
            randomizeArray(a, N);

            start = System.currentTimeMillis();
            for (int i = 0; i < repeats; i++) findZigZag(a, 2);
            end = System.currentTimeMillis();
            duration = end - start;
            zigSum += duration;
            zigMin = Math.min(zigMin, duration);
            zigMax = Math.max(zigMax, duration);

            start = System.currentTimeMillis();
            for (int i = 0; i < repeats; i++) findBinarySearch(a, 2);
            end = System.currentTimeMillis();
            duration = end - start;
            binSum += duration;
            binMin = Math.min(binMin, duration);
            binMax = Math.max(binMax, duration);
        }
        zigAvg = zigSum / randoms;
        binAvg = binSum / randoms;

        System.out.println(findZigZag(a, 2) ?
                "Found via zigzag method. " : "ERROR. ");
        //System.out.println("min search time: " + zigMin + "ms");
        System.out.println("max search time: " + zigMax + "ms");
        System.out.println("avg search time: " + zigAvg + "ms");

        System.out.println();

        System.out.println(findBinarySearch(a, 2) ?
                "Found via binary search method. " : "ERROR. ");
        //System.out.println("min search time: " + binMin + "ms");
        System.out.println("max search time: " + binMax + "ms");
        System.out.println("avg search time: " + binAvg + "ms");
    }
}

1
+1 Yay, dữ liệu. :) Cũng có thể thú vị khi xem cách hai phương pháp này tiếp cận với các mảng NxM, vì tìm kiếm nhị phân có vẻ như nó sẽ trở nên hữu ích hơn một cách trực quan khi chúng ta tiếp cận trường hợp 1 chiều.
Nate Kohl

5

Đây là một bằng chứng ngắn gọn về giới hạn dưới của vấn đề.

Bạn không thể làm điều đó tốt hơn thời gian tuyến tính (về kích thước mảng, không phải số lượng phần tử). Trong mảng bên dưới, mỗi phần tử được đánh dấu *có thể là 5 hoặc 6 (độc lập với các phần tử khác). Vì vậy, nếu giá trị mục tiêu của bạn là 6 (hoặc 5), thuật toán cần phải kiểm tra tất cả chúng.

1 2 3 4 *
2 3 4 * 7
3 4 * 7 8
4 * 7 8 9
* 7 8 9 10

Tất nhiên điều này cũng mở rộng sang các mảng lớn hơn. Điều này có nghĩa là câu trả lời này là tối ưu.

Cập nhật: Như đã chỉ ra bởi Jeffrey L Whitledge, nó chỉ tối ưu khi có giới hạn dưới tiệm cận về thời gian chạy so với kích thước dữ liệu đầu vào (được coi là một biến duy nhất). Thời gian chạy được coi là hàm hai biến trên cả hai chiều mảng có thể được cải thiện.


Bạn đã không chứng minh rằng câu trả lời đó là tối ưu. Ví dụ: hãy xem xét một mảng có mười trên và một triệu xuống, trong đó hàng thứ năm chứa các giá trị đều cao hơn giá trị mục tiêu. Trong trường hợp đó, thuật toán được đề xuất sẽ thực hiện tìm kiếm nhỏ hơn 999.995 giá trị trước khi đến gần mục tiêu. Một thuật toán phân đôi như của tôi sẽ chỉ tìm kiếm 18 giá trị trước khi gần mục tiêu. Và nó hoạt động (về mặt không đối xứng) không tệ hơn thuật toán được đề xuất trong tất cả các trường hợp khác.
Jeffrey L Whitledge, 19/03

@Jeffrey: Đó là giới hạn thấp hơn về vấn đề đối với trường hợp bi quan. Bạn có thể tối ưu hóa cho các đầu vào tốt, nhưng tồn tại các đầu vào mà bạn không thể làm tốt hơn tuyến tính.
Rafał Dowgird

Có, có những đầu vào tồn tại mà bạn không thể làm tốt hơn tuyến tính. Trong trường hợp đó, thuật toán của tôi thực hiện tìm kiếm tuyến tính đó. Nhưng có những đầu vào khác mà bạn có thể làm theo cách tốt hơn là tuyến tính. Do đó, giải pháp được đề xuất không phải là tối ưu, vì nó luôn thực hiện tìm kiếm tuyến tính.
Jeffrey L Whitledge,

Điều này cho thấy thuật toán phải sử dụng thời gian BigOmega (tối thiểu (n, m)), không phải BigOmega (n + m). Đó là lý do tại sao bạn có thể làm tốt hơn nhiều khi một thứ nguyên nhỏ hơn đáng kể. Ví dụ, nếu bạn biết sẽ chỉ có 1 hàng, bạn có thể giải bài toán theo thời gian logarit. Tôi nghĩ rằng một thuật toán tối ưu sẽ mất thời gian O (min (n + m, n lg m, m lg n)).
Craig Gidney

Đã cập nhật câu trả lời cho phù hợp.
Rafał Dowgird

4

Tôi nghĩ Đây là câu trả lời và nó hoạt động cho bất kỳ loại ma trận được sắp xếp nào

bool findNum(int arr[][ARR_MAX],int xmin, int xmax, int ymin,int ymax,int key)
{
    if (xmin > xmax || ymin > ymax || xmax < xmin || ymax < ymin) return false;
    if ((xmin == xmax) && (ymin == ymax) && (arr[xmin][ymin] != key)) return false;
    if (arr[xmin][ymin] > key || arr[xmax][ymax] < key) return false;
    if (arr[xmin][ymin] == key || arr[xmax][ymax] == key) return true;

    int xnew = (xmin + xmax)/2;
    int ynew = (ymin + ymax)/2;

    if (arr[xnew][ynew] == key) return true;
    if (arr[xnew][ynew] < key)
    {
        if (findNum(arr,xnew+1,xmax,ymin,ymax,key))
            return true;
        return (findNum(arr,xmin,xmax,ynew+1,ymax,key));
    } else {
        if (findNum(arr,xmin,xnew-1,ymin,ymax,key))
            return true;
        return (findNum(arr,xmin,xmax,ymin,ynew-1,key));
    }
}

1

Câu hỏi thú vị. Hãy xem xét ý tưởng này - tạo một ranh giới nơi tất cả các con số lớn hơn mục tiêu của bạn và một ranh giới khác nơi tất cả các con số nhỏ hơn mục tiêu của bạn. Nếu bất cứ điều gì còn lại giữa hai, đó là mục tiêu của bạn.

Nếu tôi đang tìm số 3 trong ví dụ của bạn, tôi sẽ đọc trên hàng đầu tiên cho đến khi đạt 4, sau đó tìm số liền kề nhỏ nhất (bao gồm cả đường chéo) lớn hơn 3:

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11

Bây giờ tôi làm tương tự cho những số nhỏ hơn 3:

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11

Bây giờ tôi hỏi, có gì bên trong hai ranh giới không? Nếu có, nó phải là 3. Nếu không, thì không có 3. Sắp xếp gián tiếp vì tôi không thực sự tìm thấy số, tôi chỉ suy ra rằng nó phải ở đó. Điều này có thêm tiền thưởng là đếm TẤT CẢ 3.

Tôi đã thử điều này trên một số ví dụ và nó có vẻ hoạt động tốt.


Một cuộc bỏ phiếu không có bình luận? Tôi nghĩ đây là O (N ^ 1/2) vì hiệu suất trong trường hợp xấu nhất yêu cầu kiểm tra đường chéo. Ít nhất hãy cho tôi xem một ví dụ truy cập nơi phương pháp này không hoạt động!
Grembo

+1: giải pháp hay ... sáng tạo và tốt là nó tìm ra tất cả các giải pháp.
Tony Delroy

1

Tìm kiếm nhị phân qua đường chéo của mảng là lựa chọn tốt nhất. Chúng ta có thể tìm xem phần tử đó nhỏ hơn hoặc bằng phần tử trong đường chéo.


0

A. Thực hiện tìm kiếm nhị phân trên những dòng có số mục tiêu.

B. Biến nó thành đồ thị: Tìm số bằng cách luôn lấy nút lân cận nhỏ nhất không được truy cập và đánh dấu lùi khi tìm thấy một số quá lớn


0

Tìm kiếm nhị phân sẽ là cách tiếp cận tốt nhất, imo. Bắt đầu từ 1/2 x, 1/2 y sẽ cắt đôi. IE một hình vuông 5x5 sẽ giống như x == 2 / y == 3. Tôi làm tròn một giá trị xuống và một giá trị lên để khoanh vùng tốt hơn theo hướng của giá trị được nhắm mục tiêu.

Để rõ ràng, lần lặp tiếp theo sẽ cung cấp cho bạn một cái gì đó như x == 1 / y == 2 HOẶC x == 3 / y == 5


0

Vâng, để bắt đầu, chúng ta hãy giả sử chúng ta đang sử dụng một hình vuông.

1 2 3
2 3 4
3 4 5

1. Tìm kiếm hình vuông

Tôi sẽ sử dụng một tìm kiếm nhị phân trên đường chéo. Mục tiêu là xác định vị trí số nhỏ hơn không thấp hơn số mục tiêu.

Giả sử tôi đang tìm kiếm 4ví dụ, sau đó tôi sẽ định vị 5tại (2,2).

Sau đó, tôi yên tâm rằng nếu 4là trong bảng, nó là ở một vị trí một trong hai (x,2)hoặc (2,x)với xtrong [0,2]. Vâng, đó chỉ là 2 tìm kiếm nhị phân.

Sự phức tạp không phải là khó khăn: O(log(N))(3 tìm kiếm nhị phân trên phạm vi độ dài N)

2. Tìm kiếm hình chữ nhật, cách tiếp cận ngây thơ

Tất nhiên, nó sẽ phức tạp hơn một chút khi NMkhác (với hình chữ nhật), hãy xem xét trường hợp suy biến này:

1  2  3  4  5  6  7  8
2  3  4  5  6  7  8  9
10 11 12 13 14 15 16 17

Và giả sử tôi đang tìm kiếm 9... Cách tiếp cận đường chéo vẫn tốt, nhưng định nghĩa về đường chéo thay đổi. Đường chéo của tôi đây [1, (5 or 6), 17]. Giả sử tôi đã chọn [1,5,17], sau đó tôi biết rằng nếu 9có trong bảng thì nó nằm trong phần phụ:

            5  6  7  8
            6  7  8  9
10 11 12 13 14 15 16

Điều này cho chúng ta 2 hình chữ nhật:

5 6 7 8    10 11 12 13 14 15 16
6 7 8 9

Vì vậy, chúng ta có thể đệ quy! có thể bắt đầu bởi cái có ít phần tử hơn (mặc dù trong trường hợp này, nó giết chúng ta).

Tôi nên lưu ý rằng nếu một trong các kích thước nhỏ hơn 3, chúng ta không thể áp dụng phương pháp đường chéo và phải sử dụng tìm kiếm nhị phân. Ở đây nó có nghĩa là:

  • Áp dụng tìm kiếm nhị phân trên 10 11 12 13 14 15 16, không tìm thấy
  • Áp dụng tìm kiếm nhị phân trên 5 6 7 8, không tìm thấy
  • Áp dụng tìm kiếm nhị phân trên 6 7 8 9, không tìm thấy

Thật khó vì để có được hiệu suất tốt, bạn có thể muốn phân biệt giữa một số trường hợp, tùy thuộc vào hình dạng chung ....

3. Tìm kiếm hình chữ nhật, cách tiếp cận tàn bạo

Sẽ dễ dàng hơn nhiều nếu chúng ta xử lý một hình vuông ... vì vậy hãy bình phương mọi thứ.

1  2  3  4  5  6  7  8
2  3  4  5  6  7  8  9
10 11 12 13 14 15 16 17
17 .  .  .  .  .  .  17
.                    .
.                    .
.                    .
17 .  .  .  .  .  .  17

Bây giờ chúng ta có một hình vuông.

Tất nhiên, chúng tôi có thể sẽ KHÔNG thực sự tạo các hàng đó, chúng tôi có thể đơn giản mô phỏng chúng.

def get(x,y):
  if x < N and y < M: return table[x][y]
  else: return table[N-1][M-1]            # the max

vì vậy nó hoạt động giống như một hình vuông mà không chiếm nhiều bộ nhớ hơn (với cái giá phải trả là tốc độ, có lẽ, phụ thuộc vào bộ nhớ cache ... ôi thôi: p)


0

BIÊN TẬP:

Tôi đã hiểu sai câu hỏi. Như các ý kiến ​​chỉ ra điều này chỉ hoạt động trong trường hợp hạn chế hơn.

Trong một ngôn ngữ như C lưu trữ dữ liệu theo thứ tự chính hàng, chỉ cần coi nó như một mảng 1D có kích thước n * m và sử dụng tìm kiếm nhị phân.


Vâng, tại sao lại làm cho nó phức tạp hơn nó phải thế.
erikkallen

Mảng không được sắp xếp, do đó không tìm kiếm bin có thể được áp dụng cho nó
Miollnyr

1
Điều này sẽ chỉ hoạt động nếu phần tử cuối cùng của mỗi hàng cao hơn phần tử đầu tiên trên hàng tiếp theo, đây là một yêu cầu hạn chế hơn nhiều so với bài toán đề xuất.
Jeffrey L Whitledge,

Cảm ơn, tôi đã chỉnh sửa câu trả lời của mình. Chưa đọc kỹ, đặc biệt là mảng ví dụ.
Hugh Brackett

0

Tôi có một Giải pháp Chia & Chinh phục đệ quy. Ý tưởng cơ bản cho một bước là: Chúng ta biết rằng Trái-Trên (LU) là nhỏ nhất và đáy phải (RB) là số lớn nhất, vì vậy No (N) đã cho phải: N> = LU và N <= RB

IF N == LU và N == RB :::: Phần tử Tìm thấy và Hủy bỏ trả về vị trí / Chỉ mục Nếu N> = LU và N <= RB = FALSE, Không có ở đó và hủy bỏ. Nếu N> = LU và N <= RB = TRUE, hãy chia mảng 2D thành 4 phần bằng nhau của mỗi mảng 2D theo cách hợp lý .. Và sau đó áp dụng cùng một bước thuật toán cho cả bốn mảng con.

Algo của tôi là đúng Tôi đã triển khai trên PC của bạn bè mình. Độ phức tạp: mỗi 4 phép so sánh b có thể được sử dụng để suy ra tổng số không có phần tử là 1/4 trong trường hợp xấu nhất của nó .. Vì vậy, độ phức tạp của tôi là 1 + 4 x lg (n) + 4 Nhưng thực sự mong đợi điều này sẽ hoạt động trên O (n)

Tôi nghĩ rằng có điều gì đó sai ở đâu đó trong cách tính Độ phức tạp của tôi, vui lòng sửa nếu đúng ..


0

Giải pháp tối ưu là bắt đầu ở góc trên cùng bên trái, có giá trị nhỏ nhất. Di chuyển theo đường chéo xuống bên phải cho đến khi bạn chạm vào một phần tử có giá trị> = giá trị của phần tử đã cho. Nếu giá trị của phần tử bằng giá trị của phần tử đã cho, trả về tìm thấy là true.

Nếu không, từ đây chúng ta có thể tiến hành theo hai cách.

Chiến lược 1:

  1. Di chuyển lên trong cột và tìm kiếm phần tử đã cho cho đến khi chúng ta đến cuối. Nếu tìm thấy, trả về tìm thấy là true
  2. Di chuyển sang trái trong hàng và tìm kiếm phần tử đã cho cho đến khi chúng ta đến cuối. Nếu tìm thấy, trả về tìm thấy là true
  3. trả về được tìm thấy là sai

Chiến lược 2: Gọi i biểu thị chỉ số hàng và j biểu thị chỉ số cột của phần tử đường chéo mà chúng ta đã dừng lại. (Ở đây, chúng ta có i = j, BTW). Cho k = 1.

  • Lặp lại các bước dưới đây cho đến khi ik> = 0
    1. Tìm kiếm xem một [ik] [j] có bằng phần tử đã cho hay không. nếu có, trả về tìm thấy là đúng.
    2. Tìm kiếm xem một [i] [jk] có bằng phần tử đã cho hay không. nếu có, trả về tìm thấy là đúng.
    3. Tăng dần k

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11


0
public boolean searchSortedMatrix(int arr[][] , int key , int minX , int maxX , int minY , int maxY){

    // base case for recursion
    if(minX > maxX || minY > maxY)
        return false ;
    // early fails
    // array not properly intialized
    if(arr==null || arr.length==0)
        return false ;
    // arr[0][0]> key return false
    if(arr[minX][minY]>key)
        return false ;
    // arr[maxX][maxY]<key return false
    if(arr[maxX][maxY]<key)
        return false ;
    //int temp1 = minX ;
    //int temp2 = minY ;
    int midX = (minX+maxX)/2 ;
    //if(temp1==midX){midX+=1 ;}
    int midY = (minY+maxY)/2 ;
    //if(temp2==midY){midY+=1 ;}


    // arr[midX][midY] = key ? then value found
    if(arr[midX][midY] == key)
        return true ;
    // alas ! i have to keep looking

    // arr[midX][midY] < key ? search right quad and bottom matrix ;
    if(arr[midX][midY] < key){
        if( searchSortedMatrix(arr ,key , minX,maxX , midY+1 , maxY))
            return true ;
        // search bottom half of matrix
        if( searchSortedMatrix(arr ,key , midX+1,maxX , minY , maxY))
            return true ;
    }
    // arr[midX][midY] > key ? search left quad matrix ;
    else {
         return(searchSortedMatrix(arr , key , minX,midX-1,minY,midY-1));
    }
    return false ;

}

0

Tôi đề nghị, lưu trữ tất cả các ký tự trong a 2D list. sau đó tìm chỉ mục của phần tử được yêu cầu nếu nó tồn tại trong danh sách.

Nếu không có, hãy in thông báo thích hợp khác, hãy in hàng và cột dưới dạng:

row = (index/total_columns)column = (index%total_columns -1)

Điều này sẽ chỉ phát sinh thời gian tìm kiếm nhị phân trong một danh sách.

Vui lòng đề xuất bất kỳ điều chỉnh nào. :)


0

Nếu nghiệm O (M log (N)) là ok cho mảng MxN -

template <size_t n>
struct MN * get(int a[][n], int k, int M, int N){
  struct MN *result = new MN;
  result->m = -1;
  result->n = -1;

  /* Do a binary search on each row since rows (and columns too) are sorted. */
  for(int i = 0; i < M; i++){
    int lo = 0; int hi = N - 1;
    while(lo <= hi){
      int mid = lo + (hi-lo)/2;
      if(k < a[i][mid]) hi = mid - 1;
      else if (k > a[i][mid]) lo = mid + 1;
      else{
        result->m = i;
        result->n = mid;
        return result;
      }
    }
  }
  return result;
}

Làm việc với C ++ demo.

Vui lòng cho tôi biết nếu điều này không hoạt động hoặc nếu có lỗi.


0

Tôi đã hỏi câu hỏi này trong các cuộc phỏng vấn trong suốt hơn một thập kỷ và tôi nghĩ chỉ có một người có thể đưa ra một thuật toán tối ưu.

Giải pháp của tôi luôn là:

  1. Tìm kiếm nhị phân theo đường chéo giữa, là đường chéo chạy xuống và sang phải, chứa mục tại (rows.count/2, columns.count/2).

  2. Nếu số đích được tìm thấy, trả về true.

  3. Nếu không, hai số ( uv) sẽ được tìm thấy sao cho unhỏ hơn mục tiêu, vlớn hơn mục tiêu và vlà một bên phải và một bên dưới u.

  4. Tìm kiếm đệ quy ma trận con ở bên phải uvà trên cùng của vvà ma trận con ở cuối uvà bên trái của v.

Tôi tin rằng đây là một cải tiến nghiêm ngặt so với thuật toán do Nate đưa ra ở đây , vì việc tìm kiếm theo đường chéo thường cho phép giảm hơn một nửa không gian tìm kiếm (nếu ma trận gần với hình vuông), trong khi tìm kiếm một hàng hoặc cột luôn dẫn đến việc loại bỏ của chính xác một nửa.

Đây là mã trong Swift (có lẽ không phải là Swifty khủng khiếp):

import Cocoa

class Solution {
    func searchMatrix(_ matrix: [[Int]], _ target: Int) -> Bool {
        if (matrix.isEmpty || matrix[0].isEmpty) {
            return false
        }

        return _searchMatrix(matrix, 0..<matrix.count, 0..<matrix[0].count, target)
    }

    func _searchMatrix(_ matrix: [[Int]], _ rows: Range<Int>, _ columns: Range<Int>, _ target: Int) -> Bool {
        if (rows.count == 0 || columns.count == 0) {
            return false
        }
        if (rows.count == 1) {
            return _binarySearch(matrix, rows.lowerBound, columns, target, true)
        }
        if (columns.count == 1) {
            return _binarySearch(matrix, columns.lowerBound, rows, target, false)
        }

        var lowerInflection = (-1, -1)
        var upperInflection = (Int.max, Int.max)
        var currentRows = rows
        var currentColumns = columns
        while (currentRows.count > 0 && currentColumns.count > 0 && upperInflection.0 > lowerInflection.0+1) {
            let rowMidpoint = (currentRows.upperBound + currentRows.lowerBound) / 2
            let columnMidpoint = (currentColumns.upperBound + currentColumns.lowerBound) / 2
            let value = matrix[rowMidpoint][columnMidpoint]
            if (value == target) {
                return true
            }

            if (value > target) {
                upperInflection = (rowMidpoint, columnMidpoint)
                currentRows = currentRows.lowerBound..<rowMidpoint
                currentColumns = currentColumns.lowerBound..<columnMidpoint
            } else {
                lowerInflection = (rowMidpoint, columnMidpoint)
                currentRows = rowMidpoint+1..<currentRows.upperBound
                currentColumns = columnMidpoint+1..<currentColumns.upperBound
            }
        }
        if (lowerInflection.0 == -1) {
            lowerInflection = (upperInflection.0-1, upperInflection.1-1)
        } else if (upperInflection.0 == Int.max) {
            upperInflection = (lowerInflection.0+1, lowerInflection.1+1)
        }

        return _searchMatrix(matrix, rows.lowerBound..<lowerInflection.0+1, upperInflection.1..<columns.upperBound, target) || _searchMatrix(matrix, upperInflection.0..<rows.upperBound, columns.lowerBound..<lowerInflection.1+1, target)
    }

    func _binarySearch(_ matrix: [[Int]], _ rowOrColumn: Int, _ range: Range<Int>, _ target: Int, _ searchRow : Bool) -> Bool {
        if (range.isEmpty) {
            return false
        }

        let midpoint = (range.upperBound + range.lowerBound) / 2
        let value = (searchRow ? matrix[rowOrColumn][midpoint] : matrix[midpoint][rowOrColumn])
        if (value == target) {
            return true
        }

        if (value > target) {
            return _binarySearch(matrix, rowOrColumn, range.lowerBound..<midpoint, target, searchRow)
        } else {
            return _binarySearch(matrix, rowOrColumn, midpoint+1..<range.upperBound, target, searchRow)
        }
    }
}

-1

Cho một ma trận vuông như sau:

[abc]
[def]
[ijk]

Ta biết rằng a <c, d <f, i <k. Những gì chúng tôi không biết là liệu d <c hay d> c, v.v. Chúng tôi chỉ đảm bảo trong 1 chiều.

Nhìn vào các phần tử cuối (c, f, k), chúng ta có thể thực hiện một loại bộ lọc: N <c? search (): next (). Do đó, chúng ta có n lần lặp trên các hàng, với mỗi hàng lấy O (log (n)) cho tìm kiếm nhị phân hoặc O (1) nếu được lọc ra.

Hãy để tôi đưa ra một VÍ DỤ trong đó N = j,

1) Kiểm tra hàng 1. j <c? (không, hãy tiếp tục)

2) Kiểm tra hàng 2. j <f? (vâng, tìm kiếm trong bin không nhận được gì)

3) Kiểm tra hàng 3. j <k? (vâng, tìm kiếm trong bin tìm thấy nó)

Hãy thử lại với N = q,

1) Kiểm tra hàng 1. q <c? (không, hãy tiếp tục)

2) Kiểm tra hàng 2. q <f? (không, hãy tiếp tục)

3) Kiểm tra hàng 3. q <k? (không, hãy tiếp tục)

Có lẽ có một giải pháp tốt hơn ngoài đó nhưng điều này rất dễ giải thích .. :)


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.