Swift Coding Test Course, Finding the Diameter of a Tree

Solving various algorithm problems for coding test preparation is very helpful in improving your programming skills. This time, we will take a deep dive into the ‘Finding the Diameter of a Tree’ problem. Understanding and knowing how to solve tree structures is very useful, as they frequently appear in many programming problems.

Problem Description

A tree is a non-linear data structure composed of nodes and edges. The diameter of a tree refers to the longest path between any two nodes in the tree. In other words, it is about finding the longest path between two nodes (the number of edges between the nodes). This problem can usually be solved using DFS (Depth-First Search) or BFS (Breadth-First Search).

Problem Definition

Given a tree, find the diameter of the tree.
Input: Number of nodes N in the tree (1 <= N <= 100,000)
      Edges in the form of N-1 pairs (u, v) representing each end node of the edge.
Output: Diameter of the tree
    

Problem Analysis

Due to the nature of trees, where all nodes are connected by exactly one path, the method for finding the longest distance between two nodes is to explore using either DFS or BFS. The general idea is as follows:

  1. Choose an arbitrary node A and find the node B that is farthest from A.
  2. Then, find the node C that is farthest from B. The distance at this point is the diameter of the tree.

Algorithm Implementation

Now let’s implement the above algorithm in Swift. We will use an adjacency list to represent the tree.

Swift Code Implementation


import Foundation

class Graph {
    var adjList: [[Int]]

    init(size: Int) {
        adjList = Array(repeating: [], count: size)
    }

    func addEdge(u: Int, v: Int) {
        adjList[u].append(v)
        adjList[v].append(u)
    }

    func bfs(start: Int) -> (Int, Int) {
        var dist = Array(repeating: -1, count: adjList.count)
        var queue: [Int] = []
        
        dist[start] = 0
        queue.append(start)

        var farthestNode = start
        var maxDistance = 0

        while !queue.isEmpty {
            let node = queue.removeFirst()
            for neighbor in adjList[node] {
                if dist[neighbor] == -1 { // If not visited
                    dist[neighbor] = dist[node] + 1
                    queue.append(neighbor)
                    if dist[neighbor] > maxDistance {
                        maxDistance = dist[neighbor]
                        farthestNode = neighbor
                    }
                }
            }
        }
        return (farthestNode, maxDistance)
    }
}

func findTreeDiameter(edges: [(Int, Int)], n: Int) -> Int {
    let graph = Graph(size: n)
    for edge in edges {
        graph.addEdge(u: edge.0 - 1, v: edge.1 - 1) // 0-indexed
    }

    let (farthestNode, _) = graph.bfs(start: 0)
    let (_, diameter) = graph.bfs(start: farthestNode)
    return diameter
}

// Example Input
let n = 5
let edges: [(Int, Int)] = [(1, 2), (1, 3), (2, 4), (2, 5)]
let diameter = findTreeDiameter(edges: edges, n: n)
print("Diameter of the tree: \(diameter)")
    

Code Explanation

This code calculates the diameter of the tree in the following way:

  1. First, we define a Graph class to store the edge list.
  2. We add edge information using the addEdge method.
  3. The bfs function performs BFS starting from a given node and returns the farthest node and its distance.
  4. The findTreeDiameter function creates a graph using the given edges and finds the diameter of the tree through two BFS calls.

Execution Result

When the code is executed, it produces the following output:

Diameter of the tree: 3

This indicates the longest path between node 4 and node 5.

Conclusion

In this lesson, we dealt with the problem of finding the diameter of a tree. This problem can be solved using DFS or BFS and helps enhance understanding of tree structures. It is a question that often appears in actual coding tests, so be sure to practice enough to become familiar with algorithm implementation. It is also beneficial to experiment with various tree cases for a deeper understanding.

In the next lesson, we will cover more algorithm problems, so please stay tuned!

Swift Coding Test Course, Finding the Parent of a Tree

In computer science, a tree is a type of hierarchical data structure and a kind of graph made up of nodes. In this course, we will solve an algorithm problem to find the parent node of a given node using Swift.

Problem Description

Write a function to find the parent node of a specific node in a given binary tree. The tree is represented as follows.


class TreeNode {
    var value: Int
    var left: TreeNode?
    var right: TreeNode?
    
    init(value: Int) {
        self.value = value
    }
}

You will be given a structure like the one above. Implement the `findParent` function to find the parent node of a specific node in the binary tree that uses this structure.

Function Signature


func findParent(root: TreeNode?, target: Int) -> Int?

Input

  • root: The root node of the binary tree (TreeNode? type)
  • target: The value of the node whose parent you want to find (Int type)

Output

The value of the parent node of the given node. If the given node does not exist or if the root node is the target, return nil.

Example

Assume there is a binary tree as follows:

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

    findParent(root: rootNode, target: 5) ➔ 2
    findParent(root: rootNode, target: 1) ➔ nil
    findParent(root: rootNode, target: 8) ➔ nil
    

Algorithm Design

To find the parent node, we need to traverse the binary tree. We can use common traversal algorithms like Depth-First Search (DFS) or Breadth-First Search (BFS). Here, we will adopt a method using DFS to recursively explore the tree.

DFS Traversal Method

  • If the current node is nil, end the search
  • Check if the value of the current node matches target
  • If matched, return the value of the current node’s parent
  • If not matched, recursively call on the left and right child nodes

Implementation

Now, let’s implement the findParent function based on the above algorithm.


func findParent(root: TreeNode?, target: Int) -> Int? {
    return findParentHelper(root, target: target, parent: nil)
}

private func findParentHelper(_ node: TreeNode?, target: Int, parent: TreeNode?) -> Int? {
    guard let node = node else { return nil } // If the current node is nil

    if node.value == target {
        return parent?.value // Return the parent node
    }

    // Search left child
    let leftResult = findParentHelper(node.left, target: target, parent: node)
    if leftResult != nil {
        return leftResult // Return if found parent in the left
    }

    // Search right child
    return findParentHelper(node.right, target: target, parent: node)
}

Test

Now, let’s test the implemented function to check if it works correctly.


let rootNode = TreeNode(value: 1)
rootNode.left = TreeNode(value: 2)
rootNode.right = TreeNode(value: 3)
rootNode.left?.left = TreeNode(value: 4)
rootNode.left?.right = TreeNode(value: 5)
rootNode.right?.left = TreeNode(value: 6)
rootNode.right?.right = TreeNode(value: 7)

print(findParent(root: rootNode, target: 5) ?? "nil") // 2
print(findParent(root: rootNode, target: 1) ?? "nil") // nil
print(findParent(root: rootNode, target: 8) ?? "nil") // nil

Conclusion

In this course, we implemented an algorithm to find the parent of a specific node in a binary tree using Swift. We applied the basic concept of DFS in the traversal problem of tree structures and learned how to write code.
Through these problems, you can enhance your understanding of trees and recursive thinking.

Additionally, I hope you build your skills by solving problems related to other properties of trees.

Swift Coding Test Course, Understanding Trees

1. Problem Description

In this course, we will enhance our understanding of tree structures using Swift and solve algorithmic problems based on it.
Specifically, we will address the following problem:

Problem: Calculate the Height of a Binary Tree

Write a function to calculate the height of a given binary tree. The height of a binary tree is defined as the length of the longest path from the root node to a leaf node.
The height of a node starts at 0, with leaf nodes having a height of 0.

2. Example

When given the following binary tree,

        1
       / \
      2   3
     / \
    4   5
    

The height of this tree is 2, as the leaf nodes 4 and 5 are two steps away from the root node 1.

3. Approach

This problem can be solved recursively, and the main ideas are as follows:

  1. Define a recursive function to calculate the height of the current node’s left and right child nodes.
  2. Compare the heights of each child node to find the maximum.
  3. The height of the root node is 1 plus the maximum of the left and right child heights.

4. Implementation

Below is the function for calculating the height of a binary tree implemented in Swift:

struct TreeNode {
    var value: Int
    var left: TreeNode?
    var right: TreeNode?
}

func height(of node: TreeNode?) -> Int {
    guard let node = node else {
        return -1 // A null node has a height of -1.
    }
    let leftHeight = height(of: node.left)
    let rightHeight = height(of: node.right)
    return max(leftHeight, rightHeight) + 1
}
    

5. Code Explanation

The function height(of node: TreeNode?) is called recursively.
First, it returns -1 if the node is nil (null), indicating that there is no height for the path including leaf nodes.
After calculating the heights of the left and right children, it selects the larger of the two values and adds 1 to return the height of the current node.

6. Time Complexity

The time complexity of this algorithm is O(N), where N is the number of nodes in the tree.
It takes this time because we have to visit every node once.

7. Additional Tasks

Users can practice more through the following additional tasks:

  • Write a function to check if a specific value exists in the given tree.
  • Write a function to return all the leaf nodes of the binary tree.
  • Implement level order tree traversal to visit all nodes in level order.

8. Conclusion

In this course, we have learned about the concept of binary trees and the basic height calculation algorithm.
Trees play a very important role in data structures, and understanding them is essential for solving algorithmic problems.
Tree-related problems are frequently encountered in coding tests, so practice enough to improve your skills.

Swift Coding Test Course, Tree Traversal

Trees are one of the important data structures in computer science.
Having a deep understanding of trees can be advantageous when solving various problems.
In this lecture, we will explore the basic concepts of trees and how to traverse trees using Swift.
At the end, we will solve a problem that may appear in a real coding test.

1. Basic Concepts of Trees

A tree is a data structure composed of nodes.
Each node has a value and other nodes (child nodes) connected to it.
Trees have the following characteristics.

  • Root Node: The topmost node of the tree. (A node with no parent)
  • Leaf Node: A node that has no children.
  • Parent Node: A node that has children.
  • Subtree: A tree with its child node as the root.

2. Tree Traversal Methods

There are several ways to traverse a tree.
The most representative methods are Pre-order, In-order, and Post-order traversals.

2.1 Pre-order Traversal

  • Order: Node => Left Subtree => Right Subtree
  • Feature: The root node is visited first.

2.2 In-order Traversal

  • Order: Left Subtree => Node => Right Subtree
  • Feature: When traversing a binary search tree, values are output in ascending order.

2.3 Post-order Traversal

  • Order: Left Subtree => Right Subtree => Node
  • Feature: All child nodes are visited before visiting the parent node.

3. Coding Test Problem: Depth of a Binary Tree

Now we will implement a binary tree and apply each traversal method.
The given problem is to calculate the depth (maximum height) of a binary tree.

Problem Description

Write a function to find the maximum depth of the given binary tree, using the root node as the reference.
The depth is defined as the number of nodes in the longest path from the root node to a leaf node.

Example

Input: 
    1
   / \
  2   3
 / \
4   5

Output: 3 (Path from root 1 to 4 or 5)

3.1 Implementing the Binary Tree Node Class

First, we will implement a class (Node) to represent the nodes of a binary tree.

class Node {
    var value: Int
    var left: Node?
    var right: Node?

    init(value: Int) {
        self.value = value
        self.left = nil
        self.right = nil
    }
}

3.2 Implementing the Depth Calculation Function

The function to calculate the maximum depth of a binary tree can be structured as follows.

func maxDepth(_ root: Node?) -> Int {
    guard let node = root else { return 0 }

    let leftDepth = maxDepth(node.left)
    let rightDepth = maxDepth(node.right)

    return max(leftDepth, rightDepth) + 1
}

3.3 Complete Code

The complete code is as follows.

class Node {
    var value: Int
    var left: Node?
    var right: Node?

    init(value: Int) {
        self.value = value
        self.left = nil
        self.right = nil
    }
}

func maxDepth(_ root: Node?) -> Int {
    guard let node = root else { return 0 }

    let leftDepth = maxDepth(node.left)
    let rightDepth = maxDepth(node.right)

    return max(leftDepth, rightDepth) + 1
}

// Example tree creation
let root = Node(value: 1)
root.left = Node(value: 2)
root.right = Node(value: 3)
root.left?.left = Node(value: 4)
root.left?.right = Node(value: 5)

// Print maximum depth
let depth = maxDepth(root)
print("The maximum depth of the tree is: \(depth)")  // Output: The maximum depth of the tree is: 3

4. Reflections and Conclusion

In this lecture, we learned about the concepts of trees and how to traverse trees using Swift,
as well as how to calculate the depth of a tree through a coding test problem.
Since trees are utilized in various algorithms and data structures,
it is important to thoroughly understand and practice these fundamental concepts.
Implementing them in Swift while debugging and optimizing the code is also essential.
I hope you gain more experience by solving more algorithm problems in the future.

Swift Coding Test Course, Try

Problem Description

In this lecture, we will learn about the Trie data structure and solve algorithmic problems using the Trie.

The problem is as follows:

Problem: Phone Number List
Write a function to check if any phone number in a given list is a prefix of another phone number.
If any of the consecutive phone numbers have a prefix relationship, return false, 
otherwise return true.

Input:
- An array of phone number strings, numbers. (1 ≤ numbers.length ≤ 1000, 1 ≤ numbers[i].length ≤ 30)

Output:
- Return false if phone numbers have a prefix relationship, otherwise return true.

Problem Solving Process

1. Understanding the Trie Data Structure

A Trie is a tree structure optimized for storing and searching strings. Each node in the Trie represents a path to the next character, making it useful for storing strings like phone numbers.
A key feature of the Trie is that it can effectively save memory when there are strings that share a common prefix.

2. Implementing the Trie

To implement a Trie, we define a node class and design the Trie as a structure that includes node objects.

class TrieNode {
    var children: [Character: TrieNode] = [:]
    var isEndOfNumber: Bool = false
}

3. Implementing the insert Method

We implement the insert method to insert a new phone number into the Trie. This method adds each character of the phone number to the node, and sets isEndOfNumber to true at the end of the phone number.

class Trie {
    var root: TrieNode
    
    init() {
        root = TrieNode()
    }
    
    func insert(_ number: String) {
        var currentNode = root
        for char in number {
            if currentNode.children[char] == nil {
                currentNode.children[char] = TrieNode()
            }
            currentNode = currentNode.children[char]!
        }
        currentNode.isEndOfNumber = true
    }
}

4. Implementing the checkPrefix Method

Next, we implement the checkPrefix method to check if a phone number in the list is a prefix of another phone number.
This method traverses the Trie and checks if the current string is the end of a phone number before reaching the end of another phone number to determine the prefix relationship.

func checkPrefix(_ number: String) -> Bool {
    var currentNode = root
    for (index, char) in number.enumerated() {
        if let node = currentNode.children[char] {
            currentNode = node
            // If the current node is an ending phone number
            if currentNode.isEndOfNumber {
                return false
            }
        } else {
            break
        }
        // If there are nodes existing beyond the last character
        if index < number.count - 1 && currentNode.isEndOfNumber {
            return false
        }
    }
    return true
}

5. Overall Solution

Finally, we insert all phone numbers into the Trie for the given list of phone numbers and call checkPrefix for each phone number to return the result.

func solution(_ numbers: [String]) -> Bool {
    let trie = Trie()
    
    for number in numbers {
        // Check if it is a prefix of an already registered phone number
        if !trie.checkPrefix(number) {
            return false
        }
        // Register the current phone number
        trie.insert(number)
    }
    return true
}

6. Time Complexity and Space Complexity

The time complexity of this algorithm using a Trie is O(N * M), where N is the number of phone numbers and M is the maximum length of the phone numbers.
The space complexity is O(N * M), representing the space needed to store the phone numbers.

Conclusion

In this lecture, we explored how to solve the problem of determining prefix relationships among a list of phone numbers using the Trie data structure.
Tries are a powerful tool for string processing and can be applied to various string-related problems.
Especially when dealing with a large number of strings, considering space complexity allows for efficient design.