왼쪽에서 오른쪽으로, 위에서 아래로 정렬 된 2D 배열에서 숫자를 어떻게 검색합니까?


90

나는 최근에이 인터뷰 질문을 받았는데 그것에 대한 좋은 해결책이 무엇인지 궁금합니다.

배열의 모든 숫자가 왼쪽에서 오른쪽으로, 위에서 아래로 증가하는 2d 배열이 있다고 가정 해 보겠습니다.

대상 번호가 어레이에 있는지 검색하고 확인하는 가장 좋은 방법은 무엇입니까?

이제 내 첫 번째 경향은 내 데이터가 정렬 되었기 때문에 이진 검색을 활용하는 것입니다. 숫자가 O (log N) 시간에 단일 행에 있는지 확인할 수 있습니다. 그러나 나를 쫓아내는 것은 두 가지 방향입니다.

내가 효과가 있다고 생각한 또 다른 해결책은 중간 어딘가에서 시작하는 것입니다. 중간 값이 내 목표보다 작 으면 중간에서 매트릭스의 왼쪽 정사각형 부분에 있는지 확인할 수 있습니다. 그런 다음 대각선으로 이동하고 다시 확인하여 대상 번호를 다듬을 때까지 대상이 잠재적으로 들어갈 수있는 사각형의 크기를 줄입니다.

누구든지이 문제를 해결하는 데 좋은 아이디어가 있습니까?

배열 예 :

왼쪽에서 오른쪽, 위에서 아래로 정렬됩니다.

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

간단한 질문 : 같은 값을 가진 이웃을 가질 수 [[1 1][1 1]]있는가?
Matthieu M.

답변:


115

다음은 간단한 접근 방식입니다.

  1. 왼쪽 하단에서 시작합니다.
  2. 목표가 그 값보다 작 으면 우리 위에 있어야하므로 하나 위로 이동 하십시오 .
  3. 그렇지 않으면 대상이 해당 열에있을 수 없다는 것을 알고 있으므로 오른쪽으로 이동하십시오 .
  4. 이동 2.

를 들어 NxM배열이 실행됩니다 O(N+M). 더 잘하기 어려울 것 같아요. :)


편집 : 많은 좋은 토론. 나는 위의 일반적인 경우에 대해 이야기하고있었습니다. 경우에 명확하게, N또는 M 작은, 당신은 로그 시간에 접근하는 것을에서이 작업을 수행하기 위해 이진 검색 방법을 사용할 수 있습니다.

궁금한 분들을위한 세부 정보는 다음과 같습니다.

역사

이 간단한 알고리즘을 새들백 검색 이라고합니다 . 한동안 사용되어 왔으며 N == M. 일부 참조 :

그러나, N < M직관은 이진 검색이 다음보다 더 잘할 수 있어야한다고 제안합니다 O(N+M). 예를 들어, N == 1순수 이진 검색은 선형 시간이 아닌 로그로 실행됩니다.

최악의 경우

Richard Bird는 2006 년 논문에서 이진 검색이 Saddleback 알고리즘을 향상시킬 수 있다는 직관을 조사했습니다.

다소 특이한 대화 기법을 사용하여 Bird는 N <= M에 대해이 문제의 하한이 Ω(N * log(M/N)). 이 한계는 우리에게 선형 성능을 제공 N == M하고 N == 1.

직사각형 배열에 대한 알고리즘

행 단위 이진 검색을 사용하는 한 가지 접근 방식은 다음과 같습니다.

  1. 직사각형 배열로 시작하십시오 N < M. N행과 M열 이라고합시다 .
  2. 중간 행에서 value. 우리가 그것을 찾으면 우리는 끝난 것입니다.
  3. 그렇지 않으면 인접한 숫자 쌍 sg, where s < value < g.
  4. 위와 왼쪽에있는 숫자의 사각형 s은보다 작 value으므로 제거 할 수 있습니다.
  5. 의 오른쪽 아래에있는 사각형 g이보다 크므로 value제거 할 수 있습니다.
  6. 나머지 두 직사각형 각각에 대해 (2) 단계로 이동합니다.

최악의 복잡성 측면에서이 알고리즘은 log(M)가능한 솔루션의 절반을 제거한 다음 두 개의 작은 문제에 대해 자신을 두 번 재귀 적으로 호출합니다. log(M)모든 행에 대해 더 작은 버전의 작업 을 반복해야 하지만 , 행 수가 열 수에 비해 적 으면 로그 시간에 해당 열을 모두 제거 할 수있는 것이 가치가 있기 시작합니다 .

이것은 알고리즘에 T(N,M) = log(M) + 2 * T(M/2, N/2)Bird가 보여주는 복잡성을 제공합니다 O(N * log(M/N)).

Craig Gidney가 게시 한 또 다른 접근 방식은 위의 접근 방식과 유사한 알고리즘을 설명합니다 M/N. 단계 크기를 사용하여 한 번에 행을 검사합니다 . 그의 분석은 이것이 O(N * log(M/N))성능에도 영향을 미친다는 것을 보여줍니다 .

성능 비교

Big-O 분석은 모두 훌륭하고 좋지만 이러한 접근 방식이 실제로 얼마나 잘 작동합니까? 아래 차트는 점점 더 "정사각형"배열에 대한 네 가지 알고리즘을 검사합니다.

알고리즘 성능 대 직각도

( "순진"알고리즘은 단순히 어레이의 모든 요소를 ​​검색합니다. "재귀"알고리즘은 위에 설명되어 있습니다. "하이브리드"알고리즘은 Gidney 알고리즘을 구현 한 것입니다 . 각 어레이 크기에 대해 각 알고리즘을 고정 세트에 타이밍하여 측정했습니다. 무작위로 생성 된 배열 1,000,000 개)

몇 가지 주목할 점 :

  • 예상대로 "이진 검색"알고리즘은 직사각형 배열에서 최고의 성능을 제공하고 Saddleback 알고리즘은 정사각형 배열에서 가장 잘 작동합니다.
  • Saddleback 알고리즘은 아마도 각 항목에 대해 다중 비교를 수행하기 때문에 1-d 배열의 "순진한"알고리즘보다 성능이 떨어집니다.
  • "이진 검색"알고리즘이 정사각형 배열에 미치는 성능 저하는 아마도 반복되는 이진 검색을 실행하는 오버 헤드 때문일 것입니다.

요약

이진 검색을 영리하게 사용 O(N * log(M/N)하면 직사각형 및 정사각형 배열 모두에 성능을 제공 할 수 있습니다 . O(N + M)"안장"알고리즘은 매우 간단하지만 성능 저하의 배열과 같은 겪고있다 점점 직사각형이된다.


6
이진 검색을 대각선 걷기에 적용하면 O (logN) 또는 O (logM) 중 더 높은 값이 있습니다.
Anurag

4
@Anurag-복잡성이 잘 작동하지 않는다고 생각합니다. 이진 검색은 시작하기에 좋은 위치를 제공하지만 한 차원 또는 다른 차원을 끝까지 걸어야하며 최악의 경우에도 여전히 한 구석에서 시작하고 다른 구석에서 끝날 수 있습니다.
Jeffrey L Whitledge

1
N = 1이고 M = 1000000이면 O (N + M)보다 더 잘할 수 있으므로 다른 솔루션은 O (N * log (M))를 가져 오는 각 행에 이진 검색을 적용하는 것입니다. 더 작은 상수.
Luka Rahne

1
나는 당신의 방법과 이진 검색 방법을 모두 사용하여 몇 가지 테스트를 수행하고 여기 에 결과를 게시했습니다 . 두 방법 모두에 대해 최악의 조건을 제대로 생성하지 못한 경우 지그재그 방법이 가장 좋습니다.
The111

1
참조의 좋은 사용! 그러나 복잡성 M==N을 원할 때는 후자가 0이 O(N)아니기 O(N*log(N/N))때문입니다. 날카로운 바인딩 "통일"A 올바른입니다 O(N*(log(M/N)+1))N<=M.
hardmath

35

이 문제는 소요 Θ(b lg(t))시간 b = min(w,h)t=b/max(w,h). 이 블로그 게시물 에서 솔루션에 대해 논의합니다 .

하한

공격자는 Ω(b lg(t))자신을 주 대각선으로 제한 하여 알고리즘이 쿼리 를 만들도록 강제 할 수 있습니다 .

주 대각선을 사용하는 적

범례 : 흰색 셀은 작은 항목, 회색 셀은 큰 항목, 노란색 셀은 작거나 같은 항목, 주황색 셀은 크거나 같은 항목입니다. 공격자는 알고리즘이 쿼리가 지속되는 노란색 또는 주황색 셀이되도록 솔루션을 강제합니다.

공지 사항이 있다는 것을 b크기의 독립적 인 정렬 된 목록을 t필요로 Ω(b lg(t))완전히 제거하기 위해 쿼리.

연산

  1. (일반성을 잃지 않고 가정 w >= h)
  2. 대상 항목을 t유효한 영역의 오른쪽 상단 모서리 왼쪽 에있는 셀과 비교합니다.
    • 셀의 항목이 일치하면 현재 위치를 반환합니다.
    • 셀의 항목이 대상 항목보다 작 으면 t이진 검색을 사용하여 행 의 나머지 셀을 제거합니다 . 이 작업을 수행하는 동안 일치하는 항목이 발견되면 해당 위치로 돌아갑니다.
    • 그렇지 않으면 셀의 항목이 대상 항목보다 많아 t짧은 열이 제거 됩니다.
  3. 유효한 영역이 남아 있지 않으면 실패를 반환합니다.
  4. 2 단계로 이동

항목 찾기 :

항목 찾기

항목이 존재하지 않는지 확인 :

항목이 존재하지 않는지 확인

범례 : 흰색 셀은 작은 항목, 회색 셀은 큰 항목, 녹색 셀은 동일한 항목입니다.

분석

b*t제거 할 짧은 열 이 있습니다 . b제거 할 긴 행 이 있습니다 . 긴 행을 제거하면 O(lg(t))시간이 걸립니다. t짧은 컬럼을 제거하면 O(1)시간이 걸립니다.

최악의 경우 모든 열과 모든 행을 제거해야하므로 시간이 걸립니다 O(lg(t)*b + b*t*1/t) = O(b lg(t)).

lg1 이상의 결과에 대한 클램프를 가정 하고 있습니다 (예 :) lg(x) = log_2(max(2,x)). 그래서 w=h, 의미 t=1, 우리는 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
흥미롭고 아마도 부분적으로 내 머리 위로. 이 "적대적인"스타일의 복잡성 분석에 익숙하지 않습니다. 공격자가 검색 할 때 실제로 어떻게 든 동적으로 배열을 변경합니까, 아니면 최악의 검색에서 발생하는 불운에 대한 이름 일 뿐입니 까?
The111

2
@ The111 불운은 지금까지 본 것을 위반하지 않는 나쁜 길을 선택하는 사람과 동일하므로 두 정의 모두 동일하게 작동합니다. 저는 실제로 계산 복잡성과 관련하여 기술을 구체적으로 설명하는 링크를 찾는 데 어려움을 겪고 있습니다 ... 저는 이것이 훨씬 더 잘 알려진 아이디어라고 생각했습니다.
Craig Gidney

로그 (1) = 0이므로, 복잡도 추정치로서 제공해야 O(b*(lg(t)+1))보다는 O(b*lg(t)). 좋은 글, 특히. "최악의 경우"경계를 표시하는 "적대적 기술"에주의를 환기시킨
hardmath

@hardmath 나는 대답에서 그것을 언급합니다. 나는 그것을 약간 명확히했다.
Craig Gidney 2014-06-22

17

이 문제에 대해 여러분이 제안한 것과 유사한 분할 및 정복 전략을 사용하지만 세부 사항은 약간 다릅니다.

이것은 행렬의 하위 범위에 대한 재귀 검색입니다.

각 단계에서 범위 중간에있는 요소를 선택합니다. 찾은 값이 찾고있는 것이라면 완료된 것입니다.

그렇지 않고 발견 된 값이 찾고있는 값보다 작 으면 현재 위치의 위와 왼쪽에있는 사분면에 없다는 것을 알 수 있습니다. 따라서 두 하위 범위를 재귀 적으로 검색합니다. 현재 위치 아래에있는 모든 것 (배타적으로)과 현재 위치에 있거나 위에있는 오른쪽에있는 모든 것 (배타적으로)입니다.

그렇지 않으면 (찾은 값이 찾고있는 값보다 큽니다) 현재 위치의 오른쪽 아래 사분면에 없다는 것을 알고 있습니다. 따라서 두 하위 범위를 재귀 적으로 검색합니다. 현재 위치의 왼쪽에있는 모든 항목 (배타적)과 현재 열 또는 오른쪽의 열에있는 현재 위치 위에있는 모든 항목 (배타적)을 검색합니다.

그리고 ba-da-bing, 당신은 그것을 찾았습니다.

각 재귀 호출은 현재 위치 위의 모든 행이 아닌 현재 하위 범위 만 처리합니다. 현재 하위 범위에있는 것들만.

다음은 몇 가지 의사 코드입니다.

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 : 이것은 O (log (N)) 전략이므로 얻을 수있는 주문만큼 좋습니다.
Rex Kerr

3
@Rex Kerr-일반적인 이진 검색이기 때문에 O (log (N))처럼 보이지만 각 수준에서 잠재적으로 두 번의 재귀 호출이 있습니다. 이것은 일반 대수보다 훨씬 나쁘다는 것을 의미합니다. 잠재적으로 모든 행 또는 모든 열을 검색해야하기 때문에 더 나쁜 경우가 O (M + N)보다 낫다고 생각하지 않습니다. 하지만이 알고리즘은 많은 값에 대해 최악의 경우를 이길 수 있다고 생각합니다. 그리고 가장 중요한 부분은 병렬화가 가능하다는 것입니다. 그 이유는 최근 하드웨어가 지향하는 곳이기 때문입니다.
Jeffrey L Whitledge

1
@JLW : O (log (N))이지만 실제로는 O (log_ (4/3) (N ^ 2)) 또는 이와 비슷한 것입니다. 아래 Svante의 답변을 참조하십시오. 귀하의 대답은 실제로 동일합니다 (내 생각대로 재귀를 의미한다면).
Rex Kerr

1
@Svante-하위 배열이 겹치지 않습니다. 첫 번째 옵션에서는 공통된 y 요소가 없습니다. 두 번째 옵션에서는 공통된 x 요소가 없습니다.
Jeffrey L Whitledge

1
이것이 로그인 지 확실하지 않습니다. 대략적인 반복 관계 T (0) = 1, T (A) = T (A / 2) + T (A / 4) + 1을 사용하여 복잡성을 계산했습니다. 여기서 A는 검색 영역이고 T ( A) = O (Fib (lg (A))), 대략 O (A ^ 0.7)이고 O (A ^ 0.5) 인 O (n + m)보다 나쁩니다. 어쩌면 내가 어리석은 실수를했을 수도 있지만, 알고리즘이 결실없는 지점으로 내려가는 데 많은 시간을 낭비하고있는 것 같습니다.
Craig Gidney

6

지금까지 제시된 두 가지 주요 답변은 O(log N)"ZigZag 방법"과 O(N+M)이진 검색 방법 인 것 같습니다. 나는 두 가지 방법을 다양한 설정과 비교하여 몇 가지 테스트를 할 것이라고 생각했습니다. 세부 사항은 다음과 같습니다.

배열은 모든 테스트에서 N x N 정사각형이며 N은 125에서 8000까지 다양합니다 (제 JVM 힙이 처리 할 수있는 최대 크기). 각 배열 크기에 대해 배열에서 임의의 위치를 ​​선택하여 단일 2. 그런 다음 3가능한 모든 곳에 (2의 오른쪽과 아래에) 배치하고 나머지 배열을 채웠습니다.1. 일부 이전 댓글 작성자는 이러한 유형의 설정이 두 알고리즘 모두에 대해 최악의 런타임을 생성 할 것이라고 생각하는 것처럼 보였습니다. 각 어레이 크기에 대해 2 (검색 대상)에 대해 100 개의 서로 다른 무작위 위치를 선택하고 테스트를 실행했습니다. 각 알고리즘에 대해 평균 실행 시간과 최악의 실행 시간을 기록했습니다. Java에서 좋은 ms 판독 값을 얻기에는 너무 빨라서 Java의 nanoTime ()을 신뢰하지 않기 때문에 항상 균일 한 편향 계수를 추가하기 위해 각 테스트를 1000 번 반복했습니다. 결과는 다음과 같습니다.

여기에 이미지 설명 입력

ZigZag는 평균 및 최악의 경우 모든 테스트에서 바이너리를 이겼지 만, 모두 서로 어느 정도의 크기 내에 있습니다.

다음은 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 예, 데이터. :)이 두 가지 접근 방식이 NxM 어레이에서 어떻게 작동하는지 보는 것도 흥미로울 수 있습니다. 이진 검색은 우리가 1 차원 사례에 접근할수록 직관적으로 더 유용 해져야하기 때문입니다.
Nate Kohl

5

이것은 문제의 하한에 대한 짧은 증거입니다.

선형 시간보다 더 잘 할 수는 없습니다 (요소 수가 아닌 배열 차원 측면에서). 아래 배열에서로 표시된 각 요소 *는 5 개 또는 6 개 (다른 요소와 독립적)가 될 수 있습니다. 따라서 목표 값이 6 (또는 5)이면 알고리즘은 이들 모두를 검사해야합니다.

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

물론 이것은 더 큰 어레이로 확장됩니다. 이것은 이 답변 이 최적 임을 의미합니다 .

업데이트 : Jeffrey L Whitledge가 지적했듯이 실행 시간 대 입력 데이터 크기 (단일 변수로 처리)에 대한 점근 적 하한으로 만 최적입니다. 두 배열 차원에서 두 변수 함수로 처리되는 실행 시간을 개선 할 수 있습니다.


그 대답이 최적이라는 것을 보여주지 않았습니다. 예를 들어, 5 번째 행에 대상 값보다 높은 값이 모두 포함 된 10 개와 100 만 개의 배열이 있다고 가정 해 보겠습니다. 이 경우 제안 된 알고리즘은 목표에 가까워지기 전에 999,995 개의 값까지 linier 검색을 수행합니다. 저와 같은 분기 알고리즘은 목표에 접근하기 전에 18 개의 값만 검색합니다. 그리고 그것은 (비대칭 적으로) 다른 모든 경우에서 제안 된 알고리즘보다 나쁘지 않습니다.
Jeffrey L Whitledge

@Jeffrey : 그것은 비관적 인 경우에 대한 문제 의 하한 입니다. 좋은 입력을 위해 최적화 할 수 있지만 선형보다 더 잘할 수없는 입력이 있습니다.
Rafał Dowgird

예, 선형보다 더 잘할 수없는 입력이 있습니다. 어떤 경우에 내 알고리즘은 선형 검색을 수행합니다. 그러나 당신이 할 수있는 다른 입력이 있습니다 방법을 선형보다 더 나은. 따라서 제안 된 솔루션은 항상 선형 검색을 수행하기 때문에 최적이 아닙니다.
Jeffrey L Whitledge

이것은 알고리즘이 BigOmega (n + m)가 아니라 BigOmega (min (n, m)) 시간을 가져야 함을 보여줍니다. 그렇기 때문에 한 차원이 훨씬 더 작을 때 훨씬 더 잘할 수 있습니다. 예를 들어 행이 1 개만 있다는 것을 알고 있다면 로그 시간으로 문제를 해결할 수 있습니다. 최적의 알고리즘은 시간 O (min (n + m, n lg m, m lg n))이 걸릴 것이라고 생각합니다.
Craig Gidney

그에 따라 답변을 업데이트했습니다.
Rafał Dowgird

4

여기에 답이 있다고 생각하며 모든 종류의 정렬 된 행렬에서 작동합니다.

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

흥미로운 질문입니다. 이 아이디어를 고려하십시오-모든 숫자가 목표보다 큰 경계와 모든 숫자가 목표보다 작은 경계를 만드십시오. 둘 사이에 아무것도 남지 않는다면 그것이 당신의 목표입니다.

귀하의 예에서 3을 찾고 있다면 4가 될 때까지 첫 번째 행을 읽은 다음 3보다 큰 가장 작은 인접 숫자 (대각선 포함)를 찾습니다.

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

이제 3보다 작은 숫자에 대해서도 똑같이합니다.

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

이제 두 경계 안에있는 것이 있습니까? 그렇다면 3이어야합니다. 그렇지 않으면 3이 없습니다. 실제로 숫자를 찾지 못하기 때문에 간접적으로 숫자가 있어야한다고 추론합니다. 이것은 모든 3을 세는 추가 보너스가 있습니다.

나는 이것을 몇 가지 예에서 시도했지만 정상적으로 작동하는 것 같습니다.


댓글이없는 반대표? 최악의 성능은 대각선을 확인해야하기 때문에 O (N ^ 1 / 2)라고 생각합니다. 적어도이 방법이 작동하지 않는 카운터 예제를 보여주세요!
Grembo

+1 : 좋은 솔루션 ... 창의적이며 모든 솔루션을 찾는 것이 좋습니다.
Tony Delroy 2011 년

1

배열의 대각선을 통한 이진 검색이 가장 좋은 옵션입니다. 요소가 대각선의 요소보다 작거나 같은지 확인할 수 있습니다.


0

A. 대상 번호가있을 수있는 라인에서 이진 검색을 수행하십시오.

B. 그래프로 만들기 : 항상 방문하지 않은 가장 작은 이웃 노드를 취하고 너무 큰 숫자가 발견되면 역 추적하여 숫자를 찾습니다.


0

이진 검색이 가장 좋은 방법입니다. 1/2 x부터 1/2 y는 반으로 자릅니다. IE 5x5 정사각형은 x == 2 / y == 3과 같습니다. 목표 값의 방향으로 더 나은 영역으로 하나의 값을 반올림하고 하나의 값을 반올림했습니다.

명확성을 위해 다음 반복은 x == 1 / y == 2 OR x == 3 / y == 5


0

우선 사각형을 사용한다고 가정 해 보겠습니다.

1 2 3
2 3 4
3 4 5

1. 사각형 검색

대각선에서 이진 검색을 사용합니다. 목표는 목표 수보다 엄격히 낮지 않은 작은 수를 찾는 것입니다.

내가 찾고 있어요 말해 4, 예를 들어, 그때의 위치를 끝낼 것이다 5(2,2).

그런 다음 4테이블에있는 경우 (x,2)또는 in (2,x)과 함께 위치에 있다고 확신합니다 . 음, 그것은 단지 2 개의 이진 검색입니다.x[0,2]

복잡성은 어렵지 않습니다. O(log(N))(길이 범위에 대한 3 개의 이진 검색 N)

2. 직사각형 탐색, 순진한 접근

물론 직사각형이있는 경우 NM다를 때 조금 더 복잡해집니다 .

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

그리고 내가 찾고 있다고 가정 해 봅시다 9. 대각선 접근 방식은 여전히 ​​좋지만 대각선의 정의는 변경됩니다. 여기 내 대각선은 [1, (5 or 6), 17]입니다. 내가 picked했다고 가정 하면 테이블에 있으면 하위 부분에 [1,5,17]있음을 압니다 9.

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

이것은 2 개의 직사각형을 제공합니다.

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

그래서 우리는 재귀 할 수 있습니다! 아마도 더 적은 요소를 가진 것으로 시작될 것입니다 (이 경우 우리를 죽이기는하지만).

차원 중 하나가보다 작 으면 3대각 방법을 적용 할 수 없으며 이진 검색을 사용해야합니다. 여기서 의미는 다음과 같습니다.

  • 에 바이너리 검색 적용 10 11 12 13 14 15 16, 찾을 수 없음
  • 에 바이너리 검색 적용 5 6 7 8, 찾을 수 없음
  • 에 바이너리 검색 적용 6 7 8 9, 찾을 수 없음

좋은 성능을 얻으려면 일반적인 모양에 따라 여러 경우를 구별하고 싶을 수 있기 때문에 까다 롭습니다 ....

3. 직사각형 검색, 잔인한 접근

정사각형을 다루면 훨씬 쉬울 것입니다. 그러니 그냥 정사각형으로합시다.

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

이제 사각형이 생겼습니다.

물론 우리는 실제로 그러한 행을 생성하지 않을 것이며 단순히 에뮬레이트 할 수 있습니다.

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

그래서 그것은 더 많은 메모리를 차지하지 않고 정사각형처럼 작동합니다 (아마도 캐시에 따라 속도를 희생하면 ... 아 글쎄 : p)


0

편집하다:

나는 질문을 오해했다. 의견이 지적했듯이 이것은 더 제한된 경우에만 작동합니다.

행 우선 순서로 데이터를 저장하는 C와 같은 언어에서는 단순히 n * m 크기의 1D 배열로 취급하고 이진 검색을 사용합니다.


예, 필요 이상으로 복잡하게 만드는 이유는 무엇입니까?
erikkallen

배열 따라서 더 빈 검색이 적용되지 않을 수 정렬되지 않습니다
Miollnyr

1
이것은 각 행의 마지막 요소가 다음 행의 첫 번째 요소보다 높은 경우에만 작동하며, 이는 문제가 제안하는 것보다 훨씬 더 제한적인 요구 사항입니다.
Jeffrey L Whitledge

감사합니다. 제 답변을 수정했습니다. 특히 예제 배열을 충분히주의 깊게 읽지 않았습니다.
Hugh Brackett

0

재귀 적 Divide & Conquer Solution이 있습니다. 한 단계에 대한 기본 아이디어는 다음과 같습니다. Left-Upper (LU)가 가장 작고 오른쪽 하단 (RB)이 가장 큰 번호라는 것을 알고 있으므로 주어진 No (N)는 N> = LU 및 N <=이어야합니다. RB

N == LU 및 N == RB :::: 요소를 찾은 후 위치 / 인덱스를 반환하는 중단 N> = LU 및 N <= RB = FALSE이면 No가없고 중단됩니다. N> = LU이고 N <= RB = TRUE이면 2D 배열을 2D 배열의 4 개의 동일한 부분으로 각각 논리적으로 나눕니다. 그런 다음 4 개의 하위 배열 모두에 동일한 algo 단계를 적용합니다.

내 알고리즘이 맞습니다. 친구 PC에 구현했습니다. 복잡성 : 각 4 개의 비교는 최악의 경우 총 요소 수를 1/4로 추론하는 데 사용할 수 있습니다. 그래서 내 복잡성은 1 + 4 x lg (n) + 4가됩니다. (엔)

복잡도 계산 어딘가에 문제가 있다고 생각합니다. 그렇다면 수정하십시오 ..


0

최적의 솔루션은 최소한의 값이있는 왼쪽 상단 모서리에서 시작하는 것입니다. 주어진 요소의 값이> = 값인 요소를 칠 때까지 대각선으로 오른쪽 아래로 이동합니다. 요소의 값이 주어진 요소의 값과 같으면 found를 true로 반환합니다.

그렇지 않으면 여기에서 두 가지 방법으로 진행할 수 있습니다.

전략 1 :

  1. 열에서 위로 이동하고 끝에 도달 할 때까지 주어진 요소를 검색합니다. 발견되면 true로 반환
  2. 행에서 왼쪽으로 이동하고 끝에 도달 할 때까지 주어진 요소를 검색합니다. 발견되면 true로 반환
  3. 거짓으로 발견 된 반환

전략 2 : i는 행 인덱스를 나타내고 j는 우리가 멈춘 대각선 요소의 열 인덱스를 나타냅니다. (여기에 i = j, BTW가 있습니다). k = 1이라고합시다.

  • ik> = 0이 될 때까지 아래 단계를 반복하십시오.
    1. a [ik] [j]가 주어진 요소와 같은지 검색합니다. 그렇다면 true로 반환됩니다.
    2. a [i] [jk]가 주어진 요소와 같은지 검색합니다. 그렇다면 true로 반환됩니다.
    3. 증가 k

12 4 5 6
2 3 5 78
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

모든 문자를 2D list. 그런 다음 목록에있는 경우 필요한 요소의 색인을 찾습니다.

없는 경우 적절한 메시지를 인쇄하고 그렇지 않으면 행과 열을 다음과 같이 인쇄하십시오.

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

목록에서 이진 검색 시간 만 발생합니다.

수정 사항을 제안하십시오. :)


0

경우 O (M 로그 (N)) 용액을는 M × N 어레이에 대한 확인 인 -

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

작동하는 C ++ 데모.

이것이 작동하지 않거나 버그가 있으면 알려주십시오.


0

저는 10 년 동안 인터뷰에서이 질문을 해왔고 최적의 알고리즘을 생각 해낼 수있는 사람은 단 한 명 뿐이라고 생각합니다.

내 솔루션은 항상 다음과 같습니다.

  1. 이진 검색 중간 대각선은 아래로 내려가는 대각선이며 (rows.count/2, columns.count/2).

  2. 대상 번호를 찾으면 true를 반환합니다.

  3. 그렇지 않으면, 두 개의 숫자 ( u그리고 v)되도록 발견 된 것이다 u목표보다 작은 경우, v목표보다 큰 경우, 그리고 v하나의 우측에서 하나의 다운 u.

  4. 의 오른쪽 u과 위쪽에 있는 부분 행렬 v과의 아래쪽 u과 왼쪽에 있는 부분 행렬을 재귀 적으로 검색합니다 v.

대각선을 검색하면 (행렬이 정사각형에 가까운 경우) 검색 공간의 절반 이상을 줄일 수 있지만 행이나 열을 검색하면 항상 제거가 발생하기 때문에 이것이 Nate가 제공 한 알고리즘보다 엄격한 개선이라고 생각합니다. 정확히 절반입니다.

다음은 Swift의 코드입니다.

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

다음과 같이 정사각형 행렬이 주어집니다.

[abc]
[데프]
[ijk]

우리는 a <c, d <f, i <k를 알고 있습니다. 우리가 모르는 것은 d <c 또는 d> c 등입니다. 우리는 1 차원에서만 보증합니다.

끝 요소 (c, f, k)를 살펴보면 일종의 필터를 수행 할 수 있습니다. is N <c? search () : next (). 따라서 행에 대해 n 개의 반복이 있으며 각 행은 이진 검색의 경우 O (log (n)) 또는 필터링 된 경우 O (1)를 사용합니다.

N = j 인 예를 들어 보겠습니다.

1) 행 1을 확인하십시오. j <c? (아니, 다음으로)

2) 행 2를 확인하십시오. j <f? (예, 빈 검색은 아무것도 얻지 못합니다)

3) 행 3을 확인하십시오. j <k? (예, 빈 검색이 찾습니다)

N = q로 다시 시도하십시오.

1) 행 1을 확인하십시오. q <c? (아니, 다음으로)

2) 행 2를 확인하십시오. q <f? (아니, 다음으로)

3) 행 3을 확인하십시오. q <k? (아니, 다음으로)

아마 더 나은 해결책이 있겠지만 이것은 설명하기 쉽습니다 .. :)


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.