简体   繁体   中英

Custom Comparator

Given the following list: "A", "B", "C", "D", "E", "F", "G"

I need a comparator that does the following sorting:

  • specify a certain element (eg "D" )
  • start with the element
  • followed by all following elements of the original list in the original order
  • followed by all preceding elements of the original list in the original order

The result would be: "D", "E", "F", "G", "A", "B", "C"


Please be aware that I know that I could just do stuff similar to the following:

List<String> following = myList.subList(myList.indexOf("D") + 1, myList.size());
List<String> preceding = myList.subList(0, myList.indexOf("D"));
List<String> newList = Stream.of(Collections.singletonList("D"), following, preceding)
                       .flatMap(List::stream)
                       .collect(Collectors.toList());

In this question I explicitly mean a Comparator implementation.


It is clear that it will have to have the list & element as a parameter, I am just not clear about the comparison algorithm itself:

private static class MyComparator<T> implements Comparator<T> {

  private final List<T> list;
  private final T element;

  private MyComparator(List<T> list, T element) {
    this.list = list;
    this.element = element;
  }

  @Override
  public int compare(T o1, T o2) {
    // Not clear
  }
}

if I got the question right you what to do Collections.sort(myList, myComparator);

then I can suggest you using a defined collator:

List<String> myList = Arrays.asList("A", "B", "C", "D", "E", "F", "G");
System.out.println(myList);
String rules = "< d,D < e,E < f,F < g,G < a,A < b,B < c,C";

RuleBasedCollator ruleBasedCollator = new RuleBasedCollator(rules);
Collections.sort(myList, ruleBasedCollator);
System.out.println(myList);

the rule here is "< d,D < e,E < f,F < g,G < a,A < b,B < c,C" which means which chars have a higher weight than others... the rest is as usual a sorting method

the output

[A, B, C, D, E, F, G]

[D, E, F, G, A, B, C]

It doesn't make much sense to use a Comparator here, since it would be very inefficient (since you would have to locate the indices of the 2 compared elements in the original list, which would cause each comparison to require linear time).

And it may fail anyway if the list contains duplicates.

However, since you asked, something like this may work:

public int compare(T o1, T o2) {
    if (o1.equals(o2)
        return true;
    int i1 = list.indexOf(o1);
    int i2 = list.indexOf(o2);
    int ie = list.indexOf(element); // this can be done in the constructor of the Comparator
    // now you have to check whether i1 and i2 are smaller than or larger than
    // ie, and based on that determine which of the corresponding elements should come
    // first
}

or, simply call

Collections.rotate(list,list.size() - list.indexOf(element));

I think this is what you want :

class ImposedOrder<T> implements Comparator<T> {

    private final List<T> list;
    private final int startIndex;

    ImposedOrder(List<T> list, T startElement) {
        this.list = new ArrayList<>(list);
        this.startIndex = list.indexOf(startElement);
        if (startIndex < 0) {
            throw new IllegalArgumentException();
        }
    }

    @Override
    public int compare(T t1, T t2) {
        int t1Index = list.indexOf(t1);
        int t2Index = list.indexOf(t2);
        return Integer.compare(adjust(t1Index), adjust(t2Index));
    }

    private int adjust(int rawIndex) {
        if (rawIndex >= startIndex) {
            return rawIndex;
        }
        return rawIndex + list.size();
    }
}

Some extra validation may be in order to avoid an imposed order list with duplicates.

The linear search, using indexOf doesn't give you a great performance, but for a small order list it may suffice. Otherwise, rather than saving a copy of the imposed order list, you could map elements to their adjusted index in the Comparator's constructor.

Like this :

class ImposedOrder<T> implements Comparator<T> {

    private final Map<T, Integer> map;
    private final int startIndex;

    ImposedOrder(List<T> list, T startElement) {
        this.startIndex = list.indexOf(startElement);
        if (startIndex < 0) {
            throw new IllegalArgumentException();
        }
        this.map = IntStream.range(0, list.size())
                .boxed()
                .collect(Collectors.toMap(
                        list::get,
                        i -> adjust(startIndex, list.size(), i)
                ));
    }

    @Override
    public int compare(T t1, T t2) {
        Integer t1Index = map.get(t1);
        Integer t2Index = map.get(t2);
        return t1Index.compareTo(t2Index);
    }

    private static int adjust(int startIndex, int size, int rawIndex) {
        if (rawIndex >= startIndex) {
            return rawIndex;
        }
        return rawIndex + size;
    }
}

I knew it was possible, but it is messy ... and unsafe

The idea to push the first block to the end is to add a String to force those value to be bigger. That way, "A" is bigger than "G" if it looks like "ZA" when it is compare with.

    List<String> list = Arrays.asList(new String[]{ "A", "B", "C", "D", "E", "F"});
    final String split = "D";
    Collections.sort(list, new Comparator<String>(){
        public int compare(String o1, String o2) {
            //Prepend with a big String value (like "ZZZZ") if it is before the value `split`.
            if(o1.compareTo(split) < 0)
                o1 = "ZZZZ" + o1;
            if(o2.compareTo(split) < 0)
                o2 = "ZZZZ" + o2;

            return o1.compareTo(o2); //then compare
        }
    });

    System.out.println(list);

[D, E, F, A, B, C]

This is not safe since this could be complicate to prepend with a safe value that will be bigger, I thought about using Character.MAX_VALUE but I am not sure this would be safer. Well, using the split value itself could be way simpler ..

You need to compare the indexes of the elements.

private static class MyComparator<T> implements Comparator<T> {

    private final List<T> list;
    private final int elementIndex;

    private MyComparator(List<T> list, T element) {
        // keep original order
        this.list = new ArrayList<>(list);
        this.elementIndex = list.indexOf(element);
    }

    @Override
    public int compare(T o1, T o2) {
        int i1 = list.indexOf(o1);
        int i2 = list.indexOf(o2);
        if (i1 == elementIndex) {
            // "shift" first element left
            return -1;
        } else if (i2 == elementIndex) {
            // "shift" secont element left
            return 1;
        }
        boolean left1 = i1 < elementIndex;
        boolean left2 = i2 < elementIndex;
        if (left1 != left2) {
            // different part
            return i2 - elementIndex;
        } else {
            // same part
            return i1 - i2;
        }
    }
}

The idea is to compare weights of the elements based on the original indexes in the list - if index of current element >= index of the specified certain element (eg "D"), then index in the new list should be moved to the left, index of "D" is exactly the required offset.

int weightO1 = list.indexOf(o1) - list.indexOf(element);

in the opposite case (list.indexOf(o1) < list.indexOf(element)) the calculation of the weight should move element to the right, eg it could be

int weightO1 = list.indexOf(o1) + list.size();

I'd incapsulate calculation of weight in local method:

   int weight (T o) {
    return int weight = list.indexOf(o) < list.indexOf(element) ? list.indexOf(o) + list.size() : list.indexOf(o) - list.indexOf(element);
   }

so the implementation of compare method would be like:

@Override
  public int compare(T o1, T o2) {
     return weight(o1) - weight(o2); 
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM