큰 단어 시퀀스에서 상위 K 개의 빈번한 단어를 찾는 가장 효율적인 방법


86

입력 : 양의 정수 K와 큰 텍스트. 텍스트는 실제로 단어 시퀀스로 볼 수 있습니다. 그래서 우리는 그것을 단어 순서로 나누는 방법에 대해 걱정할 필요가 없습니다.
출력 : 텍스트에서 가장 빈번한 K 단어.

제 생각은 이렇습니다.

  1. 해시 테이블을 사용하여 전체 단어 시퀀스를 순회하면서 모든 단어의 빈도를 기록합니다. 이 단계에서 키는 "단어"이고 값은 "단어 빈도"입니다. O (n) 시간이 걸립니다.

  2. (단어, 단어 빈도) 쌍을 정렬하십시오. 핵심은 "단어 빈도"입니다. 일반 정렬 알고리즘에서는 O (n * lg (n)) 시간이 걸립니다.

  3. 정렬 후 처음 K 단어 만 가져옵니다. 이것은 O (K) 시간이 걸립니다.

요약하면 총 시간은 O (n + n lg (n) + K) , K는 N보다 확실히 작기 때문에 실제로는 O (n lg (n))입니다.

우리는 이것을 개선 할 수 있습니다. 사실 우리는 상위 K 단어를 원합니다. 다른 단어의 빈도는 우리에게 중요하지 않습니다. 따라서 "부분 힙 정렬"을 사용할 수 있습니다. 2) 및 3) 단계에서는 정렬 만 수행하지 않습니다. 대신, 우리는 그것을

2 ') "단어 빈도"를 키로 사용하여 (단어, 단어 빈도) 쌍의 힙을 작성하십시오. 힙을 빌드하는 데 O (n) 시간이 걸립니다.

3 ') 힙에서 상위 K 단어를 추출합니다. 각 추출은 O (lg (n))입니다. 따라서 총 시간은 O (k * lg (n))입니다.

요약하면,이 솔루션은 시간 O (n + k * lg (n))을 소모합니다.

이것은 단지 내 생각입니다. 1 단계를 개선 할 방법을 찾지 못했습니다.
일부 정보 검색 전문가가이 질문에 대해 더 많은 정보를 얻을 수 있기를 바랍니다.


병합 정렬 또는 빠른 정렬을 O (n * logn) 정렬에 사용 하시겠습니까?
committedandroider

1
실제 사용을 위해 Aaron Maenpaa의 샘플 계수에 대한 답변 이 가장 좋습니다. 그것은하지 같은 가장 흔한 샘플에서 숨 깁니다 단어. 복잡성 괴짜에게는 샘플 크기가 고정되어 있으므로 O (1)입니다. 정확한 카운트를 얻지는 못하지만 그들도 요구하지 않습니다.
Nikana Reklawyks 2015

당신이 원하는 것이 당신의 복잡성 분석에 대한 검토라면, 나는 더 잘 언급하고 싶습니다 : n 이 당신의 텍스트에있는 단어의 수이고 m다른 단어의 수 (유형, 우리는 그들을 부른다)라면, 1 단계는 O ( n ), 그러나 2 단계는 O ( m .lg ( m ))이고 m << n입니다 (십억 개의 단어가 있고 백만 개의 유형에 도달하지 못할 수도 있습니다. 시도해보십시오). 따라서 더미 알고리즘을 사용하더라도 여전히 O ( n + m lg ( m )) = O ( n )입니다.
Nikana Reklawyks

1
Pls는 우리가 큰 텍스트의 모든 단어를 담을 수있을만큼 충분한 메인 메모리가 있다는 가정을 추가합니다. 10GB 파일에서 k = 100 단어를 찾는 방법을 보는 것은 흥미로울 것입니다 (즉, 모든 단어가 4GB RAM에 맞지 않습니다) !!
KGhatak

@KGhatak RAM 크기를 초과하면 어떻게할까요?
user7098526

답변:


67

이것은 O (n) 시간 내에 수행 될 수 있습니다.

해결책 1 :

단계 :

  1. 단어를 세고 해시하면 다음과 같은 구조가됩니다.

    var hash = {
      "I" : 13,
      "like" : 3,
      "meow" : 3,
      "geek" : 3,
      "burger" : 2,
      "cat" : 1,
      "foo" : 100,
      ...
      ...
    
  2. 해시를 탐색하여 가장 자주 사용되는 단어 (이 경우 "foo"100)를 찾은 다음 해당 크기의 배열을 만듭니다.

  3. 그런 다음 해시를 다시 탐색하고 단어 발생 수를 배열 인덱스로 사용할 수 있습니다. 인덱스에 아무것도 없으면 배열을 만들고 배열에 추가합니다. 그런 다음 다음과 같은 배열로 끝납니다.

      0   1      2            3                  100
    [[ ],[cat],[burger],[like, meow, geek],[]...[foo]]
    
  4. 그런 다음 끝에서 배열을 횡단하고 k 단어를 수집합니다.

해결책 2 :

단계 :

  1. 같은 상기와
  2. 최소 힙을 사용하고 최소 힙의 크기를 k로 유지하고 해시의 각 단어에 대해 단어 발생을 최소와 비교합니다. 1) 최소값보다 크면 최소값을 제거합니다 (최소 힙은 k와 같음) 최소 힙에 숫자를 삽입합니다. 2) 간단한 조건을 쉬십시오.
  3. 배열을 순회 한 후 최소 힙을 배열로 변환하고 배열을 반환합니다.

16
솔루션 (1)은 표준 O (n lg n) 비교 정렬을 대체하는 O (n) 버킷 정렬입니다. 접근 방식은 버킷 구조를위한 추가 공간이 필요하지만 비교 정렬은 제자리에서 수행 할 수 있습니다. 솔루션 (2)는 O (n lg k) 시간에 실행됩니다. 즉, O (n)은 모든 단어를 반복하고 O (lg k)는 각 단어를 힙에 추가합니다.
stackoverflowuser2010

4
첫 번째 솔루션은 더 많은 공간을 필요로하지만 실제로 시간상 O (n)임을 강조하는 것이 중요합니다. 1 : 단어로 키가 지정된 해시 주파수, O (n); 2 : 주파수 해시를 통과하고 주파수로 키가 지정된 두 번째 해시를 만듭니다. 이것은 해시를 순회하는 O (n)이고 해당 빈도로 단어 목록에 단어를 추가하는 O (1)입니다. 3 : k를 누를 때까지 최대 주파수에서 아래로 해시를 이동합니다. 기껏해야 O (n). 합계 = 3 * O (n) = O (n).
BringMyCakeBack 2014

3
일반적으로 단어를 계산할 때 솔루션 1의 버킷 수는 광범위하게 과대 평가되므로 (가장 자주 사용되는 단어 중 하나가 두 번째 및 세 번째로 좋은 단어보다 훨씬 더 자주 발생하기 때문에) 배열이 희소하고 비효율적입니다.
Nikana Reklawyks

k (빈번한 단어 수)가 가장 빈번한 단어 (즉,이 경우 100)의 발생 횟수보다 적 으면 솔루션 # 1이 작동하지 않습니다. 물론 실제로는 발생하지 않을 수 있지만 가정하지 마십시오!
One Two Three

@OneTwoThree 제안 된 솔루션은 단지 예일뿐입니다. 숫자는 수요에 따라 결정됩니다.
Chihung Yu

22

설명한 솔루션보다 일반적으로 더 나은 런타임을 얻을 수는 없습니다. 모든 단어를 평가하기 위해 최소한 O (n) 작업을 수행 한 다음 상위 k 개 용어를 찾기 위해 O (k) 추가 작업을 수행해야합니다.

문제 세트가 정말 큰 경우 map / reduce와 같은 분산 솔루션을 사용할 수 있습니다. n 명의지도 작업자가 각각 텍스트의 1 / n에서 빈도를 세게하고 각 단어에 대해 단어의 해시를 기반으로 계산 된 m 개의 감속기 작업자 중 하나에게 전송합니다. 감속기는 카운트를 합산합니다. 감속기의 출력에 대한 병합 정렬은 인기순으로 가장 인기있는 단어를 제공합니다.


13

솔루션의 작은 변형 은 상위 K 순위에 신경 쓰지 않으면 O (n) 알고리즘을 생성 하고 O (n + k * lg (k)) 솔루션을 생성합니다. 나는이 두 경계가 일정한 요인 내에서 최적이라고 믿습니다.

여기에서 최적화는 목록을 실행하여 해시 테이블에 삽입 한 후 다시 발생합니다. 중앙값 알고리즘 의 중앙값을 사용 하여 목록에서 K 번째로 큰 요소를 선택할 수 있습니다 . 이 알고리즘은 증명할 수있는 O (n)입니다.

K 번째로 작은 요소를 선택한 후 퀵 정렬에서와 마찬가지로 해당 요소를 기준으로 목록을 분할합니다. 이것은 분명히 O (n)입니다. 피벗의 "왼쪽"에있는 모든 것은 K 요소 그룹에 있으므로 완료되었습니다 (계속 진행하면서 다른 모든 것을 버릴 수 있음).

따라서이 전략은 다음과 같습니다.

  1. 각 단어를 살펴보고 해시 테이블에 삽입합니다. O (n)
  2. K 번째로 작은 요소 선택 : O (n)
  3. 해당 요소 주변의 파티션 : O (n)

K 요소의 순위를 매기려면 O (k * lg (k)) 시간에 효율적인 비교 정렬로 정렬하면 총 실행 시간이 O (n + k * lg (k))가됩니다.

O (n) 시간 제한은 각 단어를 한 번 이상 검사해야하기 때문에 상수 요인 내에서 최적입니다.

O (n + k * lg (k)) 시간 제한은 k * lg (k) 시간 미만으로 k 요소를 정렬하는 비교 기반 방법이 없기 때문에 최적입니다.


K 번째 가장 작은 요소를 선택하면 K 번째로 작은 해시 키가 선택됩니다. 3 단계의 왼쪽 분할 영역에 정확히 K 개의 단어가있을 필요는 없습니다.
Prakash Murali

2
스왑을 수행하므로 해시 테이블에서 "중앙값 중앙값"을 실행할 수 없습니다. 해시 테이블의 데이터를 임시 배열로 복사해야합니다. 따라서 O (n) 스토리지가 필요합니다.
user674669

O (n)에서 K 번째로 작은 요소를 어떻게 선택할 수 있는지 모르겠습니다.
마이클 호 미끼

O (n)에서 K 번째로 작은 요소를 찾는 알고리즘을 확인하십시오.- wikiwand.com
Piyush

해시 테이블 + 최소 힙을 사용하더라도 복잡성은 동일합니다. 최적화가 보이지 않습니다.
Vinay

8

"큰 단어 목록"이 충분히 크면 간단히 샘플링하여 견적을 얻을 수 있습니다. 그렇지 않으면 해시 집계를 좋아합니다.

편집 :

샘플을 통해 일부 페이지 하위 집합을 선택하고 해당 페이지에서 가장 자주 사용되는 단어를 계산합니다. 합리적인 방법으로 페이지를 선택하고 통계적으로 유의미한 샘플을 선택했다면 가장 빈번한 단어에 대한 추정치는 합리적이어야합니다.

이 접근 방식은 데이터가 너무 많아 모든 것을 처리하는 것이 어리석은 경우에만 합리적입니다. 메그가 몇 개 밖에 없다면 추정값을 계산하는 데 신경 쓰지 않고 데이터를 찢고 정확한 답을 계산할 수 있어야합니다.


예를 들어 웹 사이트 또는 주제별로 자주 사용하는 단어 목록을 얻으려면이 작업을 여러 번 반복해야합니다. 그럴 때 "땀을 흘리지 않고"는 실제로 그것을 자르지 않습니다. 가능한 한 효율적으로 할 수있는 방법을 찾아야합니다.
itsadok

1
관련없는 복잡성 문제를 해결하지 않는 실용적인 답변에 +1. @itsadok : 각 실행에 대해 : 충분히 크면 샘플링하십시오. 그렇지 않은 경우 로그 계수를 얻는 것은 관련이 없습니다.
Nikana Reklawyks

2

단어의 첫 글자를 사용하여 분할 한 다음 k 개의 단일 단어 세트를 가질 때까지 다음 문자를 사용하여 가장 큰 다중 단어 세트를 분할하여 시간을 더 줄일 수 있습니다. 리프에 부분 / 완전 단어 목록이있는 일종의 256-way 트리를 사용합니다. 모든 곳에서 문자열을 복사하지 않도록 매우주의해야합니다.

이 알고리즘은 O (m)이며, 여기서 m은 문자 수입니다. k에 대한 의존성을 피합니다. 이것은 큰 k에 매우 좋습니다. [게시 된 실행 시간이 잘못 되었기 때문에 O (n * lg (k)) 여야하며, 그게 무엇인지 잘 모르겠습니다. 미디엄].

두 알고리즘을 나란히 실행하면 점근 적으로 최적 인 O (min (m, n * lg (k))) 알고리즘을 얻을 수 있지만 관련이 없기 때문에 평균적으로 더 빠릅니다. 해싱 또는 정렬.


7
당신이 설명하는 것을 'trie'라고합니다.
Nick Johnson

안녕 Strilanc. 분할 과정을 자세히 설명해 주시겠습니까?
Morgan Cheng

1
이것은 어떻게 정렬을 포함하지 않습니까 ?? 일단 트라이가 있으면 가장 큰 주파수를 가진 k 단어를 어떻게 뽑아 낼 수 있습니까? 말이 안
보통

2

설명에 버그가 있습니다. 계산에는 O (n) 시간이 걸리지 만 정렬에는 O (m * lg (m))가 걸립니다. 여기서 m은 고유 한 단어 의 수입니다 . 이것은 일반적으로 총 단어 수보다 훨씬 적으므로 해시 작성 방법을 최적화해야합니다.



2

당신이 추구 하는 것이 실제 k 와 자연어 에 대한 텍스트에서 가장 자주 사용되는 k 단어 목록이라면 알고리즘의 복잡성은 관련이 없습니다.

그냥 샘플 어떤 알고리즘을 것을, 말, 텍스트에서 몇 백만 단어, 공정 초 만에 , 그리고 가장 자주 카운트 매우 정확합니다.

참고로, 더미 알고리즘의 복잡성 (1. 모두 계산 2. 계산 정렬 3. 최선을 다함)은 O (n + m * log (m))이며, 여기서 m은 사용자의 다른 단어 수입니다. 본문. log (m)은 (n / m)보다 훨씬 작으므로 O (n)으로 유지됩니다.

실제로 긴 단계가 중요합니다.


2
  1. 메모리 효율적인 데이터 구조를 활용하여 단어 저장
  2. MaxHeap을 사용하여 자주 사용되는 상위 K 단어를 찾습니다.

다음은 코드입니다.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

import com.nadeem.app.dsa.adt.Trie;
import com.nadeem.app.dsa.adt.Trie.TrieEntry;
import com.nadeem.app.dsa.adt.impl.TrieImpl;

public class TopKFrequentItems {

private int maxSize;

private Trie trie = new TrieImpl();
private PriorityQueue<TrieEntry> maxHeap;

public TopKFrequentItems(int k) {
    this.maxSize = k;
    this.maxHeap = new PriorityQueue<TrieEntry>(k, maxHeapComparator());
}

private Comparator<TrieEntry> maxHeapComparator() {
    return new Comparator<TrieEntry>() {
        @Override
        public int compare(TrieEntry o1, TrieEntry o2) {
            return o1.frequency - o2.frequency;
        }           
    };
}

public void add(String word) {
    this.trie.insert(word);
}

public List<TopK> getItems() {

    for (TrieEntry trieEntry : this.trie.getAll()) {
        if (this.maxHeap.size() < this.maxSize) {
            this.maxHeap.add(trieEntry);
        } else if (this.maxHeap.peek().frequency < trieEntry.frequency) {
            this.maxHeap.remove();
            this.maxHeap.add(trieEntry);
        }
    }
    List<TopK> result = new ArrayList<TopK>();
    for (TrieEntry entry : this.maxHeap) {
        result.add(new TopK(entry));
    }       
    return result;
}

public static class TopK {
    public String item;
    public int frequency;

    public TopK(String item, int frequency) {
        this.item = item;
        this.frequency = frequency;
    }
    public TopK(TrieEntry entry) {
        this(entry.word, entry.frequency);
    }
    @Override
    public String toString() {
        return String.format("TopK [item=%s, frequency=%s]", item, frequency);
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + frequency;
        result = prime * result + ((item == null) ? 0 : item.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        TopK other = (TopK) obj;
        if (frequency != other.frequency)
            return false;
        if (item == null) {
            if (other.item != null)
                return false;
        } else if (!item.equals(other.item))
            return false;
        return true;
    }

}   

}

다음은 단위 테스트입니다.

@Test
public void test() {
    TopKFrequentItems stream = new TopKFrequentItems(2);

    stream.add("hell");
    stream.add("hello");
    stream.add("hello");
    stream.add("hello");
    stream.add("hello");
    stream.add("hello");
    stream.add("hero");
    stream.add("hero");
    stream.add("hero");
    stream.add("hello");
    stream.add("hello");
    stream.add("hello");
    stream.add("home");
    stream.add("go");
    stream.add("go");
    assertThat(stream.getItems()).hasSize(2).contains(new TopK("hero", 3), new TopK("hello", 8));
}

자세한 내용은 이 테스트 케이스를 참조 하세요.


1
  1. 해시 테이블을 사용하여 전체 단어 시퀀스를 순회하면서 모든 단어의 빈도를 기록합니다. 이 단계에서 키는 "단어"이고 값은 "단어 빈도"입니다. O (n) 시간이 걸리며 위에서 설명한 모든 작업과 동일합니다.

  2. hashmap에 삽입하는 동안 상위 10 개 자주 사용되는 단어를 유지하기 위해 크기 10 (k = 10)의 Treeset (Java에만 해당되며 모든 언어에 구현이 있음)을 유지합니다. 크기가 10 미만이 될 때까지 계속 추가하십시오. 크기가 10이면 삽입 된 요소가 최소 요소, 즉 첫 번째 요소보다 큰 경우. 그렇다면 제거하고 새 요소를 삽입하십시오.

트리 세트의 크기를 제한하려면 이 링크를 참조하십시오.


0

"ad" "ad" "boy" "big" "bad" "com" "come" "cold"라는 단어 시퀀스가 ​​있다고 가정합니다. 그리고 K = 2. "단어의 첫 글자를 사용하여 파티션 나누기"를 언급했듯이 ( "ad", "ad") ( "boy", "big", "bad") ( "com" "come" "cold") "then k 개의 단일 단어 세트를 가질 때까지 다음 문자를 사용하여 가장 큰 다중 단어 세트를 분할합니다. " 파티션 ( "boy", "big", "bad") ( "com" "come" "cold"), 첫 번째 파티션 ( "ad", "ad")은 누락되고 "ad"는 실제로 가장 빈번한 단어.

아마도 나는 당신의 요지를 오해 할 것입니다. 파티션에 대한 프로세스를 자세히 설명해 주시겠습니까?


0

이 문제는 O (n) 알고리즘으로 해결할 수 있다고 생각합니다. 즉석에서 정렬 할 수 있습니다. 즉,이 경우 정렬은 해시 테이블에 액세스 할 때마다 하나의 카운터 만 하나씩 증가하므로 기존 정렬 문제의 하위 문제입니다. 처음에는 모든 카운터가 0이므로 목록이 정렬됩니다. 해시 테이블에서 카운터를 계속 증가시키면서 다음과 같이 빈도별로 정렬 된 또 다른 해시 값 배열을 기록합니다. 카운터를 증가시킬 때마다 순위 배열에서 인덱스를 확인하고 그 개수가 목록의 이전 항목을 초과하는지 확인합니다. 그렇다면이 두 요소를 바꿉니다. 따라서 우리는 최대 O (n)의 해를 얻습니다. 여기서 n은 원본 텍스트의 단어 수입니다.


이것은 일반적으로 좋은 방향이지만 결함이 있습니다. 개수가 증가하면 "전임자"만 확인하는 것이 아니라 "전임자"를 확인해야합니다. 예를 들어, 배열이 [4,3,1,1,1,1,1,1,1,1,1]이 될 가능성이 큽니다. 1이 많을 수 있으므로 효율성이 떨어집니다. 스왑 할 적절한 것을 찾기 위해 모든 전임자를 뒤돌아 봐야하기 때문입니다.
Shawn

실제로 이것은 O (n)보다 훨씬 나쁘지 않습니까? 본질적으로 비효율적 인 정렬이므로 O (n ^ 2)와 더 비슷합니까?
dcarr622 2013-10-22

안녕 션. 그래 나도 너와 같은 생각이야. 그러나 나는 당신이 언급 한 문제가 문제의 근본적인 것이라고 생각합니다. 사실, 정렬 된 값 배열을 유지하는 대신 (값, 인덱스) 쌍의 배열을 유지할 수 있습니다. 여기서 인덱스가 반복되는 요소의 첫 번째 발생을 가리키는 경우 문제는 O에서 해결할 수 있어야합니다. (n) 시간. 예를 들어 [4,3,1,1,1,1,1,1,1,1,1]은 [(4,0), (3,1), (1,2), (1) 2), (1, 2, ..., (1,2)] 인덱스는 0부터 시작
알리 Farahat에게

0

나는 이것으로도 어려움을 겪었고 @aly에서 영감을 얻었습니다. 나중에 정렬하는 대신 사전 정렬 된 단어 목록 ( List<Set<String>>)을 유지할 수 있으며 단어는 X 위치에서 집합에있게됩니다. 여기서 X는 단어의 현재 개수입니다. 일반적으로 작동 방식은 다음과 같습니다.

  1. 각 단어에 대해 발생지도의 일부로 저장합니다. Map<String, Integer> .
  2. 그런 다음 카운트를 기반으로 이전 카운트 세트에서 제거하고 새 카운트 세트에 추가합니다.

이것의 단점은 목록이 클 수도 있다는 것입니다. TreeMap<Integer, Set<String>> 것입니다.- 있지만 이것은 약간의 오버 헤드를 추가합니다. 궁극적으로 우리는 HashMap 또는 자체 데이터 구조를 혼합하여 사용할 수 있습니다.

코드

public class WordFrequencyCounter {
    private static final int WORD_SEPARATOR_MAX = 32; // UNICODE 0000-001F: control chars
    Map<String, MutableCounter> counters = new HashMap<String, MutableCounter>();
    List<Set<String>> reverseCounters = new ArrayList<Set<String>>();

    private static class MutableCounter {
        int i = 1;
    }

    public List<String> countMostFrequentWords(String text, int max) {
        int lastPosition = 0;
        int length = text.length();
        for (int i = 0; i < length; i++) {
            char c = text.charAt(i);
            if (c <= WORD_SEPARATOR_MAX) {
                if (i != lastPosition) {
                    String word = text.substring(lastPosition, i);
                    MutableCounter counter = counters.get(word);
                    if (counter == null) {
                        counter = new MutableCounter();
                        counters.put(word, counter);
                    } else {
                        Set<String> strings = reverseCounters.get(counter.i);
                        strings.remove(word);
                        counter.i ++;
                    }
                    addToReverseLookup(counter.i, word);
                }
                lastPosition = i + 1;
            }
        }

        List<String> ret = new ArrayList<String>();
        int count = 0;
        for (int i = reverseCounters.size() - 1; i >= 0; i--) {
            Set<String> strings = reverseCounters.get(i);
            for (String s : strings) {
                ret.add(s);
                System.out.print(s + ":" + i);
                count++;
                if (count == max) break;
            }
            if (count == max) break;
        }
        return ret;
    }

    private void addToReverseLookup(int count, String word) {
        while (count >= reverseCounters.size()) {
            reverseCounters.add(new HashSet<String>());
        }
        Set<String> strings = reverseCounters.get(count);
        strings.add(word);
    }

}

0

이 문제에 대한 다른 해결책을 찾았습니다. 그러나 나는 그것이 옳은지 확신하지 못한다. 해결책:

  1. 해시 테이블을 사용하여 모든 단어의 빈도 T (n) = O (n) 기록
  2. 해시 테이블의 처음 k 요소를 선택하고 하나의 버퍼 (공간 = k)로 복원합니다. T (n) = O (k)
  3. 매번, 먼저 버퍼의 현재 최소 요소를 찾고 버퍼의 최소 요소를 해시 테이블의 (n-k) 요소와 하나씩 비교해야합니다. 해시 테이블의 요소가이 버퍼의 최소 요소보다 크면 현재 버퍼의 최소값을 삭제하고 해시 테이블의 요소를 추가합니다. 따라서 버퍼에서 최소값을 찾을 때마다 T (n) = O (k)가 필요하고 전체 해시 테이블을 통과하려면 T (n) = O (n-k)가 필요합니다. 따라서이 프로세스의 전체 시간 복잡도는 T (n) = O ((nk) * k)입니다.
  4. 전체 해시 테이블을 순회 한 후 결과는이 버퍼에 있습니다.
  5. 전체 시간 복잡도 : T (n) = O (n) + O (k) + O (kn-k ^ 2) = O (kn + n-k ^ 2 + k). 왜냐하면 k는 일반적으로 n보다 실제로 작습니다. 따라서이 솔루션의 경우 시간 복잡도는 T (n) = O (kn) 입니다. k가 정말 작은 선형 시간입니다. 맞아? 정말 잘 모르겠습니다.

0

이런 종류의 문제에 접근하기 위해 특별한 데이터 구조를 생각해보십시오. 이 경우 문자열을 특정 방식으로 저장하려는 시도와 같은 특별한 종류의 트리는 매우 효율적입니다. 또는 단어 세기와 같은 자체 솔루션을 구축하는 두 번째 방법입니다. 이 TB의 데이터는 영어로되어 있고 일반적으로 약 600,000 개의 단어가 있으므로 해당 단어 만 저장하고 반복되는 문자열을 세는 것이 가능합니다.이 솔루션은 일부 특수 문자를 제거하기 위해 정규식이 필요합니다. 첫 번째 솔루션이 더 빠를 것입니다.

http://en.wikipedia.org/wiki/Trie



0

가장 자주 사용되는 단어를 얻는 가장 간단한 코드입니다.

 function strOccurence(str){
    var arr = str.split(" ");
    var length = arr.length,temp = {},max; 
    while(length--){
    if(temp[arr[length]] == undefined && arr[length].trim().length > 0)
    {
        temp[arr[length]] = 1;
    }
    else if(arr[length].trim().length > 0)
    {
        temp[arr[length]] = temp[arr[length]] + 1;

    }
}
    console.log(temp);
    var max = [];
    for(i in temp)
    {
        max[temp[i]] = i;
    }
    console.log(max[max.length])
   //if you want second highest
   console.log(max[max.length - 2])
}

0

이러한 상황에서는 Java 내장 기능을 사용하는 것이 좋습니다. 왜냐하면 그들은 이미 잘 테스트되고 안정적입니다. 이 문제에서는 HashMap 데이터 구조를 사용하여 단어의 반복을 찾습니다. 그런 다음 결과를 객체 배열에 푸시합니다. Arrays.sort ()로 객체를 정렬하고 상위 k 단어와 그 반복을 인쇄합니다.

import java.io.*;
import java.lang.reflect.Array;
import java.util.*;

public class TopKWordsTextFile {

    static class SortObject implements Comparable<SortObject>{

        private String key;
        private int value;

        public SortObject(String key, int value) {
            super();
            this.key = key;
            this.value = value;
        }

        @Override
        public int compareTo(SortObject o) {
            //descending order
            return o.value - this.value;
        }
    }


    public static void main(String[] args) {
        HashMap<String,Integer> hm = new HashMap<>();
        int k = 1;
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("words.in")));

            String line;
            while ((line = br.readLine()) != null) {
                // process the line.
                //System.out.println(line);
                String[] tokens = line.split(" ");
                for(int i=0; i<tokens.length; i++){
                    if(hm.containsKey(tokens[i])){
                        //If the key already exists
                        Integer prev = hm.get(tokens[i]);
                        hm.put(tokens[i],prev+1);
                    }else{
                        //If the key doesn't exist
                        hm.put(tokens[i],1);
                    }
                }
            }
            //Close the input
            br.close();
            //Print all words with their repetitions. You can use 3 for printing top 3 words.
            k = hm.size();
            // Get a set of the entries
            Set set = hm.entrySet();
            // Get an iterator
            Iterator i = set.iterator();
            int index = 0;
            // Display elements
            SortObject[] objects = new SortObject[hm.size()];
            while(i.hasNext()) {
                Map.Entry e = (Map.Entry)i.next();
                //System.out.print("Key: "+e.getKey() + ": ");
                //System.out.println(" Value: "+e.getValue());
                String tempS = (String) e.getKey();
                int tempI = (int) e.getValue();
                objects[index] = new SortObject(tempS,tempI);
                index++;
            }
            System.out.println();
            //Sort the array
            Arrays.sort(objects);
            //Print top k
            for(int j=0; j<k; j++){
                System.out.println(objects[j].key+":"+objects[j].value);
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

자세한 내용은 https://github.com/m-vahidalizadeh/foundations/blob/master/src/algorithms/TopKWordsTextFile.java 를 방문 하십시오 . 도움이되기를 바랍니다.


이것은 질문에서 스케치 한 접근 방식에서 어떤 방식으로 개선됩니까? (마십시오 없습니다 . (SE에 제시 한 코드에서 주석을 생략) I recommend to use Java built-in features처럼 foreach는 루프처리 스트림 ?)
수염이 희끗 희끗 한

아시다시피 효율적인 알고리즘을 설계하는 데 가장 중요한 요소 중 하나는 올바른 데이터 구조를 선택하는 것입니다. 그런 다음 문제에 접근하는 방법이 중요합니다. 예를 들어, 분할 및 정복으로 문제를 공격해야합니다. 욕심으로 다른 사람을 공격해야합니다. 아시다시피 Oracle 회사는 Java에서 작업하고 있습니다. 그들은 세계 최고의 기술 회사 중 하나입니다. Java 내장 기능에 대해 작업하는 가장 뛰어난 엔지니어가 있습니다. 따라서 이러한 기능은 잘 테스트되고 방탄입니다. 우리가 그것들을 활용할 수 있다면 제 생각에는 그것들을 사용하는 것이 낫습니다.
Mohammad

0
**

위의 생각의 C ++ 11 구현

**

class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {

    unordered_map<int,int> map;
    for(int num : nums){
        map[num]++;
    }

    vector<int> res;
    // we use the priority queue, like the max-heap , we will keep (size-k) smallest elements in the queue
    // pair<first, second>: first is frequency,  second is number 
    priority_queue<pair<int,int>> pq; 
    for(auto it = map.begin(); it != map.end(); it++){
        pq.push(make_pair(it->second, it->first));

        // onece the size bigger than size-k, we will pop the value, which is the top k frequent element value 

        if(pq.size() > (int)map.size() - k){
            res.push_back(pq.top().second);
            pq.pop();
        }
    }
    return res;

}

};

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