In my understanding of this problem the most suitable data structure for solving it is an acyclic disjointed Graph.
In general case, a graph will be comprised of several unconnected clusters. Each cluster will have a tree-like structure, in the edge case it'll form a linked list.
Basically, the most simple naive approach on how to solve this problem is to create a bunch of linked list based on each line, and iterate over them. The drawbacks are: duplication of nodes (greater memory consumption), greater time-complexity (more operations required) and it's more error-prone because more manual actions are needed.
The description of the Graph
So I'll stick with the graph as the data structure for this problem and try to keep things as simple as possible.
Let's consider the following input:
"Mary had a little lamb named Willy"
"Mary had a little ham"
"A B C"
The graphical representation of the graph will look like this;

The two first lines will constitute a cluster formed from a linked list (the head part) and a tree (the tail part). The second cluster will be represented by a linked list, its vertices aren't connected with vertices formed from other strings.
It's not the only way the vertexes can be structured, the head could spawn an N-tree and a linked list could be observed somewhere in the middle.
The main takeaway is that in order to solve the problem, we need to track the chain of vertexes through all the branches until the vertexes overlap. In these parts of the graph, every prefix-strings and suffix-string that is common among two or more lines will be represented by a single vertex (node).
To maintain the number of strings that are mapped to a particular vertex, each vertex should have a variable (int groupCount in the code below), which is assigned with a default value of 1 when a vertex is being created and incremented each time a new string gets mapped to this vertex.
Each vertex contains a map that holds references to its neighbours. When a new neighbour-vertex is being added, either new Vertex in being created based on the given string or the count of an existing vertex gets incremented.
In order to conform to this task, the graph should maintain references to all head-vertexes and tail-vertexes. For simplicity, instead of maintaining two groups of references to adjacent nodes, and two separate count variables (because suffix-count and prefix-count will differ) in each vertex, in this solution graph is actually comprised of two graph (suffix-graph and prefix-graph). And for that reason, the implementing class in named MultiGraph.
In order to populate both suffix-graph and prefix-graph with vertexes, method addCluster() iterates over the string of the given line by the means of Iterator in normal or reversed order, depending on which graph is being populated.
Depth first search
The next step after the graphs are populated is to generate the maps of strings with the frequency of 2 and greater.
For that, the classical depth first search algorithm is being used.
In order to implement the DFS, a mutable container that will be used as a stack is required (ArrayDeque is being used for that purpose). The first element that is taken from the map of heads/tails will be placed on the top of the stack and an instance of StringBuilder holding the name of this element will be placed in the map.
Then, to restore a string with a particular count, vertexes will be popped from the top of the stack and their neighbours with count > 1 in turn will be placed on top of the stack. A copy of the current prefix with the delimiter and the neighbour's name appended will get mapped to the neighbour-vertex.
If a count changes, that indicates that the current prefix represents the longest common string between at least two lines. In this case, prefix and count are being added to the resulting map.
Implementation
The following implementation consists of two classes that are narrow-focused and self-contained. The MultiGraph class acts exclusively as data structure, maintaining two graphs. The pluming code, like splitting the lines of strings, is extracted into a separate class GraphManager.
Graph
public class MultiGraph {
private final Map<String, Vertex> heads = new HashMap<>();
private final Map<String, Vertex> tails = new HashMap<>();
public void addCluster(Deque<String> names) {
addCluster(heads, names.iterator());
addCluster(tails, names.descendingIterator());
}
private void addCluster(Map<String, Vertex> clusters, Iterator<String> names) {
String rootName = names.next();
if (clusters.containsKey(rootName)) {
clusters.get(rootName).incrementGroupCount();
} else {
clusters.put(rootName, new Vertex(rootName));
}
Vertex current = clusters.get(rootName);
while (names.hasNext()) {
current = current.addNext(names.next());
}
}
public Map<String, Integer> generatePrefixMap(String delimiter) {
Map<String, Integer> countByPrefix = new HashMap<>();
for (Vertex next: heads.values()) {
if (next.getGroupCount() == 1) {
continue;
}
performDFS(heads, countByPrefix, delimiter, next);
}
return countByPrefix;
}
public Map<String, Integer> generateSuffixMap(String delimiter) {
Map<String, Integer> countBySuffix = new HashMap<>();
for (Vertex next: tails.values()) {
if (next.getGroupCount() == 1) {
continue;
}
performDFS(tails, countBySuffix, delimiter, next);
}
return countBySuffix;
}
// implementation of the Depth First Search algorithm
public void performDFS(Map<String, Vertex> clusters,
Map<String, Integer> countByPrefix,
String delimiter, Vertex next) {
StringBuilder prefix = null;
Vertex current = next;
int count = next.getGroupCount();
Deque<Vertex> stack = new ArrayDeque<>(); // create as stack
Map<Vertex, StringBuilder> prefixByVert = new HashMap<>();
stack.push(next); // place the first element on the stack
prefixByVert.put(current, new StringBuilder(current.getName()));
while (!stack.isEmpty()) {
current = stack.pop();
if (current.getGroupCount() < count) { // the number of strings mapped to the current Vertex has been changed
countByPrefix.put(prefix.toString(), count); // saving the result
count = current.getGroupCount();
}
prefix = prefixByVert.get(current);
for (Vertex neighbour: current.getNextVertByVal().values()) {
if (next.getGroupCount() == 1) {
continue;
}
stack.push(neighbour);
prefixByVert.put(neighbour, new StringBuilder(prefix)
.append(delimiter)
.append(neighbour.getName()));
}
}
if (prefix != null && count > 1) {
countByPrefix.putIfAbsent(prefix.toString(), count);
}
}
private static class Vertex {
private final String name;
private int groupCount = 1;
private final Map<String, Vertex> nextVertByVal = new HashMap<>();
public Vertex(String name) {
this.name = name;
}
public Vertex addNext(String value) {
if (nextVertByVal.containsKey(value)) {
nextVertByVal.get(value).incrementGroupCount();
} else {
nextVertByVal.put(value, new Vertex(value));
}
return nextVertByVal.get(value);
}
public void incrementGroupCount() {
this.groupCount++;
}
public String getName() {
return name;
}
public int getGroupCount() {
return groupCount;
}
public Map<String, Vertex> getNextVertByVal() {
return nextVertByVal;
}
}
}
The following class deals with the task of processing the input data: it splits the lines, takes care of discarding the empty string which might take place, and packs the input into a Deque to accommodate the iteration in both directions in a convenient way.
It also instantiates the graph and governs it's work. GraphManager takes care of providing the delimiter to the graph in order to restore the initial shape of strings while the resulting maps are being created. With that you can split the given lines on a white space, by empty string to process lines character by character or by punctuation marks without changing a single line in these two classes.
GraphManager
public class GraphManager {
private MultiGraph graph = new MultiGraph();
private String delimiter;
private GraphManager(String delimiter) {
this.delimiter = delimiter;
}
public static GraphManager getInstance(Iterable<String> lines, String delimiter) {
GraphManager gm = new GraphManager(delimiter);
gm.init(lines);
return gm;
}
private void init(Iterable<String> lines) {
for (String line: lines) {
Deque<String> names = new ArrayDeque<>();
for (String name: line.split(delimiter)) {
if (!name.isEmpty()) {
names.add(name);
}
}
addCluster(names);
}
}
private void addCluster(Deque<String> names) {
graph.addCluster(names);
}
public Map<String, Integer> getPrefixMap() {
return graph.generatePrefixMap(delimiter);
}
public Map<String, Integer> getSuffixMap() {
return graph.generateSuffixMap(delimiter);
}
}
main()
public static void main(String[] args) {
List<String> lines = List.of(
"Mary had a little lamb named Willy", "Mary had a little ham",
"Old McDonald had a farm named Willy", "Willy had a little dog named ham",
"( abc )", "( xyz )", "Visit Target Store", "Visit Walmart Store");
GraphManager gm = GraphManager.getInstance(lines, " ");
System.out.println("Prefixes:");
for (Map.Entry<String, Integer> entry: gm.getPrefixMap().entrySet()) {
System.out.println(entry.getValue() + " " + entry.getKey());
}
System.out.println("\nSuffixes:");
for (Map.Entry<String, Integer> entry: gm.getSuffixMap().entrySet()) {
System.out.println(entry.getValue() + " " + entry.getKey());
}
}
Output
Prefixes:
2 Mary had a little
2 Visit
2 (
Suffixes:
2 ham
2 )
2 Store
2 Willy named