Lesson 38 of 164
Union and Intersection of Linked Lists
Given two singly linked lists, build two new lists:
- the union: every distinct value that appears in either list
- the intersection: every distinct value that appears in both lists
Values appear in the order they're first seen: for the union, list 1 first and then list 2. For the intersection, the order of list 1. The input lists are only read, not changed.
Example
list1: 10 -> 20 -> 80 -> 60
list2: 15 -> 20 -> 30 -> 60 -> 45
union: 10 -> 20 -> 80 -> 60 -> 15 -> 30 -> 45
intersection: 20 -> 60graph LR
subgraph l1["list 1"]
a1["10"] --> a2["20"] --> a3["80"] --> a4["60"]
end
subgraph l2["list 2"]
b1["15"] --> b2["20"] --> b3["30"] --> b4["60"] --> b5["45"]
end20 and 60 are in both lists, so they're the intersection.
Brute force: scan the lists
For every candidate value, scan a list to answer "is this value already there?":
- Union: for each value in list 1 and then list 2, add it if the result doesn't contain it yet.
- Intersection: for each value in list 1, add it if list 2 contains it and the result doesn't yet.
A list node here is an object with a mutable next field, and null marks the end of the list. The result is built with a dummy node and a tail pointer. tail always points at the last node of the result, so appending is O(1). The dummy node means the first append needs no special case, and dummy.next is the result head.
public class Main {
static class ListNode {
int val;
ListNode next;
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static boolean contains(ListNode list, int val) {
for (ListNode n = list; n != null; n = n.next) {
if (n.val == val) return true;
}
return false;
}
static ListNode union(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0, null);
ListNode tail = dummy;
for (ListNode list : new ListNode[]{a, b}) {
for (ListNode n = list; n != null; n = n.next) {
if (!contains(dummy.next, n.val)) {
tail.next = new ListNode(n.val, null);
tail = tail.next;
}
}
}
return dummy.next;
}
static ListNode intersection(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0, null);
ListNode tail = dummy;
for (ListNode n = a; n != null; n = n.next) {
if (contains(b, n.val) && !contains(dummy.next, n.val)) {
tail.next = new ListNode(n.val, null);
tail = tail.next;
}
}
return dummy.next;
}
static ListNode fromArray(int... values) {
ListNode head = null;
for (int i = values.length - 1; i >= 0; i--) {
head = new ListNode(values[i], head);
}
return head;
}
static String show(ListNode list) {
StringBuilder sb = new StringBuilder("[");
for (ListNode n = list; n != null; n = n.next) {
if (n != list) sb.append(", ");
sb.append(n.val);
}
return sb.append("]").toString();
}
public static void main(String[] args) {
ListNode a = fromArray(10, 20, 80, 60);
ListNode b = fromArray(15, 20, 30, 60, 45);
System.out.println(show(union(a, b)));
System.out.println(show(intersection(a, b)));
}
}[10, 20, 80, 60, 15, 30, 45]
[20, 60]Time: O((n + m)²): each of the n + m values scans a list of up to n + m nodes. Space: O(1) besides the output.
Optimal: hash sets
A HashSet answers "have I seen this value?" in O(1) expected time.
- Union: walk list 1 and then list 2. Append a value when
seen.add(v)returnstrue, which means it's the first time the value has been seen. - Intersection: put all of list 2's values into a set. Walk list 1 and append a value when
inB.remove(v)returnstrue. Removing it means that if the value appears again in list 1, it won't be added twice.
The result is still built with the dummy node and tail, so the output order follows the walk order, not the set order. The sets are only used for membership tests, so their unspecified iteration order does not matter.
Walkthrough for the intersection. inB starts as {15, 20, 30, 60, 45}:
| list 1 value | inB.remove(v) |
appended? | inB after |
|---|---|---|---|
| 10 | false | no | {15, 20, 30, 60, 45} |
| 20 | true | yes | {15, 30, 60, 45} |
| 80 | false | no | {15, 30, 60, 45} |
| 60 | true | yes | {15, 30, 45} |
import java.util.HashSet;
import java.util.Set;
public class Main {
static class ListNode {
int val;
ListNode next;
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static ListNode union(ListNode a, ListNode b) {
Set<Integer> seen = new HashSet<>();
ListNode dummy = new ListNode(0, null);
ListNode tail = dummy;
for (ListNode list : new ListNode[]{a, b}) {
for (ListNode n = list; n != null; n = n.next) {
if (seen.add(n.val)) {
tail.next = new ListNode(n.val, null);
tail = tail.next;
}
}
}
return dummy.next;
}
static ListNode intersection(ListNode a, ListNode b) {
Set<Integer> inB = new HashSet<>();
for (ListNode n = b; n != null; n = n.next) inB.add(n.val);
ListNode dummy = new ListNode(0, null);
ListNode tail = dummy;
for (ListNode n = a; n != null; n = n.next) {
if (inB.remove(n.val)) {
tail.next = new ListNode(n.val, null);
tail = tail.next;
}
}
return dummy.next;
}
static ListNode fromArray(int... values) {
ListNode head = null;
for (int i = values.length - 1; i >= 0; i--) {
head = new ListNode(values[i], head);
}
return head;
}
static String show(ListNode list) {
StringBuilder sb = new StringBuilder("[");
for (ListNode n = list; n != null; n = n.next) {
if (n != list) sb.append(", ");
sb.append(n.val);
}
return sb.append("]").toString();
}
public static void main(String[] args) {
ListNode a = fromArray(10, 20, 80, 60);
ListNode b = fromArray(15, 20, 30, 60, 45);
System.out.println(show(union(a, b)));
System.out.println(show(intersection(a, b)));
System.out.println(show(intersection(a, null)));
}
}[10, 20, 80, 60, 15, 30, 45]
[20, 60]
[]Time: O(n + m) on average. Space: O(n + m) for the sets and the output.
Set<Integer> stores boxed Integer objects, and remove(n.val) boxes the int automatically. HashSet compares values with equals, so equal numbers match even when they are different Integer objects.
Edge cases
- One list empty: the union is the other list without duplicates, and the intersection is empty.
- Duplicates inside one list: they appear once in each result.
- No common values: the intersection is empty.
