3

I prototype in python and I'm used the zip function for this, I'm not sure how to do this in Java. Basically I have two lists (one is names and one is data) and want them sorted in relation to each other. My program only processes a list (data, in this case) but I use the names as a reference to what data I'm processing and I want to try to experiment with processing my data in a different order. Here's an example of the structure (in reality my data is not given to me stored but I would do either a basic sort or a reverse sort on it, nothing fancy).

String[] names = new String[]{"Monkey1", "Dog2", "Horse3", "Cow4", "Spider5"};
int[] data = new int[]{1, 2, 3, 4, 5};

so the inverse would be

name = Spider5, Cow4, Horse3, Dog2, Monkey1
data = 5, 4, 3, 2, 1

I found this question: Is there an accepted Java equivalent to Python's zip(), but I would rather (if possible and for the faint of heart) do this using libraries I already have (Java commons, apache commons, etc). If there's no other way then I'll give functional java a shot. Any suggestions?

Lostsoul
  • 25,013
  • 48
  • 144
  • 239

7 Answers7

7

If you really don't want to redo your data-structures to combine the infos, you can use a Multimap to do it.

This example utilizes the excellent Google-Guava Library, which you should be using anyway :) https://code.google.com/p/guava-libraries/

String[] names = new String[] {"Monkey1", "Dog2", "Horse3", "Cow4", "Spider5"};
int[] data = new int[] {1,2,3,4,5};

/* guava, throws an IllegalStateException if your array aren't of the same length */
Preconditions.checkState(names.length == data.length, "data and names must be of equal length");

/* put your values in a MultiMap */
Multimap<String, Integer> multiMap = LinkedListMultimap.create();
for (int i=0; i<names.length; i++) {
    mmap.put(names[i], data[i]);
}

/* our output, 'newArrayList()' is just a guava convenience function */
List<String> sortedNames = Lists.newArrayList();
List<Integer> sortedData = Lists.newArrayList();

/* cycle through a sorted copy of the MultiMap's keys... */
for (String name : Ordering.natural().sortedCopy(mmap.keys())) {

    /* ...and add all of the associated values to the lists */
    for (Integer value : mmap.get(name)) {
        sortedNames.add(name);
        sortedData.add(value);
    }
}
Fabian Zeindl
  • 5,860
  • 8
  • 54
  • 78
6

Here's complete code:

StringIntTuple.java:

public class StringIntTuple{
    public final int intValue;
    public final String stringValue;
    public StringIntTuple(int intValue, String stringValue){
        this.intValue = intValue;
        this.stringValue = stringValue;
    }
    public String toString(){
        return "(" + this.intValue + ", " + this.stringValue + ")";
    }

}

StringIntTupleStringComparator.java:

import java.util.Comparator;


public class StringIntTupleStringComparator implements
        Comparator<StringIntTuple> {

    @Override
    public int compare(StringIntTuple a, StringIntTuple b) {
        // TODO Auto-generated method stub
        return a.stringValue.compareTo(b.stringValue);
    }

}

StringIntTupleIntComparator.java:

import java.util.Comparator;


public class StringIntTupleIntComparator implements Comparator<StringIntTuple> {

    @Override
    public int compare(StringIntTuple a,
            StringIntTuple b) {
        return ((Integer)a.intValue).compareTo((Integer)b.intValue);
    }

}

Driver.java:

import java.util.ArrayList;
import java.util.Collections;


public class Driver {

    /**
     * @param args
     */
    public static String[] names = new String[] {"Monkey1", "Dog2", "Horse3", "Cow4", "Spider5"};
    public static int[] data = new int[] {1,2,3,4,5};
    public static void main(String[] args) {
        ArrayList<StringIntTuple> list = new ArrayList<StringIntTuple>();
        for(int i =0; i<names.length; i++){
            list.add(new StringIntTuple(data[i],names[i]));
        }
        Collections.sort(list, new StringIntTupleIntComparator());
        System.out.println(list.toString());
        Collections.sort(list, new StringIntTupleStringComparator());
        System.out.println(list.toString());
    }


}

Output (sorted first by int field, then by String field):

[(1, Monkey1), (2, Dog2), (3, Horse3), (4, Cow4), (5, Spider5)]

[(4, Cow4), (2, Dog2), (3, Horse3), (1, Monkey1), (5, Spider5)]

EDIT 1 (extra info):

If you want to make this work for any Tuple, i.e. which doesn't constrain the field types to int, String, you can simply do the same operation with generics, i.e.:

public class Tuple<A,B>{
    public Tuple(A aValue, B bValue){
        this.aValue = aValue;
        this.bValue = bValue;
    }
    public final A aValue;
    public final B bValue;

}

Then, just tweak the Comparators accordingly, and you have a generic solution. EDIT 2(After lunch): Here it is.

public class TupleAComparator<A extends Comparable<A>,B extends Comparable<B>> implements Comparator<Tuple<A,B>> {

    @Override
    public int compare(Tuple<A, B> t1, Tuple<A, B> t2) {
        return t1.aValue.compareTo(t2.aValue);
    }

}

EDIT 3: Code supplement as answer to Comment #1 (augmenting comment #2) TupleArrayList.java:

import java.util.ArrayList;
import java.util.List;


public class TupleArrayList<A,B> extends ArrayList<Tuple<A,B>> {

    /**
     * An ArrayList for tuples that can generate a List of tuples' elements from a specific position within each tuple
     */
    private static final long serialVersionUID = -6931669375802967253L;

    public List<A> GetAValues(){
        ArrayList<A> aArr = new ArrayList<A>(this.size());
        for(Tuple<A,B> tuple : this){
            aArr.add(tuple.aValue);
        }
        return aArr;
    }

    public List<B> GetBValues(){
        ArrayList<B> bArr = new ArrayList<B>(this.size());
        for(Tuple<A,B> tuple : this){
            bArr.add(tuple.bValue);
        }
        return bArr;
    }

}
Greg Kramida
  • 4,064
  • 5
  • 30
  • 46
2

The "right" way to do this in Java is to create a combined object that holds the corresponding elements, and to sort that.

Example:

class NameAndData {
  private final String name;
  private final int data;
}

List<NameAndData> toBeSorted;

and then you create a list of the combined elements and sort that. Basically, you're writing your own specific Pair class. (I, and many Java developers, think that adding a Pair class to Java would just lead to more obfuscated code -- a LatLong class, for example, is much less ambiguous about what it means than a Pair<Double, Double>.)

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • Sorry, I'm really new to Java. What do you mean by combined? Like merging them together like dictionary and sorting by values? – Lostsoul Apr 18 '12 at 16:17
  • @learningJava I updated my response to give you an idea what we are talking about. – John B Apr 18 '12 at 16:20
2

So the obvious answer here is to wrap the name and data values in a class. Then maintain a List of that class. The class should implement equals, hashCode and Comparable which then then allow sorting the list using Collections.sort.

Maintain related data in two different lists is anti-OOP.

Something like this.

class MyWrapper implements Comparable<MyWrapper>{
   private String name;
   private int data;
}

List<MyWrapper> listToBeSorted;
John B
  • 32,493
  • 6
  • 77
  • 98
  • I kind of get what your saying. Never thought or tried it, I'll give it a shot and see how it goes. – Lostsoul Apr 18 '12 at 16:19
2

In some cases it doesn't make much sense to create a new class just to do concurrent sorting.

Here, is a function that can be used to sort an arbitrary number of Lists with arbitrary types based on a key that implement Comparable (Ideone Example here).


Usage

Here is an example of how you can use the function to sort multiple lists of arbitrary types:

// Can be any type that implements Comparable, Dupes are allowed
List<Integer> key = Arrays.asList(4, 3, 1, 2, 1);

// List Types do not need to be the same
List<String> list1 = Arrays.asList("Four", "Three", "One", "Two", "One");
List<Character> list2 = Arrays.asList('d', 'c', 'a', 'b', 'a');

// Sorts key, list1, list2
// Remove second key if you don't want to sort key.
multiSort(key, key, list1, list2);

Output:

key:   [1, 1, 2, 3, 4]
list1: [One, One, Two, Three, Four]
list2: [a, a, b, c, d]

Code

An Ideone Example can be found here which includes validation of parameters and a test case.

public static <T extends Comparable<T>> void multiSort(
    final List<T> key, List<?>... lists){
  // Create a List of indices
  List<Integer> indices = new ArrayList<Integer>();
  for(int i = 0; i < key.size(); i++) {
    indices.add(i);
  }

  // Sort the indices list based on the key
  Collections.sort(indices, new Comparator<Integer>() {
    @Override public int compare(Integer i, Integer j) {
      return key.get(i).compareTo(key.get(j));
    }
  });

  // Create a mapping that allows sorting of the List by N swaps.
  // Only swaps can be used since we do not know the type of the lists
  Map<Integer,Integer> swapMap = new HashMap<Integer, Integer>(indices.size());
  List<Integer> swapFrom = new ArrayList<Integer>(indices.size()),
                swapTo   = new ArrayList<Integer>(indices.size());
  for (int i = 0; i < key.size(); i++) {
    int k = indices.get(i);
    while (i != k && swapMap.containsKey(k)) {
      k = swapMap.get(k);
    }

      swapFrom.add(i);
      swapTo.add(k);
      swapMap.put(i, k);
  }

  // use the swap order to sort each list by swapping elements
  for (List<?> list : lists)
    for (int i = 0; i < list.size(); i++)
      Collections.swap(list, swapFrom.get(i), swapTo.get(i));
}
bcorso
  • 45,608
  • 10
  • 63
  • 75
1

You could use a ConcurrentSkipListMap which can provide forward and reverse iterators over the keys. If you are looking for arbitrary re-orderings besides a fixed forward and reverse ordering, you'll have to go to something else. Or you can always keep a simple HashMap or whatever to maintain parallel item associations, and then construct a SortedMap (Treemap or ConcurrentSkipListMap) as needed by providing an appropriate Comparator.

The disadvantage of this approach is that the associations between keys/values are much more transient, and can be more easily and accidentally broken by updates to the map. All of the other answers that create Tuples, Pairs, or other explicit 1-1 relationships address that better. Of course, if you intend for the associations to be more fluid, then just using a map adds a bit of an advantage.

Kevin Welker
  • 7,719
  • 1
  • 40
  • 56
  • You can use a BiMap to get a 1:1 relationship: http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/BiMap.html But using a normal Map might not be sufficient here, because the source-lists could contains a "key" multiple times. – Fabian Zeindl May 01 '12 at 19:42
  • Good point on BiMap giving 1:1, but it doesn't in itself support ordered iteration, both forwards and backwards. You could certainly sort the keys yourself manually, which might actually make your approach more flexible in terms of ordered iteration, while preserving 1:1 relationship. – Kevin Welker May 01 '12 at 20:31
1

Assuming that the lengths of these two arrays are the same, you can create a list of map entries containing pairs of elements from these arrays, and sort this list in reverse order by key as follows:

String[] names = new String[]{"Monkey1", "Dog2", "Horse3", "Cow4", "Spider5"};
int[] data = new int[]{1, 2, 3, 4, 5};

List<Map.Entry<Integer, String>> entryList = IntStream
        .range(0, names.length)
        .mapToObj(i -> Map.entry(data[i], names[i]))
        .sorted(Map.Entry.<Integer, String>comparingByKey().reversed())
        .collect(Collectors.toList());

System.out.println(entryList);
// [5=Spider5, 4=Cow4, 3=Horse3, 2=Dog2, 1=Monkey1]

If you want to replace the contents of the arrays:

IntStream.range(0, entryList.size()).forEach(i -> {
    data[i] = entryList.get(i).getKey();
    names[i] = entryList.get(i).getValue();
});

System.out.println(Arrays.toString(data));
// [5, 4, 3, 2, 1]
System.out.println(Arrays.toString(names));
// [Spider5, Cow4, Horse3, Dog2, Monkey1]

See also: Sorting two parallel arrays