关于词典:在java treemap 找元素位置

Find element position in a Java TreeMap

我正在使用字符串的treemap TreeMap,并使用它来实现单词的措辞。

然后我有一个文件集合,并希望在字典定义的向量空间(单词空间)中创建每个文件的表示。

每个文件都应该有一个向量,用以下属性表示它:

  • 矢量的大小应与字典的大小相同
  • 对于文件中包含的每个单词,矢量在字典中与单词位置相对应的位置应具有1。
  • 对于文件中未包含的每个单词,矢量在字典中单词位置对应的位置应具有-1

所以我的想法是使用Vector来实现这些向量。(这种表示集合中文档的方式称为布尔模型-http://www.site.uottawa.ca/~diana/csi4107/l3.pdf)

在创建这个向量的过程中,我面临的问题是,我需要一种方法来查找单词在字典中的位置,如下所示:

1
2
String key;
int i = get_position_of_key_in_Treemap(key); <--- purely invented method...

1)Treemap上有没有这种方法?如果没有,你能提供一些代码来帮助我自己实现它吗?

2)treemap上是否有迭代器(按字母顺序排列在键上),我可以从中得到位置?

3)最后我应该使用另一个类来实现字典吗?(如果你认为Treemaps不能满足我的需求)如果是的,哪一个?

事先谢谢。

增加部分:

DasBlinkenLight提出的解决方案看起来很好,但存在复杂性问题(由于将键复制到数组中,所以与字典的维度呈线性关系),并且对每个文件都这样做的想法是不可接受的。

我的问题还有其他的想法吗?


构建树映射后,将其排序的键复制到一个数组中,并使用Arrays.binarySearch在o(logn)时间内查找索引。如果需要该值,也可以在原始地图上进行查找。

编辑:这是将键复制到数组中的方法

1
2
3
4
5
String[] mapKeys = new String[treeMap.size()];
int pos = 0;
for (String key : treeMap.keySet()) {
    mapKeys[pos++] = key;
}


另一种解决方案是使用TreeMapheadMap方法。如果该词存在于TreeMap中,则其头映射的size()等于字典中该词的索引。与我的另一个答案相比,这可能有点浪费。

下面是如何在Java中编码它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.*;

class Test {
    public static void main(String[] args) {
        TreeMap<String,String> tm = new TreeMap<String,String>();
        tm.put("quick","one");
        tm.put("brown","two");
        tm.put("fox","three");
        tm.put("jumps","four");
        tm.put("over","five");
        tm.put("the","six");
        tm.put("lazy","seven");
        tm.put("dog","eight");
        for (String s : new String[] {
           "quick","brown","fox","jumps","over",
           "the","lazy","dog","before","way_after"}
        ) {
            if (tm.containsKey(s)) {
                // Here is the operation you are looking for.
                // It does not work for items not in the dictionary.
                int pos = tm.headMap(s).size();
                System.out.println("Key '"+s+"' is at the position"+pos);
            } else {
                System.out.println("Key '"+s+"' is not found");
            }
        }
    }
}

下面是程序生成的输出:

1
2
3
4
5
6
7
8
9
10
Key 'quick' is at the position 6
Key 'brown' is at the position 0
Key 'fox' is at the position 2
Key 'jumps' is at the position 3
Key 'over' is at the position 5
Key 'the' is at the position 7
Key 'lazy' is at the position 4
Key 'dog' is at the position 1
Key 'before' is not found
Key 'way_after' is not found


我也有同样的问题。所以我用java.util.treemap的源代码编写了indexedtreemap。它实现了我自己的indexednavigablemap:

1
2
3
4
5
public interface IndexedNavigableMap<K, V> extends NavigableMap<K, V> {
   K exactKey(int index);
   Entry<K, V> exactEntry(int index);
   int keyIndex(K k);
}

实现的基础是更新红黑树中的节点权重。权重是给定节点下的子节点数,加上一个节点本身。例如,当树向左旋转时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    private void rotateLeft(Entry<K, V> p) {
    if (p != null) {
        Entry<K, V> r = p.right;

        int delta = getWeight(r.left) - getWeight(p.right);
        p.right = r.left;
        p.updateWeight(delta);

        if (r.left != null) {
            r.left.parent = p;
        }

        r.parent = p.parent;


        if (p.parent == null) {
            root = r;
        } else if (p.parent.left == p) {
            delta = getWeight(r) - getWeight(p.parent.left);
            p.parent.left = r;
            p.parent.updateWeight(delta);
        } else {
            delta = getWeight(r) - getWeight(p.parent.right);
            p.parent.right = r;
            p.parent.updateWeight(delta);
        }

        delta = getWeight(p) - getWeight(r.left);
        r.left = p;
        r.updateWeight(delta);

        p.parent = r;
    }
  }

updateWeight只更新到根的权重:

1
2
3
4
5
6
7
8
   void updateWeight(int delta) {
        weight += delta;
        Entry<K, V> p = parent;
        while (p != null) {
            p.weight += delta;
            p = p.parent;
        }
    }

当我们需要通过索引找到元素时,这里是使用权重的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public K exactKey(int index) {
    if (index < 0 || index > size() - 1) {
        throw new ArrayIndexOutOfBoundsException();
    }
    return getExactKey(root, index);
}

private K getExactKey(Entry<K, V> e, int index) {
    if (e.left == null && index == 0) {
        return e.key;
    }
    if (e.left == null && e.right == null) {
        return e.key;
    }
    if (e.left != null && e.left.weight > index) {
        return getExactKey(e.left, index);
    }
    if (e.left != null && e.left.weight == index) {
        return e.key;
    }
    return getExactKey(e.right, index - (e.left == null ? 0 : e.left.weight) - 1);
}

找到一个键的索引也非常方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    public int keyIndex(K key) {
    if (key == null) {
        throw new NullPointerException();
    }
    Entry<K, V> e = getEntry(key);
    if (e == null) {
        throw new NullPointerException();
    }
    if (e == root) {
        return getWeight(e) - getWeight(e.right) - 1;//index to return
    }
    int index = 0;
    int cmp;
    if (e.left != null) {
        index += getWeight(e.left);
    }
    Entry<K, V> p = e.parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        while (p != null) {
            cmp = cpr.compare(key, p.key);
            if (cmp > 0) {
                index += getWeight(p.left) + 1;
            }
            p = p.parent;
        }
    } else {
        Comparable<? super K> k = (Comparable<? super K>) key;
        while (p != null) {
            if (k.compareTo(p.key) > 0) {
                index += getWeight(p.left) + 1;
            }
            p = p.parent;
        }
    }
    return index;
}

我将很快实现indexedtreeset,同时您可以使用indexedtreemap中的密钥集。

更新:现在实现了indexedtreeset。

你可以在https://github.com/geniot/indexed-tree-map上找到这项工作的结果。


我要感谢你们为回答我的问题所付出的努力,他们都是非常有用的,并且从他们每个人身上得到最好的帮助,使我找到了我在我的项目中实际实施的解决方案。

对于我的单个问题,我认为最好的答案是:

2)treemaps上没有定义为@isoliveira sais的迭代器:

1
2
3
4
5
There's no such implementation in the JDK itself.
Although TreeMap iterates in natural key ordering,
its internal data structures are all based on trees and not arrays
(remember that Maps do not order keys, by definition,
in spite of that the very common use case).

正如我在这篇文章中发现的,回答如何迭代Treemap?在Map中迭代元素的唯一方法是使用map.entrySet()并使用在Set上定义的迭代器(或使用迭代器的其他类)。

3)可以使用TreeMap来实现字典,但这将在查找包含单词的索引(在树数据结构中查找的成本)时增加o(logn)的复杂性。

使用同一程序的HashMap将具有复杂性o(1)。

1)没有这种方法。唯一的解决方案是完全实现它。

正如@保罗所言

1
Assumes that once getPosition() has been called, the dictionary is not changed.

解决方案的假设是,一旦创建字典,以后就不会更改:这样,单词的位置将始终相同。

给出这个假设,我找到了一个解决方案,可以用复杂性o(n)构建字典,并且在garantues之后,可以在lookup中获取constat time o(1)中包含的单词的索引。

我把字典定义为这样的一个HashMap

1
public HashMap<String, WordStruct> dictionary = new HashMap<String, WordStruct>();
  • key——>表示字典中所含单词的String
  • 值——>创建的类WordStructObject

其中WordStruct类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WordStruct {

    private int DictionaryPosition;    // defines the position of word in dictionary once it is alphabetically ordered

    public WordStruct(){

    }

    public SetWordPosition(int pos){
        this.DictionaryPosition = pos;
    }

}

并且允许我保存任何类型的属性,我喜欢与字典的词条配对。

现在,我用字典迭代我收集的所有文件中包含的所有单词:

1
2
3
4
5
6
7
8
9
10
11
12
13
THE FOLLOWING IS PSEUDOCODE

for(int i = 0; i < number_of_files ; i++){

        get_file(i);

        while (file_contais_words){

            dictionary.put( word(j) , new LemmaStruct());

        }

}

一旦hashmap以任何顺序被填充,我就使用@dasbinkenlight所指示的过程来一次性地对其进行排序,并且具有复杂性o(n)

1
2
3
4
5
6
7
8
9
    Object[] dictionaryArray = dictionary.keySet().toArray();
    Arrays.sort(dictionaryArray);

    for(int i = 0; i < dictionaryArray.length; i++){

        String word = (String) dictionaryArray[i];
        dictionary.get(word).SetWordPosition(i);

    }

从现在起,在字典中按词的字母顺序排列索引位置,唯一需要做的就是计算它的变量DictionaryPosition

因为Word知道你只需要访问它,所以在HashMap中它有固定的成本。

再次感谢,祝大家圣诞快乐!!


JDK本身没有这样的实现。虽然TreeMap以自然键顺序迭代,但其内部数据结构都是基于树而不是数组(请记住,根据定义,Maps不排序键,尽管这是非常常见的用例)。

也就是说,您必须做出选择,因为不可能有o(1)个计算时间用于您的比较标准,无论是插入到Map还是indexOf(key)计算中。这是因为在可变数据结构中,词典编纂顺序不稳定(例如,与插入顺序相反)。例如:一旦将第一个键值对(条目)插入到映射中,它的位置将始终是一个。但是,根据插入的第二个键,该位置可能会发生变化,因为新键可能比Map中的键"更大"或"更低"。您当然可以通过在插入操作期间维护和更新一个索引键列表来实现这一点,但是您的插入操作将有O(n log(n))(这需要重新排序一个数组)。根据您的数据访问模式,这可能是可取的或不可取的。

ApacheCommons中的ListOrderedMapLinkedMap都接近于您所需要的,但依赖于插入顺序。我认为,您可以检查它们的实现,并用很少或中等的努力来开发自己的问题解决方案(这应该只是用排序列表替换ListOrderedMap的内部支持数组的问题,例如,在ApacheCommons中的TreeList)。

您还可以自己计算索引,方法是减去低于给定键的元素数(在最常见的情况下,这应该比遍历搜索元素的列表更快,因为您没有比较任何内容)。


你有没有想过让你的TreeMap中的值包含你字典中的位置?我在这里使用一个BitSet来获取我的文件详细信息。

这和我下面的其他想法不太一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Map<String,Integer> dictionary = new TreeMap<String,Integer> ();

private void test () {
  // Construct my dictionary.
  buildDictionary();
  // Make my file data.
  String [] file1 = new String[] {
   "1","3","5"
  };
  BitSet fileDetails = getFileDetails(file1, dictionary);
  printFileDetails("File1", fileDetails);
}

private void printFileDetails(String fileName, BitSet details) {
  System.out.println("File:"+fileName);
  for ( int i = 0; i < details.length(); i++ ) {
    System.out.print ( details.get(i) ? 1: -1 );
    if ( i < details.length() - 1 ) {
      System.out.print ("," );
    }
  }
}

private BitSet getFileDetails(String [] file, Map<String, Integer> dictionary ) {
  BitSet details = new BitSet();
  for ( String word : file ) {
    // The value in the dictionary is the index of the word in the dictionary.
    details.set(dictionary.get(word));
  }
  return details;
}

String [] dictionaryWords = new String[] {
 "1","2","3","4","5"
};

private void buildDictionary () {
  for ( String word : dictionaryWords ) {
    // Initially make the value 0. We will change that later.
    dictionary.put(word, 0);
  }
  // Make the indexes.
  int wordNum = 0;
  for ( String word : dictionary.keySet() ) {
    dictionary.put(word, wordNum++);
  }
}

在这里,文件细节的构建包括在TreeMap中对文件中的每个单词进行一次查找。

如果你打算用字典TreeMap中的value来做别的东西,你可以用Integer来组成它。

补充

再想一想,如果Mapvalue字段被指定用于某个对象,您可以始终使用特殊键来计算它们在Map中的位置,并像String一样进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
private void test () {
  // Dictionary
  Map<PosKey, String> dictionary = new TreeMap<PosKey, String> ();
  // Fill it with words.
  String[] dictWords = new String[] {
                      "0","1","2","3","4","5"};
  for ( String word : dictWords ) {
    dictionary.put( new PosKey( dictionary, word ), word );
  }
  // File
  String[] fileWords = new String[] {
                      "0","2","3","5"};
  int[] file = new int[dictionary.size()];
  // Initially all -1.
  for ( int i = 0; i < file.length; i++ ) {
    file[i] = -1;
  }
  // Temp file words set.
  Set fileSet = new HashSet( Arrays.asList( fileWords ) );
  for ( PosKey key : dictionary.keySet() ) {
    if ( fileSet.contains( key.getKey() ) ) {
      file[key.getPosiion()] = 1;
    }
  }

  // Print out.
  System.out.println( Arrays.toString( file ) );
  // Prints: [1, -1, 1, 1, -1, 1]

}

class PosKey
    implements Comparable {
  final String key;
  // Initially -1
  int position = -1;
  // The map I am keying on.
  Map<PosKey, ?> map;

  public PosKey ( Map<PosKey, ?> map, String word ) {
    this.key = word;
    this.map = map;
  }

  public int getPosiion () {
    if ( position == -1 ) {
      // First access to the key.
      int pos = 0;
      // Calculate all positions in one loop.
      for ( PosKey k : map.keySet() ) {
        k.position = pos++;
      }
    }
    return position;
  }

  public String getKey () {
    return key;
  }

  public int compareTo ( Object it ) {
    return key.compareTo( ( ( PosKey )it ).key );
  }

  public int hashCode () {
    return key.hashCode();
  }
}

注:假设调用getPosition()后,字典不会更改。


我同意伊索维埃拉的观点。也许最好的方法是使用与treemap不同的结构。

但是,如果您仍然想继续计算键的索引,一个解决方案是计算出有多少键低于您要查找的键。

以下是代码段:

1
2
3
4
5
6
7
8
9
10
11
    java.util.SortedMap<String, String> treeMap = new java.util.TreeMap<String, String>();
    treeMap.put("d","content 4");
    treeMap.put("b","content 2");
    treeMap.put("c","content 3");
    treeMap.put("a","content 1");

    String key ="d"; // key to get the index for
    System.out.println( treeMap.keySet() );

    final String firstKey = treeMap.firstKey(); // assuming treeMap structure doesn't change in the mean time
    System.out.format("Index of %s is %d %n", key, treeMap.subMap(firstKey, key).size() );

我建议您编写一个skiplist来存储您的字典,因为它仍然提供O(log n)查找、插入和删除,同时还能够提供索引(由于节点不知道索引,所以树实现通常不能返回索引,并且需要花费一定的成本来更新它们)。不幸的是,CONTRONTSKILIPSIMAP的Java实现不提供索引,因此您需要实现自己的版本。

获取一个项目的索引将是O(log n),如果您不进行2次查找就同时需要索引和值,那么您将需要返回一个包含这两者的包装器对象。