Greetings, adventurous coder! In this unit, we're embarking on an exciting journey through the world of numbers, where connected paths reveal patterns in data. We'll explore hashing and binning, all converging into collections! Throughout this adventure, we'll use the powerful and expressive language of Kotlin to design effective solutions. Fasten your seatbelt, and let's embark on this problem-solving expedition!
Task Statement
The task for this unit is to create a Kotlin function that takes two lists of unique integers as input and returns another list containing the elements that are common to both input lists. This task offers an intriguing look at identifying similarities between two sequences of data, a scenario frequently encountered in data comparisons and analytics.
The commonElements(list1, list2) function should sift through these lists of integers and extract the common elements between them.
The expected result in this scenario would be:
Kotlin
val result = listOf(2, 3, 5, 13, 21)
Brute Force Solution and Complexity Analysis
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal
Before diving into the optimized solution, let's consider a basic or naïve approach to tackle this problem and understand its complexity. Our initial intuitive method might involve iterating over both lists in nested loops and identifying common elements. In this approach, for each element in the first list, we check for its presence in the second list. If found, we add it to our result list. Here's how such a solution would look in Kotlin:
Kotlin
fun commonElementsSlow(list1: List<Int>, list2: List<Int>): List<Int> { val common = mutableListOf<Int>() list1.forEach { num1 -> list2.forEach { num2 -> if (num1 == num2) { common.add(num1) return@forEach // Breaks inner loop once the number is found in list2 } } } return common}fun main() { val list1 = listOf(1, 2, 3, 5, 8, 13, 21, 34) val list2 = listOf(2, 3, 5, 7, 13, 21, 31) val result = commonElementsSlow(list1, list2) println(result) // Prints: [2, 3, 5, 13, 21]}
This method, however, is not very efficient. The worst-case scenario involves checking every element in both lists, resulting in O(n⋅m) complexity, where n and m stand for the number of elements in list1 and list2, respectively. For large lists, this iterative approach is inefficient and slow.
To optimize our algorithm and achieve a faster solution, we will use HashSet in Kotlin, which drastically reduces the computational time.
Introduction to HashSet Solution
Solution Building: Step 1
The first step in developing our solution is to transform one of these lists into a Kotlin HashSet. Operations like finding intersections are highly optimized with HashSet, and we'll take advantage of this optimization.
fun commonElements(list1: List<Int>, list2: List<Int>): List<Int> { val set1 = list1.toHashSet() // Implementation continues in the next section}
Solution Building: Step 2
After converting one of our lists into a HashSet, we're ready to determine the common elements between the two sets of data. We'll iterate through the second list and check if each element is present in the HashSet.
fun commonElements(list1: List<Int>, list2: List<Int>): List<Int> { val set1 = list1.toHashSet() val common = mutableListOf<Int>() list2.forEach { num -> if (num in set1) { common.add(num) } } return common}fun main() { val list1 = listOf(1, 2, 3, 5, 8, 13, 21, 34) val list2 = listOf(2, 3, 5, 7, 13, 21, 31) val result = commonElements(list1, list2) println(result) // Prints: [2, 3, 5, 13, 21]}
Additional HashSet Methods
Lesson Summary
Congratulations! You've gained a solid understanding of Kotlin lists and HashSets, along with their operations. It's rare to encounter solutions that balance elegance with performance efficiency, but today's task presented us with precisely that opportunity, and you've tackled it exceptionally well.
However, the journey doesn't end here. It's now your chance to explore similar challenges in the upcoming practice session. Don't hesitate to experiment with various data sequences to enhance your learning experience. Happy coding!
By following these detailed instructions, you should now have a robust foundation for understanding how to operate with HashSet in Kotlin and develop efficient algorithms using them.
Given the inefficiency of our earlier approach, we aim to reduce the time complexity by decreasing the number of operations required. Kotlin's HashSet is a suitable tool for this task.
HashSet in Kotlin uses hash tables under the hood, allowing operations such as insertion, removal, and search to be performed in average constant time, i.e., O(1). This improvement provides a significant performance edge over the nested loop solution. The overall time complexity will be reduced to O(n+m) for traversing both lists and processing them with HashSet.
Let's start building our optimized solution in the next step.
Kotlin
fun commonElements(list1: List<Int>, list2: List<Int>): List<Int> { val set1 = list1.toHashSet() // Implementation continues in the next section}
Kotlin
fun commonElements(list1: List<Int>, list2: List<Int>): List<Int> { val set1 = list1.toHashSet() val common = mutableListOf<Int>() list2.forEach { num -> if (num in set1) { common.add(num) } } return common}fun main() { val list1 = listOf(1, 2, 3, 5, 8, 13, 21, 34) val list2 = listOf(2, 3, 5, 7, 13, 21, 31) val result = commonElements(list1, list2) println(result) // Prints: [2, 3, 5, 13, 21]}
Kotlin's HashSet is not only efficient but also loaded with numerous useful methods for performing various operations. Let's explore some practical methods:
1. Insertion
The add method allows you to add elements to a HashSet. If the element already exists, the insertion will not change the set. The average time complexity for insertion is O(1).
fun main() { val mySet = hashSetOf(1, 2, 3) mySet.add(4) // O(1) mySet.add(2) // O(1), no effect as 2 is already in the set println(mySet) // Prints: [1, 2, 3, 4] (order may vary)}
2. Removal
The remove method removes elements from a HashSet. If the element doesn't exist, the remove operation does nothing. The average time complexity for removal is O(1).
fun main() { val mySet = hashSetOf(1, 2, 3, 4) mySet.remove(3) // O(1) mySet.remove(5) // O(1), no effect as 5 is not in the set println(mySet) // Prints: [1, 2, 4] (order may vary)}
3. Checking Membership
The contains method checks if a HashSet contains a specified element. It returns true if the element is present, otherwise false. The average time complexity is O(1).
The size method returns the number of items in a HashSet. The complexity of this operation is O(1).
fun main() { val mySet = hashSetOf(1, 2, 3, 4, 5) println(mySet.size) // O(1), prints: 5}
5. Clear
The clear method removes all elements from a HashSet. The complexity for this operation is O(n), where n is the element count in the set.
fun main() { val mySet = hashSetOf(1, 2, 3, 4, 5) mySet.clear() // O(n) println(mySet.size) // O(1), prints: 0}
Understanding these operations allows you to exploit Kotlin's HashSets effectively and design efficient algorithms for a variety of scenarios.
Kotlin
fun main() { val mySet = hashSetOf(1, 2, 3) mySet.add(4) // O(1) mySet.add(2) // O(1), no effect as 2 is already in the set println(mySet) // Prints: [1, 2, 3, 4] (order may vary)}
Kotlin
fun main() { val mySet = hashSetOf(1, 2, 3, 4) mySet.remove(3) // O(1) mySet.remove(5) // O(1), no effect as 5 is not in the set println(mySet) // Prints: [1, 2, 4] (order may vary)}