C, PHP, VB, .NET

Дневникът на Филип Петров


* Множества: хеш таблици и червено-черни дървета

Публикувано на 15 ноември 2015 в раздел ПИК3 Java.

Множествата са по принцип по-прости структури спрямо речниците. Те се базират на един основен интерфейс Set. Докато при речниците имахме комбинации от Key и Value, при множествата има само един единствен елемент, който е едновремено Key и Value. Или казано по друг начин множествата са речници при които всяко Value се използва за Key само за себе си. Веднага можете да се досетите, че е много лесно един Set да бъде реализиран чрез Map - например ако подавате един и същи обект за Key и Value в който и да е Map, той ще работи като Set.

С казаното дотук не е изненада, че в Java популярните имплементиращи интерфейс Set класове HashSet, LinkedHashSet и TreeSet всъщност използват именно HashMap, LinkedHashMap и TreeMap вътре в себе си. Например извиквайки метод add на един HashSet, той извиква метод put на вградения в него HashMap, като му подава параметри put(yourObject, dummyObject), където yourObject е вашия обект (използва се за key), а DummyObject e вградена в HashSet референция към следното:

private static final Object dummyObject = new Object();

Както виждате този dummyObject е винаги един и същи за всички елементи на множеството. В така създадения речник не можете да добавите друг Value за същия Key, защото интерфейс Set и неговите стандартни имплементации няма да ви предожат подобен метод. Другите популярни класове в стандартните библиотеки - LinkedHashSet и TreeSet - също използват тази техника.

От всичко казано дотук, можем да си изведем и най-важното свойство на класовете имплементиращи Set - не позволяват един и същи обект да бъде вкаран два пъти в множеството! Действително ако вие се опитате да добавите един и същи обект два пъти в Set, това ще кореспондира на дублираща се комбинация Key (обекта) и Value (dummy обекта) в съответния Map. Забележете, че това няма общо с колизиите при HashSet - и тук е възможно два или повече различни обекта да генерират един и същи хеш код и в тези случаи обектите отново се подреждат в линеен списък.

1. Интерфейсът Set

Интерфейс Set се различава съществено от интерфейс Map не само по входните параметри на методите (вместо два вече ще е само един), но и по имената им.

  • boolean add(E e) - добавя елемент в множеството. Ако такъв елемент вече съществува, връща false, иначе true;
  • boolean addAll(Collection<? extends E> c) - добавя всички елементи от дадена колекция в множеството. Ако има някаква промяна в множеството (имало е недублиращ елемент и той е добавен), връща true. Ако всички елементи вече са били налични в множеството, връща false;
  • void clear() - изтрива всички елементи в множеството;
  • boolean contains(Object o) - проверява дали подадения елемент присъства в множеството;
  • boolean containsAll(Collection<?> c) - проверява дали всички елементи от подадената колекция се съдържат в множеството;
  • boolean equals(Object o) - сравнява множеството с друго множество;
  • int hashCode() - хеш код за самото множество (ще е нужно ако самото то бъде използвано като елемент в друго множество);
  • boolean isEmpty() - показва дали множеството е празно или не;
  • Iterator<E> iterator() - дава итератор;
  • boolean remove(Object o) - изтрива елемент от множеството. Ако е имало такъв елемент и е изтрит, връща true. В противен случай връща false;
  • boolean removeAll(Collection<?> c) - изтрива всички елементи от множеството, които съвпадат с подадената колекция. Ако поне един е изтрит, връща true. В противен случай връща false;
  • boolean retainAll(Collection<?> c) - премахва от множеството всички елементи, които не присъстват в подадената колекция. Ако поне един е изтрит, връща true. В противен случай връща false;
  • int size() - указва колко елемента има записани в множеството;
  • default Spliterator<E> spliterator() - връща Spliterator;
  • Object[] toArray() - превръща множеството в масив от обекти;
  • <T> T[] toArray(T[] a) - превръща множеството в масив от обекти, като типа на масива ще съвпадне с типа на подадения като входен параметър обект. Това е доста специфичен метод, който прави лесен преход между колекциите и масивите - ще покажем как се използва по-долу.

2. Примери

Задача 1. Намерете уникалните думи във файла problems.txt и ги изведете на екрана.

Решение: Понеже не се изисква сортиране, ще използваме HashSet:

import java.util.HashSet;
import java.util.Scanner;
import java.io.FileReader;
import java.io.IOException;
import java.util.Iterator;

public class HashSetExample{
  public static void main(String[] args){
    Scanner sc = null;
    try{
      sc = new Scanner(new FileReader("problems.txt"));
    }
    catch(IOException e){
      System.out.println("Error reading the file");
      return;
    }
    
    HashSet<String> uniqueWords = new HashSet<String>();
    while(sc.hasNext()){
      uniqueWords.add(sc.next());
    }
    
    System.out.println("Уникалните думи са:");
    for(String word: uniqueWords){
      System.out.println(word);
    }
    
    System.out.println("\nСъщото нещо реализирано чрез iterator:");
    Iterator<String> it = uniqueWords.iterator();
    while(it.hasNext()){
      System.out.println(it.next());
    }
  }
}

Задача 2. Решете задача 1, но този път изведете думите подредени по азбучен ред

Решение: Просто заменете HashSet с TreeSet.

Задача 3. Демонстрирайте как горния HashSet се превръща в обикновен масив

Решение:

import java.util.HashSet;

public class HashSetExample{
  public static void main(String[] args){
    HashSet hs = new HashSet();
    hs.add("abc");
    hs.add("def");
    hs.add("abc"); // нарочно - ще бъде пропуснато
        
    String[] arr = hs.toArray(new String[0]);
    for(String el:arr) System.out.println(el);
  }
}

3. Множествата в многонишкова среда

HashSet, LinkedHashSet и TreeSet не са thread-safe, т.е. не са синхронизирани. Синхронизацията трябва да извършвате или ръчно, или чрез удобния Collections.synchronizedSet(Set set) метод. Ето как можем да направим това:

import java.util.Set;
import java.util.HashSet;
import java.util.Collections;

public class HashSetExample{
  public static void main(String[] args){
    Set<String> hs = Collections.synchronizedSet(new HashSet<String>());
    hs.add("abc");
    hs.add("def");
    hs.add("abc"); // нарочно - ще бъде пропуснато
        
    hs.forEach(System.out::println);
  }
}

Трябва да знаете, че Java не предлага еквивалентен на ConcurrentHashMap метод! За сметка на това можете много лесно да си създадете без проблем метод newSetFromMap на клас Collections, в който като параметър да подадете ConcurrentHashMap обект. Горният пример ще изглежда по следния начин:

import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Collections;

public class HashSetExample{
  public static void main(String[] args){
    Set<String> hs = Collections.newSetFromMap(new ConcurrentHashMap<String,Boolean>());
    hs.add("abc");
    hs.add("def");
    hs.add("abc"); // нарочно - ще бъде пропуснато
        
    hs.forEach(System.out::println);
  }
}

Поради същите съображения, които бяха изложени в предишната статия, втория пример е по-ефективен от първия.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*