Tree Traversal

The topic of today is Tree Traversal. A tree is a type of data structure that is hierarchically organized with elements that have specific relationships. Understanding trees, one of the commonly used data structures, and implementing basic traversal algorithms is very useful in coding tests. In this article, we will start with the basic concepts of trees, explore various traversal methods, and discuss the implementation in Kotlin.

1. Basic Concepts of Trees

A tree is a data structure composed of nodes. Each node can have values and child nodes. A tree can be described using the following key terms:

  • Root Node: The topmost node of the tree.
  • Leaf Node: A node that has no child nodes.
  • Internal Node: A node that has child nodes.
  • Subtree: A tree with a specific node as its root.

2. Types of Tree Traversal

Tree traversal methods can generally be divided into three types:

  1. Pre-order Traversal: Visit the node – traverse the left subtree – traverse the right subtree
  2. In-order Traversal: Traverse the left subtree – visit the node – traverse the right subtree
  3. Post-order Traversal: Traverse the left subtree – traverse the right subtree – visit the node

3. Problem Description

Let’s assume we are given a binary tree as follows:

                 1
               /   \
              2     3
             / \   / \
            4   5 6   7
    

Please list the results of pre-order, in-order, and post-order traversals for this tree.

4. Problem Solving Process

The first requirement to solve this problem is to define a class structure for the tree. In Kotlin, we can implement the nodes of a binary tree in the following way:

4.1) Define the Binary Tree Node Class

class TreeNode(val value: Int) {
        var left: TreeNode? = null
        var right: TreeNode? = null
    }

The above class has a value that stores the node’s value and variables left and right that reference the left and right child nodes.

4.2) Implementing Pre-order Traversal

Pre-order traversal visits the node before visiting its children. The function for this is as follows:

fun preOrderTraversal(node: TreeNode?) {
        if (node == null) return
        print("${node.value} ")
        preOrderTraversal(node.left)
        preOrderTraversal(node.right)
    }

4.3) Implementing In-order Traversal

In-order traversal first visits the left child and then the current node. It can be implemented as follows:

fun inOrderTraversal(node: TreeNode?) {
        if (node == null) return
        inOrderTraversal(node.left)
        print("${node.value} ")
        inOrderTraversal(node.right)
    }

4.4) Implementing Post-order Traversal

Post-order traversal visits both children before visiting the current node. The implementation is as follows:

fun postOrderTraversal(node: TreeNode?) {
        if (node == null) return
        postOrderTraversal(node.left)
        postOrderTraversal(node.right)
        print("${node.value} ")
    }

5. Creating and Testing the Tree

Now let’s create the tree and test each traversal method. You can structure and execute the tree traversal with the following code:

fun main() {
        val root = TreeNode(1)
        root.left = TreeNode(2)
        root.right = TreeNode(3)
        root.left?.left = TreeNode(4)
        root.left?.right = TreeNode(5)
        root.right?.left = TreeNode(6)
        root.right?.right = TreeNode(7)

        print("Pre-order Traversal: ")
        preOrderTraversal(root)
        println()

        print("In-order Traversal: ")
        inOrderTraversal(root)
        println()

        print("Post-order Traversal: ")
        postOrderTraversal(root)
        println()
    }

6. Code Execution Results

When executing the above program, you can obtain the following results:

Pre-order Traversal: 1 2 4 5 3 6 7 
In-order Traversal: 4 2 5 1 6 3 7 
Post-order Traversal: 4 5 2 6 7 3 1 

7. Summary

In this article, we learned the basic concepts of trees and the three methods of tree traversal: pre-order, in-order, and post-order. We implemented each traversal method in Kotlin and could understand the tree structure through a simple example that can be used in real situations. Since tree problems are frequently encountered in coding tests, it is important to practice sufficiently to become familiar with them.

8. Additional Practice Problems

Enhance your understanding of tree traversal by solving the following problems:

  1. Write a function to calculate the depth of a given binary tree.
  2. Write a function to find the maximum path sum in a binary tree.

9. Conclusion

Now that you have a basic understanding of tree structures and traversal methods, you are prepared to challenge yourself with more complex coding problems. As you solve various algorithm problems involving trees, continue to build your skills in this area. In the next lesson, we will cover graph search. Thank you!

Copyright © 2023 – Coding Test Course

Kotlin Coding Test Course, Try

One of the data structures that often appears in coding tests is the Trie, which is very useful for
efficiently storing and searching strings. In this lesson, we will understand the basic concept of Tries,
implement a Trie using Kotlin, and closely examine the process of solving specific problems.

1. What is a Trie?

A Trie is a tree data structure used for multi-prefix string searching.
Each node represents the path for each character of the string, and the path from root to leaf forms
a single string. Therefore, using a Trie allows sharing common parts of strings, resulting in high
memory efficiency.

For example, when we have words like “cat”, “cab”, “dog”, these words can be stored in a Trie as follows:

                (root)
                ├── c
                │   ├── a
                │   │   ├── b
                │   │   └── t
                │   └── d
                └── d
                    └── o
                        └── g
            

Thus, Tries are a powerful tool for efficient storage and retrieval of a set of strings.
They are particularly effective for prefix searching, word completion, and lexicographical ordering problems.

2. Implementation of the Trie Data Structure

To implement a Trie, we will define two main classes:
TrieNode and Trie.

                class TrieNode {
                    val children: MutableMap = mutableMapOf()
                    var isEndOfWord: Boolean = false
                }
                
                class Trie {
                    private val root: TrieNode = TrieNode()
                    
                    fun insert(word: String) {
                        var node = root
                        for (char in word) {
                            node = node.children.getOrPut(char) { TrieNode() }
                        }
                        node.isEndOfWord = true
                    }
                    
                    fun search(word: String): Boolean {
                        var node = root
                        for (char in word) {
                            node = node.children[char] ?: return false
                        }
                        return node.isEndOfWord
                    }
                }
            

The code above provides the basic insert and search functionality of a Trie. The TrieNode
class contains a collection of child nodes and a flag indicating the end of a word.
The Trie class provides insert and search functions, using the getOrPut function to
dynamically create child nodes.

3. Problem Description: Word Search

Problem: Given a list of words and a list of queries, write a program to check whether
each query is included in the word list. Specifically, it should be able to efficiently search for words
that share common prefixes.

Input:
– Word List: [“apple”, “app”, “bat”, “ball”, “batman”]
– Query List: [“app”, “bat”, “banana”, “appl”, “ball”]

Output: Return a list indicating whether each query exists in the word list,
represented as Boolean values. For example, [true, true, false, false, true] should be output.

4. Problem Solving Process

To solve the problem, we will first insert the word list into the Trie,
and then check each query by searching in the Trie.

                fun wordSearch(words: List, queries: List): List {
                    val trie = Trie()
                    for (word in words) {
                        trie.insert(word)
                    }
                    return queries.map { trie.search(it) }
                }
            

The wordSearch function first creates a Trie object and
inserts words into the Trie using the provided word list.
It then iterates through the query list and calls the search method to
check whether each query exists.

5. Code Execution Example

                fun main() {
                    val words = listOf("apple", "app", "bat", "ball", "batman")
                    val queries = listOf("app", "bat", "banana", "appl", "ball")
                    val result = wordSearch(words, queries)
                    println(result) // [true, true, false, false, true]
                }
            

Running the above code block will output a list containing Boolean values
that represent answers for the queries. In this way, Tries
simplify and efficiently solve string search problems.

6. Utility and Extension of Tries

Tries can be applied to several other problems besides string searching.
For example, autocomplete features and prefix searching are
some of the most common use cases for Tries. Additionally,
you can extend the Trie to incorporate additional functionalities.
You can implement a feature to find all words starting with a
specific prefix or find words that start with specific patterns.

For instance, you could add a startsWith method to find all words starting with a specific prefix as follows.

                fun startsWith(prefix: String): Boolean {
                    var node = root
                    for (char in prefix) {
                        node = node.children[char] ?: return false
                    }
                    return true
                }
            

This method checks whether the given prefix exists in the Trie.
Such extension functionalities are particularly useful when processing large
datasets.

7. Conclusion

In this lesson, we have implemented a Trie using Kotlin and explored the process of
solving string search problems. Tries are a very useful data structure for
managing and searching strings efficiently, and they frequently appear in
coding tests. By attempting various approaches to solve problems and
utilizing Tries appropriately, you will be able to solve a wide range of
algorithmic problems with ease.

In the next lesson, we will cover variations of Tries and other advanced string algorithms.
Please continue to build your skills through more practice problems and actual coding test problems.
Thank you!

Kotlin Coding Test Course, Two Pointers

This article explains the two-pointer technique for solving algorithm problems using Kotlin. The two-pointer technique is an algorithmic technique that uses two pointers to find values or ranges that satisfy specific conditions within an array or list, and it is commonly used in problems related to sorted arrays.

Problem Description

Here is a problem that can be solved using the two-pointer technique.

Problem: Two Sum

Given an integer array nums and an integer target, return the indices of the two numbers such that they add up to target. Assume that each input has exactly one solution, and you may not use the same element twice.

For example:

  • Input: nums = [2, 7, 11, 15], target = 9
  • Output: [0, 1] (nums[0] + nums[1] = 2 + 7 = 9)

Problem Approach

This problem can be solved using the two-pointer technique. However, we need to sort the array first, which requires tracking the original indices. We can solve the problem using a typical two-pointer approach.

Step 1: Sort the Input Array

Sort the array so that each pointer can compare values at the appropriate positions. In this case, we need to store each element along with its index.

Step 2: Set Up Two Pointers

One pointer starts at the beginning of the array, while the other pointer starts at the end. Move the pointers if the sum of the values they point to is equal to or less than target.

Step 3: Check the Condition

If the sum is greater than target, move the right pointer to the left; if it is less, move the left pointer to the right. Repeat this process until the two pointers meet.

Kotlin Code

Below is the code implemented in Kotlin:


fun twoSum(nums: IntArray, target: Int): IntArray {
    val indexedNums = nums.mapIndexed { index, num -> index to num }.sortedBy { it.second }
    var left = 0
    var right = indexedNums.size - 1

    while (left < right) {
        val sum = indexedNums[left].second + indexedNums[right].second
        when {
            sum == target -> return intArrayOf(indexedNums[left].first, indexedNums[right].first)
            sum < target -> left++
            else -> right--
        }
    }
    throw IllegalArgumentException("No two sum solution")
}

Time Complexity

The time complexity of this algorithm is O(n log n). This is due to the time spent sorting the array, which is the most significant factor. After that, the time taken to find the two sums using the two-pointer method is O(n).

Conclusion

The two-pointer technique is a very useful algorithmic approach that can efficiently solve many problems. Through this problem, we learned how to manage arrays and find values using the two-pointer technique. With practice, you can master this technique and develop the ability to solve more complex problems.

Additional Practice Problems

Here are some additional two-pointer problems to practice:

  • Finding all pairs in a given integer array that sum up to a specific value
  • Finding the number of pairs of two numbers that add up to k using two pointers in a sorted array
  • Using two pointers to determine if a string is a palindrome

By practicing these problems, you can deepen your understanding of the two-pointer technique.

Kotlin Coding Test Course, Preparing for Resignation

Kotlin has become a very popular programming language in recent years. It is primarily used in Android development, but its utilization is also increasing in areas such as server-side development, data science, and coding tests. In this post, we will explore how to prepare for leaving a job by solving coding test problems using Kotlin.

Problem: Maximum Subarray Sum

The problem is to find the maximum sum of a contiguous subarray in a given integer array. This problem can be approached in various ways, but here we will look at how to solve it efficiently using “Kadane’s Algorithm.”

Problem Description

Given an integer array, find the maximum sum of a contiguous subarray. For example, if the array [−2,1,−3,4,−1,2,1,−5,4] is given, the contiguous subarray [4,−1,2,1] has a sum of 6, which is the maximum sum.

Input

  • Length of the array n (1 ≤ n ≤ 105)
  • Integer array nums (-104 ≤ nums[i] ≤ 104)

Output

Output the maximum sum of the subarray.

Problem Solving Process

Step 1: Understanding and Analyzing the Problem

Since this problem requires considering the sum of contiguous elements, we need to explore several contiguous subarrays that include each element. At this point, we should think about how to reduce unnecessary repetitions and efficiently find the maximum sum.

Step 2: Selecting an Algorithm

Generally, using nested loops to explore all subarrays results in a time complexity of O(n2), which is inefficient. Instead, using Kadane’s Algorithm can reduce the time complexity to O(n). The idea of this algorithm is to maintain the maximum subarray sum up to the current index while repeatedly updating the current index’s sum.

Step 3: Implementing Kadane’s Algorithm

The implementation process of Kadane’s Algorithm is as follows:

fun maxSubArray(nums: IntArray): Int {
    var maxSoFar = nums[0]
    var maxEndingHere = nums[0]

    for (i in 1 until nums.size) {
        maxEndingHere = maxOf(nums[i], maxEndingHere + nums[i])
        maxSoFar = maxOf(maxSoFar, maxEndingHere)
    }
    return maxSoFar
}
    

Code Explanation

– Initially, two variables are initialized: maxSoFar is the maximum subarray sum found so far, and maxEndingHere is the maximum subarray sum ending at the current index.
– As we iterate through each element, we update maxEndingHere to be the maximum of the current element and maxEndingHere + current element. This gives us the maximum sum ending at the current position.
– Additionally, we update maxSoFar to continuously refresh the maximum value we can consider.

Step 4: Testing and Validation

We will test the recently implemented maxSubArray function with various cases to ensure it works correctly.

fun main() {
    val nums = intArrayOf(-2, 1, -3, 4, -1, 2, 1, -5, 4)
    println("Maximum Subarray Sum: ${maxSubArray(nums)}") // 6
}
    

Step 5: Analyzing Time Complexity

This algorithm inspects each element of the array only once in a single loop, resulting in a time complexity of O(n). The space complexity is O(1) as it does not use any additional space.

Tips for Preparing for Resignation

Preparing for coding tests is a good way to get ready for a new job after leaving your current one. Here are some useful tips for preparing for resignation alongside coding tests:

  • Regular Practice: Periodically solve algorithm problems and get familiar with various types of problems.
  • Error Analysis: Review mistakes made while solving problems and analyze their causes.
  • Mock Interviews: Conduct mock interviews with friends or colleagues to experience real interview scenarios.
  • Utilizing Books and Online Materials: Expand your knowledge of algorithms and coding tests using good resources.

Conclusion

Professional coding test preparation takes time, but it can be accomplished through consistent learning and practice. By solving algorithm problems using Kotlin, you will further develop your coding skills and face new challenges with confidence.

I hope this article helps you in preparing for your resignation and coding tests.

Kotlin Coding Test Course, Go Fast with Time Machine

Problem Description

Imagine! You, from the future, decide to travel to a specific time using a time machine. However, this time machine can only operate through a complex algorithm. The problem is to calculate the minimum number of jumps needed to reach the given time.

Problem Definition

Two integers S and E are given. S is the starting time and E is the arrival time. The time machine operates according to the following rules:

  • From the current time, you can jump to the next time by performing +1, +2, or *2 operations.
  • Write a program to calculate and output the minimum number of jumps taken by the time machine to operate.

Input

In the first line, two integers S (1 ≤ S ≤ 105) and E (1 ≤ E ≤ 105) are given.

Output

Output the minimum number of jumps required for the time machine to reach E.

Example

    Input:
    4 7

    Output:
    2
    

Problem Solving Process

This problem can be solved using the BFS (Breadth-First Search) algorithm. BFS starts from a specific node and explores all nodes connected to it before exploring the nodes of the next layer. In this problem, each time zone can be represented as a node in a graph, while each jump can be represented as an edge.

Step-by-Step Solution

  1. Initialize the Queue: To perform BFS, first initialize the queue. Add the starting time S and the current jump count to the queue.
  2. Visit Array: Create a visit array to record the times that have already been visited. This prevents visiting the same time multiple times.
  3. Perform BFS: Remove a node from the queue and explore the next times by performing all possible jumps. If the next jump reaches the arrival time E, return the current jump count.
  4. Termination Condition: If the queue becomes empty without reaching the arrival time, terminate the jump process.

Code Implementation


fun minJumps(S: Int, E: Int): Int {
    val queue: Queue> = LinkedList()
    val visited = BooleanArray(100001) // maximum time range 100000
    queue.add(Pair(S, 0))
    visited[S] = true

    while (queue.isNotEmpty()) {
        val (currentTime, jumps) = queue.poll()

        // Reached the destination
        if (currentTime == E) {
            return jumps
        }

        // Next possible times
        val nextTimes = listOf(currentTime + 1, currentTime + 2, currentTime * 2)

        for (nextTime in nextTimes) {
            if (nextTime in 1..100000 && !visited[nextTime]) {
                visited[nextTime] = true
                queue.add(Pair(nextTime, jumps + 1))
            }
        }
    }
    return -1 // unreachable case
}

fun main() {
    val input = readLine()!!.split(" ")
    val S = input[0].toInt()
    val E = input[1].toInt()
    println(minJumps(S, E))
}
    

Time Complexity Analysis

The time complexity of this algorithm is O(n). Since all time zones will be visited once, performing BFS for up to 100,000 nodes is efficient.

Conclusion

In this lecture, we explored how to apply the BFS algorithm to solve the time machine problem and calculate the shortest distance. Understanding and applying the advantages of implementing it in Kotlin as well as the basic principles of BFS will help you in coding tests. In the next session, we will prepare a lecture to explore other problems and more diverse algorithms!