Cách hiệu quả nhất để tăng giá trị Bản đồ trong Java


377

Tôi hy vọng câu hỏi này không được coi là quá cơ bản cho diễn đàn này, nhưng chúng ta sẽ thấy. Tôi đang tự hỏi làm thế nào để cấu trúc lại một số mã để có hiệu năng tốt hơn đang được chạy rất nhiều lần.

Giả sử tôi đang tạo danh sách tần suất từ, sử dụng Bản đồ (có thể là HashMap), trong đó mỗi khóa là một Chuỗi có từ được tính và giá trị là một Số nguyên tăng lên mỗi khi tìm thấy mã thông báo của từ đó.

Trong Perl, việc tăng giá trị như vậy sẽ rất dễ dàng:

$map{$word}++;

Nhưng trong Java, nó phức tạp hơn nhiều. Đây là cách tôi đang làm:

int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);

Tất nhiên, điều này phụ thuộc vào tính năng autoboxing trong các phiên bản Java mới hơn. Tôi tự hỏi nếu bạn có thể đề xuất một cách hiệu quả hơn để tăng giá trị như vậy. Thậm chí còn có lý do hiệu suất tốt để tránh khung công tác Bộ sưu tập và sử dụng cái gì khác thay thế?

Cập nhật: Tôi đã thực hiện một bài kiểm tra một số câu trả lời. Xem bên dưới.


Tôi nghĩ nó sẽ giống với java.util.Hashtable.
jrudolph

2
Tất nhiên nếu như vậy, bởi vì Hashtable không hoàn thành Bản đồ.
whiskeyierra

Java 8: compute IfAbsent ví dụ: stackoverflow.com/a/37439971/1216775
akhil_mittal

Câu trả lời:


367

Một số kết quả kiểm tra

Tôi đã nhận được rất nhiều câu trả lời hay cho câu hỏi này - cảm ơn mọi người - vì vậy tôi quyết định thực hiện một số thử nghiệm và tìm ra phương pháp nào thực sự nhanh nhất. Năm phương pháp tôi đã thử nghiệm là:

  • phương pháp "ContainsKey" mà tôi đã trình bày trong câu hỏi
  • phương pháp "TestForNull" được đề xuất bởi Aleksandar Dimitrov
  • phương pháp "AtomicLong" được đề xuất bởi Hank Gay
  • phương pháp "Trove" được đề xuất bởi jrudolph
  • phương pháp "MutableInt" được đề xuất bởi phax.myopenid.com

phương pháp

Đây là những gì tôi đã làm ...

  1. đã tạo ra năm lớp giống hệt nhau ngoại trừ sự khác biệt được hiển thị bên dưới. Mỗi lớp phải thực hiện một thao tác điển hình cho kịch bản tôi đã trình bày: mở tệp 10 MB và đọc nó, sau đó thực hiện đếm tần số của tất cả các mã thông báo từ trong tệp. Vì việc này chỉ mất trung bình 3 giây, tôi đã thực hiện việc đếm tần số (không phải I / O) 10 lần.
  2. đã tính thời gian của vòng lặp 10 lần nhưng không phải là thao tác I / O và ghi lại tổng thời gian thực hiện (tính bằng giây đồng hồ) về cơ bản bằng phương pháp của Ian Darwin trong Sách dạy nấu ăn Java .
  3. thực hiện tất cả năm bài kiểm tra theo chuỗi, và sau đó thực hiện thêm ba lần nữa.
  4. Tính trung bình bốn kết quả cho mỗi phương pháp.

Các kết quả

Tôi sẽ trình bày kết quả trước và mã dưới đây cho những ai quan tâm.

Các containsKey phương pháp được, như mong đợi, chậm nhất, vì vậy tôi sẽ cung cấp cho tốc độ của từng phương pháp so với tốc độ của phương pháp đó.

  • Chứa khóa: 30.654 giây (đường cơ sở)
  • AtomicLong: 29,780 giây (nhanh gấp 1,03 lần)
  • TestForNull: 28,80 giây (nhanh gấp 1,06 lần)
  • Trove: 26.313 giây (nhanh gấp 1,16 lần)
  • MutableInt: 25,747 giây (nhanh gấp 1,19 lần)

Kết luận

Dường như chỉ có phương pháp MutableInt và phương pháp Trove nhanh hơn đáng kể, trong đó chỉ có chúng giúp tăng hiệu suất hơn 10%. Tuy nhiên, nếu phân luồng là một vấn đề, thì AtomicLong có thể hấp dẫn hơn các luồng khác (tôi không thực sự chắc chắn). Tôi cũng đã chạy TestForNull với finalcác biến, nhưng sự khác biệt là không đáng kể.

Lưu ý rằng tôi chưa sử dụng bộ nhớ trong các tình huống khác nhau. Tôi rất vui khi được nghe từ bất kỳ ai có hiểu biết tốt về cách các phương thức MutableInt và Trove có thể ảnh hưởng đến việc sử dụng bộ nhớ.

Cá nhân, tôi thấy phương thức MutableInt hấp dẫn nhất, vì nó không yêu cầu tải bất kỳ lớp bên thứ ba nào. Vì vậy, trừ khi tôi phát hiện ra vấn đề với nó, đó là cách tôi thích nhất.

Mật mã

Đây là mã quan trọng từ mỗi phương pháp.

Chứa khóa

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
int count = freq.containsKey(word) ? freq.get(word) : 0;
freq.put(word, count + 1);

TestForNull

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
Integer count = freq.get(word);
if (count == null) {
    freq.put(word, 1);
}
else {
    freq.put(word, count + 1);
}

Nguyên tử

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
...
final ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();
...
map.putIfAbsent(word, new AtomicLong(0));
map.get(word).incrementAndGet();

Quân đội

import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(word, 1, 1);

MutableInt

import java.util.HashMap;
import java.util.Map;
...
class MutableInt {
  int value = 1; // note that we start at 1 since we're counting
  public void increment () { ++value;      }
  public int  get ()       { return value; }
}
...
Map<String, MutableInt> freq = new HashMap<String, MutableInt>();
...
MutableInt count = freq.get(word);
if (count == null) {
    freq.put(word, new MutableInt());
}
else {
    count.increment();
}

3
Công việc tuyệt vời, được thực hiện tốt. Một nhận xét nhỏ - lệnh gọi put IfAbsent () trong mã AtomicLong sẽ khởi tạo một AtomicLong mới (0) ngay cả khi đã có trong bản đồ. Nếu bạn điều chỉnh điều này để sử dụng if (map.get (key) == null), bạn có thể sẽ nhận được sự cải thiện trong các kết quả thử nghiệm đó.
Leigh Caldwell

2
Tôi đã làm điều tương tự gần đây với một cách tiếp cận tương tự như MutableInt. Tôi rất vui khi biết đó là giải pháp tối ưu (tôi chỉ cho rằng đó là, mà không cần thực hiện bất kỳ thử nghiệm nào).
Kip

Thật vui khi biết rằng bạn nhanh hơn tôi, Kip. ;-) Hãy cho tôi biết nếu bạn phát hiện ra bất kỳ nhược điểm nào đối với phương pháp đó.
phái

4
Trong trường hợp Nguyên tử dài sẽ không hiệu quả hơn khi thực hiện trong một bước (vì vậy bạn chỉ có 1 thao tác đắt tiền thay vì 2) "map.put IfAbsent (từ, AtomicLong mới (0)). Gia tăngAndGet ();"
smartnut007

1
@gregory bạn đã xem xét Java 8 freq.compute(word, (key, count) -> count == null ? 1 : count + 1)chưa? Trong nội bộ, nó thực hiện một tra cứu ít băm hơn containsKey, sẽ rất thú vị khi xem nó so sánh với người khác như thế nào, vì lambda.
TWiStErRob

255

Bây giờ có một cách ngắn hơn với Java 8 bằng cách sử dụng Map::merge.

myMap.merge(key, 1, Integer::sum)

Những gì nó làm:

  • nếu khóa không tồn tại, đặt 1 làm giá trị
  • mặt khác tổng 1 với giá trị được liên kết với khóa

Thêm thông tin ở đây .


luôn luôn yêu java 8. Đây có phải là nguyên tử? hoặc tôi nên bao quanh nó với một đồng bộ hóa?
Tiina

4
điều này dường như không hiệu quả với tôi nhưng map.merge(key, 1, (a, b) -> a + b); đã làm
russter 27/03/18

2
@Tiina Đặc tính nguyên tử là cụ thể triển khai, xem các tài liệu : "Việc triển khai mặc định không đảm bảo về các đặc tính đồng bộ hóa hoặc nguyên tử của phương thức này. Bất kỳ việc triển khai nào đảm bảo tính nguyên tử phải ghi đè lên phương thức này và ghi lại các thuộc tính tương tranh của nó. Đặc biệt, tất cả các triển khai của SubinterfaceMap phải ghi lại liệu hàm có được áp dụng một lần không chỉ nguyên tử nếu giá trị không có mặt. "
jensgram

2
Đối với Groovy, nó sẽ không chấp nhận Integer::sumlà một BiFactor và không thích @russter trả lời cách viết. Điều này làm việc cho tôiMap.merge(key, 1, { a, b -> a + b})
jookyone

2
@russter, tôi biết nhận xét của bạn đã hơn một năm trước nhưng bạn có nhớ tại sao nó không hiệu quả với bạn không? Bạn đã nhận được một lỗi biên dịch hoặc giá trị không được tăng lên?
Paul

44

Một nghiên cứu nhỏ trong năm 2016: https://github.com/leventov/java-word-count , mã nguồn chuẩn

Kết quả tốt nhất cho mỗi phương pháp (nhỏ hơn là tốt hơn):

                 time, ms
kolobokeCompile  18.8
koloboke         19.8
trove            20.8
fastutil         22.7
mutableInt       24.3
atomicInteger    25.3
eclipse          26.9
hashMap          28.0
hppc             33.6
hppcRt           36.5

Thời gian \ kết quả không gian:


2
Cảm ơn, điều này thực sự hữu ích. Sẽ thật tuyệt khi thêm Multiset của Guava (ví dụ: HashMultiset) vào điểm chuẩn.
cabad

34

Google Guava là bạn của bạn ...

... ít nhất là trong một số trường hợp. Họ có AtomicLongMap đẹp này . Đặc biệt là tốt đẹp bởi vì bạn đang đối phó với dài như giá trị trong bản đồ của bạn.

Ví dụ

AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(word);

Cũng có thể thêm nhiều hơn 1 vào giá trị:

map.getAndAdd(word, 112L); 

7
AtomicLongMap#getAndAddcó một lớp nguyên thủy longvà không phải là lớp bao bọc; không có điểm nào trong việc làm new Long(). Và AtomicLongMaplà một loại tham số; bạn nên tuyên bố nó là AtomicLongMap<String>.
Pereira

32

@Hank Gay

Theo dõi nhận xét (khá vô dụng) của riêng tôi: Trove trông giống như con đường để đi. Nếu vì bất cứ lý do nào, bạn muốn gắn bó với JDK chuẩn, ConcurrentMapAtomicLong có thể làm cho đoạn code một nhỏ chút đẹp hơn, mặc dù YMMV.

    final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.putIfAbsent("foo", new AtomicLong(0));
    map.get("foo").incrementAndGet();

sẽ để lại 1như là giá trị trong bản đồ cho foo. Trên thực tế, tăng sự thân thiện với luồng là tất cả những gì phương pháp này phải khuyến nghị.


9
Put IfAbsent () trả về giá trị. Nó có thể là một cải tiến lớn để lưu trữ giá trị được trả về trong một biến cục bộ và sử dụng nó để tăngAndGet () thay vì gọi lại.
smartnut007

put IfAbsent có thể trả về giá trị null nếu khóa được chỉ định chưa được liên kết với giá trị bên trong Map nên tôi sẽ cẩn thận sử dụng giá trị được trả về. docs.oracle.com/javase/8/docs/api/java/util/ mẹo
bumbur

27
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0); // ensure count will be one of 0,1,2,3,...
map.put(key, count + 1);

Và đó là cách bạn tăng giá trị bằng mã đơn giản.

Lợi ích:

  • Không cần thêm một lớp mới hoặc sử dụng một khái niệm khác về intable mutable
  • Không dựa vào bất kỳ thư viện
  • Dễ hiểu chính xác những gì đang diễn ra (Không quá trừu tượng)

Nhược điểm:

  • Bản đồ băm sẽ được tìm kiếm hai lần cho get () và put (). Vì vậy, nó sẽ không phải là mã hiệu suất nhất.

Về mặt lý thuyết, một khi bạn gọi get (), bạn đã biết nơi đặt (), vì vậy bạn không cần phải tìm kiếm lại. Nhưng tìm kiếm trong bản đồ băm thường mất một thời gian rất nhỏ mà bạn có thể bỏ qua vấn đề hiệu suất này.

Nhưng nếu bạn rất nghiêm túc về vấn đề này, bạn là người cầu toàn, một cách khác là sử dụng phương thức hợp nhất, điều này (có thể) hiệu quả hơn đoạn mã trước đó vì bạn sẽ (về mặt lý thuyết) chỉ tìm kiếm bản đồ một lần: (mặc dù) mã này không rõ ràng từ cái nhìn đầu tiên, nó ngắn và hiệu quả)

map.merge(key, 1, (a,b) -> a+b);

Đề xuất: bạn nên quan tâm đến khả năng đọc mã nhiều hơn là tăng hiệu suất trong hầu hết thời gian. Nếu đoạn mã đầu tiên dễ hiểu hơn thì hãy sử dụng nó. Nhưng nếu bạn có thể hiểu thứ 2 tốt thì bạn cũng có thể đi cho nó!


Phương thức getOfDefault không khả dụng trong JAVA 7. Làm cách nào tôi có thể đạt được điều này trong JAVA 7?
tanvi

1
Bạn có thể phải dựa vào câu trả lời khác sau đó. Điều này chỉ hoạt động trong Java 8.
off99555

1
+1 cho giải pháp hợp nhất, đây sẽ là chức năng có hiệu suất cao nhất vì bạn chỉ phải trả 1 lần cho phép tính mã băm (trong trường hợp Bản đồ bạn đang sử dụng trên phương thức hỗ trợ đúng cách), thay vì có khả năng trả tiền cho nó 3 lần
Ferrybig

2
Sử dụng phương pháp suy luận: map.merge (key, 1, Integer :: sum)
earandap

25

Luôn luôn là một ý tưởng tốt để xem Thư viện Bộ sưu tập của Google cho loại điều này. Trong trường hợp này, Multiset sẽ thực hiện thủ thuật:

Multiset bag = Multisets.newHashMultiset();
String word = "foo";
bag.add(word);
bag.add(word);
System.out.println(bag.count(word)); // Prints 2

Có các phương pháp giống như Bản đồ để lặp lại các khóa / mục, v.v. Trong thực tế, việc triển khai hiện đang sử dụng a HashMap<E, AtomicInteger>, do đó bạn sẽ không phải chịu chi phí đấm bốc.


Người trả lời ở trên cần phải phản ánh phản ứng từ chối. Api đã thay đổi kể từ khi được đăng (3 năm trước :))
Steve

Liệu các count()phương pháp trên chạy MultiSet trong thời gian O (1) hoặc O (n) thời gian (worstcase)? Các tài liệu không rõ ràng về điểm này.
Adam Parkin

Thuật toán của tôi cho loại điều này: if (hasApacheLib (thing)) return apacheLib; khác nếu (hasOnGuava (điều)) trả lại ổi. Thông thường tôi không vượt qua hai bước này. :)
digao_mb

22

Bạn nên biết rằng thực tế là nỗ lực ban đầu của bạn

int đếm = map.containsKey (từ)? map.get (từ): 0;

chứa hai hoạt động có khả năng tốn kém trên bản đồ, cụ thể là containsKeyget. Cái trước thực hiện một thao tác có khả năng khá giống với cái sau, vì vậy bạn đang thực hiện cùng một công việc hai lần !

Nếu bạn xem API cho Bản đồ, các getthao tác thường trở lại nullkhi bản đồ không chứa phần tử được yêu cầu.

Lưu ý rằng điều này sẽ làm cho một giải pháp như

map.put (khóa, map.get (khóa) + 1);

nguy hiểm, vì nó có thể mang lại NullPointerExceptions. Bạn nên kiểm tra nulltrước.

Cũng lưu ý , và điều này là rất quan trọng, đó HashMapcó thể chứa nullstheo định nghĩa. Vì vậy, không phải mọi trả lại đều nullnói "không có yếu tố đó". Về mặt này, containsKeyhành xử khác với getthực tế cho bạn biết liệu có một yếu tố như vậy. Tham khảo API để biết chi tiết.

Tuy nhiên, đối với trường hợp của bạn, bạn có thể không muốn phân biệt giữa lưu trữ nullvà "noSuchEuity". Nếu bạn không muốn cho phép null, bạn có thể thích a Hashtable. Sử dụng thư viện trình bao bọc như đã được đề xuất trong các câu trả lời khác có thể là một giải pháp tốt hơn để xử lý thủ công, tùy thuộc vào mức độ phức tạp của ứng dụng của bạn.

Để hoàn thành câu trả lời (và tôi đã quên đưa nó vào lúc đầu, nhờ vào chức năng chỉnh sửa!), Cách tốt nhất để thực hiện nó một cách tự nhiên, là getvào một finalbiến, kiểm tra nullputquay lại với a 1. Biến nên là finalvì dù sao nó cũng bất biến. Trình biên dịch có thể không cần gợi ý này, nhưng nó rõ ràng hơn theo cách đó.

bản đồ HashMap cuối cùng = createdRandomHashMap ();
Khóa đối tượng cuối cùng = fetchSomeKey ();
số nguyên cuối cùng i = map.get (khóa);
if (i! = null) {
    map.put (i + 1);
} khác {
    // làm việc gì đó
}

Nếu bạn không muốn dựa vào autoboxing, map.put(new Integer(1 + i.getValue()));thay vào đó bạn nên nói một cái gì đó như thế.


Để tránh vấn đề về các giá trị null / null ban đầu trong Groovy, cuối cùng tôi sẽ thực hiện: Counts.put (key, (Counts.get (key) ?: 0) + 1) // phiên bản quá phức tạp của ++
Joe Atzberger

2
Hoặc, đơn giản nhất là: đếm = [:]. WithDefault {0} // ++ đi
Joe Atzberger

18

Một cách khác sẽ là tạo một số nguyên có thể thay đổi:

class MutableInt {
  int value = 0;
  public void inc () { ++value; }
  public int get () { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt> ();
MutableInt value = map.get (key);
if (value == null) {
  value = new MutableInt ();
  map.put (key, value);
} else {
  value.inc ();
}

tất nhiên điều này có nghĩa là tạo ra một đối tượng bổ sung nhưng chi phí so với việc tạo một Integer (ngay cả với Integer.valueOf) không nên quá nhiều.


5
Bạn không muốn bắt đầu MutableInt ở lần đầu tiên khi bạn đặt nó vào bản đồ?
Tom Hawtin - tackline

5
Commons-lang của Apache đã có MutableInt được viết cho bạn.
SingleShot

11

Bạn có thể sử dụng phương thức compute IfAbsent trong Mapgiao diện được cung cấp trong Java 8 .

final Map<String,AtomicLong> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("B", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet(); //[A=2, B=1]

Phương thức computeIfAbsentkiểm tra xem khóa đã chỉ định đã được liên kết với một giá trị hay chưa? Nếu không có giá trị liên quan thì nó cố gắng tính giá trị của nó bằng hàm ánh xạ đã cho. Trong mọi trường hợp, nó trả về giá trị hiện tại (hiện tại hoặc được tính toán) được liên kết với khóa được chỉ định hoặc null nếu giá trị được tính là null.

Mặt khác, nếu bạn gặp tình huống nhiều luồng cập nhật một tổng chung, bạn có thể xem lớp LongAdder. Trong khi tranh chấp cao, thông lượng dự kiến ​​của lớp này cao hơn đáng kể so với AtomicLongchi phí tiêu thụ không gian cao hơn.


Tại sao đồng thờiHashmap và AtomicLong?
ealeon

7

Xoay vòng bộ nhớ có thể là một vấn đề ở đây, vì mỗi quyền anh của một int lớn hơn hoặc bằng 128 gây ra sự phân bổ đối tượng (xem Integer.valueOf (int)). Mặc dù trình thu gom rác xử lý rất hiệu quả với các đối tượng có thời gian tồn tại ngắn, nhưng hiệu suất sẽ bị ảnh hưởng ở một mức độ nào đó.

Nếu bạn biết rằng số lượng gia tăng được thực hiện sẽ nhiều hơn số lượng khóa (= từ trong trường hợp này), hãy xem xét sử dụng một người giữ int thay thế. Phax đã trình bày mã cho điều này. Đây là một lần nữa, với hai thay đổi (lớp chủ được thực hiện giá trị tĩnh và giá trị ban đầu được đặt thành 1):

static class MutableInt {
  int value = 1;
  void inc() { ++value; }
  int get() { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt>();
MutableInt value = map.get(key);
if (value == null) {
  value = new MutableInt();
  map.put(key, value);
} else {
  value.inc();
}

Nếu bạn cần hiệu năng cao, hãy tìm kiếm triển khai Bản đồ được điều chỉnh trực tiếp theo các loại giá trị nguyên thủy. jrudolph đã đề cập đến GNU Trove .

Nhân tiện, một thuật ngữ tìm kiếm tốt cho chủ đề này là "biểu đồ".


5

Thay vì gọi chứaKeyKey (), chỉ cần gọi map.get và kiểm tra xem giá trị trả về có phải là null hay không.

    Integer count = map.get(word);
    if(count == null){
        count = 0;
    }
    map.put(word, count + 1);

3

Bạn có chắc chắn rằng đây là một nút cổ chai? Bạn đã thực hiện bất kỳ phân tích hiệu suất?

Hãy thử sử dụng trình tạo hồ sơ NetBeans (miễn phí và được tích hợp vào NB 6.1) để xem các điểm nóng.

Cuối cùng, một bản nâng cấp JVM (giả sử từ 1.5-> 1.6) thường là một bộ tăng cường hiệu năng giá rẻ. Ngay cả một bản nâng cấp về số lượng bản dựng cũng có thể giúp tăng hiệu suất tốt. Nếu bạn đang chạy trên Windows và đây là một ứng dụng lớp máy chủ, hãy sử dụng -server trên dòng lệnh để sử dụng JVM của Hotspot máy chủ. Trên máy Linux và Solaris, điều này được tự động phát hiện.


3

Có một vài cách tiếp cận:

  1. Sử dụng một alorithm Bag giống như các bộ có trong Bộ sưu tập của Google.

  2. Tạo vùng chứa có thể thay đổi mà bạn có thể sử dụng trong Bản đồ:


    class My{
        String word;
        int count;
    }

Và sử dụng put ("word", My mới ("Word")); Sau đó, bạn có thể kiểm tra nếu nó tồn tại và gia tăng khi thêm.

Tránh cuộn giải pháp của riêng bạn bằng cách sử dụng danh sách, bởi vì nếu bạn nhận được tìm kiếm và sắp xếp bên trong, hiệu suất của bạn sẽ bốc mùi. Giải pháp HashMap đầu tiên thực sự khá nhanh, nhưng một giải pháp phù hợp như được tìm thấy trong Bộ sưu tập của Google có lẽ tốt hơn.

Đếm các từ bằng cách sử dụng Bộ sưu tập của Google, trông giống như thế này:



    HashMultiset s = new HashMultiset();
    s.add("word");
    s.add("word");
    System.out.println(""+s.count("word") );


Sử dụng HashMultiset khá thanh lịch, bởi vì thuật toán túi chỉ là thứ bạn cần khi đếm từ.


3

Tôi nghĩ rằng giải pháp của bạn sẽ là cách tiêu chuẩn, nhưng - như bạn đã lưu ý - có lẽ đó không phải là cách nhanh nhất có thể.

Bạn có thể nhìn vào GNU Trove . Đó là một thư viện chứa tất cả các loại Bộ sưu tập nguyên thủy nhanh chóng. Ví dụ của bạn sẽ sử dụng TObjectIntHashMap có phương thức điều chỉnhOrPutValue thực hiện chính xác những gì bạn muốn.


Liên kết đến TObjectIntHashMap bị hỏng. Đây là liên kết chính xác: trove4j.sourceforge.net/javadocs/gnu/trove/map/ mẹo
Erel Segal-Halevi

Cảm ơn, Erel, tôi đã sửa liên kết.
jrudolph

3

Một biến thể của cách tiếp cận MutableInt thậm chí có thể nhanh hơn, nếu một chút hack, là sử dụng một mảng int phần tử đơn:

Map<String,int[]> map = new HashMap<String,int[]>();
...
int[] value = map.get(key);
if (value == null) 
  map.put(key, new int[]{1} );
else
  ++value[0];

Sẽ rất thú vị nếu bạn có thể chạy lại các bài kiểm tra hiệu suất của mình với biến thể này. Nó có thể là nhanh nhất.


Chỉnh sửa: Mẫu trên hoạt động tốt với tôi, nhưng cuối cùng tôi đã thay đổi sử dụng các bộ sưu tập của Trove để giảm kích thước bộ nhớ trong một số bản đồ rất lớn mà tôi đang tạo - và như một phần thưởng, nó cũng nhanh hơn.

Một tính năng thực sự hay là TObjectIntHashMaplớp có một adjustOrPutValuecuộc gọi duy nhất , tùy thuộc vào việc đã có một giá trị tại khóa đó hay chưa, sẽ đặt một giá trị ban đầu hoặc tăng giá trị hiện có. Điều này là hoàn hảo để tăng:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);

3

Bộ sưu tập Google HashMultiset:
- khá thanh lịch để sử dụng
- nhưng tiêu thụ CPU và bộ nhớ

Tốt nhất là có một phương pháp như: Entry<K,V> getOrPut(K); (thanh lịch và chi phí thấp)

Phương pháp như vậy sẽ tính toán hàm băm và chỉ mục một lần, và sau đó chúng ta có thể làm những gì chúng ta muốn với mục nhập (thay thế hoặc cập nhật giá trị).

Thanh lịch hơn:
- thực hiện HashSet<Entry>
- mở rộng nó để get(K)đặt Entry mới nếu cần
- Entry có thể là đối tượng của riêng bạn.
->(new MyHashSet()).get(k).increment();


3

Khá đơn giản, chỉ cần sử dụng chức năng tích hợp sẵn Map.javanhư sau

map.put(key, map.getOrDefault(key, 0) + 1);

Điều này không làm tăng giá trị, nó chỉ đặt giá trị hiện tại hoặc 0 nếu không có giá trị nào được gán cho khóa.
siegi

Bạn có thể tăng giá trị bằng ++... OMG, thật đơn giản. @siegi
sudoz

Đối với bản ghi: ++không hoạt động ở bất cứ đâu trong biểu thức này vì một biến là cần thiết như toán hạng của nó nhưng chỉ có các giá trị. Bổ sung của bạn các + 1công trình mặc dù. Bây giờ giải pháp của bạn giống như trong câu trả lời của off99555 .
siegi

2

"Đặt" cần "lấy" (để đảm bảo không có khóa trùng lặp).
Vì vậy, trực tiếp thực hiện "đặt"
và nếu có giá trị trước đó, thì hãy thực hiện bổ sung:

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

Nếu số bắt đầu bằng 0, sau đó thêm 1: (hoặc bất kỳ giá trị nào khác ...)

Map map = new HashMap ();

MutableInt newValue = new MutableInt (0); // default
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.setValue(oldValue + 1); // old + inc
}

Lưu ý: Mã này không phải là chủ đề an toàn. Sử dụng nó để xây dựng sau đó sử dụng bản đồ, không đồng thời cập nhật nó.

Tối ưu hóa: Trong một vòng lặp, giữ giá trị cũ để trở thành giá trị mới của vòng lặp tiếp theo.

Map map = new HashMap ();
final int defaut = 0;
final int inc = 1;

MutableInt oldValue = new MutableInt (default);
while(true) {
  MutableInt newValue = oldValue;

  oldValue = map.put (key, newValue); // insert or...
  if (oldValue != null) {
    newValue.setValue(oldValue + inc); // ...update

    oldValue.setValue(default); // reuse
  } else
    oldValue = new MutableInt (default); // renew
  }
}

1

Các trình bao bọc nguyên thủy khác nhau, ví dụ, Integerlà bất biến nên thực sự không có cách nào ngắn gọn hơn để làm những gì bạn yêu cầu trừ khi bạn có thể làm điều đó với một cái gì đó như AtomicLong . Tôi có thể cho nó đi trong một phút và cập nhật. BTW, Hashtable một phần của Bộ sưu tập Khung .


1

Tôi sẽ sử dụng Bản đồ lười biếng của Bộ sưu tập Apache (để khởi tạo giá trị thành 0) và sử dụng MutableIntegers từ Apache Lang làm giá trị trong bản đồ đó.

Chi phí lớn nhất là phải kiểm tra bản đồ hai lần trong phương pháp của bạn. Trong tôi bạn phải làm điều đó một lần. Chỉ cần lấy giá trị (nó sẽ được khởi tạo nếu vắng mặt) và tăng nó.


1

Cơ sở hạ tầng của thư viện Java Chức năngTreeMap có một updatephương thức trong phần đầu thân mới nhất:

public TreeMap<K, V> update(final K k, final F<V, V> f)

Ví dụ sử dụng:

import static fj.data.TreeMap.empty;
import static fj.function.Integers.add;
import static fj.pre.Ord.stringOrd;
import fj.data.TreeMap;

public class TreeMap_Update
  {public static void main(String[] a)
    {TreeMap<String, Integer> map = empty(stringOrd);
     map = map.set("foo", 1);
     map = map.update("foo", add.f(1));
     System.out.println(map.get("foo").some());}}

Chương trình này in "2".


1

@Vilmantas Baranauskas: Về câu trả lời này, tôi sẽ bình luận nếu tôi có điểm đại diện, nhưng tôi thì không. Tôi muốn lưu ý rằng lớp Counter được định nghĩa là KHÔNG an toàn luồng vì nó không đủ để chỉ đồng bộ hóa inc () mà không đồng bộ hóa giá trị (). Các luồng khác gọi giá trị () không được đảm bảo để xem giá trị trừ khi mối quan hệ xảy ra trước khi được thiết lập với bản cập nhật.


Nếu bạn muốn tham khảo câu trả lời của ai đó, hãy sử dụng @ [Tên người dùng] ở đầu, ví dụ: @Vilmantas Baranauskas <Nội dung tại đây>
Hank Gay

Tôi đã thực hiện sửa đổi đó để làm sạch nó.
Alex Miller

1

Tôi không biết hiệu quả của nó như thế nào nhưng đoạn mã dưới đây cũng hoạt động. Bạn cần xác định BiFunctionngay từ đầu. Thêm vào đó, bạn có thể thực hiện nhiều hơn là chỉ tăng với phương pháp này.

public static Map<String, Integer> strInt = new HashMap<String, Integer>();

public static void main(String[] args) {
    BiFunction<Integer, Integer, Integer> bi = (x,y) -> {
        if(x == null)
            return y;
        return x+y;
    };
    strInt.put("abc", 0);


    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abcd", 1, bi);

    System.out.println(strInt.get("abc"));
    System.out.println(strInt.get("abcd"));
}

đầu ra là

3
1

1

Nếu bạn đang sử dụng Bộ sưu tập Eclipse , bạn có thể sử dụng a HashBag. Nó sẽ là cách tiếp cận hiệu quả nhất về mặt sử dụng bộ nhớ và nó cũng sẽ hoạt động tốt về tốc độ thực thi.

HashBagđược hỗ trợ bởi một MutableObjectIntMapnơi lưu trữ ints nguyên thủy thay vì các Counterđối tượng. Điều này làm giảm chi phí bộ nhớ và cải thiện tốc độ thực hiện.

HashBagcung cấp API bạn cần vì nó Collectioncũng cho phép bạn truy vấn số lần xuất hiện của một mặt hàng.

Đây là một ví dụ từ Bộ sưu tập Eclipse Kata .

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

Lưu ý: Tôi là người đi làm cho Bộ sưu tập Eclipse.


1

Tôi đề nghị sử dụng Java 8 Map :: compute (). Nó cũng xem xét trường hợp khi khóa không tồn tại.

Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);

mymap.merge(key, 1, Integer::sum)?
Det

-2

Vì nhiều người tìm kiếm các chủ đề Java cho câu trả lời Groovy, đây là cách bạn có thể làm điều đó trong Groovy:

dev map = new HashMap<String, Integer>()
map.put("key1", 3)

map.merge("key1", 1) {a, b -> a + b}
map.merge("key2", 1) {a, b -> a + b}

-2

Cách đơn giản và dễ dàng trong java 8 là như sau:

final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.computeIfAbsent("foo", key -> new AtomicLong(0)).incrementAndGet();

-3

Hy vọng tôi hiểu chính xác câu hỏi của bạn, tôi đến Java từ Python để tôi có thể đồng cảm với cuộc đấu tranh của bạn.

nếu bạn có

map.put(key, 1)

bạn sẽ làm

map.put(key, map.get(key) + 1)

Hi vọng điêu nay co ich!

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.